From 9997c23708a831f1d9ec4ac63468cdb60b8873c1 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 16 Oct 2020 14:25:40 +0200 Subject: [PATCH 0001/3587] resources#relativePath does not honour path casing strategy --- src/vs/base/common/resources.ts | 20 ++++++++++++++----- src/vs/base/test/common/resources.test.ts | 2 ++ src/vs/platform/workspace/common/workspace.ts | 10 ++++++---- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/vs/base/common/resources.ts b/src/vs/base/common/resources.ts index 621c97368c75..88cbade64115 100644 --- a/src/vs/base/common/resources.ts +++ b/src/vs/base/common/resources.ts @@ -8,7 +8,7 @@ import * as paths from 'vs/base/common/path'; import { URI, uriToFsPath } from 'vs/base/common/uri'; import { equalsIgnoreCase, compare as strCompare } from 'vs/base/common/strings'; import { Schemas } from 'vs/base/common/network'; -import { isWindows, isLinux } from 'vs/base/common/platform'; +import { isLinux } from 'vs/base/common/platform'; import { CharCode } from 'vs/base/common/charCode'; import { ParsedExpression, IExpression, parse } from 'vs/base/common/glob'; import { TernarySearchTree } from 'vs/base/common/map'; @@ -229,11 +229,10 @@ export class ExtUri implements IExtUri { if (from.scheme !== to.scheme || !isEqualAuthority(from.authority, to.authority)) { return undefined; } - if (from.scheme === Schemas.file) { - const relativePath = paths.relative(originalFSPath(from), originalFSPath(to)); - return isWindows ? extpath.toSlashes(relativePath) : relativePath; - } let fromPath = from.path || '/', toPath = to.path || '/'; + if (getWindowsDriveLetter(fromPath) !== getWindowsDriveLetter(toPath)) { + return undefined; // no relative path possible if the drive letter doesn't match + } if (this._ignorePathCasing(from)) { // make casing of fromPath match toPath let i = 0; @@ -393,6 +392,17 @@ export function distinctParents(items: T[], resourceAccessor: (item: T) => UR return distinctParents; } +/** + * Given a URI path (not a fs path!), tests if the path looks like a Window path with drive letter and returns the lowercase variant of that drive letter. + * @param path returns the drive letter (lower case) or undefined if the path does not look like a windows path + */ +function getWindowsDriveLetter(path: string): string | undefined { + if (/^\/[a-zA-Z]:(\/|$)/.test(path)) { + return path.charAt(1).toLowerCase(); + } + return undefined; +} + /** * Data URI related helpers. */ diff --git a/src/vs/base/test/common/resources.test.ts b/src/vs/base/test/common/resources.test.ts index 8026e240bba4..c608e113a3d8 100644 --- a/src/vs/base/test/common/resources.test.ts +++ b/src/vs/base/test/common/resources.test.ts @@ -273,6 +273,8 @@ suite('Resources', () => { assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://A/FOO/BAR/GOO'), 'BAR/GOO', false, true); assertRelativePath(URI.parse('foo://a/foo/xoo'), URI.parse('foo://A/FOO/BAR/GOO'), '../BAR/GOO', false, true); assertRelativePath(URI.parse('foo:///c:/a/foo'), URI.parse('foo:///C:/a/foo/xoo/'), 'xoo', false, true); + assertRelativePath(URI.parse('foo:///c:/a/foo'), URI.parse('foo:///D:/a/foo/xoo/'), undefined, false, true); + assertRelativePath(URI.parse('file:///c:/a/foo'), URI.parse('file:///D:/a/foo/xoo/'), undefined, false, true); if (isWindows) { assertRelativePath(URI.file('c:\\foo\\bar'), URI.file('c:\\foo\\bar'), ''); diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index c0c34d74f572..3e7183ea6d4c 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -236,12 +236,14 @@ export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], let result: WorkspaceFolder[] = []; let seen: Set = new Set(); - const relativeTo = resources.dirname(workspaceConfigFile); + const extUri = resources.extUriBiasedIgnorePathCase; + + const relativeTo = extUri.dirname(workspaceConfigFile); for (let configuredFolder of configuredFolders) { let uri: URI | null = null; if (isRawFileWorkspaceFolder(configuredFolder)) { if (configuredFolder.path) { - uri = resources.resolvePath(relativeTo, configuredFolder.path); + uri = extUri.resolvePath(relativeTo, configuredFolder.path); } } else if (isRawUriWorkspaceFolder(configuredFolder)) { try { @@ -257,11 +259,11 @@ export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], } if (uri) { // remove duplicates - let comparisonKey = resources.getComparisonKey(uri); + let comparisonKey = extUri.getComparisonKey(uri); if (!seen.has(comparisonKey)) { seen.add(comparisonKey); - const name = configuredFolder.name || resources.basenameOrAuthority(uri); + const name = configuredFolder.name || extUri.basenameOrAuthority(uri); result.push(new WorkspaceFolder({ uri, name, index: result.length }, configuredFolder)); } } From da712f4071300472ce9dcac45ab6da524ecfade4 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 16 Oct 2020 16:30:41 +0200 Subject: [PATCH 0002/3587] more changes --- src/vs/platform/workspace/common/workspace.ts | 2 +- src/vs/platform/workspaces/common/workspaces.ts | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 3e7183ea6d4c..d79c047f066b 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -236,7 +236,7 @@ export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], let result: WorkspaceFolder[] = []; let seen: Set = new Set(); - const extUri = resources.extUriBiasedIgnorePathCase; + const extUri = resources.extUriBiasedIgnorePathCase; // To be replaced by the UriIdentityService as parameter: #108793 const relativeTo = extUri.dirname(workspaceConfigFile); for (let configuredFolder of configuredFolders) { diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index d6643801ab6c..a4837165c061 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -9,7 +9,7 @@ import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/works import { URI, UriComponents } from 'vs/base/common/uri'; import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; import { extname, isAbsolute } from 'vs/base/common/path'; -import { dirname, resolvePath, isEqualAuthority, relativePath, extname as resourceExtname, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { isEqualAuthority, extname as resourceExtname, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import * as jsonEdit from 'vs/base/common/jsonEdit'; import * as json from 'vs/base/common/json'; import { Schemas } from 'vs/base/common/network'; @@ -217,7 +217,9 @@ export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, return { name: folderName, uri: folderURI.toString(true) }; } - let folderPath = !forceAbsolute ? relativePath(targetConfigFolderURI, folderURI) : undefined; + const extUri = extUriBiasedIgnorePathCase; // To be replaced by the UriIdentityService as parameter: #108793 + + let folderPath = !forceAbsolute ? extUri.relativePath(targetConfigFolderURI, folderURI) : undefined; if (folderPath !== undefined) { if (folderPath.length === 0) { folderPath = '.'; @@ -258,14 +260,16 @@ export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, configPathURI: URI, isFromUntitledWorkspace: boolean, targetConfigPathURI: URI) { let storedWorkspace = doParseStoredWorkspace(configPathURI, rawWorkspaceContents); - const sourceConfigFolder = dirname(configPathURI); - const targetConfigFolder = dirname(targetConfigPathURI); + const extUri = extUriBiasedIgnorePathCase; // To be replaced by the UriIdentityService as parameter: #108793 + + const sourceConfigFolder = extUri.dirname(configPathURI); + const targetConfigFolder = extUri.dirname(targetConfigPathURI); const rewrittenFolders: IStoredWorkspaceFolder[] = []; const slashForPath = useSlashForPath(storedWorkspace.folders); for (const folder of storedWorkspace.folders) { - const folderURI = isRawFileWorkspaceFolder(folder) ? resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri); + const folderURI = isRawFileWorkspaceFolder(folder) ? extUri.resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri); let absolute; if (isFromUntitledWorkspace) { // if it was an untitled workspace, try to make paths relative From 8308a6dc10eb29477dc23b8b8e9d8c5074903cc0 Mon Sep 17 00:00:00 2001 From: Orta Date: Fri, 16 Apr 2021 22:27:19 +0100 Subject: [PATCH 0003/3587] Adds the ability to resolve @types and JSDoc imports in inline --- .../server/src/modes/javascriptMode.ts | 128 +++++++++++++----- .../server/src/test/completions.test.ts | 24 +++- .../src/test/jsdocImportFixtures/index.html | 1 + .../src/test/jsdocImportFixtures/index.js | 4 + .../test/jsdocImportFixtures/jsDocTypes.ts | 9 ++ 5 files changed, 125 insertions(+), 41 deletions(-) create mode 100644 extensions/html-language-features/server/src/test/jsdocImportFixtures/index.html create mode 100644 extensions/html-language-features/server/src/test/jsdocImportFixtures/index.js create mode 100644 extensions/html-language-features/server/src/test/jsdocImportFixtures/jsDocTypes.ts diff --git a/extensions/html-language-features/server/src/modes/javascriptMode.ts b/extensions/html-language-features/server/src/modes/javascriptMode.ts index 01e0b381d112..ea51fa0e4b21 100644 --- a/extensions/html-language-features/server/src/modes/javascriptMode.ts +++ b/extensions/html-language-features/server/src/modes/javascriptMode.ts @@ -12,20 +12,34 @@ import { } from './languageModes'; import { getWordAtText, isWhitespaceOnly, repeat } from '../utils/strings'; import { HTMLDocumentRegions } from './embeddedSupport'; +import { normalize, sep } from 'path'; import * as ts from 'typescript'; import { getSemanticTokens, getSemanticTokenLegend } from './javascriptSemanticTokens'; const JS_WORD_REGEX = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g; +/** TypeScript does not handle schemes on file references, so normalize and remove the schemes when communicating with tsserver */ +function deschemeURI(uri: string) { + if (!uri.startsWith('file://')) { + return uri ; + } + // This is replicating the logic in TypeScriptServiceClient.normalizedPath + const newPath = normalize(uri.replace('file://', '')); + + // Both \ and / must be escaped in regular expressions + return newPath.replace(new RegExp('\\' + sep, 'g'), '/'); +} + function getLanguageServiceHost(scriptKind: ts.ScriptKind) { const compilerOptions: ts.CompilerOptions = { allowNonTsExtensions: true, allowJs: true, lib: ['lib.es6.d.ts'], target: ts.ScriptTarget.Latest, moduleResolution: ts.ModuleResolutionKind.Classic, experimentalDecorators: false }; let currentTextDocument = TextDocument.create('init', 'javascript', 1, ''); + let currentWorkspace: Workspace = undefined!; const jsLanguageService = import(/* webpackChunkName: "javascriptLibs" */ './javascriptLibs').then(libs => { const host: ts.LanguageServiceHost = { getCompilationSettings: () => compilerOptions, - getScriptFileNames: () => [currentTextDocument.uri, 'jquery'], + getScriptFileNames: () => [deschemeURI(currentTextDocument.uri), 'jquery'], getScriptKind: (fileName) => { if (fileName === currentTextDocument.uri) { return scriptKind; @@ -33,15 +47,17 @@ function getLanguageServiceHost(scriptKind: ts.ScriptKind) { return fileName.substr(fileName.length - 2) === 'ts' ? ts.ScriptKind.TS : ts.ScriptKind.JS; }, getScriptVersion: (fileName: string) => { - if (fileName === currentTextDocument.uri) { + if (fileName === deschemeURI(currentTextDocument.uri)) { return String(currentTextDocument.version); } return '1'; // default lib an jquery.d.ts are static }, getScriptSnapshot: (fileName: string) => { let text = ''; - if (fileName === currentTextDocument.uri) { + if (fileName === deschemeURI(currentTextDocument.uri)) { text = currentTextDocument.getText(); + } else if (ts.sys.fileExists(fileName)) { + text = ts.sys.readFile(fileName, 'utf8')!; } else { text = libs.loadLibrary(fileName); } @@ -51,14 +67,24 @@ function getLanguageServiceHost(scriptKind: ts.ScriptKind) { getChangeRange: () => undefined }; }, - getCurrentDirectory: () => '', - getDefaultLibFileName: (_options: ts.CompilerOptions) => 'es6' + getCurrentDirectory: () => { + const workspace = currentWorkspace && currentWorkspace.folders.find(ws => deschemeURI(currentTextDocument.uri).startsWith(deschemeURI(ws.uri))); + return workspace ? deschemeURI(workspace.uri) : ''; + }, + getDefaultLibFileName: (_options: ts.CompilerOptions) => 'es6', + fileExists: ts.sys.fileExists, + readFile: ts.sys.readFile, + readDirectory: ts.sys.readDirectory, + directoryExists: ts.sys.directoryExists, + getDirectories: ts.sys.getDirectories, }; + return ts.createLanguageService(host); }); return { - async getLanguageService(jsDocument: TextDocument): Promise { + async getLanguageService(jsDocument: TextDocument, workspace: Workspace): Promise { currentTextDocument = jsDocument; + currentWorkspace = workspace; return jsLanguageService; }, getCompilationSettings() { @@ -84,9 +110,11 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { host.getCompilationSettings()['experimentalDecorators'] = settings && settings.javascript && settings.javascript.implicitProjectConfig.experimentalDecorators; const jsDocument = jsDocuments.get(document); - const languageService = await host.getLanguageService(jsDocument); - const syntaxDiagnostics: ts.Diagnostic[] = languageService.getSyntacticDiagnostics(jsDocument.uri); - const semanticDiagnostics = languageService.getSemanticDiagnostics(jsDocument.uri); + const languageService = await host.getLanguageService(jsDocument, workspace); + const filePath = deschemeURI(jsDocument.uri); + + const syntaxDiagnostics: ts.Diagnostic[] = languageService.getSyntacticDiagnostics(filePath); + const semanticDiagnostics = languageService.getSemanticDiagnostics(filePath); return syntaxDiagnostics.concat(semanticDiagnostics).map((diag: ts.Diagnostic): Diagnostic => { return { range: convertRange(jsDocument, diag), @@ -98,9 +126,12 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); + const jsLanguageService = await host.getLanguageService(jsDocument, workspace); + const filePath = deschemeURI(jsDocument.uri); + let offset = jsDocument.offsetAt(position); - let completions = jsLanguageService.getCompletionsAtPosition(jsDocument.uri, offset, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); + let completions = jsLanguageService.getCompletionsAtPosition(filePath, offset, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); + if (!completions) { return { isIncomplete: false, items: [] }; } @@ -126,9 +157,11 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); + const jsLanguageService = await host.getLanguageService(jsDocument, workspace); + const filePath = deschemeURI(jsDocument.uri); + // @ts-expect-error until 4.3 protocol update - let details = jsLanguageService.getCompletionEntryDetails(jsDocument.uri, item.data.offset, item.label, undefined, undefined, undefined, undefined); + let details = jsLanguageService.getCompletionEntryDetails(filePath, item.data.offset, item.label, undefined, undefined, undefined, undefined); if (details) { item.detail = ts.displayPartsToString(details.displayParts); item.documentation = ts.displayPartsToString(details.documentation); @@ -138,8 +171,10 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - let info = jsLanguageService.getQuickInfoAtPosition(jsDocument.uri, jsDocument.offsetAt(position)); + const jsLanguageService = await host.getLanguageService(jsDocument, workspace); + const filePath = deschemeURI(jsDocument.uri); + + let info = jsLanguageService.getQuickInfoAtPosition(filePath, jsDocument.offsetAt(position)); if (info) { const contents = ts.displayPartsToString(info.displayParts); return { @@ -151,8 +186,10 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - let signHelp = jsLanguageService.getSignatureHelpItems(jsDocument.uri, jsDocument.offsetAt(position), undefined); + const jsLanguageService = await host.getLanguageService(jsDocument, workspace); + const filePath = deschemeURI(jsDocument.uri); + + let signHelp = jsLanguageService.getSignatureHelpItems(filePath, jsDocument.offsetAt(position), undefined); if (signHelp) { let ret: SignatureHelp = { activeSignature: signHelp.selectedItemIndex, @@ -189,13 +226,15 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { @@ -211,8 +250,10 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - const highlights = jsLanguageService.getDocumentHighlights(jsDocument.uri, jsDocument.offsetAt(position), [jsDocument.uri]); + const jsLanguageService = await host.getLanguageService(jsDocument, workspace); + const filePath = deschemeURI(jsDocument.uri); + + const highlights = jsLanguageService.getDocumentHighlights(filePath, jsDocument.offsetAt(position), [filePath]); const out: DocumentHighlight[] = []; for (const entry of highlights || []) { for (const highlight of entry.highlightSpans) { @@ -226,8 +267,10 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - let items = jsLanguageService.getNavigationBarItems(jsDocument.uri); + const jsLanguageService = await host.getLanguageService(jsDocument, workspace); + const filePath = deschemeURI(jsDocument.uri); + + let items = jsLanguageService.getNavigationBarItems(filePath); if (items) { let result: SymbolInformation[] = []; let existing = Object.create(null); @@ -263,8 +306,10 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - let definition = jsLanguageService.getDefinitionAtPosition(jsDocument.uri, jsDocument.offsetAt(position)); + const jsLanguageService = await host.getLanguageService(jsDocument, workspace); + const filePath = deschemeURI(jsDocument.uri); + + let definition = jsLanguageService.getDefinitionAtPosition(filePath, jsDocument.offsetAt(position)); if (definition) { return definition.filter(d => d.fileName === jsDocument.uri).map(d => { return { @@ -277,10 +322,12 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - let references = jsLanguageService.getReferencesAtPosition(jsDocument.uri, jsDocument.offsetAt(position)); + const jsLanguageService = await host.getLanguageService(jsDocument, workspace); + const filePath = deschemeURI(jsDocument.uri); + + let references = jsLanguageService.getReferencesAtPosition(filePath, jsDocument.offsetAt(position)); if (references) { - return references.filter(d => d.fileName === jsDocument.uri).map(d => { + return references.filter(d => d.fileName === filePath).map(d => { return { uri: document.uri, range: convertRange(jsDocument, d.textSpan) @@ -291,17 +338,20 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); + const jsLanguageService = await host.getLanguageService(jsDocument, workspace); + const filePath = deschemeURI(jsDocument.uri); + function convertSelectionRange(selectionRange: ts.SelectionRange): SelectionRange { const parent = selectionRange.parent ? convertSelectionRange(selectionRange.parent) : undefined; return SelectionRange.create(convertRange(jsDocument, selectionRange.textSpan), parent); } - const range = jsLanguageService.getSmartSelectionRange(jsDocument.uri, jsDocument.offsetAt(position)); + const range = jsLanguageService.getSmartSelectionRange(filePath, jsDocument.offsetAt(position)); return convertSelectionRange(range); }, async format(document: TextDocument, range: Range, formatParams: FormattingOptions, settings: Settings = globalSettings): Promise { const jsDocument = documentRegions.get(document).getEmbeddedDocument('javascript', true); - const jsLanguageService = await host.getLanguageService(jsDocument); + const jsLanguageService = await host.getLanguageService(jsDocument, workspace); + const filePath = deschemeURI(jsDocument.uri); let formatterSettings = settings && settings.javascript && settings.javascript.format; @@ -314,7 +364,7 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - let spans = jsLanguageService.getOutliningSpans(jsDocument.uri); + const jsLanguageService = await host.getLanguageService(jsDocument, workspace); + const filePath = deschemeURI(jsDocument.uri); + + let spans = jsLanguageService.getOutliningSpans(filePath); let ranges: FoldingRange[] = []; for (let span of spans) { let curr = convertRange(jsDocument, span.textSpan); @@ -360,8 +412,10 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - return getSemanticTokens(jsLanguageService, jsDocument, jsDocument.uri); + const jsLanguageService = await host.getLanguageService(jsDocument, workspace); + const filePath = deschemeURI(jsDocument.uri); + + return getSemanticTokens(jsLanguageService, jsDocument, filePath); }, getSemanticTokenLegend(): { types: string[], modifiers: string[] } { return getSemanticTokenLegend(); diff --git a/extensions/html-language-features/server/src/test/completions.test.ts b/extensions/html-language-features/server/src/test/completions.test.ts index 0371a2f74cd0..f0d18af2df70 100644 --- a/extensions/html-language-features/server/src/test/completions.test.ts +++ b/extensions/html-language-features/server/src/test/completions.test.ts @@ -94,11 +94,27 @@ suite('HTML Completion', () => { }); }); +const triggerSuggestCommand = { + title: 'Suggest', + command: 'editor.action.triggerSuggest' +}; + +suite('JSDoc Imports', () => { + const fixtureRoot = path.resolve(__dirname, '../../src/test/jsdocImportFixtures'); + const fixtureWorkspace = { name: 'fixture', uri: URI.file(fixtureRoot).toString() }; + const indexHtmlUri = URI.file(path.resolve(fixtureRoot, 'index.html')).toString(); + + test('Imports across files', async () => { + await testCompletionFor('', { + items: [ + { label: 'other', }, + { label: 'property', }, + ] + }, indexHtmlUri, [fixtureWorkspace] ); + }); +}); + suite('HTML Path Completion', () => { - const triggerSuggestCommand = { - title: 'Suggest', - command: 'editor.action.triggerSuggest' - }; const fixtureRoot = path.resolve(__dirname, '../../src/test/pathCompletionFixtures'); const fixtureWorkspace = { name: 'fixture', uri: URI.file(fixtureRoot).toString() }; diff --git a/extensions/html-language-features/server/src/test/jsdocImportFixtures/index.html b/extensions/html-language-features/server/src/test/jsdocImportFixtures/index.html new file mode 100644 index 000000000000..c5bd93d63657 --- /dev/null +++ b/extensions/html-language-features/server/src/test/jsdocImportFixtures/index.html @@ -0,0 +1 @@ + ${this._getStyles(resourceProvider, sourceUri, config, imageInfo)} - ${body.html} ${this._getScripts(resourceProvider, nonce)} `; From 330ab6c2928963dd83a7d90a271d9f2eb8db90d2 Mon Sep 17 00:00:00 2001 From: Tony <68118705+Legend-Master@users.noreply.github.com> Date: Wed, 18 Dec 2024 01:50:35 +0800 Subject: [PATCH 0174/3587] Reland fix custom task shell doesn't work without manually passing in "run command" arg/flag (#236058) --- src/vs/platform/terminal/common/terminal.ts | 1 + .../tasks/browser/terminalTaskSystem.ts | 33 +++++++++++-------- .../browser/terminalProfileResolverService.ts | 1 + 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index c638fb625a20..8235e8a0ab40 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -871,6 +871,7 @@ export interface ITerminalProfile { overrideName?: boolean; color?: string; icon?: ThemeIcon | URI | { light: URI; dark: URI }; + isAutomationShell?: boolean; } export interface ITerminalDimensionsOverride extends Readonly { diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index e44eaa47cf96..3b2c1e51c7f0 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -1112,7 +1112,6 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { color: task.configurationProperties.icon?.color || undefined, waitOnExit }; - let shellSpecified: boolean = false; const shellOptions: IShellConfiguration | undefined = task.command.options && task.command.options.shell; if (shellOptions) { if (shellOptions.executable) { @@ -1121,12 +1120,12 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { shellLaunchConfig.args = undefined; } shellLaunchConfig.executable = await this._resolveVariable(variableResolver, shellOptions.executable); - shellSpecified = true; } if (shellOptions.args) { shellLaunchConfig.args = await this._resolveVariables(variableResolver, shellOptions.args.slice()); } } + const taskShellArgsSpecified = (defaultProfile.isAutomationShell ? shellLaunchConfig.args : shellOptions?.args) !== undefined; if (shellLaunchConfig.args === undefined) { shellLaunchConfig.args = []; } @@ -1139,29 +1138,34 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { windowsShellArgs = true; // If we don't have a cwd, then the terminal uses the home dir. const userHome = await this._pathService.userHome(); - if (basename === 'cmd.exe' && ((options.cwd && isUNC(options.cwd)) || (!options.cwd && isUNC(userHome.fsPath)))) { - return undefined; - } - if ((basename === 'powershell.exe') || (basename === 'pwsh.exe')) { - if (!shellSpecified) { + if (basename === 'cmd.exe') { + if ((options.cwd && isUNC(options.cwd)) || (!options.cwd && isUNC(userHome.fsPath))) { + return undefined; + } + if (!taskShellArgsSpecified) { + toAdd.push('/d', '/c'); + } + } else if ((basename === 'powershell.exe') || (basename === 'pwsh.exe')) { + if (!taskShellArgsSpecified) { toAdd.push('-Command'); } } else if ((basename === 'bash.exe') || (basename === 'zsh.exe')) { windowsShellArgs = false; - if (!shellSpecified) { + if (!taskShellArgsSpecified) { toAdd.push('-c'); } } else if (basename === 'wsl.exe') { - if (!shellSpecified) { + if (!taskShellArgsSpecified) { toAdd.push('-e'); } } else { - if (!shellSpecified) { - toAdd.push('/d', '/c'); + if (!taskShellArgsSpecified) { + // Push `-c` for unknown shells if the user didn't specify the args + toAdd.push('-c'); } } } else { - if (!shellSpecified) { + if (!taskShellArgsSpecified) { // Under Mac remove -l to not start it as a login shell. if (platform === Platform.Platform.Mac) { // Background on -l on osx https://github.com/microsoft/vscode/issues/107563 @@ -1268,11 +1272,12 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { const combinedShellArgs: string[] = Objects.deepClone(configuredShellArgs); shellCommandArgs.forEach(element => { const shouldAddShellCommandArg = configuredShellArgs.every((arg, index) => { - if ((arg.toLowerCase() === element) && (configuredShellArgs.length > index + 1)) { + const isDuplicated = arg.toLowerCase() === element.toLowerCase(); + if (isDuplicated && (configuredShellArgs.length > index + 1)) { // We can still add the argument, but only if not all of the following arguments begin with "-". return !configuredShellArgs.slice(index + 1).every(testArg => testArg.startsWith('-')); } else { - return arg.toLowerCase() !== element; + return !isDuplicated; } }); if (shouldAddShellCommandArg) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts index 6208cfc0413c..e54a12a1b44d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts @@ -268,6 +268,7 @@ export abstract class BaseTerminalProfileResolverService extends Disposable impl const automationProfile = this._configurationService.getValue(`terminal.integrated.automationProfile.${this._getOsKey(options.os)}`); if (this._isValidAutomationProfile(automationProfile, options.os)) { automationProfile.icon = this._getCustomIcon(automationProfile.icon) || Codicon.tools; + automationProfile.isAutomationShell = true; return automationProfile; } From ca721227517f2ad0f737f0fde18df588beb96c67 Mon Sep 17 00:00:00 2001 From: Parasaran Date: Tue, 17 Dec 2024 23:32:42 +0530 Subject: [PATCH 0175/3587] fix 235221: Encode and decode markdown content to escape illegal chars --- extensions/markdown-language-features/preview-src/index.ts | 5 ++++- .../src/preview/documentRenderer.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/extensions/markdown-language-features/preview-src/index.ts b/extensions/markdown-language-features/preview-src/index.ts index d7fa1f18d769..336245a1fdfd 100644 --- a/extensions/markdown-language-features/preview-src/index.ts +++ b/extensions/markdown-language-features/preview-src/index.ts @@ -355,7 +355,10 @@ document.addEventListener('click', event => { window.addEventListener('load', () => { const htmlParser = new DOMParser(); - const markDownHtml = htmlParser.parseFromString(getData('data-md-content'), 'text/html'); + const markDownHtml = htmlParser.parseFromString( + decodeURIComponent(getData('data-md-content')), + 'text/html' + ); document.body.appendChild(markDownHtml.body); }); diff --git a/extensions/markdown-language-features/src/preview/documentRenderer.ts b/extensions/markdown-language-features/src/preview/documentRenderer.ts index f2447532e002..13e709c765f0 100644 --- a/extensions/markdown-language-features/src/preview/documentRenderer.ts +++ b/extensions/markdown-language-features/src/preview/documentRenderer.ts @@ -99,7 +99,7 @@ export class MdDocumentRenderer { data-settings="${escapeAttribute(JSON.stringify(initialData))}" data-strings="${escapeAttribute(JSON.stringify(previewStrings))}" data-state="${escapeAttribute(JSON.stringify(state || {}))}" - data-md-content="${escapeAttribute(JSON.stringify(body.html))}"> + data-md-content="${escapeAttribute(JSON.stringify(encodeURIComponent(body.html)))}"> ${this._getStyles(resourceProvider, sourceUri, config, imageInfo)} From dfa26a2c5698f8bb8b0bb504d8cbc0207762dc4b Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 17 Dec 2024 19:39:48 +0100 Subject: [PATCH 0176/3587] Adjusts zIndex for notebooks. (#236380) --- .../browser/view/inlineEdits/sideBySideDiff.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index d58c42f83f96..a64c52dcd90c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -616,7 +616,7 @@ export class InlineEditsSideBySideDiff extends Disposable { class: 'inline-edits-view', style: { overflow: 'visible', - zIndex: '10', + zIndex: '20', display: this._display, }, }, [ From 625bae23758002b62954fd10b53d25409421d718 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 17 Dec 2024 19:44:54 +0100 Subject: [PATCH 0177/3587] debt: clean up obsolete file usage (#236379) - remove it from scanner, with profiles reading this file is not needed - rename it usage for removal in management service --- .../common/extensionManagement.ts | 4 +- .../common/extensionsScannerService.ts | 51 ++---- .../node/extensionManagementService.ts | 163 +++++++++--------- .../node/extensionsWatcher.ts | 20 +-- .../node/extensionsScannerService.test.ts | 33 +--- 5 files changed, 115 insertions(+), 156 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 155079831fcc..f08b46ae65b3 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -456,8 +456,8 @@ export const enum ExtensionManagementErrorCode { Extract = 'Extract', Scanning = 'Scanning', ScanningExtension = 'ScanningExtension', - ReadUninstalled = 'ReadUninstalled', - UnsetUninstalled = 'UnsetUninstalled', + ReadRemoved = 'ReadRemoved', + UnsetRemoved = 'UnsetRemoved', Delete = 'Delete', Rename = 'Rename', IntializeDefaultProfile = 'IntializeDefaultProfile', diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 99868cddb5d6..a186b6fa0456 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -7,7 +7,6 @@ import { coalesce } from '../../../base/common/arrays.js'; import { ThrottledDelayer } from '../../../base/common/async.js'; import * as objects from '../../../base/common/objects.js'; import { VSBuffer } from '../../../base/common/buffer.js'; -import { IStringDictionary } from '../../../base/common/collections.js'; import { getErrorMessage } from '../../../base/common/errors.js'; import { getNodeType, parse, ParseError } from '../../../base/common/json.js'; import { getParseErrorMessage } from '../../../base/common/jsonErrorMessages.js'; @@ -18,12 +17,11 @@ import * as platform from '../../../base/common/platform.js'; import { basename, isEqual, joinPath } from '../../../base/common/resources.js'; import * as semver from '../../../base/common/semver/semver.js'; import Severity from '../../../base/common/severity.js'; -import { isEmptyObject } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; import { IProductVersion, Metadata } from './extensionManagement.js'; -import { areSameExtensions, computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from './extensionManagementUtil.js'; +import { areSameExtensions, computeTargetPlatform, getExtensionId, getGalleryExtensionId } from './extensionManagementUtil.js'; import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, ExtensionIdentifierMap, parseEnabledApiProposalNames } from '../../extensions/common/extensions.js'; import { validateExtensionManifest } from '../../extensions/common/extensionValidator.js'; import { FileOperationResult, IFileService, toFileOperationResult } from '../../files/common/files.js'; @@ -106,7 +104,6 @@ export type ScanOptions = { readonly profileLocation?: URI; readonly includeInvalid?: boolean; readonly includeAllVersions?: boolean; - readonly includeUninstalled?: boolean; readonly checkControlFile?: boolean; readonly language?: string; readonly useCache?: boolean; @@ -145,10 +142,9 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem private readonly _onDidChangeCache = this._register(new Emitter()); readonly onDidChangeCache = this._onDidChangeCache.event; - private readonly obsoleteFile = joinPath(this.userExtensionsLocation, '.obsolete'); - private readonly systemExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile, this.obsoleteFile)); - private readonly userExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile, this.obsoleteFile)); - private readonly extensionsScanner = this._register(this.instantiationService.createInstance(ExtensionsScanner, this.obsoleteFile)); + private readonly systemExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile)); + private readonly userExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile)); + private readonly extensionsScanner = this._register(this.instantiationService.createInstance(ExtensionsScanner)); constructor( readonly systemExtensionsLocation: URI, @@ -199,8 +195,8 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem const location = scanOptions.profileLocation ?? this.userExtensionsLocation; this.logService.trace('Started scanning user extensions', location); const profileScanOptions: IProfileExtensionsScanOptions | undefined = this.uriIdentityService.extUri.isEqual(scanOptions.profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource) ? { bailOutWhenFileNotFound: true } : undefined; - const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion()); - const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode && extensionsScannerInput.excludeObsolete ? this.userExtensionsCachedScanner : this.extensionsScanner; + const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode ? this.userExtensionsCachedScanner : this.extensionsScanner; let extensions: IRelaxedScannedExtension[]; try { extensions = await extensionsScanner.scanExtensions(extensionsScannerInput); @@ -221,7 +217,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionDevelopmentLocationURI) { const extensions = (await Promise.all(this.environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file) .map(async extensionDevelopmentLocationURI => { - const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, true, scanOptions.language, false /* do not validate */, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, scanOptions.language, false /* do not validate */, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(input); return extensions.map(extension => { // Override the extension type from the existing extensions @@ -237,7 +233,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extension = await this.extensionsScanner.scanExtension(extensionsScannerInput); if (!extension) { return null; @@ -249,7 +245,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(extensionsScannerInput); return this.applyScanOptions(extensions, extensionType, scanOptions, true); } @@ -405,7 +401,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem private async scanDefaultSystemExtensions(useCache: boolean, language: string | undefined): Promise { this.logService.trace('Started scanning system extensions'); - const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, true, language, true, undefined, this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, language, true, undefined, this.getProductVersion()); const extensionsScanner = useCache && !extensionsScannerInput.devMode ? this.systemExtensionsCachedScanner : this.extensionsScanner; const result = await extensionsScanner.scanExtensions(extensionsScannerInput); this.logService.trace('Scanned system extensions:', result.length); @@ -435,7 +431,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem break; } } - const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, true, language, true, undefined, this.getProductVersion()))))); + const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, language, true, undefined, this.getProductVersion()))))); this.logService.trace('Scanned dev system extensions:', result.length); return coalesce(result); } @@ -449,7 +445,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } } - private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, excludeObsolete: boolean, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined, productVersion: IProductVersion): Promise { + private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined, productVersion: IProductVersion): Promise { const translations = await this.getTranslations(language ?? platform.language); const mtime = await this.getMtime(location); const applicationExtensionsLocation = profile && !this.uriIdentityService.extUri.isEqual(location, this.userDataProfilesService.defaultProfile.extensionsResource) ? this.userDataProfilesService.defaultProfile.extensionsResource : undefined; @@ -462,7 +458,6 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem profile, profileScanOptions, type, - excludeObsolete, validate, productVersion.version, productVersion.date, @@ -504,7 +499,6 @@ export class ExtensionScannerInput { public readonly profile: boolean, public readonly profileScanOptions: IProfileExtensionsScanOptions | undefined, public readonly type: ExtensionType, - public readonly excludeObsolete: boolean, public readonly validate: boolean, public readonly productVersion: string, public readonly productDate: string | undefined, @@ -534,7 +528,6 @@ export class ExtensionScannerInput { && a.profile === b.profile && objects.equals(a.profileScanOptions, b.profileScanOptions) && a.type === b.type - && a.excludeObsolete === b.excludeObsolete && a.validate === b.validate && a.productVersion === b.productVersion && a.productDate === b.productDate @@ -558,7 +551,6 @@ class ExtensionsScanner extends Disposable { private readonly extensionsEnabledWithApiProposalVersion: string[]; constructor( - private readonly obsoleteFile: URI, @IExtensionsProfileScannerService protected readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, @IFileService protected readonly fileService: IFileService, @@ -571,15 +563,9 @@ class ExtensionsScanner extends Disposable { } async scanExtensions(input: ExtensionScannerInput): Promise { - const extensions = input.profile ? await this.scanExtensionsFromProfile(input) : await this.scanExtensionsFromLocation(input); - let obsolete: IStringDictionary = {}; - if (input.excludeObsolete && input.type === ExtensionType.User) { - try { - const raw = (await this.fileService.readFile(this.obsoleteFile)).value.toString(); - obsolete = JSON.parse(raw); - } catch (error) { /* ignore */ } - } - return isEmptyObject(obsolete) ? extensions : extensions.filter(e => !obsolete[ExtensionKey.create(e).toString()]); + return input.profile + ? this.scanExtensionsFromProfile(input) + : this.scanExtensionsFromLocation(input); } private async scanExtensionsFromLocation(input: ExtensionScannerInput): Promise { @@ -596,7 +582,7 @@ class ExtensionsScanner extends Disposable { if (input.type === ExtensionType.User && basename(c.resource).indexOf('.') === 0) { return null; } - const extensionScannerInput = new ExtensionScannerInput(c.resource, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.excludeObsolete, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); + const extensionScannerInput = new ExtensionScannerInput(c.resource, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); return this.scanExtension(extensionScannerInput); })); return coalesce(extensions) @@ -622,7 +608,7 @@ class ExtensionsScanner extends Disposable { const extensions = await Promise.all( scannedProfileExtensions.map(async extensionInfo => { if (filter(extensionInfo)) { - const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.excludeObsolete, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); + const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); return this.scanExtension(extensionScannerInput, extensionInfo.metadata); } return null; @@ -891,7 +877,6 @@ class CachedExtensionsScanner extends ExtensionsScanner { constructor( private readonly currentProfile: IUserDataProfile, - obsoleteFile: URI, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IExtensionsProfileScannerService extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService uriIdentityService: IUriIdentityService, @@ -900,7 +885,7 @@ class CachedExtensionsScanner extends ExtensionsScanner { @IEnvironmentService environmentService: IEnvironmentService, @ILogService logService: ILogService ) { - super(obsoleteFile, extensionsProfileScannerService, uriIdentityService, fileService, productService, environmentService, logService); + super(extensionsProfileScannerService, uriIdentityService, fileService, productService, environmentService, logService); } override async scanExtensions(input: ExtensionScannerInput): Promise { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 92405eefb753..015c3dff393a 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -60,7 +60,7 @@ export interface INativeServerExtensionManagementService extends IExtensionManag readonly _serviceBrand: undefined; scanAllUserInstalledExtensions(): Promise; scanInstalledExtensionAtLocation(location: URI): Promise; - markAsUninstalled(...extensions: IExtension[]): Promise; + deleteExtensions(...extensions: IExtension[]): Promise; } type ExtractExtensionResult = { readonly local: ILocalExtension; readonly verificationStatus?: ExtensionSignatureVerificationCode }; @@ -222,8 +222,8 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return this.extensionsScanner.copyExtensions(fromProfileLocation, toProfileLocation, { version: this.productService.version, date: this.productService.date }); } - markAsUninstalled(...extensions: IExtension[]): Promise { - return this.extensionsScanner.setUninstalled(...extensions); + deleteExtensions(...extensions: IExtension[]): Promise { + return this.extensionsScanner.setExtensionsForRemoval(...extensions); } async cleanUp(): Promise { @@ -480,8 +480,20 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi continue; } - // Check if this is a directory - if (!(await this.fileService.stat(resource)).isDirectory) { + // Ignore changes to the deleted folder + if (this.uriIdentityService.extUri.basename(resource).endsWith(DELETED_FOLDER_POSTFIX)) { + continue; + } + + try { + // Check if this is a directory + if (!(await this.fileService.stat(resource)).isDirectory) { + continue; + } + } catch (error) { + if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { + this.logService.error(error); + } continue; } @@ -502,23 +514,10 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi private async addExtensionsToProfile(extensions: [ILocalExtension, Metadata | undefined][], profileLocation: URI): Promise { const localExtensions = extensions.map(e => e[0]); - await this.setInstalled(localExtensions); + await this.extensionsScanner.unsetExtensionsForRemoval(...localExtensions.map(extension => ExtensionKey.create(extension))); await this.extensionsProfileScannerService.addExtensionsToProfile(extensions, profileLocation); this._onDidInstallExtensions.fire(localExtensions.map(local => ({ local, identifier: local.identifier, operation: InstallOperation.None, profileLocation }))); } - - private async setInstalled(extensions: ILocalExtension[]): Promise { - const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); - for (const extension of extensions) { - const extensionKey = ExtensionKey.create(extension); - if (!uninstalled[extensionKey.toString()]) { - continue; - } - this.logService.trace('Removing the extension from uninstalled list:', extensionKey.id); - await this.extensionsScanner.setInstalled(extensionKey); - this.logService.info('Removed the extension from uninstalled list:', extensionKey.id); - } - } } type UpdateMetadataErrorClassification = { @@ -536,8 +535,8 @@ type UpdateMetadataErrorEvent = { export class ExtensionsScanner extends Disposable { - private readonly uninstalledResource: URI; - private readonly uninstalledFileLimiter: Queue; + private readonly obsoletedResource: URI; + private readonly obsoleteFileLimiter: Queue; private readonly _onExtract = this._register(new Emitter()); readonly onExtract = this._onExtract.event; @@ -555,13 +554,13 @@ export class ExtensionsScanner extends Disposable { @ILogService private readonly logService: ILogService, ) { super(); - this.uninstalledResource = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete'); - this.uninstalledFileLimiter = new Queue(); + this.obsoletedResource = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete'); + this.obsoleteFileLimiter = new Queue(); } async cleanUp(): Promise { await this.removeTemporarilyDeletedFolders(); - await this.removeUninstalledExtensions(); + await this.deleteExtensionsMarkedForRemoval(); await this.initializeMetadata(); } @@ -720,40 +719,38 @@ export class ExtensionsScanner extends Disposable { return this.scanLocalExtension(local.location, local.type, profileLocation); } - async getUninstalledExtensions(): Promise> { - try { - return await this.withUninstalledExtensions(); - } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.ReadUninstalled); - } - } - - async setUninstalled(...extensions: IExtension[]): Promise { + async setExtensionsForRemoval(...extensions: IExtension[]): Promise { const extensionKeys: ExtensionKey[] = extensions.map(e => ExtensionKey.create(e)); - await this.withUninstalledExtensions(uninstalled => + await this.withRemovedExtensions(removedExtensions => extensionKeys.forEach(extensionKey => { - uninstalled[extensionKey.toString()] = true; - this.logService.info('Marked extension as uninstalled', extensionKey.toString()); + removedExtensions[extensionKey.toString()] = true; + this.logService.info('Marked extension as removed', extensionKey.toString()); })); } - async setInstalled(extensionKey: ExtensionKey): Promise { + async unsetExtensionsForRemoval(...extensionKeys: ExtensionKey[]): Promise { try { - await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); + const results: boolean[] = []; + await this.withRemovedExtensions(removedExtensions => + extensionKeys.forEach(extensionKey => { + if (removedExtensions[extensionKey.toString()]) { + results.push(true); + delete removedExtensions[extensionKey.toString()]; + } else { + results.push(false); + } + })); + return results; } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.UnsetUninstalled); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UnsetRemoved); } } - async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { + async deleteExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { if (this.uriIdentityService.extUri.isEqualOrParent(extension.location, this.extensionsScannerService.userExtensionsLocation)) { return this.deleteExtensionFromLocation(extension.identifier.id, extension.location, type); } - } - - async removeUninstalledExtension(extension: ILocalExtension | IScannedExtension): Promise { - await this.removeExtension(extension, 'uninstalled'); - await this.withUninstalledExtensions(uninstalled => delete uninstalled[ExtensionKey.create(extension).toString()]); + await this.unsetExtensionsForRemoval(ExtensionKey.create(extension)); } async copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata: Partial): Promise { @@ -792,11 +789,11 @@ export class ExtensionsScanner extends Disposable { this.logService.info(`Deleted ${type} extension from disk`, id, location.fsPath); } - private withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { - return this.uninstalledFileLimiter.queue(async () => { + private withRemovedExtensions(updateFn?: (removed: IStringDictionary) => void): Promise> { + return this.obsoleteFileLimiter.queue(async () => { let raw: string | undefined; try { - const content = await this.fileService.readFile(this.uninstalledResource, 'utf8'); + const content = await this.fileService.readFile(this.obsoletedResource, 'utf8'); raw = content.value.toString(); } catch (error) { if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { @@ -804,23 +801,23 @@ export class ExtensionsScanner extends Disposable { } } - let uninstalled = {}; + let removed = {}; if (raw) { try { - uninstalled = JSON.parse(raw); + removed = JSON.parse(raw); } catch (e) { /* ignore */ } } if (updateFn) { - updateFn(uninstalled); - if (Object.keys(uninstalled).length) { - await this.fileService.writeFile(this.uninstalledResource, VSBuffer.fromString(JSON.stringify(uninstalled))); + updateFn(removed); + if (Object.keys(removed).length) { + await this.fileService.writeFile(this.obsoletedResource, VSBuffer.fromString(JSON.stringify(removed))); } else { - await this.fileService.del(this.uninstalledResource); + await this.fileService.del(this.obsoletedResource); } } - return uninstalled; + return removed; }); } @@ -898,19 +895,25 @@ export class ExtensionsScanner extends Disposable { })); } - private async removeUninstalledExtensions(): Promise { - const uninstalled = await this.getUninstalledExtensions(); - if (Object.keys(uninstalled).length === 0) { - this.logService.debug(`No uninstalled extensions found.`); + private async deleteExtensionsMarkedForRemoval(): Promise { + let removed: IStringDictionary; + try { + removed = await this.withRemovedExtensions(); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.ReadRemoved); + } + + if (Object.keys(removed).length === 0) { + this.logService.debug(`No extensions are marked as removed.`); return; } - this.logService.debug(`Removing uninstalled extensions:`, Object.keys(uninstalled)); + this.logService.debug(`Deleting extensions marked as removed:`, Object.keys(removed)); - const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions + const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeInvalid: true }); // All user extensions const installed: Set = new Set(); for (const e of extensions) { - if (!uninstalled[ExtensionKey.create(e).toString()]) { + if (!removed[ExtensionKey.create(e).toString()]) { installed.add(e.identifier.id.toLowerCase()); } } @@ -928,8 +931,8 @@ export class ExtensionsScanner extends Disposable { this.logService.error(error); } - const toRemove = extensions.filter(e => e.metadata /* Installed by System */ && uninstalled[ExtensionKey.create(e).toString()]); - await Promise.allSettled(toRemove.map(e => this.removeUninstalledExtension(e))); + const toRemove = extensions.filter(e => e.metadata /* Installed by System */ && removed[ExtensionKey.create(e).toString()]); + await Promise.allSettled(toRemove.map(e => this.deleteExtension(e, 'marked for removal'))); } private async removeTemporarilyDeletedFolders(): Promise { @@ -1021,7 +1024,7 @@ class InstallExtensionInProfileTask extends AbstractExtensionTask { - const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); - if (!uninstalled[extensionKey.toString()]) { - return undefined; + private async unsetIfRemoved(extensionKey: ExtensionKey): Promise { + // If the same version of extension is marked as removed, remove it from there and return the local. + const [removed] = await this.extensionsScanner.unsetExtensionsForRemoval(extensionKey); + if (removed) { + this.logService.info('Removed the extension from removed list:', extensionKey.id); + const userExtensions = await this.extensionsScanner.scanAllUserExtensions(true); + return userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)); } - - this.logService.trace('Removing the extension from uninstalled list:', extensionKey.id); - // If the same version of extension is marked as uninstalled, remove it from there and return the local. - await this.extensionsScanner.setInstalled(extensionKey); - this.logService.info('Removed the extension from uninstalled list:', extensionKey.id); - - const userExtensions = await this.extensionsScanner.scanAllUserExtensions(true); - return userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)); + return undefined; } private async updateMetadata(extension: ILocalExtension, token: CancellationToken): Promise { @@ -1149,8 +1148,8 @@ class UninstallExtensionInProfileTask extends AbstractExtensionTask implem super(); } - protected async doRun(token: CancellationToken): Promise { - await this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension, this.options.profileLocation); + protected doRun(token: CancellationToken): Promise { + return this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension, this.options.profileLocation); } } diff --git a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts index d2b65eaea553..2c4e976a5a64 100644 --- a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts +++ b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts @@ -48,7 +48,7 @@ export class ExtensionsWatcher extends Disposable { await this.extensionsScannerService.initializeDefaultProfileExtensions(); await this.onDidChangeProfiles(this.userDataProfilesService.profiles); this.registerListeners(); - await this.uninstallExtensionsNotInProfiles(); + await this.deleteExtensionsNotInProfiles(); } private registerListeners(): void { @@ -102,7 +102,7 @@ export class ExtensionsWatcher extends Disposable { } private async onDidRemoveExtensions(e: DidRemoveProfileExtensionsEvent): Promise { - const extensionsToUninstall: IExtension[] = []; + const extensionsToDelete: IExtension[] = []; const promises: Promise[] = []; for (const extension of e.extensions) { const key = this.getKey(extension.identifier, extension.version); @@ -115,7 +115,7 @@ export class ExtensionsWatcher extends Disposable { promises.push(this.extensionManagementService.scanInstalledExtensionAtLocation(extension.location) .then(result => { if (result) { - extensionsToUninstall.push(result); + extensionsToDelete.push(result); } else { this.logService.info('Extension not found at the location', extension.location.toString()); } @@ -125,8 +125,8 @@ export class ExtensionsWatcher extends Disposable { } try { await Promise.all(promises); - if (extensionsToUninstall.length) { - await this.uninstallExtensionsNotInProfiles(extensionsToUninstall); + if (extensionsToDelete.length) { + await this.deleteExtensionsNotInProfiles(extensionsToDelete); } } catch (error) { this.logService.error(error); @@ -180,13 +180,13 @@ export class ExtensionsWatcher extends Disposable { } } - private async uninstallExtensionsNotInProfiles(toUninstall?: IExtension[]): Promise { - if (!toUninstall) { + private async deleteExtensionsNotInProfiles(toDelete?: IExtension[]): Promise { + if (!toDelete) { const installed = await this.extensionManagementService.scanAllUserInstalledExtensions(); - toUninstall = installed.filter(installedExtension => !this.allExtensions.has(this.getKey(installedExtension.identifier, installedExtension.manifest.version))); + toDelete = installed.filter(installedExtension => !this.allExtensions.has(this.getKey(installedExtension.identifier, installedExtension.manifest.version))); } - if (toUninstall.length) { - await this.extensionManagementService.markAsUninstalled(...toUninstall); + if (toDelete.length) { + await this.extensionManagementService.deleteExtensions(...toDelete); } } diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts index 74d3ffcd738f..551ba576d445 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -224,31 +224,6 @@ suite('NativeExtensionsScanerService Test', () => { assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); }); - test('scan exclude uninstalled extensions', async () => { - await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); - await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); - await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true }))); - const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - - const actual = await testObject.scanUserExtensions({}); - - assert.deepStrictEqual(actual.length, 1); - assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); - }); - - test('scan include uninstalled extensions', async () => { - await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); - await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); - await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true }))); - const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - - const actual = await testObject.scanUserExtensions({ includeUninstalled: true }); - - assert.deepStrictEqual(actual.length, 2); - assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); - assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' }); - }); - test('scan include invalid extensions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } })); @@ -351,7 +326,7 @@ suite('ExtensionScannerInput', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('compare inputs - location', () => { - const anInput = (location: URI, mtime: number | undefined) => new ExtensionScannerInput(location, mtime, undefined, undefined, false, undefined, ExtensionType.User, true, true, '1.1.1', undefined, undefined, true, undefined, {}); + const anInput = (location: URI, mtime: number | undefined) => new ExtensionScannerInput(location, mtime, undefined, undefined, false, undefined, ExtensionType.User, true, '1.1.1', undefined, undefined, true, undefined, {}); assert.strictEqual(ExtensionScannerInput.equals(anInput(ROOT, undefined), anInput(ROOT, undefined)), true); assert.strictEqual(ExtensionScannerInput.equals(anInput(ROOT, 100), anInput(ROOT, 100)), true); @@ -361,7 +336,7 @@ suite('ExtensionScannerInput', () => { }); test('compare inputs - application location', () => { - const anInput = (location: URI, mtime: number | undefined) => new ExtensionScannerInput(ROOT, undefined, location, mtime, false, undefined, ExtensionType.User, true, true, '1.1.1', undefined, undefined, true, undefined, {}); + const anInput = (location: URI, mtime: number | undefined) => new ExtensionScannerInput(ROOT, undefined, location, mtime, false, undefined, ExtensionType.User, true, '1.1.1', undefined, undefined, true, undefined, {}); assert.strictEqual(ExtensionScannerInput.equals(anInput(ROOT, undefined), anInput(ROOT, undefined)), true); assert.strictEqual(ExtensionScannerInput.equals(anInput(ROOT, 100), anInput(ROOT, 100)), true); @@ -371,7 +346,7 @@ suite('ExtensionScannerInput', () => { }); test('compare inputs - profile', () => { - const anInput = (profile: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined) => new ExtensionScannerInput(ROOT, undefined, undefined, undefined, profile, profileScanOptions, ExtensionType.User, true, true, '1.1.1', undefined, undefined, true, undefined, {}); + const anInput = (profile: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined) => new ExtensionScannerInput(ROOT, undefined, undefined, undefined, profile, profileScanOptions, ExtensionType.User, true, '1.1.1', undefined, undefined, true, undefined, {}); assert.strictEqual(ExtensionScannerInput.equals(anInput(true, { bailOutWhenFileNotFound: true }), anInput(true, { bailOutWhenFileNotFound: true })), true); assert.strictEqual(ExtensionScannerInput.equals(anInput(false, { bailOutWhenFileNotFound: true }), anInput(false, { bailOutWhenFileNotFound: true })), true); @@ -384,7 +359,7 @@ suite('ExtensionScannerInput', () => { }); test('compare inputs - extension type', () => { - const anInput = (type: ExtensionType) => new ExtensionScannerInput(ROOT, undefined, undefined, undefined, false, undefined, type, true, true, '1.1.1', undefined, undefined, true, undefined, {}); + const anInput = (type: ExtensionType) => new ExtensionScannerInput(ROOT, undefined, undefined, undefined, false, undefined, type, true, '1.1.1', undefined, undefined, true, undefined, {}); assert.strictEqual(ExtensionScannerInput.equals(anInput(ExtensionType.System), anInput(ExtensionType.System)), true); assert.strictEqual(ExtensionScannerInput.equals(anInput(ExtensionType.User), anInput(ExtensionType.User)), true); From d3b582676fa88ec3c4bdfd350aafebf851fcda28 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 17 Dec 2024 12:57:14 -0600 Subject: [PATCH 0178/3587] add rerun action to terminal chat to align w editor (#236381) --- .../chat/browser/terminalChat.ts | 1 + .../chat/browser/terminalChatActions.ts | 45 +++++++++++++++++++ .../chat/browser/terminalChatWidget.ts | 2 + 3 files changed, 48 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts index b182074753e8..5f16923d086b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -17,6 +17,7 @@ export const enum TerminalChatCommandId { InsertCommand = 'workbench.action.terminal.chat.insertCommand', InsertFirstCommand = 'workbench.action.terminal.chat.insertFirstCommand', ViewInChat = 'workbench.action.terminal.chat.viewInChat', + RerunRequest = 'workbench.action.terminal.chat.rerunRequest', } export const MENU_TERMINAL_CHAT_WIDGET_INPUT_SIDE_TOOLBAR = MenuId.for('terminalChatWidget'); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 38bb507fe3b6..deeb48f48c79 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -8,6 +8,9 @@ import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { localize2 } from '../../../../../nls.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { IChatWidgetService } from '../../../chat/browser/chat.js'; +import { ChatAgentLocation } from '../../../chat/common/chatAgents.js'; +import { IChatService } from '../../../chat/common/chatService.js'; import { AbstractInlineChatAction } from '../../../inlineChat/browser/inlineChatActions.js'; import { isDetachedTerminalInstance } from '../../../terminal/browser/terminal.js'; import { registerActiveXtermAction } from '../../../terminal/browser/terminalActions.js'; @@ -209,6 +212,48 @@ registerActiveXtermAction({ } }); +registerActiveXtermAction({ + id: TerminalChatCommandId.RerunRequest, + title: localize2('chat.rerun.label', "Rerun Request"), + f1: false, + icon: Codicon.refresh, + category: AbstractInlineChatAction.category, + precondition: ContextKeyExpr.and( + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalChatContextKeys.requestActive.negate(), + ), + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyR + }, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_STATUS, + group: '0_main', + order: 5, + when: ContextKeyExpr.and(TerminalChatContextKeys.inputHasText.toNegated(), TerminalChatContextKeys.requestActive.negate()) + }, + run: async (_xterm, _accessor, activeInstance) => { + const chatService = _accessor.get(IChatService); + const chatWidgetService = _accessor.get(IChatWidgetService); + const contr = TerminalChatController.activeChatController; + const model = contr?.terminalChatWidget?.inlineChatWidget.chatWidget.viewModel?.model; + if (!model) { + return; + } + + const lastRequest = model.getRequests().at(-1); + if (lastRequest) { + const widget = chatWidgetService.getWidgetBySessionId(model.sessionId); + await chatService.resendRequest(lastRequest, { + noCommandDetection: false, + attempt: lastRequest.attempt + 1, + location: ChatAgentLocation.Terminal, + userSelectedModelId: widget?.input.currentLanguageModel + }); + } + } +}); + registerActiveXtermAction({ id: TerminalChatCommandId.ViewInChat, title: localize2('viewInChat', 'View in Chat'), diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index b668b55abc5d..d1a310aae291 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -127,6 +127,8 @@ export class TerminalChatWidget extends Disposable { menu: MENU_TERMINAL_CHAT_WIDGET_STATUS, options: { buttonConfigProvider: action => ({ + showLabel: action.id !== TerminalChatCommandId.RerunRequest, + showIcon: action.id === TerminalChatCommandId.RerunRequest, isSecondary: action.id !== TerminalChatCommandId.RunCommand && action.id !== TerminalChatCommandId.RunFirstCommand }) } From d08f30ded43d3fef500c50a22d2476e896e6d447 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Tue, 17 Dec 2024 10:57:39 -0800 Subject: [PATCH 0179/3587] Update area labels (#236116) --- .vscode/notebooks/grooming.github-issues | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/notebooks/grooming.github-issues b/.vscode/notebooks/grooming.github-issues index 4a4955dd7678..e408bbd74215 100644 --- a/.vscode/notebooks/grooming.github-issues +++ b/.vscode/notebooks/grooming.github-issues @@ -27,7 +27,7 @@ { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode assignee:$assignee is:open type:issue -label:\"info-needed\" -label:api -label:api-finalization -label:api-proposal -label:authentication -label:bisect-ext -label:bracket-pair-colorization -label:bracket-pair-guides -label:breadcrumbs -label:callhierarchy -label:chrome-devtools -label:cloud-changes -label:code-lens -label:command-center -label:comments -label:config -label:containers -label:context-keys -label:continue-working-on -label:css-less-scss -label:custom-editors -label:debug -label:debug-disassembly -label:dialogs -label:diff-editor -label:dropdown -label:editor-api -label:editor-autoclosing -label:editor-autoindent -label:editor-bracket-matching -label:editor-clipboard -label:editor-code-actions -label:editor-color-picker -label:editor-columnselect -label:editor-commands -label:editor-comments -label:editor-contrib -label:editor-core -label:editor-drag-and-drop -label:editor-error-widget -label:editor-find -label:editor-folding -label:editor-highlight -label:editor-hover -label:editor-indent-detection -label:editor-indent-guides -label:editor-input -label:editor-input-IME -label:editor-insets -label:editor-minimap -label:editor-multicursor -label:editor-parameter-hints -label:editor-render-whitespace -label:editor-rendering -label:editor-widgets -label:editor-RTL -label:editor-scrollbar -label:editor-sorting -label:editor-sticky-scroll -label:editor-symbols -label:editor-synced-region -label:editor-textbuffer -label:editor-theming -label:editor-wordnav -label:editor-wrapping -label:emmet -label:emmet-parse -label:error-list -label:extension-activation -label:extension-host -label:extension-prerelease -label:extension-recommendations -label:extensions -label:extensions-development -label:file-decorations -label:file-encoding -label:file-explorer -label:file-glob -label:file-io -label:file-nesting -label:file-watcher -label:font-rendering -label:formatting -label:getting-started -label:ghost-text -label:git -label:github -label:github-repositories -label:gpu -label:grammar -label:grid-widget -label:html -label:icon-brand -label:icons-product -label:image-preview -label:inlay-hints -label:inline-completions -label:install-update -label:intellisense-config -label:interactive-playground -label:interactive-window -label:issue-bot -label:issue-reporter -label:javascript -label:json -label:keybindings -label:keybindings-editor -label:keyboard-layout -label:chat -label:l10n-platform -label:label-provider -label:languages-basic -label:languages-diagnostics -label:languages-guessing -label:layout -label:lcd-text-rendering -label:list-widget -label:live-preview -label:log -label:markdown -label:marketplace -label:menus -label:merge-conflict -label:merge-editor -label:merge-editor-workbench -label:monaco-editor -label:native-file-dialog -label:network -label:notebook -label:notebook-accessibility -label:notebook-api -label:notebook-builtin-renderers -label:notebook-cell-editor -label:notebook-celltoolbar -label:notebook-clipboard -label:notebook-commands -label:notebook-commenting -label:notebook-debugging -label:notebook-diff -label:notebook-dnd -label:notebook-execution -label:notebook-find -label:notebook-folding -label:notebook-getting-started -label:notebook-globaltoolbar -label:notebook-ipynb -label:notebook-kernel -label:notebook-kernel-picker -label:notebook-language -label:notebook-layout -label:notebook-markdown -label:notebook-minimap -label:notebook-multiselect -label:notebook-output -label:notebook-perf -label:notebook-remote -label:notebook-rendering -label:notebook-serialization -label:notebook-statusbar -label:notebook-toc-outline -label:notebook-undo-redo -label:notebook-variables -label:notebook-workbench-integration -label:notebook-workflow -label:notebook-sticky-scroll -label:notebook-format -label:notebook-code-actions -label:open-editors -label:opener -label:outline -label:output -label:packaging -label:perf -label:perf-bloat -label:perf-startup -label:php -label:portable-mode -label:proxy -label:quick-open -label:quick-pick -label:references-viewlet -label:release-notes -label:remote -label:remote-connection -label:remote-explorer -label:remote-tunnel -label:rename -label:runCommands -label:sandbox -label:sash-widget -label:scm -label:screencast-mode -label:search -label:search-api -label:search-editor -label:search-replace -label:semantic-tokens -label:server -label:settings-editor -label:settings-sync -label:settings-sync-server -label:shared-process -label:simple-file-dialog -label:smart-select -label:snap -label:snippets -label:splitview-widget -label:ssh -label:suggest -label:table-widget -label:tasks -label:telemetry -label:terminal -label:terminal-accessibility -label:terminal-conpty -label:terminal-editors -label:terminal-external -label:terminal-find -label:terminal-input -label:terminal-layout -label:terminal-links -label:terminal-local-echo -label:terminal-persistence -label:terminal-process -label:terminal-profiles -label:terminal-quick-fix -label:terminal-rendering -label:terminal-shell-bash -label:terminal-shell-cmd -label:terminal-shell-fish -label:terminal-shell-git-bash -label:terminal-shell-integration -label:terminal-shell-pwsh -label:terminal-shell-zsh -label:terminal-sticky-scroll -label:terminal-tabs -label:testing -label:themes -label:timeline -label:timeline-git -label:timeline-local-history -label:titlebar -label:tokenization -label:touch/pointer -label:trackpad/scroll -label:tree-views -label:tree-widget -label:typescript -label:undo-redo -label:unicode-highlight -label:uri -label:user-profiles -label:ux -label:variable-resolving -label:VIM -label:virtual-workspaces -label:vscode-website -label:vscode.dev -label:web -label:webview -label:webview-views -label:workbench-actions -label:workbench-banner -label:workbench-cli -label:workbench-diagnostics -label:workbench-dnd -label:workbench-editor-grid -label:workbench-editor-groups -label:workbench-editor-resolver -label:workbench-editors -label:workbench-electron -label:workbench-fonts -label:workbench-history -label:workbench-hot-exit -label:workbench-hover -label:workbench-launch -label:workbench-link -label:workbench-multiroot -label:workbench-notifications -label:workbench-os-integration -label:workbench-rapid-render -label:workbench-run-as-admin -label:workbench-state -label:workbench-status -label:workbench-tabs -label:workbench-touchbar -label:workbench-untitled-editors -label:workbench-views -label:workbench-welcome -label:workbench-window -label:workbench-workspace -label:workbench-zen -label:workspace-edit -label:workspace-symbols -label:workspace-trust -label:zoom -label:inline-chat -label:panel-chat -label:quick-chat -label:tasks -label:error-list -label:winget -label:tree-views -label:freeze-slow-crash-leak -label:engineering -label:cross-file-editing" + "value": "repo:microsoft/vscode assignee:$assignee is:open type:issue -label:\"info-needed\" -label:api -label:api-finalization -label:api-proposal -label:authentication -label:bisect-ext -label:bracket-pair-colorization -label:bracket-pair-guides -label:breadcrumbs -label:callhierarchy -label:chrome-devtools -label:cloud-changes -label:code-lens -label:command-center -label:comments -label:config -label:containers -label:context-keys -label:continue-working-on -label:css-less-scss -label:custom-editors -label:debug -label:debug-disassembly -label:dialogs -label:diff-editor -label:dropdown -label:editor-api -label:editor-autoclosing -label:editor-autoindent -label:editor-bracket-matching -label:editor-clipboard -label:editor-code-actions -label:editor-color-picker -label:editor-columnselect -label:editor-commands -label:editor-comments -label:editor-contrib -label:editor-core -label:editor-drag-and-drop -label:editor-error-widget -label:editor-find -label:editor-folding -label:editor-highlight -label:editor-hover -label:editor-indent-detection -label:editor-indent-guides -label:editor-input -label:editor-input-IME -label:editor-insets -label:editor-minimap -label:editor-multicursor -label:editor-parameter-hints -label:editor-render-whitespace -label:editor-rendering -label:editor-widgets -label:editor-RTL -label:editor-scrollbar -label:editor-sorting -label:editor-sticky-scroll -label:editor-symbols -label:editor-synced-region -label:editor-textbuffer -label:editor-theming -label:editor-wordnav -label:editor-wrapping -label:emmet -label:emmet-parse -label:error-list -label:extension-activation -label:extension-host -label:extension-prerelease -label:extension-recommendations -label:extensions -label:extensions-development -label:file-decorations -label:file-encoding -label:file-explorer -label:file-glob -label:file-io -label:file-nesting -label:file-watcher -label:font-rendering -label:formatting -label:getting-started -label:ghost-text -label:git -label:github -label:github-repositories -label:gpu -label:grammar -label:grid-widget -label:html -label:icon-brand -label:icons-product -label:image-preview -label:inlay-hints -label:inline-completions -label:install-update -label:intellisense-config -label:interactive-playground -label:interactive-window -label:issue-bot -label:issue-reporter -label:javascript -label:json -label:keybindings -label:keybindings-editor -label:keyboard-layout -label:chat -label:l10n-platform -label:label-provider -label:languages-basic -label:languages-diagnostics -label:languages-guessing -label:layout -label:lcd-text-rendering -label:list-widget -label:live-preview -label:log -label:markdown -label:marketplace -label:menus -label:merge-conflict -label:merge-editor -label:merge-editor-workbench -label:monaco-editor -label:native-file-dialog -label:network -label:notebook -label:notebook-accessibility -label:notebook-api -label:notebook-cell-editor -label:notebook-celltoolbar -label:notebook-clipboard -label:notebook-commands -label:notebook-debugging -label:notebook-diff -label:notebook-dnd -label:notebook-execution -label:notebook-find -label:notebook-folding -label:notebook-getting-started -label:notebook-globaltoolbar -label:notebook-ipynb -label:notebook-kernel -label:notebook-kernel-picker -label:notebook-language -label:notebook-layout -label:notebook-markdown -label:notebook-output -label:notebook-perf -label:notebook-remote -label:notebook-serialization -label:notebook-statusbar -label:notebook-toc-outline -label:notebook-undo-redo -label:notebook-variables -label:notebook-workbench-integration -label:notebook-workflow -label:notebook-sticky-scroll -label:notebook-format -label:notebook-code-actions -label:open-editors -label:opener -label:outline -label:output -label:packaging -label:perf -label:perf-bloat -label:perf-startup -label:php -label:portable-mode -label:proxy -label:quick-open -label:quick-pick -label:references-viewlet -label:release-notes -label:remote -label:remote-connection -label:remote-explorer -label:remote-tunnel -label:rename -label:runCommands -label:sandbox -label:sash-widget -label:scm -label:screencast-mode -label:search -label:search-api -label:search-editor -label:search-replace -label:semantic-tokens -label:server -label:settings-editor -label:settings-sync -label:settings-sync-server -label:shared-process -label:simple-file-dialog -label:smart-select -label:snap -label:snippets -label:splitview-widget -label:ssh -label:suggest -label:table-widget -label:tasks -label:telemetry -label:terminal -label:terminal-accessibility -label:terminal-conpty -label:terminal-editors -label:terminal-external -label:terminal-find -label:terminal-input -label:terminal-layout -label:terminal-links -label:terminal-local-echo -label:terminal-persistence -label:terminal-process -label:terminal-profiles -label:terminal-quick-fix -label:terminal-rendering -label:terminal-shell-bash -label:terminal-shell-cmd -label:terminal-shell-fish -label:terminal-shell-git-bash -label:terminal-shell-integration -label:terminal-shell-pwsh -label:terminal-shell-zsh -label:terminal-sticky-scroll -label:terminal-tabs -label:testing -label:themes -label:timeline -label:timeline-git -label:timeline-local-history -label:titlebar -label:tokenization -label:touch/pointer -label:trackpad/scroll -label:tree-views -label:tree-widget -label:typescript -label:undo-redo -label:unicode-highlight -label:uri -label:user-profiles -label:ux -label:variable-resolving -label:VIM -label:virtual-workspaces -label:vscode-website -label:vscode.dev -label:web -label:webview -label:webview-views -label:workbench-actions -label:workbench-banner -label:workbench-cli -label:workbench-diagnostics -label:workbench-dnd -label:workbench-editor-grid -label:workbench-editor-groups -label:workbench-editor-resolver -label:workbench-editors -label:workbench-electron -label:workbench-fonts -label:workbench-history -label:workbench-hot-exit -label:workbench-hover -label:workbench-launch -label:workbench-link -label:workbench-multiroot -label:workbench-notifications -label:workbench-os-integration -label:workbench-rapid-render -label:workbench-run-as-admin -label:workbench-state -label:workbench-status -label:workbench-tabs -label:workbench-touchbar -label:workbench-untitled-editors -label:workbench-views -label:workbench-welcome -label:workbench-window -label:workbench-workspace -label:workbench-zen -label:workspace-edit -label:workspace-symbols -label:workspace-trust -label:zoom -label:inline-chat -label:panel-chat -label:quick-chat -label:tasks -label:error-list -label:winget -label:tree-views -label:freeze-slow-crash-leak -label:engineering -label:cross-file-editing" }, { "kind": 1, From 2bb6383e85fdfc2c28bd479675b9b296ebc7f268 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 17 Dec 2024 10:59:05 -0800 Subject: [PATCH 0180/3587] Pick up latest TS nightly --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index fd395ebd4320..60dc545ca375 100644 --- a/package-lock.json +++ b/package-lock.json @@ -153,7 +153,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^5.8.0-dev.20241212", + "typescript": "^5.8.0-dev.20241217", "typescript-eslint": "^8.8.0", "util": "^0.12.4", "webpack": "^5.94.0", @@ -17599,9 +17599,9 @@ "dev": true }, "node_modules/typescript": { - "version": "5.8.0-dev.20241212", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.0-dev.20241212.tgz", - "integrity": "sha512-DL+rd76Ze4iHIFTT6+f8SNdxkTYnR0cy6e0QRljOfyr2s0TrO2L9pAOB1dJnSizTAjxou7lIRpUWwxVOIyiMWg==", + "version": "5.8.0-dev.20241217", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.0-dev.20241217.tgz", + "integrity": "sha512-Q/I+eHfiwN0aWhitenThTT2FcA1lTlUZR1z+6d2WaD/8/wHfdjQjdHynCpYXjAwDkfG8Apf9LdzZ3rLRD3O9iQ==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index eb0abcc3221b..8dd5e0a86a37 100644 --- a/package.json +++ b/package.json @@ -211,7 +211,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^5.8.0-dev.20241212", + "typescript": "^5.8.0-dev.20241217", "typescript-eslint": "^8.8.0", "util": "^0.12.4", "webpack": "^5.94.0", From 068676662c8067ea8f6a96ebcc9d9e9332e2b2ba Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 17 Dec 2024 20:05:05 +0100 Subject: [PATCH 0181/3587] Fixes https://github.com/microsoft/vscode-copilot/issues/10296 (#236383) --- .../controller/inlineCompletionsController.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index 6be0ecd1c90f..f3720436c2f6 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -38,6 +38,8 @@ import { InlineCompletionContextKeys } from './inlineCompletionContextKeys.js'; import { InlineCompletionsView } from '../view/inlineCompletionsView.js'; export class InlineCompletionsController extends Disposable { + private static readonly _instances = new Set(); + public static hot = createHotClass(InlineCompletionsController); public static ID = 'editor.contrib.inlineCompletionsController'; @@ -116,6 +118,22 @@ export class InlineCompletionsController extends Disposable { ) { super(); + InlineCompletionsController._instances.add(this); + this._register(toDisposable(() => InlineCompletionsController._instances.delete(this))); + + this._register(autorun(reader => { + // Cancel all other inline completions when a new one starts + const model = this.model.read(reader); + if (!model) { return; } + if (model.state.read(reader) !== undefined) { + for (const ctrl of InlineCompletionsController._instances) { + if (ctrl !== this) { + ctrl.reject(); + } + } + } + })); + this._register(runOnChange(this._editorObs.onDidType, (_value, _changes) => { if (this._enabled.get()) { this.model.get()?.trigger(); From e689b912ba8d46d224fc80ca56c641772476a7da Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Tue, 17 Dec 2024 12:12:49 -0700 Subject: [PATCH 0182/3587] Update telemetry package (#236378) --- extensions/git/package-lock.json | 141 ++++++++++-------- extensions/git/package.json | 2 +- .../github-authentication/package-lock.json | 141 ++++++++++-------- extensions/github-authentication/package.json | 2 +- extensions/github/package-lock.json | 141 ++++++++++-------- extensions/github/package.json | 2 +- .../html-language-features/package-lock.json | 141 ++++++++++-------- .../html-language-features/package.json | 2 +- .../json-language-features/package-lock.json | 141 ++++++++++-------- .../json-language-features/package.json | 2 +- .../package-lock.json | 141 ++++++++++-------- .../markdown-language-features/package.json | 2 +- extensions/media-preview/package-lock.json | 141 ++++++++++-------- extensions/media-preview/package.json | 2 +- extensions/merge-conflict/package-lock.json | 141 ++++++++++-------- extensions/merge-conflict/package.json | 2 +- .../package-lock.json | 141 ++++++++++-------- .../microsoft-authentication/package.json | 2 +- extensions/simple-browser/package-lock.json | 141 ++++++++++-------- extensions/simple-browser/package.json | 2 +- .../package-lock.json | 141 ++++++++++-------- .../typescript-language-features/package.json | 2 +- 22 files changed, 847 insertions(+), 726 deletions(-) diff --git a/extensions/git/package-lock.json b/extensions/git/package-lock.json index cf05db079db0..bc150555c70c 100644 --- a/extensions/git/package-lock.json +++ b/extensions/git/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@joaomoreno/unique-names-generator": "^5.2.0", - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "@vscode/iconv-lite-umd": "0.7.0", "byline": "^5.0.0", "file-type": "16.5.4", @@ -40,118 +40,128 @@ } }, "node_modules/@microsoft/1ds-core-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.0.3.tgz", - "integrity": "sha512-FrxNLVAPsAvD7+l63TlNS/Kodvpct2WulpDSn1dI4Xuy0kF4E2H867kHdwL/iY1Bj3zA3FSy/jvE4+OcDws7ug==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz", + "integrity": "sha512-3gbDUQgAO8EoyQTNcAEkxpuPnioC0May13P1l1l0NKZ128L9Ts/sj8QsfwCRTjHz0HThlA+4FptcAJXNYUy3rg==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/1ds-post-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.0.3.tgz", - "integrity": "sha512-uewvmUtXKd7ttypiKQGdYI6i7UUpPkOznLayzIFrJ4r2xnG6jhPjpKRncHFXPQcM4XSWO3yf5PQ3xAbPq9t7ZQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.4.tgz", + "integrity": "sha512-nlKjWricDj0Tn68Dt0P8lX9a+X7LYrqJ6/iSfQwMfDhRIGLqW+wxx8gxS+iGWC/oc8zMQAeiZaemUpCwQcwpRQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "4.0.3", + "@microsoft/1ds-core-js": "4.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.4.tgz", - "integrity": "sha512-6TlfExmErQ8Y+/ChbkyWl+jyt4wg3T6p7lwXDsUCB0LgZmlEWMaCUS0YlT73JCWmE8j7vxW8yUm0lgsgmHns3A==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.4.tgz", + "integrity": "sha512-Z4nrxYwGKP9iyrYtm7iPQXVOFy4FsEsX0nDKkAi96Qpgw+vEh6NH4ORxMMuES0EollBQ3faJyvYCwckuCVIj0g==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-common": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.4.tgz", - "integrity": "sha512-r5gWaw/K9+tKfuo2GtDiDiKASgOkPOCrKW+wZzFvuR06uuwvWjbVQ6yW/YbnfuhRF5M65ksUiMi0eCMwEOGq7Q==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.4.tgz", + "integrity": "sha512-4ms16MlIvcP4WiUPqopifNxcWCcrXQJ2ADAK/75uok2mNQe6ZNRsqb/P+pvhUxc8A5HRlvoXPP1ptDSN5Girgw==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.4.tgz", - "integrity": "sha512-anxy5kEkqBmVoEqJiJzaaXXA0wzqZi9U4zGd05xFJ04lWckP8dG3zyT3+GGdg7rDelqLTNGxndeYoFmDv63u1g==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.4.tgz", + "integrity": "sha512-MummANF0mgKIkdvVvfmHQTBliK114IZLRhTL0X0Ep+zjDwWMHqYZgew0nlFKAl6ggu42abPZFK5afpE7qjtYJA==", + "license": "MIT", "dependencies": { "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-shims": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "license": "MIT", "dependencies": { "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.4.tgz", - "integrity": "sha512-KfoxPlLlf0JT12ADb23C5iGye/yFouoMgHEKULxkSQcYY9SsW/8rVrqqvoYKAL+u215CZU2A8Kc8sR3ehEaPCQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.4.tgz", + "integrity": "sha512-OpEPXr8vU/t/M8T9jvWJzJx/pCyygIiR1nGM/2PTde0wn7anl71Gxl5fWol7K/WwFEORNjkL3CEyWOyDc+28AA==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-channel-js": "3.0.4", - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-channel-js": "3.3.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/dynamicproto-js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", - "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" } }, "node_modules/@nevware21/ts-async": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", - "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", + "integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + "@nevware21/ts-utils": ">= 0.11.6 < 2.x" } }, "node_modules/@nevware21/ts-utils": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", - "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.6.tgz", + "integrity": "sha512-OUUJTh3fnaUSzg9DEHgv3d7jC+DnPL65mIO7RaR+jWve7+MmcgIvF79gY97DPQ4frH+IpNR78YAYd/dW4gK3kg==", + "license": "MIT" }, "node_modules/@tokenizer/token": { "version": "0.3.0", @@ -195,13 +205,14 @@ "dev": true }, "node_modules/@vscode/extension-telemetry": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.0.tgz", - "integrity": "sha512-37RxGHXrs3GoXPgCUKQhghEu0gxs8j27RLjQwwtSf4WhPdJKz8UrqMYzpsXlliQ05zURYmtdGZst9C6+hfWXaQ==", + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.8.tgz", + "integrity": "sha512-7YcKoUvmHlIB8QYCE4FNzt3ErHi9gQPhdCM3ZWtpw1bxPT0I+lMdx52KHlzTNoJzQ2NvMX7HyzyDwBEiMgTrWQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "^4.0.3", - "@microsoft/1ds-post-js": "^4.0.3", - "@microsoft/applicationinsights-web-basic": "^3.0.4" + "@microsoft/1ds-core-js": "^4.3.4", + "@microsoft/1ds-post-js": "^4.3.4", + "@microsoft/applicationinsights-web-basic": "^3.3.4" }, "engines": { "vscode": "^1.75.0" diff --git a/extensions/git/package.json b/extensions/git/package.json index 9aedfbb16e3a..8b8f52643a89 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -3471,7 +3471,7 @@ }, "dependencies": { "@joaomoreno/unique-names-generator": "^5.2.0", - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "@vscode/iconv-lite-umd": "0.7.0", "byline": "^5.0.0", "file-type": "16.5.4", diff --git a/extensions/github-authentication/package-lock.json b/extensions/github-authentication/package-lock.json index c150fa7acc36..cbc9e16b75f3 100644 --- a/extensions/github-authentication/package-lock.json +++ b/extensions/github-authentication/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.2", "license": "MIT", "dependencies": { - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "node-fetch": "2.6.7", "vscode-tas-client": "^0.1.84" }, @@ -23,118 +23,128 @@ } }, "node_modules/@microsoft/1ds-core-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.0.3.tgz", - "integrity": "sha512-FrxNLVAPsAvD7+l63TlNS/Kodvpct2WulpDSn1dI4Xuy0kF4E2H867kHdwL/iY1Bj3zA3FSy/jvE4+OcDws7ug==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz", + "integrity": "sha512-3gbDUQgAO8EoyQTNcAEkxpuPnioC0May13P1l1l0NKZ128L9Ts/sj8QsfwCRTjHz0HThlA+4FptcAJXNYUy3rg==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/1ds-post-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.0.3.tgz", - "integrity": "sha512-uewvmUtXKd7ttypiKQGdYI6i7UUpPkOznLayzIFrJ4r2xnG6jhPjpKRncHFXPQcM4XSWO3yf5PQ3xAbPq9t7ZQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.4.tgz", + "integrity": "sha512-nlKjWricDj0Tn68Dt0P8lX9a+X7LYrqJ6/iSfQwMfDhRIGLqW+wxx8gxS+iGWC/oc8zMQAeiZaemUpCwQcwpRQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "4.0.3", + "@microsoft/1ds-core-js": "4.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.4.tgz", - "integrity": "sha512-6TlfExmErQ8Y+/ChbkyWl+jyt4wg3T6p7lwXDsUCB0LgZmlEWMaCUS0YlT73JCWmE8j7vxW8yUm0lgsgmHns3A==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.4.tgz", + "integrity": "sha512-Z4nrxYwGKP9iyrYtm7iPQXVOFy4FsEsX0nDKkAi96Qpgw+vEh6NH4ORxMMuES0EollBQ3faJyvYCwckuCVIj0g==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-common": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.4.tgz", - "integrity": "sha512-r5gWaw/K9+tKfuo2GtDiDiKASgOkPOCrKW+wZzFvuR06uuwvWjbVQ6yW/YbnfuhRF5M65ksUiMi0eCMwEOGq7Q==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.4.tgz", + "integrity": "sha512-4ms16MlIvcP4WiUPqopifNxcWCcrXQJ2ADAK/75uok2mNQe6ZNRsqb/P+pvhUxc8A5HRlvoXPP1ptDSN5Girgw==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.4.tgz", - "integrity": "sha512-anxy5kEkqBmVoEqJiJzaaXXA0wzqZi9U4zGd05xFJ04lWckP8dG3zyT3+GGdg7rDelqLTNGxndeYoFmDv63u1g==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.4.tgz", + "integrity": "sha512-MummANF0mgKIkdvVvfmHQTBliK114IZLRhTL0X0Ep+zjDwWMHqYZgew0nlFKAl6ggu42abPZFK5afpE7qjtYJA==", + "license": "MIT", "dependencies": { "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-shims": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "license": "MIT", "dependencies": { "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.4.tgz", - "integrity": "sha512-KfoxPlLlf0JT12ADb23C5iGye/yFouoMgHEKULxkSQcYY9SsW/8rVrqqvoYKAL+u215CZU2A8Kc8sR3ehEaPCQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.4.tgz", + "integrity": "sha512-OpEPXr8vU/t/M8T9jvWJzJx/pCyygIiR1nGM/2PTde0wn7anl71Gxl5fWol7K/WwFEORNjkL3CEyWOyDc+28AA==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-channel-js": "3.0.4", - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-channel-js": "3.3.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/dynamicproto-js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", - "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" } }, "node_modules/@nevware21/ts-async": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", - "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", + "integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + "@nevware21/ts-utils": ">= 0.11.6 < 2.x" } }, "node_modules/@nevware21/ts-utils": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", - "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.6.tgz", + "integrity": "sha512-OUUJTh3fnaUSzg9DEHgv3d7jC+DnPL65mIO7RaR+jWve7+MmcgIvF79gY97DPQ4frH+IpNR78YAYd/dW4gK3kg==", + "license": "MIT" }, "node_modules/@types/mocha": { "version": "9.1.1", @@ -162,13 +172,14 @@ } }, "node_modules/@vscode/extension-telemetry": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.0.tgz", - "integrity": "sha512-37RxGHXrs3GoXPgCUKQhghEu0gxs8j27RLjQwwtSf4WhPdJKz8UrqMYzpsXlliQ05zURYmtdGZst9C6+hfWXaQ==", + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.8.tgz", + "integrity": "sha512-7YcKoUvmHlIB8QYCE4FNzt3ErHi9gQPhdCM3ZWtpw1bxPT0I+lMdx52KHlzTNoJzQ2NvMX7HyzyDwBEiMgTrWQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "^4.0.3", - "@microsoft/1ds-post-js": "^4.0.3", - "@microsoft/applicationinsights-web-basic": "^3.0.4" + "@microsoft/1ds-core-js": "^4.3.4", + "@microsoft/1ds-post-js": "^4.3.4", + "@microsoft/applicationinsights-web-basic": "^3.3.4" }, "engines": { "vscode": "^1.75.0" diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index cbc1ddc11dd5..80b5d2c920e8 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -61,7 +61,7 @@ }, "dependencies": { "node-fetch": "2.6.7", - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "vscode-tas-client": "^0.1.84" }, "devDependencies": { diff --git a/extensions/github/package-lock.json b/extensions/github/package-lock.json index 41de66d1a9ba..1b7dc727a928 100644 --- a/extensions/github/package-lock.json +++ b/extensions/github/package-lock.json @@ -12,7 +12,7 @@ "@octokit/graphql": "5.0.5", "@octokit/graphql-schema": "14.4.0", "@octokit/rest": "19.0.4", - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "tunnel": "^0.0.6" }, "devDependencies": { @@ -23,118 +23,128 @@ } }, "node_modules/@microsoft/1ds-core-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.0.3.tgz", - "integrity": "sha512-FrxNLVAPsAvD7+l63TlNS/Kodvpct2WulpDSn1dI4Xuy0kF4E2H867kHdwL/iY1Bj3zA3FSy/jvE4+OcDws7ug==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz", + "integrity": "sha512-3gbDUQgAO8EoyQTNcAEkxpuPnioC0May13P1l1l0NKZ128L9Ts/sj8QsfwCRTjHz0HThlA+4FptcAJXNYUy3rg==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/1ds-post-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.0.3.tgz", - "integrity": "sha512-uewvmUtXKd7ttypiKQGdYI6i7UUpPkOznLayzIFrJ4r2xnG6jhPjpKRncHFXPQcM4XSWO3yf5PQ3xAbPq9t7ZQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.4.tgz", + "integrity": "sha512-nlKjWricDj0Tn68Dt0P8lX9a+X7LYrqJ6/iSfQwMfDhRIGLqW+wxx8gxS+iGWC/oc8zMQAeiZaemUpCwQcwpRQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "4.0.3", + "@microsoft/1ds-core-js": "4.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.4.tgz", - "integrity": "sha512-6TlfExmErQ8Y+/ChbkyWl+jyt4wg3T6p7lwXDsUCB0LgZmlEWMaCUS0YlT73JCWmE8j7vxW8yUm0lgsgmHns3A==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.4.tgz", + "integrity": "sha512-Z4nrxYwGKP9iyrYtm7iPQXVOFy4FsEsX0nDKkAi96Qpgw+vEh6NH4ORxMMuES0EollBQ3faJyvYCwckuCVIj0g==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-common": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.4.tgz", - "integrity": "sha512-r5gWaw/K9+tKfuo2GtDiDiKASgOkPOCrKW+wZzFvuR06uuwvWjbVQ6yW/YbnfuhRF5M65ksUiMi0eCMwEOGq7Q==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.4.tgz", + "integrity": "sha512-4ms16MlIvcP4WiUPqopifNxcWCcrXQJ2ADAK/75uok2mNQe6ZNRsqb/P+pvhUxc8A5HRlvoXPP1ptDSN5Girgw==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.4.tgz", - "integrity": "sha512-anxy5kEkqBmVoEqJiJzaaXXA0wzqZi9U4zGd05xFJ04lWckP8dG3zyT3+GGdg7rDelqLTNGxndeYoFmDv63u1g==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.4.tgz", + "integrity": "sha512-MummANF0mgKIkdvVvfmHQTBliK114IZLRhTL0X0Ep+zjDwWMHqYZgew0nlFKAl6ggu42abPZFK5afpE7qjtYJA==", + "license": "MIT", "dependencies": { "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-shims": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "license": "MIT", "dependencies": { "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.4.tgz", - "integrity": "sha512-KfoxPlLlf0JT12ADb23C5iGye/yFouoMgHEKULxkSQcYY9SsW/8rVrqqvoYKAL+u215CZU2A8Kc8sR3ehEaPCQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.4.tgz", + "integrity": "sha512-OpEPXr8vU/t/M8T9jvWJzJx/pCyygIiR1nGM/2PTde0wn7anl71Gxl5fWol7K/WwFEORNjkL3CEyWOyDc+28AA==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-channel-js": "3.0.4", - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-channel-js": "3.3.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/dynamicproto-js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", - "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" } }, "node_modules/@nevware21/ts-async": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", - "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", + "integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + "@nevware21/ts-utils": ">= 0.11.6 < 2.x" } }, "node_modules/@nevware21/ts-utils": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", - "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.6.tgz", + "integrity": "sha512-OUUJTh3fnaUSzg9DEHgv3d7jC+DnPL65mIO7RaR+jWve7+MmcgIvF79gY97DPQ4frH+IpNR78YAYd/dW4gK3kg==", + "license": "MIT" }, "node_modules/@octokit/auth-token": { "version": "3.0.1", @@ -393,13 +403,14 @@ } }, "node_modules/@vscode/extension-telemetry": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.0.tgz", - "integrity": "sha512-37RxGHXrs3GoXPgCUKQhghEu0gxs8j27RLjQwwtSf4WhPdJKz8UrqMYzpsXlliQ05zURYmtdGZst9C6+hfWXaQ==", + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.8.tgz", + "integrity": "sha512-7YcKoUvmHlIB8QYCE4FNzt3ErHi9gQPhdCM3ZWtpw1bxPT0I+lMdx52KHlzTNoJzQ2NvMX7HyzyDwBEiMgTrWQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "^4.0.3", - "@microsoft/1ds-post-js": "^4.0.3", - "@microsoft/applicationinsights-web-basic": "^3.0.4" + "@microsoft/1ds-core-js": "^4.3.4", + "@microsoft/1ds-post-js": "^4.3.4", + "@microsoft/applicationinsights-web-basic": "^3.3.4" }, "engines": { "vscode": "^1.75.0" diff --git a/extensions/github/package.json b/extensions/github/package.json index e84b35d19b76..f99a41d5979e 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -183,7 +183,7 @@ "@octokit/graphql-schema": "14.4.0", "@octokit/rest": "19.0.4", "tunnel": "^0.0.6", - "@vscode/extension-telemetry": "^0.9.0" + "@vscode/extension-telemetry": "^0.9.8" }, "devDependencies": { "@types/node": "20.x" diff --git a/extensions/html-language-features/package-lock.json b/extensions/html-language-features/package-lock.json index fbb95e522dd2..20b145615338 100644 --- a/extensions/html-language-features/package-lock.json +++ b/extensions/html-language-features/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "vscode-languageclient": "^10.0.0-next.13", "vscode-uri": "^3.0.8" }, @@ -21,118 +21,128 @@ } }, "node_modules/@microsoft/1ds-core-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.0.3.tgz", - "integrity": "sha512-FrxNLVAPsAvD7+l63TlNS/Kodvpct2WulpDSn1dI4Xuy0kF4E2H867kHdwL/iY1Bj3zA3FSy/jvE4+OcDws7ug==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz", + "integrity": "sha512-3gbDUQgAO8EoyQTNcAEkxpuPnioC0May13P1l1l0NKZ128L9Ts/sj8QsfwCRTjHz0HThlA+4FptcAJXNYUy3rg==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/1ds-post-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.0.3.tgz", - "integrity": "sha512-uewvmUtXKd7ttypiKQGdYI6i7UUpPkOznLayzIFrJ4r2xnG6jhPjpKRncHFXPQcM4XSWO3yf5PQ3xAbPq9t7ZQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.4.tgz", + "integrity": "sha512-nlKjWricDj0Tn68Dt0P8lX9a+X7LYrqJ6/iSfQwMfDhRIGLqW+wxx8gxS+iGWC/oc8zMQAeiZaemUpCwQcwpRQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "4.0.3", + "@microsoft/1ds-core-js": "4.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.4.tgz", - "integrity": "sha512-6TlfExmErQ8Y+/ChbkyWl+jyt4wg3T6p7lwXDsUCB0LgZmlEWMaCUS0YlT73JCWmE8j7vxW8yUm0lgsgmHns3A==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.4.tgz", + "integrity": "sha512-Z4nrxYwGKP9iyrYtm7iPQXVOFy4FsEsX0nDKkAi96Qpgw+vEh6NH4ORxMMuES0EollBQ3faJyvYCwckuCVIj0g==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-common": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.4.tgz", - "integrity": "sha512-r5gWaw/K9+tKfuo2GtDiDiKASgOkPOCrKW+wZzFvuR06uuwvWjbVQ6yW/YbnfuhRF5M65ksUiMi0eCMwEOGq7Q==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.4.tgz", + "integrity": "sha512-4ms16MlIvcP4WiUPqopifNxcWCcrXQJ2ADAK/75uok2mNQe6ZNRsqb/P+pvhUxc8A5HRlvoXPP1ptDSN5Girgw==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.4.tgz", - "integrity": "sha512-anxy5kEkqBmVoEqJiJzaaXXA0wzqZi9U4zGd05xFJ04lWckP8dG3zyT3+GGdg7rDelqLTNGxndeYoFmDv63u1g==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.4.tgz", + "integrity": "sha512-MummANF0mgKIkdvVvfmHQTBliK114IZLRhTL0X0Ep+zjDwWMHqYZgew0nlFKAl6ggu42abPZFK5afpE7qjtYJA==", + "license": "MIT", "dependencies": { "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-shims": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "license": "MIT", "dependencies": { "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.4.tgz", - "integrity": "sha512-KfoxPlLlf0JT12ADb23C5iGye/yFouoMgHEKULxkSQcYY9SsW/8rVrqqvoYKAL+u215CZU2A8Kc8sR3ehEaPCQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.4.tgz", + "integrity": "sha512-OpEPXr8vU/t/M8T9jvWJzJx/pCyygIiR1nGM/2PTde0wn7anl71Gxl5fWol7K/WwFEORNjkL3CEyWOyDc+28AA==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-channel-js": "3.0.4", - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-channel-js": "3.3.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/dynamicproto-js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", - "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" } }, "node_modules/@nevware21/ts-async": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", - "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", + "integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + "@nevware21/ts-utils": ">= 0.11.6 < 2.x" } }, "node_modules/@nevware21/ts-utils": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", - "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.6.tgz", + "integrity": "sha512-OUUJTh3fnaUSzg9DEHgv3d7jC+DnPL65mIO7RaR+jWve7+MmcgIvF79gY97DPQ4frH+IpNR78YAYd/dW4gK3kg==", + "license": "MIT" }, "node_modules/@types/node": { "version": "20.11.24", @@ -144,13 +154,14 @@ } }, "node_modules/@vscode/extension-telemetry": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.0.tgz", - "integrity": "sha512-37RxGHXrs3GoXPgCUKQhghEu0gxs8j27RLjQwwtSf4WhPdJKz8UrqMYzpsXlliQ05zURYmtdGZst9C6+hfWXaQ==", + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.8.tgz", + "integrity": "sha512-7YcKoUvmHlIB8QYCE4FNzt3ErHi9gQPhdCM3ZWtpw1bxPT0I+lMdx52KHlzTNoJzQ2NvMX7HyzyDwBEiMgTrWQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "^4.0.3", - "@microsoft/1ds-post-js": "^4.0.3", - "@microsoft/applicationinsights-web-basic": "^3.0.4" + "@microsoft/1ds-core-js": "^4.3.4", + "@microsoft/1ds-post-js": "^4.3.4", + "@microsoft/applicationinsights-web-basic": "^3.3.4" }, "engines": { "vscode": "^1.75.0" diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index 60b9718e6c38..be411fe63a2b 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -258,7 +258,7 @@ ] }, "dependencies": { - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "vscode-languageclient": "^10.0.0-next.13", "vscode-uri": "^3.0.8" }, diff --git a/extensions/json-language-features/package-lock.json b/extensions/json-language-features/package-lock.json index f28a8c68df02..bde80cbfbe47 100644 --- a/extensions/json-language-features/package-lock.json +++ b/extensions/json-language-features/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "request-light": "^0.8.0", "vscode-languageclient": "^10.0.0-next.13" }, @@ -21,118 +21,128 @@ } }, "node_modules/@microsoft/1ds-core-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.0.3.tgz", - "integrity": "sha512-FrxNLVAPsAvD7+l63TlNS/Kodvpct2WulpDSn1dI4Xuy0kF4E2H867kHdwL/iY1Bj3zA3FSy/jvE4+OcDws7ug==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz", + "integrity": "sha512-3gbDUQgAO8EoyQTNcAEkxpuPnioC0May13P1l1l0NKZ128L9Ts/sj8QsfwCRTjHz0HThlA+4FptcAJXNYUy3rg==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/1ds-post-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.0.3.tgz", - "integrity": "sha512-uewvmUtXKd7ttypiKQGdYI6i7UUpPkOznLayzIFrJ4r2xnG6jhPjpKRncHFXPQcM4XSWO3yf5PQ3xAbPq9t7ZQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.4.tgz", + "integrity": "sha512-nlKjWricDj0Tn68Dt0P8lX9a+X7LYrqJ6/iSfQwMfDhRIGLqW+wxx8gxS+iGWC/oc8zMQAeiZaemUpCwQcwpRQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "4.0.3", + "@microsoft/1ds-core-js": "4.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.4.tgz", - "integrity": "sha512-6TlfExmErQ8Y+/ChbkyWl+jyt4wg3T6p7lwXDsUCB0LgZmlEWMaCUS0YlT73JCWmE8j7vxW8yUm0lgsgmHns3A==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.4.tgz", + "integrity": "sha512-Z4nrxYwGKP9iyrYtm7iPQXVOFy4FsEsX0nDKkAi96Qpgw+vEh6NH4ORxMMuES0EollBQ3faJyvYCwckuCVIj0g==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-common": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.4.tgz", - "integrity": "sha512-r5gWaw/K9+tKfuo2GtDiDiKASgOkPOCrKW+wZzFvuR06uuwvWjbVQ6yW/YbnfuhRF5M65ksUiMi0eCMwEOGq7Q==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.4.tgz", + "integrity": "sha512-4ms16MlIvcP4WiUPqopifNxcWCcrXQJ2ADAK/75uok2mNQe6ZNRsqb/P+pvhUxc8A5HRlvoXPP1ptDSN5Girgw==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.4.tgz", - "integrity": "sha512-anxy5kEkqBmVoEqJiJzaaXXA0wzqZi9U4zGd05xFJ04lWckP8dG3zyT3+GGdg7rDelqLTNGxndeYoFmDv63u1g==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.4.tgz", + "integrity": "sha512-MummANF0mgKIkdvVvfmHQTBliK114IZLRhTL0X0Ep+zjDwWMHqYZgew0nlFKAl6ggu42abPZFK5afpE7qjtYJA==", + "license": "MIT", "dependencies": { "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-shims": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "license": "MIT", "dependencies": { "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.4.tgz", - "integrity": "sha512-KfoxPlLlf0JT12ADb23C5iGye/yFouoMgHEKULxkSQcYY9SsW/8rVrqqvoYKAL+u215CZU2A8Kc8sR3ehEaPCQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.4.tgz", + "integrity": "sha512-OpEPXr8vU/t/M8T9jvWJzJx/pCyygIiR1nGM/2PTde0wn7anl71Gxl5fWol7K/WwFEORNjkL3CEyWOyDc+28AA==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-channel-js": "3.0.4", - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-channel-js": "3.3.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/dynamicproto-js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", - "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" } }, "node_modules/@nevware21/ts-async": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", - "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", + "integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + "@nevware21/ts-utils": ">= 0.11.6 < 2.x" } }, "node_modules/@nevware21/ts-utils": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", - "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.6.tgz", + "integrity": "sha512-OUUJTh3fnaUSzg9DEHgv3d7jC+DnPL65mIO7RaR+jWve7+MmcgIvF79gY97DPQ4frH+IpNR78YAYd/dW4gK3kg==", + "license": "MIT" }, "node_modules/@types/node": { "version": "20.11.24", @@ -144,13 +154,14 @@ } }, "node_modules/@vscode/extension-telemetry": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.0.tgz", - "integrity": "sha512-37RxGHXrs3GoXPgCUKQhghEu0gxs8j27RLjQwwtSf4WhPdJKz8UrqMYzpsXlliQ05zURYmtdGZst9C6+hfWXaQ==", + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.8.tgz", + "integrity": "sha512-7YcKoUvmHlIB8QYCE4FNzt3ErHi9gQPhdCM3ZWtpw1bxPT0I+lMdx52KHlzTNoJzQ2NvMX7HyzyDwBEiMgTrWQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "^4.0.3", - "@microsoft/1ds-post-js": "^4.0.3", - "@microsoft/applicationinsights-web-basic": "^3.0.4" + "@microsoft/1ds-core-js": "^4.3.4", + "@microsoft/1ds-post-js": "^4.3.4", + "@microsoft/applicationinsights-web-basic": "^3.3.4" }, "engines": { "vscode": "^1.75.0" diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index ff620435b98b..cf4a7f162dab 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -167,7 +167,7 @@ ] }, "dependencies": { - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "request-light": "^0.8.0", "vscode-languageclient": "^10.0.0-next.13" }, diff --git a/extensions/markdown-language-features/package-lock.json b/extensions/markdown-language-features/package-lock.json index 91a1a6c5cb9f..c13d2a007ad7 100644 --- a/extensions/markdown-language-features/package-lock.json +++ b/extensions/markdown-language-features/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "dompurify": "^3.1.7", "highlight.js": "^11.8.0", "markdown-it": "^12.3.2", @@ -39,118 +39,128 @@ } }, "node_modules/@microsoft/1ds-core-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.0.3.tgz", - "integrity": "sha512-FrxNLVAPsAvD7+l63TlNS/Kodvpct2WulpDSn1dI4Xuy0kF4E2H867kHdwL/iY1Bj3zA3FSy/jvE4+OcDws7ug==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz", + "integrity": "sha512-3gbDUQgAO8EoyQTNcAEkxpuPnioC0May13P1l1l0NKZ128L9Ts/sj8QsfwCRTjHz0HThlA+4FptcAJXNYUy3rg==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/1ds-post-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.0.3.tgz", - "integrity": "sha512-uewvmUtXKd7ttypiKQGdYI6i7UUpPkOznLayzIFrJ4r2xnG6jhPjpKRncHFXPQcM4XSWO3yf5PQ3xAbPq9t7ZQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.4.tgz", + "integrity": "sha512-nlKjWricDj0Tn68Dt0P8lX9a+X7LYrqJ6/iSfQwMfDhRIGLqW+wxx8gxS+iGWC/oc8zMQAeiZaemUpCwQcwpRQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "4.0.3", + "@microsoft/1ds-core-js": "4.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.4.tgz", - "integrity": "sha512-6TlfExmErQ8Y+/ChbkyWl+jyt4wg3T6p7lwXDsUCB0LgZmlEWMaCUS0YlT73JCWmE8j7vxW8yUm0lgsgmHns3A==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.4.tgz", + "integrity": "sha512-Z4nrxYwGKP9iyrYtm7iPQXVOFy4FsEsX0nDKkAi96Qpgw+vEh6NH4ORxMMuES0EollBQ3faJyvYCwckuCVIj0g==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-common": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.4.tgz", - "integrity": "sha512-r5gWaw/K9+tKfuo2GtDiDiKASgOkPOCrKW+wZzFvuR06uuwvWjbVQ6yW/YbnfuhRF5M65ksUiMi0eCMwEOGq7Q==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.4.tgz", + "integrity": "sha512-4ms16MlIvcP4WiUPqopifNxcWCcrXQJ2ADAK/75uok2mNQe6ZNRsqb/P+pvhUxc8A5HRlvoXPP1ptDSN5Girgw==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.4.tgz", - "integrity": "sha512-anxy5kEkqBmVoEqJiJzaaXXA0wzqZi9U4zGd05xFJ04lWckP8dG3zyT3+GGdg7rDelqLTNGxndeYoFmDv63u1g==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.4.tgz", + "integrity": "sha512-MummANF0mgKIkdvVvfmHQTBliK114IZLRhTL0X0Ep+zjDwWMHqYZgew0nlFKAl6ggu42abPZFK5afpE7qjtYJA==", + "license": "MIT", "dependencies": { "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-shims": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "license": "MIT", "dependencies": { "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.4.tgz", - "integrity": "sha512-KfoxPlLlf0JT12ADb23C5iGye/yFouoMgHEKULxkSQcYY9SsW/8rVrqqvoYKAL+u215CZU2A8Kc8sR3ehEaPCQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.4.tgz", + "integrity": "sha512-OpEPXr8vU/t/M8T9jvWJzJx/pCyygIiR1nGM/2PTde0wn7anl71Gxl5fWol7K/WwFEORNjkL3CEyWOyDc+28AA==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-channel-js": "3.0.4", - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-channel-js": "3.3.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/dynamicproto-js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", - "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" } }, "node_modules/@nevware21/ts-async": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", - "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", + "integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + "@nevware21/ts-utils": ">= 0.11.6 < 2.x" } }, "node_modules/@nevware21/ts-utils": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", - "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.6.tgz", + "integrity": "sha512-OUUJTh3fnaUSzg9DEHgv3d7jC+DnPL65mIO7RaR+jWve7+MmcgIvF79gY97DPQ4frH+IpNR78YAYd/dW4gK3kg==", + "license": "MIT" }, "node_modules/@types/dompurify": { "version": "3.0.5", @@ -223,13 +233,14 @@ "dev": true }, "node_modules/@vscode/extension-telemetry": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.0.tgz", - "integrity": "sha512-37RxGHXrs3GoXPgCUKQhghEu0gxs8j27RLjQwwtSf4WhPdJKz8UrqMYzpsXlliQ05zURYmtdGZst9C6+hfWXaQ==", + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.8.tgz", + "integrity": "sha512-7YcKoUvmHlIB8QYCE4FNzt3ErHi9gQPhdCM3ZWtpw1bxPT0I+lMdx52KHlzTNoJzQ2NvMX7HyzyDwBEiMgTrWQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "^4.0.3", - "@microsoft/1ds-post-js": "^4.0.3", - "@microsoft/applicationinsights-web-basic": "^3.0.4" + "@microsoft/1ds-core-js": "^4.3.4", + "@microsoft/1ds-post-js": "^4.3.4", + "@microsoft/applicationinsights-web-basic": "^3.3.4" }, "engines": { "vscode": "^1.75.0" diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 3f1c3a5fd581..6ed7ff1e62e1 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -763,7 +763,7 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "dependencies": { - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "dompurify": "^3.1.7", "highlight.js": "^11.8.0", "markdown-it": "^12.3.2", diff --git a/extensions/media-preview/package-lock.json b/extensions/media-preview/package-lock.json index 68391b8c4be9..d26855f3ad25 100644 --- a/extensions/media-preview/package-lock.json +++ b/extensions/media-preview/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "vscode-uri": "^3.0.6" }, "engines": { @@ -17,127 +17,138 @@ } }, "node_modules/@microsoft/1ds-core-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.0.3.tgz", - "integrity": "sha512-FrxNLVAPsAvD7+l63TlNS/Kodvpct2WulpDSn1dI4Xuy0kF4E2H867kHdwL/iY1Bj3zA3FSy/jvE4+OcDws7ug==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz", + "integrity": "sha512-3gbDUQgAO8EoyQTNcAEkxpuPnioC0May13P1l1l0NKZ128L9Ts/sj8QsfwCRTjHz0HThlA+4FptcAJXNYUy3rg==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/1ds-post-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.0.3.tgz", - "integrity": "sha512-uewvmUtXKd7ttypiKQGdYI6i7UUpPkOznLayzIFrJ4r2xnG6jhPjpKRncHFXPQcM4XSWO3yf5PQ3xAbPq9t7ZQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.4.tgz", + "integrity": "sha512-nlKjWricDj0Tn68Dt0P8lX9a+X7LYrqJ6/iSfQwMfDhRIGLqW+wxx8gxS+iGWC/oc8zMQAeiZaemUpCwQcwpRQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "4.0.3", + "@microsoft/1ds-core-js": "4.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.4.tgz", - "integrity": "sha512-6TlfExmErQ8Y+/ChbkyWl+jyt4wg3T6p7lwXDsUCB0LgZmlEWMaCUS0YlT73JCWmE8j7vxW8yUm0lgsgmHns3A==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.4.tgz", + "integrity": "sha512-Z4nrxYwGKP9iyrYtm7iPQXVOFy4FsEsX0nDKkAi96Qpgw+vEh6NH4ORxMMuES0EollBQ3faJyvYCwckuCVIj0g==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-common": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.4.tgz", - "integrity": "sha512-r5gWaw/K9+tKfuo2GtDiDiKASgOkPOCrKW+wZzFvuR06uuwvWjbVQ6yW/YbnfuhRF5M65ksUiMi0eCMwEOGq7Q==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.4.tgz", + "integrity": "sha512-4ms16MlIvcP4WiUPqopifNxcWCcrXQJ2ADAK/75uok2mNQe6ZNRsqb/P+pvhUxc8A5HRlvoXPP1ptDSN5Girgw==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.4.tgz", - "integrity": "sha512-anxy5kEkqBmVoEqJiJzaaXXA0wzqZi9U4zGd05xFJ04lWckP8dG3zyT3+GGdg7rDelqLTNGxndeYoFmDv63u1g==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.4.tgz", + "integrity": "sha512-MummANF0mgKIkdvVvfmHQTBliK114IZLRhTL0X0Ep+zjDwWMHqYZgew0nlFKAl6ggu42abPZFK5afpE7qjtYJA==", + "license": "MIT", "dependencies": { "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-shims": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "license": "MIT", "dependencies": { "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.4.tgz", - "integrity": "sha512-KfoxPlLlf0JT12ADb23C5iGye/yFouoMgHEKULxkSQcYY9SsW/8rVrqqvoYKAL+u215CZU2A8Kc8sR3ehEaPCQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.4.tgz", + "integrity": "sha512-OpEPXr8vU/t/M8T9jvWJzJx/pCyygIiR1nGM/2PTde0wn7anl71Gxl5fWol7K/WwFEORNjkL3CEyWOyDc+28AA==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-channel-js": "3.0.4", - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-channel-js": "3.3.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/dynamicproto-js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", - "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" } }, "node_modules/@nevware21/ts-async": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", - "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", + "integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + "@nevware21/ts-utils": ">= 0.11.6 < 2.x" } }, "node_modules/@nevware21/ts-utils": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", - "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.6.tgz", + "integrity": "sha512-OUUJTh3fnaUSzg9DEHgv3d7jC+DnPL65mIO7RaR+jWve7+MmcgIvF79gY97DPQ4frH+IpNR78YAYd/dW4gK3kg==", + "license": "MIT" }, "node_modules/@vscode/extension-telemetry": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.0.tgz", - "integrity": "sha512-37RxGHXrs3GoXPgCUKQhghEu0gxs8j27RLjQwwtSf4WhPdJKz8UrqMYzpsXlliQ05zURYmtdGZst9C6+hfWXaQ==", + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.8.tgz", + "integrity": "sha512-7YcKoUvmHlIB8QYCE4FNzt3ErHi9gQPhdCM3ZWtpw1bxPT0I+lMdx52KHlzTNoJzQ2NvMX7HyzyDwBEiMgTrWQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "^4.0.3", - "@microsoft/1ds-post-js": "^4.0.3", - "@microsoft/applicationinsights-web-basic": "^3.0.4" + "@microsoft/1ds-core-js": "^4.3.4", + "@microsoft/1ds-post-js": "^4.3.4", + "@microsoft/applicationinsights-web-basic": "^3.3.4" }, "engines": { "vscode": "^1.75.0" diff --git a/extensions/media-preview/package.json b/extensions/media-preview/package.json index b42256a22601..e7ddad4354e5 100644 --- a/extensions/media-preview/package.json +++ b/extensions/media-preview/package.json @@ -126,7 +126,7 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "dependencies": { - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "vscode-uri": "^3.0.6" }, "repository": { diff --git a/extensions/merge-conflict/package-lock.json b/extensions/merge-conflict/package-lock.json index a57272606cd0..5ee68d290f01 100644 --- a/extensions/merge-conflict/package-lock.json +++ b/extensions/merge-conflict/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@vscode/extension-telemetry": "^0.9.0" + "@vscode/extension-telemetry": "^0.9.8" }, "devDependencies": { "@types/node": "20.x" @@ -19,118 +19,128 @@ } }, "node_modules/@microsoft/1ds-core-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.0.3.tgz", - "integrity": "sha512-FrxNLVAPsAvD7+l63TlNS/Kodvpct2WulpDSn1dI4Xuy0kF4E2H867kHdwL/iY1Bj3zA3FSy/jvE4+OcDws7ug==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz", + "integrity": "sha512-3gbDUQgAO8EoyQTNcAEkxpuPnioC0May13P1l1l0NKZ128L9Ts/sj8QsfwCRTjHz0HThlA+4FptcAJXNYUy3rg==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/1ds-post-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.0.3.tgz", - "integrity": "sha512-uewvmUtXKd7ttypiKQGdYI6i7UUpPkOznLayzIFrJ4r2xnG6jhPjpKRncHFXPQcM4XSWO3yf5PQ3xAbPq9t7ZQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.4.tgz", + "integrity": "sha512-nlKjWricDj0Tn68Dt0P8lX9a+X7LYrqJ6/iSfQwMfDhRIGLqW+wxx8gxS+iGWC/oc8zMQAeiZaemUpCwQcwpRQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "4.0.3", + "@microsoft/1ds-core-js": "4.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.4.tgz", - "integrity": "sha512-6TlfExmErQ8Y+/ChbkyWl+jyt4wg3T6p7lwXDsUCB0LgZmlEWMaCUS0YlT73JCWmE8j7vxW8yUm0lgsgmHns3A==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.4.tgz", + "integrity": "sha512-Z4nrxYwGKP9iyrYtm7iPQXVOFy4FsEsX0nDKkAi96Qpgw+vEh6NH4ORxMMuES0EollBQ3faJyvYCwckuCVIj0g==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-common": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.4.tgz", - "integrity": "sha512-r5gWaw/K9+tKfuo2GtDiDiKASgOkPOCrKW+wZzFvuR06uuwvWjbVQ6yW/YbnfuhRF5M65ksUiMi0eCMwEOGq7Q==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.4.tgz", + "integrity": "sha512-4ms16MlIvcP4WiUPqopifNxcWCcrXQJ2ADAK/75uok2mNQe6ZNRsqb/P+pvhUxc8A5HRlvoXPP1ptDSN5Girgw==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.4.tgz", - "integrity": "sha512-anxy5kEkqBmVoEqJiJzaaXXA0wzqZi9U4zGd05xFJ04lWckP8dG3zyT3+GGdg7rDelqLTNGxndeYoFmDv63u1g==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.4.tgz", + "integrity": "sha512-MummANF0mgKIkdvVvfmHQTBliK114IZLRhTL0X0Ep+zjDwWMHqYZgew0nlFKAl6ggu42abPZFK5afpE7qjtYJA==", + "license": "MIT", "dependencies": { "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-shims": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "license": "MIT", "dependencies": { "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.4.tgz", - "integrity": "sha512-KfoxPlLlf0JT12ADb23C5iGye/yFouoMgHEKULxkSQcYY9SsW/8rVrqqvoYKAL+u215CZU2A8Kc8sR3ehEaPCQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.4.tgz", + "integrity": "sha512-OpEPXr8vU/t/M8T9jvWJzJx/pCyygIiR1nGM/2PTde0wn7anl71Gxl5fWol7K/WwFEORNjkL3CEyWOyDc+28AA==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-channel-js": "3.0.4", - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-channel-js": "3.3.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/dynamicproto-js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", - "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" } }, "node_modules/@nevware21/ts-async": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", - "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", + "integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + "@nevware21/ts-utils": ">= 0.11.6 < 2.x" } }, "node_modules/@nevware21/ts-utils": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", - "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.6.tgz", + "integrity": "sha512-OUUJTh3fnaUSzg9DEHgv3d7jC+DnPL65mIO7RaR+jWve7+MmcgIvF79gY97DPQ4frH+IpNR78YAYd/dW4gK3kg==", + "license": "MIT" }, "node_modules/@types/node": { "version": "20.11.24", @@ -142,13 +152,14 @@ } }, "node_modules/@vscode/extension-telemetry": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.0.tgz", - "integrity": "sha512-37RxGHXrs3GoXPgCUKQhghEu0gxs8j27RLjQwwtSf4WhPdJKz8UrqMYzpsXlliQ05zURYmtdGZst9C6+hfWXaQ==", + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.8.tgz", + "integrity": "sha512-7YcKoUvmHlIB8QYCE4FNzt3ErHi9gQPhdCM3ZWtpw1bxPT0I+lMdx52KHlzTNoJzQ2NvMX7HyzyDwBEiMgTrWQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "^4.0.3", - "@microsoft/1ds-post-js": "^4.0.3", - "@microsoft/applicationinsights-web-basic": "^3.0.4" + "@microsoft/1ds-core-js": "^4.3.4", + "@microsoft/1ds-post-js": "^4.3.4", + "@microsoft/applicationinsights-web-basic": "^3.3.4" }, "engines": { "vscode": "^1.75.0" diff --git a/extensions/merge-conflict/package.json b/extensions/merge-conflict/package.json index cdda46fab322..de56c9c22cfe 100644 --- a/extensions/merge-conflict/package.json +++ b/extensions/merge-conflict/package.json @@ -166,7 +166,7 @@ } }, "dependencies": { - "@vscode/extension-telemetry": "^0.9.0" + "@vscode/extension-telemetry": "^0.9.8" }, "devDependencies": { "@types/node": "20.x" diff --git a/extensions/microsoft-authentication/package-lock.json b/extensions/microsoft-authentication/package-lock.json index 9f69f13972cb..4aae40c5f170 100644 --- a/extensions/microsoft-authentication/package-lock.json +++ b/extensions/microsoft-authentication/package-lock.json @@ -12,7 +12,7 @@ "@azure/ms-rest-azure-env": "^2.0.0", "@azure/msal-node": "^2.16.2", "@azure/msal-node-extensions": "^1.5.0", - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "keytar": "file:./packageMocks/keytar", "vscode-tas-client": "^0.1.84" }, @@ -78,118 +78,128 @@ "license": "MIT" }, "node_modules/@microsoft/1ds-core-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.0.3.tgz", - "integrity": "sha512-FrxNLVAPsAvD7+l63TlNS/Kodvpct2WulpDSn1dI4Xuy0kF4E2H867kHdwL/iY1Bj3zA3FSy/jvE4+OcDws7ug==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz", + "integrity": "sha512-3gbDUQgAO8EoyQTNcAEkxpuPnioC0May13P1l1l0NKZ128L9Ts/sj8QsfwCRTjHz0HThlA+4FptcAJXNYUy3rg==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/1ds-post-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.0.3.tgz", - "integrity": "sha512-uewvmUtXKd7ttypiKQGdYI6i7UUpPkOznLayzIFrJ4r2xnG6jhPjpKRncHFXPQcM4XSWO3yf5PQ3xAbPq9t7ZQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.4.tgz", + "integrity": "sha512-nlKjWricDj0Tn68Dt0P8lX9a+X7LYrqJ6/iSfQwMfDhRIGLqW+wxx8gxS+iGWC/oc8zMQAeiZaemUpCwQcwpRQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "4.0.3", + "@microsoft/1ds-core-js": "4.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.4.tgz", - "integrity": "sha512-6TlfExmErQ8Y+/ChbkyWl+jyt4wg3T6p7lwXDsUCB0LgZmlEWMaCUS0YlT73JCWmE8j7vxW8yUm0lgsgmHns3A==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.4.tgz", + "integrity": "sha512-Z4nrxYwGKP9iyrYtm7iPQXVOFy4FsEsX0nDKkAi96Qpgw+vEh6NH4ORxMMuES0EollBQ3faJyvYCwckuCVIj0g==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-common": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.4.tgz", - "integrity": "sha512-r5gWaw/K9+tKfuo2GtDiDiKASgOkPOCrKW+wZzFvuR06uuwvWjbVQ6yW/YbnfuhRF5M65ksUiMi0eCMwEOGq7Q==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.4.tgz", + "integrity": "sha512-4ms16MlIvcP4WiUPqopifNxcWCcrXQJ2ADAK/75uok2mNQe6ZNRsqb/P+pvhUxc8A5HRlvoXPP1ptDSN5Girgw==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.4.tgz", - "integrity": "sha512-anxy5kEkqBmVoEqJiJzaaXXA0wzqZi9U4zGd05xFJ04lWckP8dG3zyT3+GGdg7rDelqLTNGxndeYoFmDv63u1g==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.4.tgz", + "integrity": "sha512-MummANF0mgKIkdvVvfmHQTBliK114IZLRhTL0X0Ep+zjDwWMHqYZgew0nlFKAl6ggu42abPZFK5afpE7qjtYJA==", + "license": "MIT", "dependencies": { "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-shims": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "license": "MIT", "dependencies": { "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.4.tgz", - "integrity": "sha512-KfoxPlLlf0JT12ADb23C5iGye/yFouoMgHEKULxkSQcYY9SsW/8rVrqqvoYKAL+u215CZU2A8Kc8sR3ehEaPCQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.4.tgz", + "integrity": "sha512-OpEPXr8vU/t/M8T9jvWJzJx/pCyygIiR1nGM/2PTde0wn7anl71Gxl5fWol7K/WwFEORNjkL3CEyWOyDc+28AA==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-channel-js": "3.0.4", - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-channel-js": "3.3.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/dynamicproto-js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", - "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" } }, "node_modules/@nevware21/ts-async": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", - "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", + "integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + "@nevware21/ts-utils": ">= 0.11.6 < 2.x" } }, "node_modules/@nevware21/ts-utils": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", - "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.6.tgz", + "integrity": "sha512-OUUJTh3fnaUSzg9DEHgv3d7jC+DnPL65mIO7RaR+jWve7+MmcgIvF79gY97DPQ4frH+IpNR78YAYd/dW4gK3kg==", + "license": "MIT" }, "node_modules/@types/node": { "version": "20.11.24", @@ -235,13 +245,14 @@ "dev": true }, "node_modules/@vscode/extension-telemetry": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.0.tgz", - "integrity": "sha512-37RxGHXrs3GoXPgCUKQhghEu0gxs8j27RLjQwwtSf4WhPdJKz8UrqMYzpsXlliQ05zURYmtdGZst9C6+hfWXaQ==", + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.8.tgz", + "integrity": "sha512-7YcKoUvmHlIB8QYCE4FNzt3ErHi9gQPhdCM3ZWtpw1bxPT0I+lMdx52KHlzTNoJzQ2NvMX7HyzyDwBEiMgTrWQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "^4.0.3", - "@microsoft/1ds-post-js": "^4.0.3", - "@microsoft/applicationinsights-web-basic": "^3.0.4" + "@microsoft/1ds-core-js": "^4.3.4", + "@microsoft/1ds-post-js": "^4.3.4", + "@microsoft/applicationinsights-web-basic": "^3.3.4" }, "engines": { "vscode": "^1.75.0" diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index c8ad1c070e26..ba7d83aa359e 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -142,7 +142,7 @@ "@azure/ms-rest-azure-env": "^2.0.0", "@azure/msal-node": "^2.16.2", "@azure/msal-node-extensions": "^1.5.0", - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "keytar": "file:./packageMocks/keytar", "vscode-tas-client": "^0.1.84" }, diff --git a/extensions/simple-browser/package-lock.json b/extensions/simple-browser/package-lock.json index d3542946e63a..c6d9b23636a9 100644 --- a/extensions/simple-browser/package-lock.json +++ b/extensions/simple-browser/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@vscode/extension-telemetry": "^0.9.0" + "@vscode/extension-telemetry": "^0.9.8" }, "devDependencies": { "@types/vscode-webview": "^1.57.0", @@ -20,118 +20,128 @@ } }, "node_modules/@microsoft/1ds-core-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.0.3.tgz", - "integrity": "sha512-FrxNLVAPsAvD7+l63TlNS/Kodvpct2WulpDSn1dI4Xuy0kF4E2H867kHdwL/iY1Bj3zA3FSy/jvE4+OcDws7ug==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz", + "integrity": "sha512-3gbDUQgAO8EoyQTNcAEkxpuPnioC0May13P1l1l0NKZ128L9Ts/sj8QsfwCRTjHz0HThlA+4FptcAJXNYUy3rg==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/1ds-post-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.0.3.tgz", - "integrity": "sha512-uewvmUtXKd7ttypiKQGdYI6i7UUpPkOznLayzIFrJ4r2xnG6jhPjpKRncHFXPQcM4XSWO3yf5PQ3xAbPq9t7ZQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.4.tgz", + "integrity": "sha512-nlKjWricDj0Tn68Dt0P8lX9a+X7LYrqJ6/iSfQwMfDhRIGLqW+wxx8gxS+iGWC/oc8zMQAeiZaemUpCwQcwpRQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "4.0.3", + "@microsoft/1ds-core-js": "4.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.4.tgz", - "integrity": "sha512-6TlfExmErQ8Y+/ChbkyWl+jyt4wg3T6p7lwXDsUCB0LgZmlEWMaCUS0YlT73JCWmE8j7vxW8yUm0lgsgmHns3A==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.4.tgz", + "integrity": "sha512-Z4nrxYwGKP9iyrYtm7iPQXVOFy4FsEsX0nDKkAi96Qpgw+vEh6NH4ORxMMuES0EollBQ3faJyvYCwckuCVIj0g==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-common": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.4.tgz", - "integrity": "sha512-r5gWaw/K9+tKfuo2GtDiDiKASgOkPOCrKW+wZzFvuR06uuwvWjbVQ6yW/YbnfuhRF5M65ksUiMi0eCMwEOGq7Q==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.4.tgz", + "integrity": "sha512-4ms16MlIvcP4WiUPqopifNxcWCcrXQJ2ADAK/75uok2mNQe6ZNRsqb/P+pvhUxc8A5HRlvoXPP1ptDSN5Girgw==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.4.tgz", - "integrity": "sha512-anxy5kEkqBmVoEqJiJzaaXXA0wzqZi9U4zGd05xFJ04lWckP8dG3zyT3+GGdg7rDelqLTNGxndeYoFmDv63u1g==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.4.tgz", + "integrity": "sha512-MummANF0mgKIkdvVvfmHQTBliK114IZLRhTL0X0Ep+zjDwWMHqYZgew0nlFKAl6ggu42abPZFK5afpE7qjtYJA==", + "license": "MIT", "dependencies": { "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-shims": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "license": "MIT", "dependencies": { "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.4.tgz", - "integrity": "sha512-KfoxPlLlf0JT12ADb23C5iGye/yFouoMgHEKULxkSQcYY9SsW/8rVrqqvoYKAL+u215CZU2A8Kc8sR3ehEaPCQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.4.tgz", + "integrity": "sha512-OpEPXr8vU/t/M8T9jvWJzJx/pCyygIiR1nGM/2PTde0wn7anl71Gxl5fWol7K/WwFEORNjkL3CEyWOyDc+28AA==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-channel-js": "3.0.4", - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-channel-js": "3.3.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/dynamicproto-js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", - "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" } }, "node_modules/@nevware21/ts-async": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", - "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", + "integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + "@nevware21/ts-utils": ">= 0.11.6 < 2.x" } }, "node_modules/@nevware21/ts-utils": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", - "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.6.tgz", + "integrity": "sha512-OUUJTh3fnaUSzg9DEHgv3d7jC+DnPL65mIO7RaR+jWve7+MmcgIvF79gY97DPQ4frH+IpNR78YAYd/dW4gK3kg==", + "license": "MIT" }, "node_modules/@types/vscode-webview": { "version": "1.57.0", @@ -147,13 +157,14 @@ "license": "CC-BY-4.0" }, "node_modules/@vscode/extension-telemetry": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.0.tgz", - "integrity": "sha512-37RxGHXrs3GoXPgCUKQhghEu0gxs8j27RLjQwwtSf4WhPdJKz8UrqMYzpsXlliQ05zURYmtdGZst9C6+hfWXaQ==", + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.8.tgz", + "integrity": "sha512-7YcKoUvmHlIB8QYCE4FNzt3ErHi9gQPhdCM3ZWtpw1bxPT0I+lMdx52KHlzTNoJzQ2NvMX7HyzyDwBEiMgTrWQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "^4.0.3", - "@microsoft/1ds-post-js": "^4.0.3", - "@microsoft/applicationinsights-web-basic": "^3.0.4" + "@microsoft/1ds-core-js": "^4.3.4", + "@microsoft/1ds-post-js": "^4.3.4", + "@microsoft/applicationinsights-web-basic": "^3.3.4" }, "engines": { "vscode": "^1.75.0" diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json index 0d812db6078a..9aba9ad25036 100644 --- a/extensions/simple-browser/package.json +++ b/extensions/simple-browser/package.json @@ -66,7 +66,7 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "dependencies": { - "@vscode/extension-telemetry": "^0.9.0" + "@vscode/extension-telemetry": "^0.9.8" }, "devDependencies": { "@types/vscode-webview": "^1.57.0", diff --git a/extensions/typescript-language-features/package-lock.json b/extensions/typescript-language-features/package-lock.json index 6a19dce700f2..4f97eb1048c4 100644 --- a/extensions/typescript-language-features/package-lock.json +++ b/extensions/typescript-language-features/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "@vscode/sync-api-client": "^0.7.2", "@vscode/sync-api-common": "^0.7.2", "@vscode/sync-api-service": "^0.7.3", @@ -28,118 +28,128 @@ } }, "node_modules/@microsoft/1ds-core-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.0.3.tgz", - "integrity": "sha512-FrxNLVAPsAvD7+l63TlNS/Kodvpct2WulpDSn1dI4Xuy0kF4E2H867kHdwL/iY1Bj3zA3FSy/jvE4+OcDws7ug==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz", + "integrity": "sha512-3gbDUQgAO8EoyQTNcAEkxpuPnioC0May13P1l1l0NKZ128L9Ts/sj8QsfwCRTjHz0HThlA+4FptcAJXNYUy3rg==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/1ds-post-js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.0.3.tgz", - "integrity": "sha512-uewvmUtXKd7ttypiKQGdYI6i7UUpPkOznLayzIFrJ4r2xnG6jhPjpKRncHFXPQcM4XSWO3yf5PQ3xAbPq9t7ZQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.4.tgz", + "integrity": "sha512-nlKjWricDj0Tn68Dt0P8lX9a+X7LYrqJ6/iSfQwMfDhRIGLqW+wxx8gxS+iGWC/oc8zMQAeiZaemUpCwQcwpRQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "4.0.3", + "@microsoft/1ds-core-js": "4.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" } }, "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.4.tgz", - "integrity": "sha512-6TlfExmErQ8Y+/ChbkyWl+jyt4wg3T6p7lwXDsUCB0LgZmlEWMaCUS0YlT73JCWmE8j7vxW8yUm0lgsgmHns3A==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.4.tgz", + "integrity": "sha512-Z4nrxYwGKP9iyrYtm7iPQXVOFy4FsEsX0nDKkAi96Qpgw+vEh6NH4ORxMMuES0EollBQ3faJyvYCwckuCVIj0g==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-common": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.4.tgz", - "integrity": "sha512-r5gWaw/K9+tKfuo2GtDiDiKASgOkPOCrKW+wZzFvuR06uuwvWjbVQ6yW/YbnfuhRF5M65ksUiMi0eCMwEOGq7Q==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.4.tgz", + "integrity": "sha512-4ms16MlIvcP4WiUPqopifNxcWCcrXQJ2ADAK/75uok2mNQe6ZNRsqb/P+pvhUxc8A5HRlvoXPP1ptDSN5Girgw==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.4.tgz", - "integrity": "sha512-anxy5kEkqBmVoEqJiJzaaXXA0wzqZi9U4zGd05xFJ04lWckP8dG3zyT3+GGdg7rDelqLTNGxndeYoFmDv63u1g==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.4.tgz", + "integrity": "sha512-MummANF0mgKIkdvVvfmHQTBliK114IZLRhTL0X0Ep+zjDwWMHqYZgew0nlFKAl6ggu42abPZFK5afpE7qjtYJA==", + "license": "MIT", "dependencies": { "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-shims": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "license": "MIT", "dependencies": { "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.4.tgz", - "integrity": "sha512-KfoxPlLlf0JT12ADb23C5iGye/yFouoMgHEKULxkSQcYY9SsW/8rVrqqvoYKAL+u215CZU2A8Kc8sR3ehEaPCQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.4.tgz", + "integrity": "sha512-OpEPXr8vU/t/M8T9jvWJzJx/pCyygIiR1nGM/2PTde0wn7anl71Gxl5fWol7K/WwFEORNjkL3CEyWOyDc+28AA==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-channel-js": "3.0.4", - "@microsoft/applicationinsights-common": "3.0.4", - "@microsoft/applicationinsights-core-js": "3.0.4", + "@microsoft/applicationinsights-channel-js": "3.3.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", "@microsoft/applicationinsights-shims": "3.0.1", - "@microsoft/dynamicproto-js": "^2.0.2", - "@nevware21/ts-async": ">= 0.3.0 < 2.x", - "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" }, "peerDependencies": { - "tslib": "*" + "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/dynamicproto-js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", - "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" } }, "node_modules/@nevware21/ts-async": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", - "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", + "integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + "@nevware21/ts-utils": ">= 0.11.6 < 2.x" } }, "node_modules/@nevware21/ts-utils": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", - "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.6.tgz", + "integrity": "sha512-OUUJTh3fnaUSzg9DEHgv3d7jC+DnPL65mIO7RaR+jWve7+MmcgIvF79gY97DPQ4frH+IpNR78YAYd/dW4gK3kg==", + "license": "MIT" }, "node_modules/@types/node": { "version": "20.11.24", @@ -157,13 +167,14 @@ "dev": true }, "node_modules/@vscode/extension-telemetry": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.0.tgz", - "integrity": "sha512-37RxGHXrs3GoXPgCUKQhghEu0gxs8j27RLjQwwtSf4WhPdJKz8UrqMYzpsXlliQ05zURYmtdGZst9C6+hfWXaQ==", + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.8.tgz", + "integrity": "sha512-7YcKoUvmHlIB8QYCE4FNzt3ErHi9gQPhdCM3ZWtpw1bxPT0I+lMdx52KHlzTNoJzQ2NvMX7HyzyDwBEiMgTrWQ==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "^4.0.3", - "@microsoft/1ds-post-js": "^4.0.3", - "@microsoft/applicationinsights-web-basic": "^3.0.4" + "@microsoft/1ds-core-js": "^4.3.4", + "@microsoft/1ds-post-js": "^4.3.4", + "@microsoft/applicationinsights-web-basic": "^3.3.4" }, "engines": { "vscode": "^1.75.0" diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index c6363dfc48e1..65038311f76b 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -40,7 +40,7 @@ "Programming Languages" ], "dependencies": { - "@vscode/extension-telemetry": "^0.9.0", + "@vscode/extension-telemetry": "^0.9.8", "@vscode/sync-api-client": "^0.7.2", "@vscode/sync-api-common": "^0.7.2", "@vscode/sync-api-service": "^0.7.3", From 1649b305c65c99dcc7b48ee1bf92453555f73203 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 17 Dec 2024 11:13:06 -0800 Subject: [PATCH 0183/3587] testing: fix can toggle inline test coverage for non-text files outside the workspace (#236386) Fixes #235346 --- .../contrib/testing/browser/codeCoverageDecorations.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts index 0d85cf22e3ab..f82948d31060 100644 --- a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts @@ -39,9 +39,8 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind import { ILogService } from '../../../../platform/log/common/log.js'; import { bindContextKey, observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { IQuickInputService, QuickPickInput } from '../../../../platform/quickinput/common/quickInput.js'; -import * as coverUtils from './codeCoverageDisplayUtils.js'; -import { testingCoverageMissingBranch, testingCoverageReport, testingFilterIcon, testingRerunIcon } from './icons.js'; -import { ManagedTestCoverageBars } from './testCoverageBars.js'; +import { ActiveEditorContext } from '../../../common/contextkeys.js'; +import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js'; import { getTestingConfiguration, TestingConfigKeys } from '../common/configuration.js'; import { TestCommandId } from '../common/constants.js'; import { FileCoverage } from '../common/testCoverage.js'; @@ -50,6 +49,9 @@ import { TestId } from '../common/testId.js'; import { ITestService } from '../common/testService.js'; import { CoverageDetails, DetailType, IDeclarationCoverage, IStatementCoverage } from '../common/testTypes.js'; import { TestingContextKeys } from '../common/testingContextKeys.js'; +import * as coverUtils from './codeCoverageDisplayUtils.js'; +import { testingCoverageMissingBranch, testingCoverageReport, testingFilterIcon, testingRerunIcon } from './icons.js'; +import { ManagedTestCoverageBars } from './testCoverageBars.js'; const CLASS_HIT = 'coverage-deco-hit'; const CLASS_MISS = 'coverage-deco-miss'; @@ -772,6 +774,7 @@ registerAction2(class FilterCoverageToTestInEditor extends Action2 { TestingContextKeys.isTestCoverageOpen, TestingContextKeys.coverageToolbarEnabled.notEqualsTo(true), TestingContextKeys.hasPerTestCoverage, + ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID), ), group: 'navigation', }, From 9bb364f3328e5fda91c2e3b77a557e2927653030 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 17 Dec 2024 11:17:01 -0800 Subject: [PATCH 0184/3587] testing: fix scrollbar overlaps coverage indicators (#236387) Fixes #235343 --- src/vs/workbench/contrib/testing/browser/media/testing.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 2ebd1fc35bca..d56ac88a1851 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -481,7 +481,7 @@ align-items: center; gap: 4px; font-size: 11px; - margin-right: 0.8em; + margin-right: 12px; } .test-coverage-bars .bar { From 6baeecd419a5197b1077d3a8e5e16e922c558258 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Tue, 17 Dec 2024 11:23:55 -0800 Subject: [PATCH 0185/3587] Add my labels (#236391) --- .vscode/notebooks/grooming.github-issues | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/notebooks/grooming.github-issues b/.vscode/notebooks/grooming.github-issues index e408bbd74215..6d9deb71792d 100644 --- a/.vscode/notebooks/grooming.github-issues +++ b/.vscode/notebooks/grooming.github-issues @@ -27,7 +27,7 @@ { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode assignee:$assignee is:open type:issue -label:\"info-needed\" -label:api -label:api-finalization -label:api-proposal -label:authentication -label:bisect-ext -label:bracket-pair-colorization -label:bracket-pair-guides -label:breadcrumbs -label:callhierarchy -label:chrome-devtools -label:cloud-changes -label:code-lens -label:command-center -label:comments -label:config -label:containers -label:context-keys -label:continue-working-on -label:css-less-scss -label:custom-editors -label:debug -label:debug-disassembly -label:dialogs -label:diff-editor -label:dropdown -label:editor-api -label:editor-autoclosing -label:editor-autoindent -label:editor-bracket-matching -label:editor-clipboard -label:editor-code-actions -label:editor-color-picker -label:editor-columnselect -label:editor-commands -label:editor-comments -label:editor-contrib -label:editor-core -label:editor-drag-and-drop -label:editor-error-widget -label:editor-find -label:editor-folding -label:editor-highlight -label:editor-hover -label:editor-indent-detection -label:editor-indent-guides -label:editor-input -label:editor-input-IME -label:editor-insets -label:editor-minimap -label:editor-multicursor -label:editor-parameter-hints -label:editor-render-whitespace -label:editor-rendering -label:editor-widgets -label:editor-RTL -label:editor-scrollbar -label:editor-sorting -label:editor-sticky-scroll -label:editor-symbols -label:editor-synced-region -label:editor-textbuffer -label:editor-theming -label:editor-wordnav -label:editor-wrapping -label:emmet -label:emmet-parse -label:error-list -label:extension-activation -label:extension-host -label:extension-prerelease -label:extension-recommendations -label:extensions -label:extensions-development -label:file-decorations -label:file-encoding -label:file-explorer -label:file-glob -label:file-io -label:file-nesting -label:file-watcher -label:font-rendering -label:formatting -label:getting-started -label:ghost-text -label:git -label:github -label:github-repositories -label:gpu -label:grammar -label:grid-widget -label:html -label:icon-brand -label:icons-product -label:image-preview -label:inlay-hints -label:inline-completions -label:install-update -label:intellisense-config -label:interactive-playground -label:interactive-window -label:issue-bot -label:issue-reporter -label:javascript -label:json -label:keybindings -label:keybindings-editor -label:keyboard-layout -label:chat -label:l10n-platform -label:label-provider -label:languages-basic -label:languages-diagnostics -label:languages-guessing -label:layout -label:lcd-text-rendering -label:list-widget -label:live-preview -label:log -label:markdown -label:marketplace -label:menus -label:merge-conflict -label:merge-editor -label:merge-editor-workbench -label:monaco-editor -label:native-file-dialog -label:network -label:notebook -label:notebook-accessibility -label:notebook-api -label:notebook-cell-editor -label:notebook-celltoolbar -label:notebook-clipboard -label:notebook-commands -label:notebook-debugging -label:notebook-diff -label:notebook-dnd -label:notebook-execution -label:notebook-find -label:notebook-folding -label:notebook-getting-started -label:notebook-globaltoolbar -label:notebook-ipynb -label:notebook-kernel -label:notebook-kernel-picker -label:notebook-language -label:notebook-layout -label:notebook-markdown -label:notebook-output -label:notebook-perf -label:notebook-remote -label:notebook-serialization -label:notebook-statusbar -label:notebook-toc-outline -label:notebook-undo-redo -label:notebook-variables -label:notebook-workbench-integration -label:notebook-workflow -label:notebook-sticky-scroll -label:notebook-format -label:notebook-code-actions -label:open-editors -label:opener -label:outline -label:output -label:packaging -label:perf -label:perf-bloat -label:perf-startup -label:php -label:portable-mode -label:proxy -label:quick-open -label:quick-pick -label:references-viewlet -label:release-notes -label:remote -label:remote-connection -label:remote-explorer -label:remote-tunnel -label:rename -label:runCommands -label:sandbox -label:sash-widget -label:scm -label:screencast-mode -label:search -label:search-api -label:search-editor -label:search-replace -label:semantic-tokens -label:server -label:settings-editor -label:settings-sync -label:settings-sync-server -label:shared-process -label:simple-file-dialog -label:smart-select -label:snap -label:snippets -label:splitview-widget -label:ssh -label:suggest -label:table-widget -label:tasks -label:telemetry -label:terminal -label:terminal-accessibility -label:terminal-conpty -label:terminal-editors -label:terminal-external -label:terminal-find -label:terminal-input -label:terminal-layout -label:terminal-links -label:terminal-local-echo -label:terminal-persistence -label:terminal-process -label:terminal-profiles -label:terminal-quick-fix -label:terminal-rendering -label:terminal-shell-bash -label:terminal-shell-cmd -label:terminal-shell-fish -label:terminal-shell-git-bash -label:terminal-shell-integration -label:terminal-shell-pwsh -label:terminal-shell-zsh -label:terminal-sticky-scroll -label:terminal-tabs -label:testing -label:themes -label:timeline -label:timeline-git -label:timeline-local-history -label:titlebar -label:tokenization -label:touch/pointer -label:trackpad/scroll -label:tree-views -label:tree-widget -label:typescript -label:undo-redo -label:unicode-highlight -label:uri -label:user-profiles -label:ux -label:variable-resolving -label:VIM -label:virtual-workspaces -label:vscode-website -label:vscode.dev -label:web -label:webview -label:webview-views -label:workbench-actions -label:workbench-banner -label:workbench-cli -label:workbench-diagnostics -label:workbench-dnd -label:workbench-editor-grid -label:workbench-editor-groups -label:workbench-editor-resolver -label:workbench-editors -label:workbench-electron -label:workbench-fonts -label:workbench-history -label:workbench-hot-exit -label:workbench-hover -label:workbench-launch -label:workbench-link -label:workbench-multiroot -label:workbench-notifications -label:workbench-os-integration -label:workbench-rapid-render -label:workbench-run-as-admin -label:workbench-state -label:workbench-status -label:workbench-tabs -label:workbench-touchbar -label:workbench-untitled-editors -label:workbench-views -label:workbench-welcome -label:workbench-window -label:workbench-workspace -label:workbench-zen -label:workspace-edit -label:workspace-symbols -label:workspace-trust -label:zoom -label:inline-chat -label:panel-chat -label:quick-chat -label:tasks -label:error-list -label:winget -label:tree-views -label:freeze-slow-crash-leak -label:engineering -label:cross-file-editing" + "value": "repo:microsoft/vscode assignee:$assignee is:open type:issue -label:\"info-needed\" -label:api -label:api-finalization -label:api-proposal -label:authentication -label:bisect-ext -label:bracket-pair-colorization -label:bracket-pair-guides -label:breadcrumbs -label:callhierarchy -label:chrome-devtools -label:cloud-changes -label:code-lens -label:command-center -label:comments -label:config -label:containers -label:context-keys -label:continue-working-on -label:css-less-scss -label:custom-editors -label:debug -label:debug-disassembly -label:dialogs -label:diff-editor -label:dropdown -label:editor-api -label:editor-autoclosing -label:editor-autoindent -label:editor-bracket-matching -label:editor-clipboard -label:editor-code-actions -label:editor-color-picker -label:editor-columnselect -label:editor-commands -label:editor-comments -label:editor-contrib -label:editor-core -label:editor-drag-and-drop -label:editor-error-widget -label:editor-find -label:editor-folding -label:editor-highlight -label:editor-hover -label:editor-indent-detection -label:editor-indent-guides -label:editor-input -label:editor-input-IME -label:editor-insets -label:editor-minimap -label:editor-multicursor -label:editor-parameter-hints -label:editor-render-whitespace -label:editor-rendering -label:editor-widgets -label:editor-RTL -label:editor-scrollbar -label:editor-sorting -label:editor-sticky-scroll -label:editor-symbols -label:editor-synced-region -label:editor-textbuffer -label:editor-theming -label:editor-wordnav -label:editor-wrapping -label:emmet -label:emmet-parse -label:error-list -label:extension-activation -label:extension-host -label:extension-prerelease -label:extension-recommendations -label:extensions -label:extensions-development -label:file-decorations -label:file-encoding -label:file-explorer -label:file-glob -label:file-io -label:file-nesting -label:file-watcher -label:font-rendering -label:formatting -label:getting-started -label:ghost-text -label:git -label:github -label:github-repositories -label:gpu -label:grammar -label:grid-widget -label:html -label:icon-brand -label:icons-product -label:image-preview -label:inlay-hints -label:inline-completions -label:install-update -label:intellisense-config -label:interactive-playground -label:interactive-window -label:issue-bot -label:issue-reporter -label:javascript -label:json -label:keybindings -label:keybindings-editor -label:keyboard-layout -label:chat -label:l10n-platform -label:label-provider -label:languages-basic -label:languages-diagnostics -label:languages-guessing -label:layout -label:lcd-text-rendering -label:list-widget -label:live-preview -label:log -label:markdown -label:marketplace -label:menus -label:merge-conflict -label:merge-editor -label:merge-editor-workbench -label:monaco-editor -label:native-file-dialog -label:network -label:notebook -label:notebook-accessibility -label:notebook-api -label:notebook-cell-editor -label:notebook-celltoolbar -label:notebook-clipboard -label:notebook-commands -label:notebook-debugging -label:notebook-diff -label:notebook-dnd -label:notebook-execution -label:notebook-find -label:notebook-folding -label:notebook-getting-started -label:notebook-globaltoolbar -label:notebook-ipynb -label:notebook-kernel -label:notebook-kernel-picker -label:notebook-language -label:notebook-layout -label:notebook-markdown -label:notebook-output -label:notebook-perf -label:notebook-remote -label:notebook-serialization -label:notebook-statusbar -label:notebook-toc-outline -label:notebook-undo-redo -label:notebook-variables -label:notebook-workbench-integration -label:notebook-workflow -label:notebook-sticky-scroll -label:notebook-format -label:notebook-code-actions -label:open-editors -label:opener -label:outline -label:output -label:packaging -label:perf -label:perf-bloat -label:perf-startup -label:php -label:portable-mode -label:proxy -label:quick-open -label:quick-pick -label:references-viewlet -label:release-notes -label:remote -label:remote-connection -label:remote-explorer -label:remote-tunnel -label:rename -label:runCommands -label:sandbox -label:sash-widget -label:scm -label:screencast-mode -label:search -label:search-api -label:search-editor -label:search-replace -label:semantic-tokens -label:server -label:settings-editor -label:settings-sync -label:settings-sync-server -label:shared-process -label:simple-file-dialog -label:smart-select -label:snap -label:snippets -label:splitview-widget -label:ssh -label:suggest -label:table-widget -label:tasks -label:telemetry -label:terminal -label:terminal-accessibility -label:terminal-conpty -label:terminal-editors -label:terminal-external -label:terminal-find -label:terminal-input -label:terminal-layout -label:terminal-links -label:terminal-local-echo -label:terminal-persistence -label:terminal-process -label:terminal-profiles -label:terminal-quick-fix -label:terminal-rendering -label:terminal-shell-bash -label:terminal-shell-cmd -label:terminal-shell-fish -label:terminal-shell-git-bash -label:terminal-shell-integration -label:terminal-shell-pwsh -label:terminal-shell-zsh -label:terminal-sticky-scroll -label:terminal-tabs -label:testing -label:themes -label:timeline -label:timeline-git -label:timeline-local-history -label:titlebar -label:tokenization -label:touch/pointer -label:trackpad/scroll -label:tree-views -label:tree-widget -label:typescript -label:undo-redo -label:unicode-highlight -label:uri -label:user-profiles -label:ux -label:variable-resolving -label:VIM -label:virtual-workspaces -label:vscode-website -label:vscode.dev -label:web -label:webview -label:webview-views -label:workbench-actions -label:workbench-banner -label:workbench-cli -label:workbench-diagnostics -label:workbench-dnd -label:workbench-editor-grid -label:workbench-editor-groups -label:workbench-editor-resolver -label:workbench-editors -label:workbench-electron -label:workbench-fonts -label:workbench-history -label:workbench-hot-exit -label:workbench-hover -label:workbench-launch -label:workbench-link -label:workbench-multiroot -label:workbench-notifications -label:workbench-os-integration -label:workbench-rapid-render -label:workbench-run-as-admin -label:workbench-state -label:workbench-status -label:workbench-tabs -label:workbench-touchbar -label:workbench-untitled-editors -label:workbench-views -label:workbench-welcome -label:workbench-window -label:workbench-workspace -label:workbench-zen -label:workspace-edit -label:workspace-symbols -label:workspace-trust -label:zoom -label:inline-chat -label:panel-chat -label:quick-chat -label:tasks -label:error-list -label:winget -label:tree-views -label:freeze-slow-crash-leak -label:engineering -label:cross-file-editing -label:microsoft-authentication -label:github-authentication -label:lm-access -label:secret-storage" }, { "kind": 1, From 7722c2bb0f4b17e1fa4cee6a24d3f0f438348ba9 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:27:55 +0100 Subject: [PATCH 0186/3587] Git - adjust command `when` clauses (#236392) * Add telemetry for troubleshooting * Adjust command when clause --- extensions/git/package.json | 24 ++++++++++++------------ extensions/git/src/commands.ts | 15 ++++++++++++++- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 8b8f52643a89..08349fd47837 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -943,19 +943,19 @@ "command": "git.stageSelectedRanges", "key": "ctrl+k ctrl+alt+s", "mac": "cmd+k cmd+alt+s", - "when": "editorTextFocus && resourceScheme =~ /^git$|^file$/" + "when": "editorTextFocus && resourceScheme == file" }, { "command": "git.unstageSelectedRanges", "key": "ctrl+k ctrl+n", "mac": "cmd+k cmd+n", - "when": "isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "editorTextFocus && isInDiffEditor && isInDiffRightEditor && resourceScheme == git" }, { "command": "git.revertSelectedRanges", "key": "ctrl+k ctrl+r", "mac": "cmd+k cmd+r", - "when": "editorTextFocus && resourceScheme =~ /^git$|^file$/" + "when": "editorTextFocus && resourceScheme == file" } ], "menus": { @@ -1026,7 +1026,7 @@ }, { "command": "git.stageSelectedRanges", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme =~ /^git$|^file$/" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme == file" }, { "command": "git.stageChange", @@ -1034,7 +1034,7 @@ }, { "command": "git.revertSelectedRanges", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme =~ /^git$|^file$/" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme == file" }, { "command": "git.revertChange", @@ -1054,7 +1054,7 @@ }, { "command": "git.unstageSelectedRanges", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && resourceScheme == git" }, { "command": "git.clean", @@ -2068,17 +2068,17 @@ { "command": "git.stageSelectedRanges", "group": "2_git@1", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file" }, { "command": "git.unstageSelectedRanges", "group": "2_git@2", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == git" }, { "command": "git.revertSelectedRanges", "group": "2_git@3", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file" }, { "command": "git.stashApplyEditor", @@ -2096,17 +2096,17 @@ { "command": "git.stageSelectedRanges", "group": "2_git@1", - "when": "isInDiffRightEditor && !isEmbeddedDiffEditor && config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file" }, { "command": "git.unstageSelectedRanges", "group": "2_git@2", - "when": "isInDiffRightEditor && !isEmbeddedDiffEditor && config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == git" }, { "command": "git.revertSelectedRanges", "group": "2_git@3", - "when": "isInDiffRightEditor && !isEmbeddedDiffEditor && config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file" } ], "editor/content": [ diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 64905cd55f36..8a3139312015 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1563,12 +1563,16 @@ export class CommandCenter { return; } + this.logger.trace(`[CommandCenter][stageSelectedChanges] changes: ${JSON.stringify(changes)}`); + const modifiedDocument = textEditor.document; const selectedLines = toLineRanges(textEditor.selections, modifiedDocument); const selectedChanges = changes .map(change => selectedLines.reduce((result, range) => result || intersectDiffWithRange(modifiedDocument, change, range), null)) .filter(d => !!d) as LineChange[]; + this.logger.trace(`[CommandCenter][stageSelectedChanges] selectedChanges: ${JSON.stringify(selectedChanges)}`); + if (!selectedChanges.length) { window.showInformationMessage(l10n.t('The selection range does not contain any changes.')); return; @@ -1745,6 +1749,8 @@ export class CommandCenter { return; } + this.logger.trace(`[CommandCenter][revertSelectedRanges] changes: ${JSON.stringify(changes)}`); + const modifiedDocument = textEditor.document; const selections = textEditor.selections; const selectedChanges = changes.filter(change => { @@ -1757,6 +1763,8 @@ export class CommandCenter { return; } + this.logger.trace(`[CommandCenter][revertSelectedRanges] selectedChanges: ${JSON.stringify(selectedChanges)}`); + const selectionsBeforeRevert = textEditor.selections; await this._revertChanges(textEditor, selectedChanges); textEditor.selections = selectionsBeforeRevert; @@ -1835,6 +1843,8 @@ export class CommandCenter { return; } + this.logger.trace(`[CommandCenter][unstageSelectedRanges] changes: ${JSON.stringify(changes)}`); + const originalUri = toGitUri(modifiedUri, 'HEAD'); const originalDocument = await workspace.openTextDocument(originalUri); const selectedLines = toLineRanges(textEditor.selections, modifiedDocument); @@ -1848,8 +1858,11 @@ export class CommandCenter { } const invertedDiffs = selectedDiffs.map(invertLineChange); - const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs); + this.logger.trace(`[CommandCenter][unstageSelectedRanges] selectedDiffs: ${JSON.stringify(selectedDiffs)}`); + this.logger.trace(`[CommandCenter][unstageSelectedRanges] invertedDiffs: ${JSON.stringify(invertedDiffs)}`); + + const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs); await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result)); } From 7cc28c3e81227c60347c0ce507f6ee2283732c38 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 17 Dec 2024 11:30:32 -0800 Subject: [PATCH 0187/3587] Add markdown validation status item Fixes #236399 --- .../src/languageFeatures/diagnostics.ts | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts index f54c136ad1a4..8ea0cac41c12 100644 --- a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts @@ -72,10 +72,61 @@ class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider { } } +function registerMarkdownStatusItem(selector: vscode.DocumentSelector, commandManager: CommandManager): vscode.Disposable { + const statusItem = vscode.languages.createLanguageStatusItem('markdownStatus', selector); + + const enabledSettingId = 'validate.enabled'; + const commandId = '_markdown.toggleValidation'; + + const commandSub = commandManager.register({ + id: commandId, + execute: (enabled: boolean) => { + vscode.workspace.getConfiguration('markdown').update(enabledSettingId, enabled); + } + }); + + const update = () => { + const activeDoc = vscode.window.activeTextEditor?.document; + const markdownDoc = activeDoc?.languageId === 'markdown' ? activeDoc : undefined; + + const enabled = vscode.workspace.getConfiguration('markdown', markdownDoc).get(enabledSettingId); + if (enabled) { + statusItem.text = vscode.l10n.t('Link validation enabled'); + statusItem.command = { + command: commandId, + arguments: [false], + title: vscode.l10n.t('Disable'), + tooltip: vscode.l10n.t('Disable validation of Markdown links'), + }; + } else { + statusItem.text = vscode.l10n.t('Link validation disabled'); + statusItem.command = { + command: commandId, + arguments: [true], + title: vscode.l10n.t('Enable'), + tooltip: vscode.l10n.t('Enable validation of Markdown links'), + }; + } + }; + update(); + + return vscode.Disposable.from( + statusItem, + commandSub, + vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('markdown.' + enabledSettingId)) { + update(); + } + }), + ); +} export function registerDiagnosticSupport( selector: vscode.DocumentSelector, commandManager: CommandManager, ): vscode.Disposable { - return AddToIgnoreLinksQuickFixProvider.register(selector, commandManager); + return vscode.Disposable.from( + AddToIgnoreLinksQuickFixProvider.register(selector, commandManager), + registerMarkdownStatusItem(selector, commandManager), + ); } From a47b13ebc2d1b5db4d815ed9cde86278248cfaa7 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 17 Dec 2024 11:59:05 -0800 Subject: [PATCH 0188/3587] Small cleanup follow up on #236145 - Don't send content as json - Reuse existing load helper --- .../preview-src/index.ts | 21 +++++++++---------- .../preview-src/settings.ts | 8 +++++-- .../src/preview/documentRenderer.ts | 2 +- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/extensions/markdown-language-features/preview-src/index.ts b/extensions/markdown-language-features/preview-src/index.ts index 336245a1fdfd..f9a0abc4f537 100644 --- a/extensions/markdown-language-features/preview-src/index.ts +++ b/extensions/markdown-language-features/preview-src/index.ts @@ -7,7 +7,7 @@ import { ActiveLineMarker } from './activeLineMarker'; import { onceDocumentLoaded } from './events'; import { createPosterForVsCode } from './messaging'; import { getEditorLineNumberForPageOffset, scrollToRevealSourceLine, getLineElementForFragment } from './scroll-sync'; -import { SettingsManager, getData } from './settings'; +import { SettingsManager, getData, getRawData } from './settings'; import throttle = require('lodash.throttle'); import morphdom from 'morphdom'; import type { ToWebviewMessage } from '../types/previewMessaging'; @@ -61,8 +61,16 @@ function doAfterImagesLoaded(cb: () => void) { } onceDocumentLoaded(() => { - const scrollProgress = state.scrollProgress; + // Load initial html + const htmlParser = new DOMParser(); + const markDownHtml = htmlParser.parseFromString( + getRawData('data-initial-md-content'), + 'text/html' + ); + document.body.appendChild(markDownHtml.body); + // Restore + const scrollProgress = state.scrollProgress; addImageContexts(); if (typeof scrollProgress === 'number' && !settings.settings.fragment) { doAfterImagesLoaded(() => { @@ -353,15 +361,6 @@ document.addEventListener('click', event => { } }, true); -window.addEventListener('load', () => { - const htmlParser = new DOMParser(); - const markDownHtml = htmlParser.parseFromString( - decodeURIComponent(getData('data-md-content')), - 'text/html' - ); - document.body.appendChild(markDownHtml.body); -}); - window.addEventListener('scroll', throttle(() => { updateScrollProgress(); diff --git a/extensions/markdown-language-features/preview-src/settings.ts b/extensions/markdown-language-features/preview-src/settings.ts index 1bbe3477f254..0fb5d0c2686c 100644 --- a/extensions/markdown-language-features/preview-src/settings.ts +++ b/extensions/markdown-language-features/preview-src/settings.ts @@ -16,18 +16,22 @@ export interface PreviewSettings { readonly webviewResourceRoot: string; } -export function getData(key: string): T { +export function getRawData(key: string): string { const element = document.getElementById('vscode-markdown-preview-data'); if (element) { const data = element.getAttribute(key); if (data) { - return JSON.parse(data); + return data; } } throw new Error(`Could not load data for ${key}`); } +export function getData(key: string): T { + return JSON.parse(getRawData(key)); +} + export class SettingsManager { private _settings: PreviewSettings = getData('data-settings'); diff --git a/extensions/markdown-language-features/src/preview/documentRenderer.ts b/extensions/markdown-language-features/src/preview/documentRenderer.ts index 13e709c765f0..eeab8e19d9d6 100644 --- a/extensions/markdown-language-features/src/preview/documentRenderer.ts +++ b/extensions/markdown-language-features/src/preview/documentRenderer.ts @@ -99,7 +99,7 @@ export class MdDocumentRenderer { data-settings="${escapeAttribute(JSON.stringify(initialData))}" data-strings="${escapeAttribute(JSON.stringify(previewStrings))}" data-state="${escapeAttribute(JSON.stringify(state || {}))}" - data-md-content="${escapeAttribute(JSON.stringify(encodeURIComponent(body.html)))}"> + data-initial-md-content="${escapeAttribute(body.html)}"> ${this._getStyles(resourceProvider, sourceUri, config, imageInfo)} From 20dc4d7d2647c3f69a55fd971f66c273666959a3 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 17 Dec 2024 21:37:33 +0100 Subject: [PATCH 0189/3587] Git - add more tracing to stage/unstage/revert commands (#236409) --- extensions/git/src/blame.ts | 13 ++++----- extensions/git/src/commands.ts | 26 +++++++++++++++++- extensions/git/src/staging.ts | 50 +++++++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 10 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 916d906a0a21..5c65fbcf1fcc 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString, TextEditorDiffInformation } from 'vscode'; +import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString } from 'vscode'; import { Model } from './model'; import { dispose, fromNow, IDisposable } from './util'; import { Repository } from './repository'; @@ -11,6 +11,7 @@ import { throttle } from './decorators'; import { BlameInformation } from './git'; import { fromGitUri, isGitUri } from './uri'; import { emojify, ensureEmojis } from './emoji'; +import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation } from './staging'; function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean { return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive); @@ -277,10 +278,6 @@ export class GitBlameController { return blameInformation; } - private _findDiffInformation(textEditor: TextEditor, ref: string): TextEditorDiffInformation | undefined { - return textEditor.diffInformation?.find(diff => diff.original && isGitUri(diff.original) && fromGitUri(diff.original).ref === ref); - } - @throttle private async _updateTextEditorBlameInformation(textEditor: TextEditor | undefined, showBlameInformationForPositionZero = false): Promise { if (!textEditor?.diffInformation || textEditor !== window.activeTextEditor) { @@ -319,7 +316,7 @@ export class GitBlameController { workingTreeAndIndexChanges = undefined; } else if (ref === '') { // Resource on the right-hand side of the diff editor when viewing a resource from the index. - const diffInformationWorkingTreeAndIndex = this._findDiffInformation(textEditor, 'HEAD'); + const diffInformationWorkingTreeAndIndex = getWorkingTreeAndIndexDiffInformation(textEditor); // Working tree + index diff information is present and it is stale if (diffInformationWorkingTreeAndIndex && diffInformationWorkingTreeAndIndex.isStale) { @@ -333,7 +330,7 @@ export class GitBlameController { } } else { // Working tree diff information. Diff Editor (Working Tree) -> Text Editor - const diffInformationWorkingTree = this._findDiffInformation(textEditor, '~') ?? this._findDiffInformation(textEditor, ''); + const diffInformationWorkingTree = getWorkingTreeDiffInformation(textEditor); // Working tree diff information is not present or it is stale if (!diffInformationWorkingTree || diffInformationWorkingTree.isStale) { @@ -341,7 +338,7 @@ export class GitBlameController { } // Working tree + index diff information - const diffInformationWorkingTreeAndIndex = this._findDiffInformation(textEditor, 'HEAD'); + const diffInformationWorkingTreeAndIndex = getWorkingTreeAndIndexDiffInformation(textEditor); // Working tree + index diff information is present and it is stale if (diffInformationWorkingTreeAndIndex && diffInformationWorkingTreeAndIndex.isStale) { diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 8a3139312015..dde1c99049a8 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -12,7 +12,7 @@ import { ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, Remo import { Git, Stash } from './git'; import { Model } from './model'; import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository'; -import { DiffEditorSelectionHunkToolbarContext, applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineChange, toLineRanges } from './staging'; +import { DiffEditorSelectionHunkToolbarContext, applyLineChanges, getModifiedRange, getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges } from './staging'; import { fromGitUri, toGitUri, isGitUri, toMergeUris, toMultiFileDiffEditorUris } from './uri'; import { dispose, grep, isDefined, isDescendant, pathEquals, relativePath, truncate } from './util'; import { GitTimelineItem } from './timelineProvider'; @@ -1565,6 +1565,12 @@ export class CommandCenter { this.logger.trace(`[CommandCenter][stageSelectedChanges] changes: ${JSON.stringify(changes)}`); + const workingTreeDiffInformation = getWorkingTreeDiffInformation(textEditor); + if (workingTreeDiffInformation) { + this.logger.trace(`[CommandCenter][stageSelectedChanges] diffInformation: ${JSON.stringify(workingTreeDiffInformation)}`); + this.logger.trace(`[CommandCenter][stageSelectedChanges] diffInformation changes: ${JSON.stringify(toLineChanges(workingTreeDiffInformation))}`); + } + const modifiedDocument = textEditor.document; const selectedLines = toLineRanges(textEditor.selections, modifiedDocument); const selectedChanges = changes @@ -1751,6 +1757,12 @@ export class CommandCenter { this.logger.trace(`[CommandCenter][revertSelectedRanges] changes: ${JSON.stringify(changes)}`); + const workingTreeDiffInformation = getWorkingTreeDiffInformation(textEditor); + if (workingTreeDiffInformation) { + this.logger.trace(`[CommandCenter][revertSelectedRanges] diffInformation: ${JSON.stringify(workingTreeDiffInformation)}`); + this.logger.trace(`[CommandCenter][revertSelectedRanges] diffInformation changes: ${JSON.stringify(toLineChanges(workingTreeDiffInformation))}`); + } + const modifiedDocument = textEditor.document; const selections = textEditor.selections; const selectedChanges = changes.filter(change => { @@ -1845,6 +1857,18 @@ export class CommandCenter { this.logger.trace(`[CommandCenter][unstageSelectedRanges] changes: ${JSON.stringify(changes)}`); + const workingTreeDiffInformation = getWorkingTreeDiffInformation(textEditor); + if (workingTreeDiffInformation) { + this.logger.trace(`[CommandCenter][unstageSelectedRanges] diffInformation (working tree): ${JSON.stringify(workingTreeDiffInformation)}`); + this.logger.trace(`[CommandCenter][unstageSelectedRanges] diffInformation changes (working tree): ${JSON.stringify(toLineChanges(workingTreeDiffInformation))}`); + } + + const workingTreeAndIndexDiffInformation = getWorkingTreeAndIndexDiffInformation(textEditor); + if (workingTreeAndIndexDiffInformation) { + this.logger.trace(`[CommandCenter][unstageSelectedRanges] diffInformation (working tree + index): ${JSON.stringify(workingTreeAndIndexDiffInformation)}`); + this.logger.trace(`[CommandCenter][unstageSelectedRanges] diffInformation changes (working tree + index): ${JSON.stringify(toLineChanges(workingTreeAndIndexDiffInformation))}`); + } + const originalUri = toGitUri(modifiedUri, 'HEAD'); const originalDocument = await workspace.openTextDocument(originalUri); const selectedLines = toLineRanges(textEditor.selections, modifiedDocument); diff --git a/extensions/git/src/staging.ts b/extensions/git/src/staging.ts index 2dcc6d54487a..38a462aedf62 100644 --- a/extensions/git/src/staging.ts +++ b/extensions/git/src/staging.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TextDocument, Range, LineChange, Selection, Uri } from 'vscode'; +import { TextDocument, Range, LineChange, Selection, Uri, TextEditor, TextEditorDiffInformation } from 'vscode'; +import { fromGitUri, isGitUri } from './uri'; export function applyLineChanges(original: TextDocument, modified: TextDocument, diffs: LineChange[]): string { const result: string[] = []; @@ -143,6 +144,53 @@ export function invertLineChange(diff: LineChange): LineChange { }; } +export function toLineChanges(diffInformation: TextEditorDiffInformation): LineChange[] { + return diffInformation.changes.map(x => { + let originalStartLineNumber: number; + let originalEndLineNumber: number; + let modifiedStartLineNumber: number; + let modifiedEndLineNumber: number; + + if (x.original.startLineNumber === x.original.endLineNumberExclusive) { + // Insertion + originalStartLineNumber = x.original.startLineNumber - 1; + originalEndLineNumber = 0; + } else { + originalStartLineNumber = x.original.startLineNumber; + originalEndLineNumber = x.original.endLineNumberExclusive - 1; + } + + if (x.modified.startLineNumber === x.modified.endLineNumberExclusive) { + // Deletion + modifiedStartLineNumber = x.modified.startLineNumber - 1; + modifiedEndLineNumber = 0; + } else { + modifiedStartLineNumber = x.modified.startLineNumber; + modifiedEndLineNumber = x.modified.endLineNumberExclusive - 1; + } + + return { + originalStartLineNumber, + originalEndLineNumber, + modifiedStartLineNumber, + modifiedEndLineNumber + }; + }); +} + +export function getWorkingTreeDiffInformation(textEditor: TextEditor): TextEditorDiffInformation | undefined { + // Working tree diff information. Diff Editor (Working Tree) -> Text Editor + return getDiffInformation(textEditor, '~') ?? getDiffInformation(textEditor, ''); +} + +export function getWorkingTreeAndIndexDiffInformation(textEditor: TextEditor): TextEditorDiffInformation | undefined { + return getDiffInformation(textEditor, 'HEAD'); +} + +function getDiffInformation(textEditor: TextEditor, ref: string): TextEditorDiffInformation | undefined { + return textEditor.diffInformation?.find(diff => diff.original && isGitUri(diff.original) && fromGitUri(diff.original).ref === ref); +} + export interface DiffEditorSelectionHunkToolbarContext { mapping: unknown; /** From 999f28e49fee2e38050fd1a9c6ab37aa76841d6a Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Tue, 17 Dec 2024 12:39:39 -0800 Subject: [PATCH 0190/3587] Extend hover styling for entire sticky line (#236410) extend hover styling for entire sticky line --- .../browser/media/notebookEditorStickyScroll.css | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookEditorStickyScroll.css b/src/vs/workbench/contrib/notebook/browser/media/notebookEditorStickyScroll.css index 25fa90546d69..a723d49cfe0d 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebookEditorStickyScroll.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookEditorStickyScroll.css @@ -38,10 +38,8 @@ .monaco-workbench .notebookOverlay .notebook-sticky-scroll-container - .notebook-sticky-scroll-element - .notebook-sticky-scroll-header:hover { + .notebook-sticky-scroll-element:hover { background-color: var(--vscode-editorStickyScrollHover-background); - width: 100%; cursor: pointer; } @@ -55,13 +53,11 @@ .monaco-workbench.hc-light .notebookOverlay .notebook-sticky-scroll-container - .notebook-sticky-scroll-element - .notebook-sticky-scroll-header:hover, + .notebook-sticky-scroll-element:hover, .monaco-workbench.hc-black .notebookOverlay .notebook-sticky-scroll-container - .notebook-sticky-scroll-element - .notebook-sticky-scroll-header:hover { + .notebook-sticky-scroll-element:hover { outline: 1px dashed var(--vscode-contrastActiveBorder); outline-offset: -2px; } From b0e6e13be600c35f7444f16b4c5f59eb503ec2f1 Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Tue, 17 Dec 2024 13:34:38 -0800 Subject: [PATCH 0191/3587] notebook find replace position vs global toolbar (#236407) * notebook find replace position vs global toolbar * use runAndSubscribe --- .../contrib/find/notebookFindReplaceWidget.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts index 6e8651a4a32d..8509900d4b07 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -20,6 +20,7 @@ import { Widget } from '../../../../../../base/browser/ui/widget.js'; import { Action, ActionRunner, IAction, IActionRunner, Separator } from '../../../../../../base/common/actions.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; +import { Event } from '../../../../../../base/common/event.js'; import { KeyCode } from '../../../../../../base/common/keyCodes.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { isSafari } from '../../../../../../base/common/platform.js'; @@ -345,6 +346,17 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._domNode = document.createElement('div'); this._domNode.classList.add('simple-fr-find-part-wrapper'); + + this._register(Event.runAndSubscribe(this._configurationService.onDidChangeConfiguration, e => { + if (!e || e.affectsConfiguration(NotebookSetting.globalToolbar)) { + if (this._notebookEditor.notebookOptions.getLayoutConfiguration().globalToolbar) { + this._domNode.style.top = '26px'; + } else { + this._domNode.style.top = '0px'; + } + } + })); + this._register(this._state.onFindReplaceStateChange((e) => this._onStateChanged(e))); this._scopedContextKeyService = contextKeyService.createScoped(this._domNode); From cece885ae0d18ae017007ccdd7fb808fa23d6b1e Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 17 Dec 2024 14:59:26 -0800 Subject: [PATCH 0192/3587] testing: show item when coverage is filtered, standardize some tree classes (#236418) Fixes #235147 --- src/vs/platform/actions/common/actions.ts | 1 + .../contrib/testing/browser/media/testing.css | 78 ++++++------ .../testing/browser/testCoverageView.ts | 113 +++++++++++++++--- .../testResultsView/testResultsTree.ts | 8 +- .../testResultsView/testResultsViewContent.ts | 2 +- .../testing/browser/testingExplorerView.ts | 4 +- 6 files changed, 137 insertions(+), 69 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 7f00003c0f30..2b784b5bf5e1 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -139,6 +139,7 @@ export class MenuId { static readonly TestPeekElement = new MenuId('TestPeekElement'); static readonly TestPeekTitle = new MenuId('TestPeekTitle'); static readonly TestCallStack = new MenuId('TestCallStack'); + static readonly TestCoverageFilterItem = new MenuId('TestCoverageFilterItem'); static readonly TouchBarContext = new MenuId('TouchBarContext'); static readonly TitleBarContext = new MenuId('TitleBarContext'); static readonly TitleBarTitleContext = new MenuId('TitleBarTitleContext'); diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index d56ac88a1851..ef2437bd5889 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -43,34 +43,41 @@ position: relative; } -.test-explorer .test-item .label, -.test-output-peek-tree .test-peek-item .name, -.test-coverage-list-item .name, -.test-coverage-list-item-label { - flex-grow: 1; - width: 0; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; +.testing-stdtree-container { + display: flex; + align-items: center; + + .label { + flex-grow: 1; + width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; - .monaco-list.horizontal-scrolling & { - width: auto; - overflow: visible; + .codicon { + vertical-align: middle; + font-size: 1em; + transform: scale(1.25); + margin: 0 0.125em; + } + + .monaco-list.horizontal-scrolling & { + width: auto; + overflow: visible; + } } -} -.test-output-peek-tree .test-peek-item .name .codicon, -.test-explorer .test-item .label .codicon { - vertical-align: middle; - font-size: 1em; - transform: scale(1.25); - margin: 0 0.125em; -} + .monaco-action-bar { + display: none; + flex-shrink: 0; + margin-right: 0.8em; + } -.test-explorer .test-item, -.test-output-peek-tree .test-peek-item { - display: flex; - align-items: center; + &:hover, &.focused { + .monaco-action-bar { + display: initial; + } + } } .test-output-peek-tree { @@ -78,14 +85,16 @@ border-left: 1px solid var(--vscode-panelSection-border); } -.test-output-peek-tree .monaco-list-row .monaco-action-bar, -.test-explorer .monaco-list-row .monaco-action-bar, .test-explorer .monaco-list-row .codicon-testing-hidden { display: none; flex-shrink: 0; margin-right: 0.8em; } +.test-explorer .monaco-list-row .monaco-action-bar.testing-is-continuous-run { + display: initial; +} + .test-explorer .monaco-list-row .monaco-action-bar .codicon-testing-continuous-is-on { color: var(--vscode-inputOption-activeForeground); border-color: var(--vscode-inputOption-activeBorder); @@ -102,14 +111,6 @@ display: block !important; } -.test-explorer .monaco-list-row:hover .monaco-action-bar, -.test-explorer .monaco-list-row.focused .monaco-action-bar, -.test-explorer .monaco-list-row .monaco-action-bar.testing-is-continuous-run, -.test-output-peek-tree .monaco-list-row:hover .monaco-action-bar, -.test-output-peek-tree .monaco-list-row.focused .monaco-action-bar { - display: initial; -} - .test-explorer .monaco-list-row .test-is-hidden .codicon-testing-hidden { display: block; margin-right: 9px; @@ -467,15 +468,6 @@ /** -- coverage */ -.coverage-view-is-filtered > .pane-header > .actions { - display: block !important; -} - -.test-coverage-list-item { - display: flex; - align-items: center; -} - .test-coverage-bars { display: flex; align-items: center; diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts index 5ee19fb10f6a..b660cc798b1c 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts +++ b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../base/browser/dom.js'; +import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IIdentityProvider, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; import { ICompressedTreeElement, ICompressedTreeNode } from '../../../../base/browser/ui/tree/compressedObjectTreeModel.js'; import { ICompressibleTreeRenderer } from '../../../../base/browser/ui/tree/objectTree.js'; @@ -24,7 +25,9 @@ import { Position } from '../../../../editor/common/core/position.js'; import { Range } from '../../../../editor/common/core/range.js'; import { localize, localize2 } from '../../../../nls.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; -import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { getActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { Action2, IMenuService, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; @@ -42,17 +45,17 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { IResourceLabel, ResourceLabels } from '../../../browser/labels.js'; import { IViewPaneOptions, ViewAction, ViewPane } from '../../../browser/parts/views/viewPane.js'; import { IViewDescriptorService } from '../../../common/views.js'; -import * as coverUtils from './codeCoverageDisplayUtils.js'; -import { testingStatesToIcons, testingWasCovered } from './icons.js'; -import { CoverageBarSource, ManagedTestCoverageBars } from './testCoverageBars.js'; +import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; import { TestCommandId, Testing } from '../common/constants.js'; import { onObservableChange } from '../common/observableUtils.js'; import { BypassedFileCoverage, ComputedFileCoverage, FileCoverage, TestCoverage, getTotalCoveragePercent } from '../common/testCoverage.js'; import { ITestCoverageService } from '../common/testCoverageService.js'; import { TestId } from '../common/testId.js'; import { TestingContextKeys } from '../common/testingContextKeys.js'; -import { CoverageDetails, DetailType, ICoverageCount, IDeclarationCoverage, TestResultState } from '../common/testTypes.js'; -import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; +import { CoverageDetails, DetailType, ICoverageCount, IDeclarationCoverage, ITestItem, TestResultState } from '../common/testTypes.js'; +import * as coverUtils from './codeCoverageDisplayUtils.js'; +import { testingStatesToIcons, testingWasCovered } from './icons.js'; +import { CoverageBarSource, ManagedTestCoverageBars } from './testCoverageBars.js'; const enum CoverageSortOrder { Coverage, @@ -95,13 +98,6 @@ export class TestCoverageView extends ViewPane { this.tree.clear(); } })); - - this._register(autorun(reader => { - this.element.classList.toggle( - 'coverage-view-is-filtered', - !!this.coverageService.filterToTest.read(reader), - ); - })); } protected override layoutBody(height: number, width: number): void { @@ -197,6 +193,16 @@ class RevealUncoveredDeclarations { constructor(public readonly n: number) { } } +class CurrentlyFilteredTo { + public readonly id = String(fnNodeId++); + + public get label() { + return localize('filteredToTest', "Showing coverage for \"{0}\"", this.testItem.label); + } + + constructor(public readonly testItem: ITestItem) { } +} + class LoadingDetails { public readonly id = String(fnNodeId++); public readonly label = localize('loadingCoverageDetails', "Loading Coverage Details..."); @@ -204,7 +210,7 @@ class LoadingDetails { /** Type of nodes returned from {@link TestCoverage}. Note: value is *always* defined. */ type TestCoverageFileNode = IPrefixTreeNode; -type CoverageTreeElement = TestCoverageFileNode | DeclarationCoverageNode | LoadingDetails | RevealUncoveredDeclarations; +type CoverageTreeElement = TestCoverageFileNode | DeclarationCoverageNode | LoadingDetails | RevealUncoveredDeclarations | CurrentlyFilteredTo; const isFileCoverage = (c: CoverageTreeElement): c is TestCoverageFileNode => typeof c === 'object' && 'value' in c; const isDeclarationCoverage = (c: CoverageTreeElement): c is DeclarationCoverageNode => c instanceof DeclarationCoverageNode; @@ -221,9 +227,12 @@ class TestCoverageTree extends Disposable { sortOrder: IObservable, @IInstantiationService instantiationService: IInstantiationService, @IEditorService editorService: IEditorService, + @ICommandService commandService: ICommandService, ) { super(); + container.classList.add('testing-stdtree'); + this.tree = instantiationService.createInstance( WorkbenchCompressibleObjectTree, 'TestCoverageView', @@ -233,6 +242,7 @@ class TestCoverageTree extends Disposable { instantiationService.createInstance(FileCoverageRenderer, labels), instantiationService.createInstance(DeclarationCoverageRenderer), instantiationService.createInstance(BasicRenderer), + instantiationService.createInstance(CurrentlyFilteredToRenderer), ], { expandOnlyOnTwistieClick: true, @@ -289,6 +299,9 @@ class TestCoverageTree extends Disposable { } else if (isDeclarationCoverage(e.element)) { resource = e.element.uri; selection = e.element.location; + } else if (e.element instanceof CurrentlyFilteredTo) { + commandService.executeCommand(TestCommandId.CoverageFilterToTest); + return; } } if (!resource) { @@ -351,7 +364,19 @@ class TestCoverageTree extends Disposable { } })); - this.tree.setChildren(null, Iterable.map(files, toChild)); + let children = Iterable.map(files, toChild); + const filteredTo = showOnlyTest && coverage.result.getTestById(showOnlyTest.toString()); + if (filteredTo) { + children = Iterable.concat( + Iterable.single>({ + element: new CurrentlyFilteredTo(filteredTo), + incompressible: true, + }), + children, + ); + } + + this.tree.setChildren(null, children); } public layout(height: number, width: number) { @@ -409,6 +434,9 @@ class TestCoverageTreeListDelegate implements IListVirtualDelegate { } } +interface IFilteredToTemplate { + label: HTMLElement; + actions: ActionBar; +} + +class CurrentlyFilteredToRenderer implements ICompressibleTreeRenderer { + public static readonly ID = 'C'; + public readonly templateId = CurrentlyFilteredToRenderer.ID; + + constructor( + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { } + + renderCompressedElements(node: ITreeNode, FuzzyScore>, index: number, templateData: IFilteredToTemplate, height: number | undefined): void { + this.renderInner(node.element.elements[node.element.elements.length - 1] as CurrentlyFilteredTo, templateData); + } + + renderTemplate(container: HTMLElement): IFilteredToTemplate { + container.classList.add('testing-stdtree-container'); + const label = dom.append(container, dom.$('.label')); + const menu = this.menuService.getMenuActions(MenuId.TestCoverageFilterItem, this.contextKeyService, { + shouldForwardArgs: true, + }); + + const actions = new ActionBar(container); + actions.push(getActionBarActions(menu, 'inline').primary, { icon: true, label: false }); + actions.domNode.style.display = 'block'; + + return { label, actions }; + } + + renderElement(element: ITreeNode, index: number, templateData: IFilteredToTemplate, height: number | undefined): void { + this.renderInner(element.element as CurrentlyFilteredTo, templateData); + } + + disposeTemplate(templateData: IFilteredToTemplate): void { + templateData.actions.dispose(); + } + + private renderInner(element: CurrentlyFilteredTo, container: IFilteredToTemplate) { + container.label.innerText = element.label; + } +} + interface FileTemplateData { container: HTMLElement; bars: ManagedTestCoverageBars; @@ -469,7 +542,7 @@ class FileCoverageRenderer implements ICompressibleTreeRenderer action instanceof MenuItemAction ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }) diff --git a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts index f93d2065f0c4..421afad78f20 100644 --- a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts +++ b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts @@ -248,7 +248,7 @@ export class TestResultsViewContent extends Disposable { this.contextKeyTestMessage = TestingContextKeys.testMessageContext.bindTo(this.messageContextKeyService); this.contextKeyResultOutdated = TestingContextKeys.testResultOutdated.bindTo(this.messageContextKeyService); - const treeContainer = dom.append(containerElement, dom.$('.test-output-peek-tree')); + const treeContainer = dom.append(containerElement, dom.$('.test-output-peek-tree.testing-stdtree')); const tree = this._register(this.instantiationService.createInstance( OutputPeekTree, treeContainer, diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 2febdc0a89c8..5e84db11b6af 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -1526,8 +1526,8 @@ class TestItemRenderer extends Disposable /** * @inheritdoc */ - public renderTemplate(container: HTMLElement): ITestElementTemplateData { - const wrapper = dom.append(container, dom.$('.test-item')); + public renderTemplate(wrapper: HTMLElement): ITestElementTemplateData { + wrapper.classList.add('testing-stdtree-container'); const icon = dom.append(wrapper, dom.$('.computed-state')); const label = dom.append(wrapper, dom.$('.label')); From 2a4ed8474889817be7266f7e675892fbb055195d Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 18 Dec 2024 00:39:13 +0100 Subject: [PATCH 0193/3587] Ensure settings and keybindings views open at correct location (#236412) * make sure settings and keybindings views open at correct location * fix tests --- .../browser/preferences.contribution.ts | 54 ++++++++++++------- .../preferences/browser/settingsEditor2.ts | 2 +- .../preferences/browser/preferencesService.ts | 41 ++++++++------ .../preferences/common/preferences.ts | 7 ++- .../test/browser/preferencesService.test.ts | 38 ++++++------- 5 files changed, 87 insertions(+), 55 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 175e8bd518b0..2a6357e15d45 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -47,6 +47,9 @@ import { IUserDataProfileService, CURRENT_PROFILE_CONTEXT } from '../../../servi import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; +import { resolveCommandsContext } from '../../../browser/parts/editor/editorCommandsContext.js'; +import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; +import { IListService } from '../../../../platform/list/browser/listService.js'; const SETTINGS_EDITOR_COMMAND_SEARCH = 'settings.action.search'; @@ -791,9 +794,10 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon ] }); } - run(accessor: ServicesAccessor, args: string | undefined) { - const query = typeof args === 'string' ? args : undefined; - return accessor.get(IPreferencesService).openGlobalKeybindingSettings(false, { query }); + run(accessor: ServicesAccessor, ...args: unknown[]) { + const query = typeof args[0] === 'string' ? args[0] : undefined; + const groupId = getEditorGroupFromArguments(accessor, args)?.id; + return accessor.get(IPreferencesService).openGlobalKeybindingSettings(false, { query, groupId }); } })); this._register(MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { @@ -834,8 +838,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon ] }); } - run(accessor: ServicesAccessor) { - return accessor.get(IPreferencesService).openGlobalKeybindingSettings(true); + run(accessor: ServicesAccessor, ...args: unknown[]) { + const groupId = getEditorGroupFromArguments(accessor, args)?.id; + return accessor.get(IPreferencesService).openGlobalKeybindingSettings(true, { groupId }); } })); this._register(registerAction2(class extends Action2 { @@ -852,8 +857,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon ] }); } - run(accessor: ServicesAccessor) { - const editorPane = accessor.get(IEditorService).activeEditorPane; + run(accessor: ServicesAccessor, ...args: unknown[]) { + const group = getEditorGroupFromArguments(accessor, args); + const editorPane = group?.activeEditorPane; if (editorPane instanceof KeybindingsEditor) { editorPane.search('@source:system'); } @@ -873,8 +879,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon ] }); } - run(accessor: ServicesAccessor) { - const editorPane = accessor.get(IEditorService).activeEditorPane; + run(accessor: ServicesAccessor, ...args: unknown[]) { + const group = getEditorGroupFromArguments(accessor, args); + const editorPane = group?.activeEditorPane; if (editorPane instanceof KeybindingsEditor) { editorPane.search('@source:extension'); } @@ -894,8 +901,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon ] }); } - run(accessor: ServicesAccessor) { - const editorPane = accessor.get(IEditorService).activeEditorPane; + run(accessor: ServicesAccessor, args: unknown[]) { + const group = getEditorGroupFromArguments(accessor, args); + const editorPane = group?.activeEditorPane; if (editorPane instanceof KeybindingsEditor) { editorPane.search('@source:user'); } @@ -1207,11 +1215,12 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon for (const folder of this.workspaceContextService.getWorkspace().folders) { const commandId = `_workbench.openFolderSettings.${folder.uri.toString()}`; if (!CommandsRegistry.getCommand(commandId)) { - CommandsRegistry.registerCommand(commandId, () => { + CommandsRegistry.registerCommand(commandId, (accessor: ServicesAccessor, ...args: any[]) => { + const groupId = getEditorGroupFromArguments(accessor, args)?.id; if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.FOLDER) { - return this.preferencesService.openWorkspaceSettings({ jsonEditor: false }); + return this.preferencesService.openWorkspaceSettings({ jsonEditor: false, groupId }); } else { - return this.preferencesService.openFolderSettings({ folderUri: folder.uri, jsonEditor: false }); + return this.preferencesService.openFolderSettings({ folderUri: folder.uri, jsonEditor: false, groupId }); } }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { @@ -1261,9 +1270,10 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo }] }); } - run(accessor: ServicesAccessor, args: IOpenSettingsActionOptions) { - args = sanitizeOpenSettingsArgs(args); - return accessor.get(IPreferencesService).openUserSettings({ jsonEditor: false, ...args }); + run(accessor: ServicesAccessor, ...args: unknown[]) { + const sanatizedArgs = sanitizeOpenSettingsArgs(args[0]); + const groupId = getEditorGroupFromArguments(accessor, args)?.id; + return accessor.get(IPreferencesService).openUserSettings({ jsonEditor: false, ...sanatizedArgs, groupId }); } }); }; @@ -1289,8 +1299,9 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo }] }); } - run(accessor: ServicesAccessor) { - const editorPane = accessor.get(IEditorService).activeEditorPane; + run(accessor: ServicesAccessor, ...args: unknown[]) { + const group = getEditorGroupFromArguments(accessor, args); + const editorPane = group?.activeEditorPane; if (editorPane instanceof SettingsEditor2) { return editorPane.switchToSettingsFile(); } @@ -1300,6 +1311,11 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo } } +function getEditorGroupFromArguments(accessor: ServicesAccessor, args: unknown[]): IEditorGroup | undefined { + const context = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); + return context.groupedEditors[0]?.group; +} + const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); registerWorkbenchContribution2(PreferencesActionsContribution.ID, PreferencesActionsContribution, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(PreferencesContribution.ID, PreferencesContribution, WorkbenchPhase.BlockStartup); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index c1d1c302db17..8e3388a7008e 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -789,7 +789,7 @@ export class SettingsEditor2 extends EditorPane { private async openSettingsFile(options?: ISettingsEditorOptions): Promise { const currentSettingsTarget = this.settingsTargetsWidget.settingsTarget; - const openOptions: IOpenSettingsOptions = { jsonEditor: true, ...options }; + const openOptions: IOpenSettingsOptions = { jsonEditor: true, groupId: this.group.id, ...options }; if (currentSettingsTarget === ConfigurationTarget.USER_LOCAL) { if (options?.revealSetting) { const configurationProperties = Registry.as(Extensions.Configuration).getConfigurationProperties(); diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index 025cc02e964a..f0bb2cb9f25b 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -29,10 +29,10 @@ import { DEFAULT_EDITOR_ASSOCIATION, IEditorPane } from '../../../common/editor. import { EditorInput } from '../../../common/editor/editorInput.js'; import { SideBySideEditorInput } from '../../../common/editor/sideBySideEditorInput.js'; import { IJSONEditingService } from '../../configuration/common/jsonEditing.js'; -import { GroupDirection, IEditorGroupsService } from '../../editor/common/editorGroupsService.js'; -import { IEditorService, SIDE_GROUP, SIDE_GROUP_TYPE } from '../../editor/common/editorService.js'; +import { GroupDirection, IEditorGroup, IEditorGroupsService } from '../../editor/common/editorGroupsService.js'; +import { IEditorService, SIDE_GROUP } from '../../editor/common/editorService.js'; import { KeybindingsEditorInput } from './keybindingsEditorInput.js'; -import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, IKeybindingsEditorOptions, IKeybindingsEditorPane, IOpenSettingsOptions, IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorOptions, ISettingsGroup, SETTINGS_AUTHORITY, USE_SPLIT_JSON_SETTING, validateSettingsEditorOptions } from '../common/preferences.js'; +import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, IKeybindingsEditorPane, IOpenKeybindingsEditorOptions, IOpenSettingsOptions, IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorOptions, ISettingsGroup, SETTINGS_AUTHORITY, USE_SPLIT_JSON_SETTING, validateSettingsEditorOptions } from '../common/preferences.js'; import { SettingsEditor2Input } from '../common/preferencesEditorInput.js'; import { defaultKeybindingsContents, DefaultKeybindingsEditorModel, DefaultRawSettingsEditorModel, DefaultSettings, DefaultSettingsEditorModel, Settings2EditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel } from '../common/preferencesModels.js'; import { IRemoteAgentService } from '../../remote/common/remoteAgentService.js'; @@ -48,6 +48,7 @@ import { IURLService } from '../../../../platform/url/common/url.js'; import { compareIgnoreCase } from '../../../../base/common/strings.js'; import { IExtensionService } from '../../extensions/common/extensions.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; +import { findGroup } from '../../editor/common/editorGroupFinder.js'; const emptyEditableSettingsContent = '{\n}'; @@ -248,8 +249,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic ...options, focusSearch: true }; - await this.editorService.openEditor(input, validateSettingsEditorOptions(options), options.openToSide ? SIDE_GROUP : undefined); - return this.editorGroupService.activeGroup.activeEditorPane!; + const group = await this.getEditorGroupFromOptions(options); + return (await group.openEditor(input, validateSettingsEditorOptions(options)))!; } openApplicationSettings(options: IOpenSettingsOptions = {}): Promise { @@ -312,7 +313,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.open(folderSettingsUri, options); } - async openGlobalKeybindingSettings(textual: boolean, options?: IKeybindingsEditorOptions): Promise { + async openGlobalKeybindingSettings(textual: boolean, options?: IOpenKeybindingsEditorOptions): Promise { options = { pinned: true, revealIfOpened: true, ...options }; if (textual) { const emptyContents = '// ' + nls.localize('emptyKeybindingsHeader', "Place your key bindings in this file to override the defaults") + '\n[\n]'; @@ -322,18 +323,18 @@ export class PreferencesService extends Disposable implements IPreferencesServic // Create as needed and open in editor await this.createIfNotExists(editableKeybindings, emptyContents); if (openDefaultKeybindings) { - const activeEditorGroup = this.editorGroupService.activeGroup; - const sideEditorGroup = this.editorGroupService.addGroup(activeEditorGroup.id, GroupDirection.RIGHT); + const sourceGroupId = options.groupId ?? this.editorGroupService.activeGroup.id; + const sideEditorGroup = this.editorGroupService.addGroup(sourceGroupId, GroupDirection.RIGHT); await Promise.all([ - this.editorService.openEditor({ resource: this.defaultKeybindingsResource, options: { pinned: true, preserveFocus: true, revealIfOpened: true, override: DEFAULT_EDITOR_ASSOCIATION.id }, label: nls.localize('defaultKeybindings', "Default Keybindings"), description: '' }), + this.editorService.openEditor({ resource: this.defaultKeybindingsResource, options: { pinned: true, preserveFocus: true, revealIfOpened: true, override: DEFAULT_EDITOR_ASSOCIATION.id }, label: nls.localize('defaultKeybindings', "Default Keybindings"), description: '' }, sourceGroupId), this.editorService.openEditor({ resource: editableKeybindings, options }, sideEditorGroup.id) ]); } else { - await this.editorService.openEditor({ resource: editableKeybindings, options }); + await this.editorService.openEditor({ resource: editableKeybindings, options }, options.groupId); } } else { - const editor = (await this.editorService.openEditor(this.instantiationService.createInstance(KeybindingsEditorInput), { ...options })) as IKeybindingsEditorPane; + const editor = (await this.editorService.openEditor(this.instantiationService.createInstance(KeybindingsEditorInput), { ...options }, options.groupId)) as IKeybindingsEditorPane; if (options.query) { editor.search(options.query); } @@ -345,8 +346,16 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.editorService.openEditor({ resource: this.defaultKeybindingsResource, label: nls.localize('defaultKeybindings', "Default Keybindings") }); } + private async getEditorGroupFromOptions(options: IOpenSettingsOptions): Promise { + let group = options?.groupId !== undefined ? this.editorGroupService.getGroup(options.groupId) ?? this.editorGroupService.activeGroup : this.editorGroupService.activeGroup; + if (options.openToSide) { + group = (await this.instantiationService.invokeFunction(findGroup, {}, SIDE_GROUP))[0]; + } + return group; + } + private async openSettingsJson(resource: URI, options: IOpenSettingsOptions): Promise { - const group = options?.openToSide ? SIDE_GROUP : undefined; + const group = await this.getEditorGroupFromOptions(options); const editor = await this.doOpenSettingsJson(resource, options, group); if (editor && options?.revealSetting) { await this.revealSetting(options.revealSetting.key, !!options.revealSetting.edit, editor, resource); @@ -354,7 +363,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic return editor; } - private async doOpenSettingsJson(resource: URI, options: ISettingsEditorOptions, group?: SIDE_GROUP_TYPE): Promise { + private async doOpenSettingsJson(resource: URI, options: ISettingsEditorOptions, group: IEditorGroup): Promise { const openSplitJSON = !!this.configurationService.getValue(USE_SPLIT_JSON_SETTING); const openDefaultSettings = !!this.configurationService.getValue(DEFAULT_SETTINGS_EDITOR_SETTING); if (openSplitJSON || openDefaultSettings) { @@ -364,15 +373,15 @@ export class PreferencesService extends Disposable implements IPreferencesServic const configurationTarget = options?.target ?? ConfigurationTarget.USER; const editableSettingsEditorInput = await this.getOrCreateEditableSettingsEditorInput(configurationTarget, resource); options = { ...options, pinned: true }; - return await this.editorService.openEditor(editableSettingsEditorInput, validateSettingsEditorOptions(options), group); + return await group.openEditor(editableSettingsEditorInput, { ...validateSettingsEditorOptions(options) }); } - private async doOpenSplitJSON(resource: URI, options: ISettingsEditorOptions = {}, group?: SIDE_GROUP_TYPE): Promise { + private async doOpenSplitJSON(resource: URI, options: ISettingsEditorOptions = {}, group: IEditorGroup,): Promise { const configurationTarget = options.target ?? ConfigurationTarget.USER; await this.createSettingsIfNotExists(configurationTarget, resource); const preferencesEditorInput = this.createSplitJsonEditorInput(configurationTarget, resource); options = { ...options, pinned: true }; - return this.editorService.openEditor(preferencesEditorInput, validateSettingsEditorOptions(options), group); + return group.openEditor(preferencesEditorInput, validateSettingsEditorOptions(options)); } public createSplitJsonEditorInput(configurationTarget: ConfigurationTarget, resource: URI): EditorInput { diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index e4ae6416bbbb..ea3855432525 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -211,6 +211,7 @@ export interface ISettingsEditorOptions extends IEditorOptions { export interface IOpenSettingsOptions extends ISettingsEditorOptions { jsonEditor?: boolean; openToSide?: boolean; + groupId?: number; } export function validateSettingsEditorOptions(options: ISettingsEditorOptions): ISettingsEditorOptions { @@ -231,6 +232,10 @@ export interface IKeybindingsEditorOptions extends IEditorOptions { query?: string; } +export interface IOpenKeybindingsEditorOptions extends IKeybindingsEditorOptions { + groupId?: number; +} + export const IPreferencesService = createDecorator('preferencesService'); export interface IPreferencesService { @@ -254,7 +259,7 @@ export interface IPreferencesService { openRemoteSettings(options?: IOpenSettingsOptions): Promise; openWorkspaceSettings(options?: IOpenSettingsOptions): Promise; openFolderSettings(options: IOpenSettingsOptions & { folderUri: IOpenSettingsOptions['folderUri'] }): Promise; - openGlobalKeybindingSettings(textual: boolean, options?: IKeybindingsEditorOptions): Promise; + openGlobalKeybindingSettings(textual: boolean, options?: IOpenKeybindingsEditorOptions): Promise; openDefaultKeybindingsFile(): Promise; openLanguageSpecificSettings(languageId: string, options?: IOpenSettingsOptions): Promise; getEditableSettingsURI(configurationTarget: ConfigurationTarget, resource?: URI): Promise; diff --git a/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts b/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts index 4f1f2493cb4f..77c36590b8a3 100644 --- a/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts +++ b/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts @@ -10,26 +10,37 @@ import { ICommandService } from '../../../../../platform/commands/common/command import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; import { IURLService } from '../../../../../platform/url/common/url.js'; -import { DEFAULT_EDITOR_ASSOCIATION } from '../../../../common/editor.js'; +import { DEFAULT_EDITOR_ASSOCIATION, IEditorPane } from '../../../../common/editor.js'; import { IJSONEditingService } from '../../../configuration/common/jsonEditing.js'; import { TestJSONEditingService } from '../../../configuration/test/common/testServices.js'; import { PreferencesService } from '../../browser/preferencesService.js'; import { IPreferencesService, ISettingsEditorOptions } from '../../common/preferences.js'; import { IRemoteAgentService } from '../../../remote/common/remoteAgentService.js'; -import { TestRemoteAgentService, ITestInstantiationService, TestEditorService, workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { TestRemoteAgentService, ITestInstantiationService, workbenchInstantiationService, TestEditorGroupView, TestEditorGroupsService } from '../../../../test/browser/workbenchTestServices.js'; +import { IEditorGroupsService } from '../../../editor/common/editorGroupsService.js'; +import { IEditorOptions } from '../../../../../platform/editor/common/editor.js'; +import { SettingsEditor2Input } from '../../common/preferencesEditorInput.js'; suite('PreferencesService', () => { let testInstantiationService: ITestInstantiationService; let testObject: PreferencesService; - let editorService: TestEditorService2; + let lastOpenEditorOptions: IEditorOptions | undefined; const disposables = ensureNoDisposablesAreLeakedInTestSuite(); setup(() => { - editorService = disposables.add(new TestEditorService2()); - testInstantiationService = workbenchInstantiationService({ - editorService: () => editorService - }, disposables); + testInstantiationService = workbenchInstantiationService({}, disposables); + class TestOpenEditorGroupView extends TestEditorGroupView { + lastOpenEditorOptions: any; + override openEditor(_editor: SettingsEditor2Input, options?: IEditorOptions): Promise { + lastOpenEditorOptions = options; + _editor.dispose(); + return Promise.resolve(undefined!); + } + } + + const testEditorGroupService = new TestEditorGroupsService([new TestOpenEditorGroupView(0)]); + testInstantiationService.stub(IEditorGroupsService, testEditorGroupService); testInstantiationService.stub(IJSONEditingService, TestJSONEditingService); testInstantiationService.stub(IRemoteAgentService, TestRemoteAgentService); testInstantiationService.stub(ICommandService, TestCommandService); @@ -42,19 +53,10 @@ suite('PreferencesService', () => { testObject = disposables.add(instantiationService.createInstance(PreferencesService)); }); test('options are preserved when calling openEditor', async () => { - testObject.openSettings({ jsonEditor: false, query: 'test query' }); - const options = editorService.lastOpenEditorOptions as ISettingsEditorOptions; + await testObject.openSettings({ jsonEditor: false, query: 'test query' }); + const options = lastOpenEditorOptions as ISettingsEditorOptions; assert.strictEqual(options.focusSearch, true); assert.strictEqual(options.override, DEFAULT_EDITOR_ASSOCIATION.id); assert.strictEqual(options.query, 'test query'); }); }); - -class TestEditorService2 extends TestEditorService { - lastOpenEditorOptions: any; - - override async openEditor(editorInput: any, options?: any): Promise { - this.lastOpenEditorOptions = options; - return super.openEditor(editorInput, options); - } -} From 3e86f1e6cd9bf88b1ce23192fd0b68380bfc69be Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 17 Dec 2024 16:38:47 -0800 Subject: [PATCH 0194/3587] cli: fix serve-web needs to wait a certain amount of time after machine startup (#236427) Fixes #233155 --- cli/src/commands/serve_web.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index 3dedf4b3f059..ddef3eef3421 100644 --- a/cli/src/commands/serve_web.rs +++ b/cli/src/commands/serve_web.rs @@ -548,9 +548,11 @@ impl ConnectionManager { Err(_) => Quality::Stable, }); + let now = Instant::now(); let latest_version = tokio::sync::Mutex::new(cache.get().first().map(|latest_commit| { ( - Instant::now() - Duration::from_secs(RELEASE_CHECK_INTERVAL), + now.checked_sub(Duration::from_secs(RELEASE_CHECK_INTERVAL)) + .unwrap_or(now), // handle 0-ish instants, #233155 Release { name: String::from("0.0.0"), // Version information not stored on cache commit: latest_commit.clone(), From 20899216df0283a9e29f1e45badeb3a8714e6b99 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Dec 2024 07:26:26 +0100 Subject: [PATCH 0195/3587] dialogs - add `Cmd+D` handling (fix #71430) (#236434) --- src/vs/base/browser/ui/dialog/dialog.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index 5fcebbd34c6c..2ed8d24fadf7 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -275,6 +275,22 @@ export class Dialog extends Disposable { return; // leave default handling } + // Cmd+D (trigger the "no"/"do not save"-button) (macOS only) + if (isMacintosh && evt.equals(KeyMod.CtrlCmd | KeyCode.KeyD)) { + EventHelper.stop(e); + + const noButton = buttonMap.find(button => button.index === 1 && button.index !== this.options.cancelId); + if (noButton) { + resolve({ + button: noButton.index, + checkboxChecked: this.checkbox ? this.checkbox.checked : undefined, + values: this.inputs.length > 0 ? this.inputs.map(input => input.value) : undefined + }); + } + + return; // leave default handling + } + if (evt.equals(KeyCode.Space)) { return; // leave default handling } From 95d0e288d11737685fc91eb083bdc2abb8cd6d78 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Dec 2024 07:43:11 +0100 Subject: [PATCH 0196/3587] fuzzy - add test coverage for path matching (#236435) --- src/vs/base/test/common/fuzzyScorer.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index b180df4422bf..dc30534270fc 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -1081,6 +1081,21 @@ suite('Fuzzy Scorer', () => { } }); + test('compareFilesByScore - skip preference on label match when using path sep', function () { + const resourceA = URI.file('djangosite/ufrela/def.py'); + const resourceB = URI.file('djangosite/urls/default.py'); + + for (const query of ['url/def']) { + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + } + }); + test('compareFilesByScore - boost shorter prefix match if multiple queries are used (#99171)', function () { const resourceA = URI.file('mesh_editor_lifetime_job.h'); const resourceB = URI.file('lifetime_job.h'); From cf2ebd91b8e42e3bc5ab0e85e3323c886a977ffe Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Dec 2024 07:45:55 +0100 Subject: [PATCH 0197/3587] Revert "debt: clean up obsolete file usage" (#236433) Revert "debt: clean up obsolete file usage (#236379)" This reverts commit 625bae23758002b62954fd10b53d25409421d718. --- .../common/extensionManagement.ts | 4 +- .../common/extensionsScannerService.ts | 51 ++++-- .../node/extensionManagementService.ts | 163 +++++++++--------- .../node/extensionsWatcher.ts | 20 +-- .../node/extensionsScannerService.test.ts | 33 +++- 5 files changed, 156 insertions(+), 115 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index f08b46ae65b3..155079831fcc 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -456,8 +456,8 @@ export const enum ExtensionManagementErrorCode { Extract = 'Extract', Scanning = 'Scanning', ScanningExtension = 'ScanningExtension', - ReadRemoved = 'ReadRemoved', - UnsetRemoved = 'UnsetRemoved', + ReadUninstalled = 'ReadUninstalled', + UnsetUninstalled = 'UnsetUninstalled', Delete = 'Delete', Rename = 'Rename', IntializeDefaultProfile = 'IntializeDefaultProfile', diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index a186b6fa0456..99868cddb5d6 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -7,6 +7,7 @@ import { coalesce } from '../../../base/common/arrays.js'; import { ThrottledDelayer } from '../../../base/common/async.js'; import * as objects from '../../../base/common/objects.js'; import { VSBuffer } from '../../../base/common/buffer.js'; +import { IStringDictionary } from '../../../base/common/collections.js'; import { getErrorMessage } from '../../../base/common/errors.js'; import { getNodeType, parse, ParseError } from '../../../base/common/json.js'; import { getParseErrorMessage } from '../../../base/common/jsonErrorMessages.js'; @@ -17,11 +18,12 @@ import * as platform from '../../../base/common/platform.js'; import { basename, isEqual, joinPath } from '../../../base/common/resources.js'; import * as semver from '../../../base/common/semver/semver.js'; import Severity from '../../../base/common/severity.js'; +import { isEmptyObject } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; import { IProductVersion, Metadata } from './extensionManagement.js'; -import { areSameExtensions, computeTargetPlatform, getExtensionId, getGalleryExtensionId } from './extensionManagementUtil.js'; +import { areSameExtensions, computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from './extensionManagementUtil.js'; import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, ExtensionIdentifierMap, parseEnabledApiProposalNames } from '../../extensions/common/extensions.js'; import { validateExtensionManifest } from '../../extensions/common/extensionValidator.js'; import { FileOperationResult, IFileService, toFileOperationResult } from '../../files/common/files.js'; @@ -104,6 +106,7 @@ export type ScanOptions = { readonly profileLocation?: URI; readonly includeInvalid?: boolean; readonly includeAllVersions?: boolean; + readonly includeUninstalled?: boolean; readonly checkControlFile?: boolean; readonly language?: string; readonly useCache?: boolean; @@ -142,9 +145,10 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem private readonly _onDidChangeCache = this._register(new Emitter()); readonly onDidChangeCache = this._onDidChangeCache.event; - private readonly systemExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile)); - private readonly userExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile)); - private readonly extensionsScanner = this._register(this.instantiationService.createInstance(ExtensionsScanner)); + private readonly obsoleteFile = joinPath(this.userExtensionsLocation, '.obsolete'); + private readonly systemExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile, this.obsoleteFile)); + private readonly userExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile, this.obsoleteFile)); + private readonly extensionsScanner = this._register(this.instantiationService.createInstance(ExtensionsScanner, this.obsoleteFile)); constructor( readonly systemExtensionsLocation: URI, @@ -195,8 +199,8 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem const location = scanOptions.profileLocation ?? this.userExtensionsLocation; this.logService.trace('Started scanning user extensions', location); const profileScanOptions: IProfileExtensionsScanOptions | undefined = this.uriIdentityService.extUri.isEqual(scanOptions.profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource) ? { bailOutWhenFileNotFound: true } : undefined; - const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion()); - const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode ? this.userExtensionsCachedScanner : this.extensionsScanner; + const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode && extensionsScannerInput.excludeObsolete ? this.userExtensionsCachedScanner : this.extensionsScanner; let extensions: IRelaxedScannedExtension[]; try { extensions = await extensionsScanner.scanExtensions(extensionsScannerInput); @@ -217,7 +221,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionDevelopmentLocationURI) { const extensions = (await Promise.all(this.environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file) .map(async extensionDevelopmentLocationURI => { - const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, scanOptions.language, false /* do not validate */, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, true, scanOptions.language, false /* do not validate */, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(input); return extensions.map(extension => { // Override the extension type from the existing extensions @@ -233,7 +237,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extension = await this.extensionsScanner.scanExtension(extensionsScannerInput); if (!extension) { return null; @@ -245,7 +249,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(extensionsScannerInput); return this.applyScanOptions(extensions, extensionType, scanOptions, true); } @@ -401,7 +405,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem private async scanDefaultSystemExtensions(useCache: boolean, language: string | undefined): Promise { this.logService.trace('Started scanning system extensions'); - const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, language, true, undefined, this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, true, language, true, undefined, this.getProductVersion()); const extensionsScanner = useCache && !extensionsScannerInput.devMode ? this.systemExtensionsCachedScanner : this.extensionsScanner; const result = await extensionsScanner.scanExtensions(extensionsScannerInput); this.logService.trace('Scanned system extensions:', result.length); @@ -431,7 +435,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem break; } } - const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, language, true, undefined, this.getProductVersion()))))); + const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, true, language, true, undefined, this.getProductVersion()))))); this.logService.trace('Scanned dev system extensions:', result.length); return coalesce(result); } @@ -445,7 +449,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } } - private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined, productVersion: IProductVersion): Promise { + private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, excludeObsolete: boolean, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined, productVersion: IProductVersion): Promise { const translations = await this.getTranslations(language ?? platform.language); const mtime = await this.getMtime(location); const applicationExtensionsLocation = profile && !this.uriIdentityService.extUri.isEqual(location, this.userDataProfilesService.defaultProfile.extensionsResource) ? this.userDataProfilesService.defaultProfile.extensionsResource : undefined; @@ -458,6 +462,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem profile, profileScanOptions, type, + excludeObsolete, validate, productVersion.version, productVersion.date, @@ -499,6 +504,7 @@ export class ExtensionScannerInput { public readonly profile: boolean, public readonly profileScanOptions: IProfileExtensionsScanOptions | undefined, public readonly type: ExtensionType, + public readonly excludeObsolete: boolean, public readonly validate: boolean, public readonly productVersion: string, public readonly productDate: string | undefined, @@ -528,6 +534,7 @@ export class ExtensionScannerInput { && a.profile === b.profile && objects.equals(a.profileScanOptions, b.profileScanOptions) && a.type === b.type + && a.excludeObsolete === b.excludeObsolete && a.validate === b.validate && a.productVersion === b.productVersion && a.productDate === b.productDate @@ -551,6 +558,7 @@ class ExtensionsScanner extends Disposable { private readonly extensionsEnabledWithApiProposalVersion: string[]; constructor( + private readonly obsoleteFile: URI, @IExtensionsProfileScannerService protected readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, @IFileService protected readonly fileService: IFileService, @@ -563,9 +571,15 @@ class ExtensionsScanner extends Disposable { } async scanExtensions(input: ExtensionScannerInput): Promise { - return input.profile - ? this.scanExtensionsFromProfile(input) - : this.scanExtensionsFromLocation(input); + const extensions = input.profile ? await this.scanExtensionsFromProfile(input) : await this.scanExtensionsFromLocation(input); + let obsolete: IStringDictionary = {}; + if (input.excludeObsolete && input.type === ExtensionType.User) { + try { + const raw = (await this.fileService.readFile(this.obsoleteFile)).value.toString(); + obsolete = JSON.parse(raw); + } catch (error) { /* ignore */ } + } + return isEmptyObject(obsolete) ? extensions : extensions.filter(e => !obsolete[ExtensionKey.create(e).toString()]); } private async scanExtensionsFromLocation(input: ExtensionScannerInput): Promise { @@ -582,7 +596,7 @@ class ExtensionsScanner extends Disposable { if (input.type === ExtensionType.User && basename(c.resource).indexOf('.') === 0) { return null; } - const extensionScannerInput = new ExtensionScannerInput(c.resource, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); + const extensionScannerInput = new ExtensionScannerInput(c.resource, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.excludeObsolete, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); return this.scanExtension(extensionScannerInput); })); return coalesce(extensions) @@ -608,7 +622,7 @@ class ExtensionsScanner extends Disposable { const extensions = await Promise.all( scannedProfileExtensions.map(async extensionInfo => { if (filter(extensionInfo)) { - const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); + const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.excludeObsolete, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); return this.scanExtension(extensionScannerInput, extensionInfo.metadata); } return null; @@ -877,6 +891,7 @@ class CachedExtensionsScanner extends ExtensionsScanner { constructor( private readonly currentProfile: IUserDataProfile, + obsoleteFile: URI, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IExtensionsProfileScannerService extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService uriIdentityService: IUriIdentityService, @@ -885,7 +900,7 @@ class CachedExtensionsScanner extends ExtensionsScanner { @IEnvironmentService environmentService: IEnvironmentService, @ILogService logService: ILogService ) { - super(extensionsProfileScannerService, uriIdentityService, fileService, productService, environmentService, logService); + super(obsoleteFile, extensionsProfileScannerService, uriIdentityService, fileService, productService, environmentService, logService); } override async scanExtensions(input: ExtensionScannerInput): Promise { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 015c3dff393a..92405eefb753 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -60,7 +60,7 @@ export interface INativeServerExtensionManagementService extends IExtensionManag readonly _serviceBrand: undefined; scanAllUserInstalledExtensions(): Promise; scanInstalledExtensionAtLocation(location: URI): Promise; - deleteExtensions(...extensions: IExtension[]): Promise; + markAsUninstalled(...extensions: IExtension[]): Promise; } type ExtractExtensionResult = { readonly local: ILocalExtension; readonly verificationStatus?: ExtensionSignatureVerificationCode }; @@ -222,8 +222,8 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return this.extensionsScanner.copyExtensions(fromProfileLocation, toProfileLocation, { version: this.productService.version, date: this.productService.date }); } - deleteExtensions(...extensions: IExtension[]): Promise { - return this.extensionsScanner.setExtensionsForRemoval(...extensions); + markAsUninstalled(...extensions: IExtension[]): Promise { + return this.extensionsScanner.setUninstalled(...extensions); } async cleanUp(): Promise { @@ -480,20 +480,8 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi continue; } - // Ignore changes to the deleted folder - if (this.uriIdentityService.extUri.basename(resource).endsWith(DELETED_FOLDER_POSTFIX)) { - continue; - } - - try { - // Check if this is a directory - if (!(await this.fileService.stat(resource)).isDirectory) { - continue; - } - } catch (error) { - if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { - this.logService.error(error); - } + // Check if this is a directory + if (!(await this.fileService.stat(resource)).isDirectory) { continue; } @@ -514,10 +502,23 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi private async addExtensionsToProfile(extensions: [ILocalExtension, Metadata | undefined][], profileLocation: URI): Promise { const localExtensions = extensions.map(e => e[0]); - await this.extensionsScanner.unsetExtensionsForRemoval(...localExtensions.map(extension => ExtensionKey.create(extension))); + await this.setInstalled(localExtensions); await this.extensionsProfileScannerService.addExtensionsToProfile(extensions, profileLocation); this._onDidInstallExtensions.fire(localExtensions.map(local => ({ local, identifier: local.identifier, operation: InstallOperation.None, profileLocation }))); } + + private async setInstalled(extensions: ILocalExtension[]): Promise { + const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); + for (const extension of extensions) { + const extensionKey = ExtensionKey.create(extension); + if (!uninstalled[extensionKey.toString()]) { + continue; + } + this.logService.trace('Removing the extension from uninstalled list:', extensionKey.id); + await this.extensionsScanner.setInstalled(extensionKey); + this.logService.info('Removed the extension from uninstalled list:', extensionKey.id); + } + } } type UpdateMetadataErrorClassification = { @@ -535,8 +536,8 @@ type UpdateMetadataErrorEvent = { export class ExtensionsScanner extends Disposable { - private readonly obsoletedResource: URI; - private readonly obsoleteFileLimiter: Queue; + private readonly uninstalledResource: URI; + private readonly uninstalledFileLimiter: Queue; private readonly _onExtract = this._register(new Emitter()); readonly onExtract = this._onExtract.event; @@ -554,13 +555,13 @@ export class ExtensionsScanner extends Disposable { @ILogService private readonly logService: ILogService, ) { super(); - this.obsoletedResource = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete'); - this.obsoleteFileLimiter = new Queue(); + this.uninstalledResource = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete'); + this.uninstalledFileLimiter = new Queue(); } async cleanUp(): Promise { await this.removeTemporarilyDeletedFolders(); - await this.deleteExtensionsMarkedForRemoval(); + await this.removeUninstalledExtensions(); await this.initializeMetadata(); } @@ -719,38 +720,40 @@ export class ExtensionsScanner extends Disposable { return this.scanLocalExtension(local.location, local.type, profileLocation); } - async setExtensionsForRemoval(...extensions: IExtension[]): Promise { + async getUninstalledExtensions(): Promise> { + try { + return await this.withUninstalledExtensions(); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.ReadUninstalled); + } + } + + async setUninstalled(...extensions: IExtension[]): Promise { const extensionKeys: ExtensionKey[] = extensions.map(e => ExtensionKey.create(e)); - await this.withRemovedExtensions(removedExtensions => + await this.withUninstalledExtensions(uninstalled => extensionKeys.forEach(extensionKey => { - removedExtensions[extensionKey.toString()] = true; - this.logService.info('Marked extension as removed', extensionKey.toString()); + uninstalled[extensionKey.toString()] = true; + this.logService.info('Marked extension as uninstalled', extensionKey.toString()); })); } - async unsetExtensionsForRemoval(...extensionKeys: ExtensionKey[]): Promise { + async setInstalled(extensionKey: ExtensionKey): Promise { try { - const results: boolean[] = []; - await this.withRemovedExtensions(removedExtensions => - extensionKeys.forEach(extensionKey => { - if (removedExtensions[extensionKey.toString()]) { - results.push(true); - delete removedExtensions[extensionKey.toString()]; - } else { - results.push(false); - } - })); - return results; + await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.UnsetRemoved); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UnsetUninstalled); } } - async deleteExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { + async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { if (this.uriIdentityService.extUri.isEqualOrParent(extension.location, this.extensionsScannerService.userExtensionsLocation)) { return this.deleteExtensionFromLocation(extension.identifier.id, extension.location, type); } - await this.unsetExtensionsForRemoval(ExtensionKey.create(extension)); + } + + async removeUninstalledExtension(extension: ILocalExtension | IScannedExtension): Promise { + await this.removeExtension(extension, 'uninstalled'); + await this.withUninstalledExtensions(uninstalled => delete uninstalled[ExtensionKey.create(extension).toString()]); } async copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata: Partial): Promise { @@ -789,11 +792,11 @@ export class ExtensionsScanner extends Disposable { this.logService.info(`Deleted ${type} extension from disk`, id, location.fsPath); } - private withRemovedExtensions(updateFn?: (removed: IStringDictionary) => void): Promise> { - return this.obsoleteFileLimiter.queue(async () => { + private withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { + return this.uninstalledFileLimiter.queue(async () => { let raw: string | undefined; try { - const content = await this.fileService.readFile(this.obsoletedResource, 'utf8'); + const content = await this.fileService.readFile(this.uninstalledResource, 'utf8'); raw = content.value.toString(); } catch (error) { if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { @@ -801,23 +804,23 @@ export class ExtensionsScanner extends Disposable { } } - let removed = {}; + let uninstalled = {}; if (raw) { try { - removed = JSON.parse(raw); + uninstalled = JSON.parse(raw); } catch (e) { /* ignore */ } } if (updateFn) { - updateFn(removed); - if (Object.keys(removed).length) { - await this.fileService.writeFile(this.obsoletedResource, VSBuffer.fromString(JSON.stringify(removed))); + updateFn(uninstalled); + if (Object.keys(uninstalled).length) { + await this.fileService.writeFile(this.uninstalledResource, VSBuffer.fromString(JSON.stringify(uninstalled))); } else { - await this.fileService.del(this.obsoletedResource); + await this.fileService.del(this.uninstalledResource); } } - return removed; + return uninstalled; }); } @@ -895,25 +898,19 @@ export class ExtensionsScanner extends Disposable { })); } - private async deleteExtensionsMarkedForRemoval(): Promise { - let removed: IStringDictionary; - try { - removed = await this.withRemovedExtensions(); - } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.ReadRemoved); - } - - if (Object.keys(removed).length === 0) { - this.logService.debug(`No extensions are marked as removed.`); + private async removeUninstalledExtensions(): Promise { + const uninstalled = await this.getUninstalledExtensions(); + if (Object.keys(uninstalled).length === 0) { + this.logService.debug(`No uninstalled extensions found.`); return; } - this.logService.debug(`Deleting extensions marked as removed:`, Object.keys(removed)); + this.logService.debug(`Removing uninstalled extensions:`, Object.keys(uninstalled)); - const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeInvalid: true }); // All user extensions + const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions const installed: Set = new Set(); for (const e of extensions) { - if (!removed[ExtensionKey.create(e).toString()]) { + if (!uninstalled[ExtensionKey.create(e).toString()]) { installed.add(e.identifier.id.toLowerCase()); } } @@ -931,8 +928,8 @@ export class ExtensionsScanner extends Disposable { this.logService.error(error); } - const toRemove = extensions.filter(e => e.metadata /* Installed by System */ && removed[ExtensionKey.create(e).toString()]); - await Promise.allSettled(toRemove.map(e => this.deleteExtension(e, 'marked for removal'))); + const toRemove = extensions.filter(e => e.metadata /* Installed by System */ && uninstalled[ExtensionKey.create(e).toString()]); + await Promise.allSettled(toRemove.map(e => this.removeUninstalledExtension(e))); } private async removeTemporarilyDeletedFolders(): Promise { @@ -1024,7 +1021,7 @@ class InstallExtensionInProfileTask extends AbstractExtensionTask { - // If the same version of extension is marked as removed, remove it from there and return the local. - const [removed] = await this.extensionsScanner.unsetExtensionsForRemoval(extensionKey); - if (removed) { - this.logService.info('Removed the extension from removed list:', extensionKey.id); - const userExtensions = await this.extensionsScanner.scanAllUserExtensions(true); - return userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)); + private async unsetIfUninstalled(extensionKey: ExtensionKey): Promise { + const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); + if (!uninstalled[extensionKey.toString()]) { + return undefined; } - return undefined; + + this.logService.trace('Removing the extension from uninstalled list:', extensionKey.id); + // If the same version of extension is marked as uninstalled, remove it from there and return the local. + await this.extensionsScanner.setInstalled(extensionKey); + this.logService.info('Removed the extension from uninstalled list:', extensionKey.id); + + const userExtensions = await this.extensionsScanner.scanAllUserExtensions(true); + return userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)); } private async updateMetadata(extension: ILocalExtension, token: CancellationToken): Promise { @@ -1148,8 +1149,8 @@ class UninstallExtensionInProfileTask extends AbstractExtensionTask implem super(); } - protected doRun(token: CancellationToken): Promise { - return this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension, this.options.profileLocation); + protected async doRun(token: CancellationToken): Promise { + await this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension, this.options.profileLocation); } } diff --git a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts index 2c4e976a5a64..d2b65eaea553 100644 --- a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts +++ b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts @@ -48,7 +48,7 @@ export class ExtensionsWatcher extends Disposable { await this.extensionsScannerService.initializeDefaultProfileExtensions(); await this.onDidChangeProfiles(this.userDataProfilesService.profiles); this.registerListeners(); - await this.deleteExtensionsNotInProfiles(); + await this.uninstallExtensionsNotInProfiles(); } private registerListeners(): void { @@ -102,7 +102,7 @@ export class ExtensionsWatcher extends Disposable { } private async onDidRemoveExtensions(e: DidRemoveProfileExtensionsEvent): Promise { - const extensionsToDelete: IExtension[] = []; + const extensionsToUninstall: IExtension[] = []; const promises: Promise[] = []; for (const extension of e.extensions) { const key = this.getKey(extension.identifier, extension.version); @@ -115,7 +115,7 @@ export class ExtensionsWatcher extends Disposable { promises.push(this.extensionManagementService.scanInstalledExtensionAtLocation(extension.location) .then(result => { if (result) { - extensionsToDelete.push(result); + extensionsToUninstall.push(result); } else { this.logService.info('Extension not found at the location', extension.location.toString()); } @@ -125,8 +125,8 @@ export class ExtensionsWatcher extends Disposable { } try { await Promise.all(promises); - if (extensionsToDelete.length) { - await this.deleteExtensionsNotInProfiles(extensionsToDelete); + if (extensionsToUninstall.length) { + await this.uninstallExtensionsNotInProfiles(extensionsToUninstall); } } catch (error) { this.logService.error(error); @@ -180,13 +180,13 @@ export class ExtensionsWatcher extends Disposable { } } - private async deleteExtensionsNotInProfiles(toDelete?: IExtension[]): Promise { - if (!toDelete) { + private async uninstallExtensionsNotInProfiles(toUninstall?: IExtension[]): Promise { + if (!toUninstall) { const installed = await this.extensionManagementService.scanAllUserInstalledExtensions(); - toDelete = installed.filter(installedExtension => !this.allExtensions.has(this.getKey(installedExtension.identifier, installedExtension.manifest.version))); + toUninstall = installed.filter(installedExtension => !this.allExtensions.has(this.getKey(installedExtension.identifier, installedExtension.manifest.version))); } - if (toDelete.length) { - await this.extensionManagementService.deleteExtensions(...toDelete); + if (toUninstall.length) { + await this.extensionManagementService.markAsUninstalled(...toUninstall); } } diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts index 551ba576d445..74d3ffcd738f 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -224,6 +224,31 @@ suite('NativeExtensionsScanerService Test', () => { assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); }); + test('scan exclude uninstalled extensions', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); + await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true }))); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + + const actual = await testObject.scanUserExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + }); + + test('scan include uninstalled extensions', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); + await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true }))); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + + const actual = await testObject.scanUserExtensions({ includeUninstalled: true }); + + assert.deepStrictEqual(actual.length, 2); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' }); + }); + test('scan include invalid extensions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } })); @@ -326,7 +351,7 @@ suite('ExtensionScannerInput', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('compare inputs - location', () => { - const anInput = (location: URI, mtime: number | undefined) => new ExtensionScannerInput(location, mtime, undefined, undefined, false, undefined, ExtensionType.User, true, '1.1.1', undefined, undefined, true, undefined, {}); + const anInput = (location: URI, mtime: number | undefined) => new ExtensionScannerInput(location, mtime, undefined, undefined, false, undefined, ExtensionType.User, true, true, '1.1.1', undefined, undefined, true, undefined, {}); assert.strictEqual(ExtensionScannerInput.equals(anInput(ROOT, undefined), anInput(ROOT, undefined)), true); assert.strictEqual(ExtensionScannerInput.equals(anInput(ROOT, 100), anInput(ROOT, 100)), true); @@ -336,7 +361,7 @@ suite('ExtensionScannerInput', () => { }); test('compare inputs - application location', () => { - const anInput = (location: URI, mtime: number | undefined) => new ExtensionScannerInput(ROOT, undefined, location, mtime, false, undefined, ExtensionType.User, true, '1.1.1', undefined, undefined, true, undefined, {}); + const anInput = (location: URI, mtime: number | undefined) => new ExtensionScannerInput(ROOT, undefined, location, mtime, false, undefined, ExtensionType.User, true, true, '1.1.1', undefined, undefined, true, undefined, {}); assert.strictEqual(ExtensionScannerInput.equals(anInput(ROOT, undefined), anInput(ROOT, undefined)), true); assert.strictEqual(ExtensionScannerInput.equals(anInput(ROOT, 100), anInput(ROOT, 100)), true); @@ -346,7 +371,7 @@ suite('ExtensionScannerInput', () => { }); test('compare inputs - profile', () => { - const anInput = (profile: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined) => new ExtensionScannerInput(ROOT, undefined, undefined, undefined, profile, profileScanOptions, ExtensionType.User, true, '1.1.1', undefined, undefined, true, undefined, {}); + const anInput = (profile: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined) => new ExtensionScannerInput(ROOT, undefined, undefined, undefined, profile, profileScanOptions, ExtensionType.User, true, true, '1.1.1', undefined, undefined, true, undefined, {}); assert.strictEqual(ExtensionScannerInput.equals(anInput(true, { bailOutWhenFileNotFound: true }), anInput(true, { bailOutWhenFileNotFound: true })), true); assert.strictEqual(ExtensionScannerInput.equals(anInput(false, { bailOutWhenFileNotFound: true }), anInput(false, { bailOutWhenFileNotFound: true })), true); @@ -359,7 +384,7 @@ suite('ExtensionScannerInput', () => { }); test('compare inputs - extension type', () => { - const anInput = (type: ExtensionType) => new ExtensionScannerInput(ROOT, undefined, undefined, undefined, false, undefined, type, true, '1.1.1', undefined, undefined, true, undefined, {}); + const anInput = (type: ExtensionType) => new ExtensionScannerInput(ROOT, undefined, undefined, undefined, false, undefined, type, true, true, '1.1.1', undefined, undefined, true, undefined, {}); assert.strictEqual(ExtensionScannerInput.equals(anInput(ExtensionType.System), anInput(ExtensionType.System)), true); assert.strictEqual(ExtensionScannerInput.equals(anInput(ExtensionType.User), anInput(ExtensionType.User)), true); From 83f695ad2ae4befe613dfd1bb1b1d8547d647d53 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:29:54 +0100 Subject: [PATCH 0198/3587] Move selected editors instead of only activ ones (#236327) * move selected editors * :lipstick: * :lipstick: --- .../browser/parts/editor/editorActions.ts | 38 +++++++------- .../browser/parts/editor/editorCommands.ts | 52 ++++++++++++------- 2 files changed, 51 insertions(+), 39 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index be79b3f0af2f..9d9766f4eb50 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -12,7 +12,7 @@ import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser import { GoFilter, IHistoryService } from '../../../services/history/common/history.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, ActiveEditorMoveCopyArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, COPY_ACTIVE_EDITOR_COMMAND_ID, SPLIT_EDITOR, TOGGLE_MAXIMIZE_EDITOR_GROUP, MOVE_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, MOVE_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID as NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID } from './editorCommands.js'; +import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, SelectedEditorsMoveCopyArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, COPY_ACTIVE_EDITOR_COMMAND_ID, SPLIT_EDITOR, TOGGLE_MAXIMIZE_EDITOR_GROUP, MOVE_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, MOVE_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID as NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID } from './editorCommands.js'; import { IEditorGroupsService, IEditorGroup, GroupsArrangement, GroupLocation, GroupDirection, preferredSideBySideGroupDirection, IFindGroupScope, GroupOrientation, EditorGroupLayout, GroupsOrder, MergeGroupMode } from '../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -1996,7 +1996,7 @@ export class MoveEditorLeftInGroupAction extends ExecuteCommandAction { }, f1: true, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'left' } satisfies ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'left' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2015,7 +2015,7 @@ export class MoveEditorRightInGroupAction extends ExecuteCommandAction { }, f1: true, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'right' } satisfies ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'right' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2034,7 +2034,7 @@ export class MoveEditorToPreviousGroupAction extends ExecuteCommandAction { }, f1: true, category: Categories.View, - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'previous', by: 'group' } satisfies ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'previous', by: 'group' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2053,7 +2053,7 @@ export class MoveEditorToNextGroupAction extends ExecuteCommandAction { } }, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'next', by: 'group' } satisfies ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'next', by: 'group' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2065,7 +2065,7 @@ export class MoveEditorToAboveGroupAction extends ExecuteCommandAction { title: localize2('moveEditorToAboveGroup', 'Move Editor into Group Above'), f1: true, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'up', by: 'group' } satisfies ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'up', by: 'group' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2077,7 +2077,7 @@ export class MoveEditorToBelowGroupAction extends ExecuteCommandAction { title: localize2('moveEditorToBelowGroup', 'Move Editor into Group Below'), f1: true, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'down', by: 'group' } satisfies ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'down', by: 'group' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2089,7 +2089,7 @@ export class MoveEditorToLeftGroupAction extends ExecuteCommandAction { title: localize2('moveEditorToLeftGroup', 'Move Editor into Left Group'), f1: true, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'left', by: 'group' } satisfies ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'left', by: 'group' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2101,7 +2101,7 @@ export class MoveEditorToRightGroupAction extends ExecuteCommandAction { title: localize2('moveEditorToRightGroup', 'Move Editor into Right Group'), f1: true, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'right', by: 'group' } satisfies ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'right', by: 'group' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2120,7 +2120,7 @@ export class MoveEditorToFirstGroupAction extends ExecuteCommandAction { } }, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'first', by: 'group' } satisfies ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'first', by: 'group' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2139,7 +2139,7 @@ export class MoveEditorToLastGroupAction extends ExecuteCommandAction { } }, category: Categories.View - }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'last', by: 'group' } satisfies ActiveEditorMoveCopyArguments); + }, MOVE_ACTIVE_EDITOR_COMMAND_ID, { to: 'last', by: 'group' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2151,7 +2151,7 @@ export class SplitEditorToPreviousGroupAction extends ExecuteCommandAction { title: localize2('splitEditorToPreviousGroup', 'Split Editor into Previous Group'), f1: true, category: Categories.View - }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'previous', by: 'group' } satisfies ActiveEditorMoveCopyArguments); + }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'previous', by: 'group' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2163,7 +2163,7 @@ export class SplitEditorToNextGroupAction extends ExecuteCommandAction { title: localize2('splitEditorToNextGroup', 'Split Editor into Next Group'), f1: true, category: Categories.View - }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'next', by: 'group' } satisfies ActiveEditorMoveCopyArguments); + }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'next', by: 'group' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2175,7 +2175,7 @@ export class SplitEditorToAboveGroupAction extends ExecuteCommandAction { title: localize2('splitEditorToAboveGroup', 'Split Editor into Group Above'), f1: true, category: Categories.View - }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'up', by: 'group' } satisfies ActiveEditorMoveCopyArguments); + }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'up', by: 'group' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2187,7 +2187,7 @@ export class SplitEditorToBelowGroupAction extends ExecuteCommandAction { title: localize2('splitEditorToBelowGroup', 'Split Editor into Group Below'), f1: true, category: Categories.View - }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'down', by: 'group' } satisfies ActiveEditorMoveCopyArguments); + }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'down', by: 'group' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2202,7 +2202,7 @@ export class SplitEditorToLeftGroupAction extends ExecuteCommandAction { title: localize2('splitEditorToLeftGroup', "Split Editor into Left Group"), f1: true, category: Categories.View - }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'left', by: 'group' } satisfies ActiveEditorMoveCopyArguments); + }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'left', by: 'group' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2214,7 +2214,7 @@ export class SplitEditorToRightGroupAction extends ExecuteCommandAction { title: localize2('splitEditorToRightGroup', 'Split Editor into Right Group'), f1: true, category: Categories.View - }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'right', by: 'group' } satisfies ActiveEditorMoveCopyArguments); + }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'right', by: 'group' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2226,7 +2226,7 @@ export class SplitEditorToFirstGroupAction extends ExecuteCommandAction { title: localize2('splitEditorToFirstGroup', 'Split Editor into First Group'), f1: true, category: Categories.View - }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'first', by: 'group' } satisfies ActiveEditorMoveCopyArguments); + }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'first', by: 'group' } satisfies SelectedEditorsMoveCopyArguments); } } @@ -2238,7 +2238,7 @@ export class SplitEditorToLastGroupAction extends ExecuteCommandAction { title: localize2('splitEditorToLastGroup', 'Split Editor into Last Group'), f1: true, category: Categories.View - }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'last', by: 'group' } satisfies ActiveEditorMoveCopyArguments); + }, COPY_ACTIVE_EDITOR_COMMAND_ID, { to: 'last', by: 'group' } satisfies SelectedEditorsMoveCopyArguments); } } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 83f88bd7b749..1bbe6258eb05 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -28,7 +28,7 @@ import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess } from './editorQuickAc import { SideBySideEditor } from './sideBySideEditor.js'; import { TextDiffEditor } from './textDiffEditor.js'; import { ActiveEditorCanSplitInGroupContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupLockedContext, ActiveEditorStickyContext, MultipleEditorGroupsContext, SideBySideEditorActiveContext, TextCompareEditorActiveContext } from '../../../common/contextkeys.js'; -import { CloseDirection, EditorInputCapabilities, EditorsOrder, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, IVisibleEditorPane, isEditorInputWithOptionsAndGroup } from '../../../common/editor.js'; +import { CloseDirection, EditorInputCapabilities, EditorsOrder, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, isEditorInputWithOptionsAndGroup } from '../../../common/editor.js'; import { DiffEditorInput } from '../../../common/editor/diffEditorInput.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { SideBySideEditorInput } from '../../../common/editor/sideBySideEditorInput.js'; @@ -108,13 +108,13 @@ export const EDITOR_CORE_NAVIGATION_COMMANDS = [ TOGGLE_MAXIMIZE_EDITOR_GROUP ]; -export interface ActiveEditorMoveCopyArguments { +export interface SelectedEditorsMoveCopyArguments { to?: 'first' | 'last' | 'left' | 'right' | 'up' | 'down' | 'center' | 'position' | 'previous' | 'next'; by?: 'tab' | 'group'; value?: number; } -const isActiveEditorMoveCopyArg = function (arg: ActiveEditorMoveCopyArguments): boolean { +const isSelectedEditorsMoveCopyArg = function (arg: SelectedEditorsMoveCopyArguments): boolean { if (!isObject(arg)) { return false; } @@ -159,14 +159,14 @@ function registerActiveEditorMoveCopyCommand(): void { weight: KeybindingWeight.WorkbenchContrib, when: EditorContextKeys.editorTextFocus, primary: 0, - handler: (accessor, args) => moveCopyActiveEditor(true, args, accessor), + handler: (accessor, args) => moveCopySelectedEditors(true, args, accessor), metadata: { description: localize('editorCommand.activeEditorMove.description', "Move the active editor by tabs or groups"), args: [ { name: localize('editorCommand.activeEditorMove.arg.name', "Active editor move argument"), description: localize('editorCommand.activeEditorMove.arg.description', "Argument Properties:\n\t* 'to': String value providing where to move.\n\t* 'by': String value providing the unit for move (by tab or by group).\n\t* 'value': Number value providing how many positions or an absolute position to move."), - constraint: isActiveEditorMoveCopyArg, + constraint: isSelectedEditorsMoveCopyArg, schema: moveCopyJSONSchema } ] @@ -178,42 +178,55 @@ function registerActiveEditorMoveCopyCommand(): void { weight: KeybindingWeight.WorkbenchContrib, when: EditorContextKeys.editorTextFocus, primary: 0, - handler: (accessor, args) => moveCopyActiveEditor(false, args, accessor), + handler: (accessor, args) => moveCopySelectedEditors(false, args, accessor), metadata: { description: localize('editorCommand.activeEditorCopy.description', "Copy the active editor by groups"), args: [ { name: localize('editorCommand.activeEditorCopy.arg.name', "Active editor copy argument"), description: localize('editorCommand.activeEditorCopy.arg.description', "Argument Properties:\n\t* 'to': String value providing where to copy.\n\t* 'value': Number value providing how many positions or an absolute position to copy."), - constraint: isActiveEditorMoveCopyArg, + constraint: isSelectedEditorsMoveCopyArg, schema: moveCopyJSONSchema } ] } }); - function moveCopyActiveEditor(isMove: boolean, args: ActiveEditorMoveCopyArguments = Object.create(null), accessor: ServicesAccessor): void { + function moveCopySelectedEditors(isMove: boolean, args: SelectedEditorsMoveCopyArguments = Object.create(null), accessor: ServicesAccessor): void { args.to = args.to || 'right'; args.by = args.by || 'tab'; args.value = typeof args.value === 'number' ? args.value : 1; - const activeEditorPane = accessor.get(IEditorService).activeEditorPane; - if (activeEditorPane) { + const activeGroup = accessor.get(IEditorGroupsService).activeGroup; + const selectedEditors = activeGroup.selectedEditors; + if (selectedEditors.length > 0) { switch (args.by) { case 'tab': if (isMove) { - return moveActiveTab(args, activeEditorPane); + return moveTabs(args, activeGroup, selectedEditors); } break; case 'group': - return moveCopyActiveEditorToGroup(isMove, args, activeEditorPane, accessor); + return moveCopyActiveEditorToGroup(isMove, args, activeGroup, selectedEditors, accessor); } } } - function moveActiveTab(args: ActiveEditorMoveCopyArguments, control: IVisibleEditorPane): void { - const group = control.group; - let index = group.getIndexOfEditor(control.input); + function moveTabs(args: SelectedEditorsMoveCopyArguments, group: IEditorGroup, editors: EditorInput[]): void { + const to = args.to; + if (to === 'first' || to === 'right') { + editors = [...editors].reverse(); + } else if (to === 'position' && (args.value ?? 1) < group.getIndexOfEditor(editors[0])) { + editors = [...editors].reverse(); + } + + for (const editor of editors) { + moveTab(args, group, editor); + } + } + + function moveTab(args: SelectedEditorsMoveCopyArguments, group: IEditorGroup, editor: EditorInput): void { + let index = group.getIndexOfEditor(editor); switch (args.to) { case 'first': index = 0; @@ -236,14 +249,13 @@ function registerActiveEditorMoveCopyCommand(): void { } index = index < 0 ? 0 : index >= group.count ? group.count - 1 : index; - group.moveEditor(control.input, group, { index }); + group.moveEditor(editor, group, { index }); } - function moveCopyActiveEditorToGroup(isMove: boolean, args: ActiveEditorMoveCopyArguments, control: IVisibleEditorPane, accessor: ServicesAccessor): void { + function moveCopyActiveEditorToGroup(isMove: boolean, args: SelectedEditorsMoveCopyArguments, sourceGroup: IEditorGroup, editors: EditorInput[], accessor: ServicesAccessor): void { const editorGroupsService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); - const sourceGroup = control.group; let targetGroup: IEditorGroup | undefined; switch (args.to) { @@ -296,9 +308,9 @@ function registerActiveEditorMoveCopyCommand(): void { if (targetGroup) { if (isMove) { - sourceGroup.moveEditor(control.input, targetGroup); + sourceGroup.moveEditors(editors.map(editor => ({ editor })), targetGroup); } else if (sourceGroup.id !== targetGroup.id) { - sourceGroup.copyEditor(control.input, targetGroup); + sourceGroup.copyEditors(editors.map(editor => ({ editor })), targetGroup); } targetGroup.focus(); } From 2fb0656601fdd8ee16e4bda6f4e788594a22e8f2 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Dec 2024 10:27:36 +0100 Subject: [PATCH 0199/3587] Removal of `chat.experimental.offerSetup` (fix microsoft/vscode-copilot#11286) (#236320) --- .../chat/browser/actions/chatActions.ts | 2 - .../contrib/chat/browser/chat.contribution.ts | 7 -- .../browser/chatParticipant.contribution.ts | 5 +- .../contrib/chat/browser/chatSetup.ts | 16 ++-- .../contrib/chat/common/chatContextKeys.ts | 36 ++++---- .../common/gettingStartedContent.ts | 84 ++++++++++--------- 6 files changed, 68 insertions(+), 82 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index de160c8c2631..92f6c42dd3df 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -534,7 +534,6 @@ MenuRegistry.appendMenuItem(MenuId.CommandCenter, { when: ContextKeyExpr.and( ContextKeyExpr.has('config.chat.commandCenter.enabled'), ContextKeyExpr.or( - ContextKeyExpr.has('config.chat.experimental.offerSetup'), ChatContextKeys.Setup.installed, ChatContextKeys.panelParticipantRegistered ) @@ -552,7 +551,6 @@ registerAction2(class ToggleCopilotControl extends ToggleTitleBarConfigAction { ContextKeyExpr.has('config.window.commandCenter'), ContextKeyExpr.or( ChatContextKeys.Setup.installed, - ContextKeyExpr.has('config.chat.experimental.offerSetup'), ChatContextKeys.panelParticipantRegistered ) ) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 2aa243649de8..b03d4605d160 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -121,13 +121,6 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('chat.commandCenter.enabled', "Controls whether the command center shows a menu for actions to control Copilot (requires {0}).", '`#window.commandCenter#`'), default: true }, - 'chat.experimental.offerSetup': { - type: 'boolean', - default: false, - scope: ConfigurationScope.APPLICATION, - markdownDescription: nls.localize('chat.experimental.offerSetup', "Controls whether setup is offered for Chat if not done already."), - tags: ['experimental', 'onExP'] - }, 'chat.editing.alwaysSaveWithGeneratedChanges': { type: 'boolean', scope: ConfigurationScope.APPLICATION, diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts index 355a3d55ff8b..6326c643d9c7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts @@ -309,10 +309,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { }, ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Panel }]), when: ContextKeyExpr.or( - ContextKeyExpr.and( - ContextKeyExpr.has('config.chat.experimental.offerSetup'), - ChatContextKeys.Setup.triggered - ), + ChatContextKeys.Setup.triggered, ChatContextKeys.Setup.installed, ChatContextKeys.panelParticipantRegistered, ChatContextKeys.extensionInvalid diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 54e081fbc0d9..aab4103ddac9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -132,12 +132,9 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr private registerActions(): void { const that = this; - const chatSetupTriggerContext = ContextKeyExpr.and( - ContextKeyExpr.has('config.chat.experimental.offerSetup'), - ContextKeyExpr.or( - ChatContextKeys.Setup.installed.negate(), - ChatContextKeys.Setup.canSignUp - ) + const chatSetupTriggerContext = ContextKeyExpr.or( + ChatContextKeys.Setup.installed.negate(), + ChatContextKeys.Setup.canSignUp ); class ChatSetupTriggerAction extends Action2 { @@ -188,10 +185,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr title: ChatSetupHideAction.TITLE, f1: true, category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and( - ChatContextKeys.Setup.installed.negate(), - ContextKeyExpr.has('config.chat.experimental.offerSetup') - ), + precondition: ChatContextKeys.Setup.installed.negate(), menu: { id: MenuId.ChatCommandCenter, group: 'z_hide', @@ -1006,6 +1000,7 @@ class ChatSetupContext extends Disposable { private readonly canSignUpContextKey = ChatContextKeys.Setup.canSignUp.bindTo(this.contextKeyService); private readonly signedOutContextKey = ChatContextKeys.Setup.signedOut.bindTo(this.contextKeyService); private readonly limitedContextKey = ChatContextKeys.Setup.limited.bindTo(this.contextKeyService); + private readonly proContextKey = ChatContextKeys.Setup.pro.bindTo(this.contextKeyService); private readonly triggeredContext = ChatContextKeys.Setup.triggered.bindTo(this.contextKeyService); private readonly installedContext = ChatContextKeys.Setup.installed.bindTo(this.contextKeyService); @@ -1108,6 +1103,7 @@ class ChatSetupContext extends Disposable { this.signedOutContextKey.set(this._state.entitlement === ChatEntitlement.Unknown); this.canSignUpContextKey.set(this._state.entitlement === ChatEntitlement.Available); this.limitedContextKey.set(this._state.entitlement === ChatEntitlement.Limited); + this.proContextKey.set(this._state.entitlement === ChatEntitlement.Pro); this.triggeredContext.set(!!this._state.triggered); this.installedContext.set(!!this._state.installed); diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 047a5824af2f..f615233de505 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -42,31 +42,31 @@ export namespace ChatContextKeys { export const languageModelsAreUserSelectable = new RawContextKey('chatModelsAreUserSelectable', false, { type: 'boolean', description: localize('chatModelsAreUserSelectable', "True when the chat model can be selected manually by the user.") }); export const Setup = { - canSignUp: new RawContextKey('chatSetupCanSignUp', false, true), // True when user can sign up to be a chat limited user. + // State signedOut: new RawContextKey('chatSetupSignedOut', false, true), // True when user is signed out. - limited: new RawContextKey('chatSetupLimited', false, true), // True when user is a chat limited user. - triggered: new RawContextKey('chatSetupTriggered', false, true), // True when chat setup is triggered. installed: new RawContextKey('chatSetupInstalled', false, true), // True when the chat extension is installed. + + // Plans + canSignUp: new RawContextKey('chatPlanCanSignUp', false, true), // True when user can sign up to be a chat limited user. + limited: new RawContextKey('chatPlanLimited', false, true), // True when user is a chat limited user. + pro: new RawContextKey('chatPlanPro', false, true) // True when user is a chat pro user. }; export const SetupViewKeys = new Set([ChatContextKeys.Setup.triggered.key, ChatContextKeys.Setup.installed.key, ChatContextKeys.Setup.signedOut.key, ChatContextKeys.Setup.canSignUp.key]); - export const SetupViewCondition = ContextKeyExpr.and( - ContextKeyExpr.has('config.chat.experimental.offerSetup'), - ContextKeyExpr.or( - ContextKeyExpr.and( - ChatContextKeys.Setup.triggered, - ChatContextKeys.Setup.installed.negate() - ), - ContextKeyExpr.and( - ChatContextKeys.Setup.canSignUp, - ChatContextKeys.Setup.installed - ), - ContextKeyExpr.and( - ChatContextKeys.Setup.signedOut, - ChatContextKeys.Setup.installed - ) + export const SetupViewCondition = ContextKeyExpr.or( + ContextKeyExpr.and( + ChatContextKeys.Setup.triggered, + ChatContextKeys.Setup.installed.negate() + ), + ContextKeyExpr.and( + ChatContextKeys.Setup.canSignUp, + ChatContextKeys.Setup.installed + ), + ContextKeyExpr.and( + ChatContextKeys.Setup.signedOut, + ChatContextKeys.Setup.installed ) )!; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index 303905c91360..7fd3e30163f3 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -246,9 +246,9 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ content: { type: 'steps', steps: [ - createCopilotSetupStep('CopilotSetupSignedOut', CopilotSignedOutButton, 'config.chat.experimental.offerSetup && chatSetupSignedOut', true), - createCopilotSetupStep('CopilotSetupComplete', CopilotCompleteButton, 'config.chat.experimental.offerSetup && chatSetupInstalled && (chatSetupEntitled || chatSetupLimited)', false), - createCopilotSetupStep('CopilotSetupSignedIn', CopilotSignedInButton, 'config.chat.experimental.offerSetup && !chatSetupSignedOut && (!chatSetupInstalled || chatSetupCanSignUp)', true), + createCopilotSetupStep('CopilotSetupSignedOut', CopilotSignedOutButton, 'chatSetupSignedOut', true), + createCopilotSetupStep('CopilotSetupComplete', CopilotCompleteButton, 'chatSetupInstalled && (chatPlanPro || chatPlanLimited)', false), + createCopilotSetupStep('CopilotSetupSignedIn', CopilotSignedInButton, '!chatSetupSignedOut && (!chatSetupInstalled || chatPlanCanSignUp)', true), { id: 'pickColorTheme', title: localize('gettingStarted.pickColor.title', "Choose your theme"), @@ -277,30 +277,31 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ type: 'svg', altText: 'Language extensions', path: 'languages.svg' }, }, - { - id: 'settings', - title: localize('gettingStarted.settings.title', "Tune your settings"), - description: localize('gettingStarted.settings.description.interpolated', "Customize every aspect of VS Code and your extensions to your liking. Commonly used settings are listed first to get you started.\n{0}", Button(localize('tweakSettings', "Open Settings"), 'command:toSide:workbench.action.openSettings')), - when: '!config.chat.experimental.offerSetup', - media: { - type: 'svg', altText: 'VS Code Settings', path: 'settings.svg' - }, - }, - { - id: 'settingsSync', - title: localize('gettingStarted.settingsSync.title', "Sync settings across devices"), - description: localize('gettingStarted.settingsSync.description.interpolated', "Keep your essential customizations backed up and updated across all your devices.\n{0}", Button(localize('enableSync', "Backup and Sync Settings"), 'command:workbench.userDataSync.actions.turnOn')), - when: '!config.chat.experimental.offerSetup && syncStatus != uninitialized', - completionEvents: ['onEvent:sync-enabled'], - media: { - type: 'svg', altText: 'The "Turn on Sync" entry in the settings gear menu.', path: 'settingsSync.svg' - }, - }, + // Hidden in favor of copilot entry (to be revisited when copilot entry moves, if at all) + // { + // id: 'settings', + // title: localize('gettingStarted.settings.title', "Tune your settings"), + // description: localize('gettingStarted.settings.description.interpolated', "Customize every aspect of VS Code and your extensions to your liking. Commonly used settings are listed first to get you started.\n{0}", Button(localize('tweakSettings', "Open Settings"), 'command:toSide:workbench.action.openSettings')), + // when: '!config.chat.experimental.offerSetup', + // media: { + // type: 'svg', altText: 'VS Code Settings', path: 'settings.svg' + // }, + // }, + // { + // id: 'settingsSync', + // title: localize('gettingStarted.settingsSync.title', "Sync settings across devices"), + // description: localize('gettingStarted.settingsSync.description.interpolated', "Keep your essential customizations backed up and updated across all your devices.\n{0}", Button(localize('enableSync', "Backup and Sync Settings"), 'command:workbench.userDataSync.actions.turnOn')), + // when: '!config.chat.experimental.offerSetup && syncStatus != uninitialized', + // completionEvents: ['onEvent:sync-enabled'], + // media: { + // type: 'svg', altText: 'The "Turn on Sync" entry in the settings gear menu.', path: 'settingsSync.svg' + // }, + // }, { id: 'settingsAndSync', title: localize('gettingStarted.settings.title', "Tune your settings"), description: localize('gettingStarted.settingsAndSync.description.interpolated', "Customize every aspect of VS Code and your extensions to your liking. [Back up and sync](command:workbench.userDataSync.actions.turnOn) your essential customizations across all your devices.\n{0}", Button(localize('tweakSettings', "Open Settings"), 'command:toSide:workbench.action.openSettings')), - when: 'config.chat.experimental.offerSetup && syncStatus != uninitialized', + when: 'syncStatus != uninitialized', completionEvents: ['onEvent:sync-enabled'], media: { type: 'svg', altText: 'VS Code Settings', path: 'settings.svg' @@ -312,24 +313,25 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ description: localize('gettingStarted.commandPalette.description.interpolated', "Run commands without reaching for your mouse to accomplish any task in VS Code.\n{0}", Button(localize('commandPalette', "Open Command Palette"), 'command:workbench.action.showCommands')), media: { type: 'svg', altText: 'Command Palette overlay for searching and executing commands.', path: 'commandPalette.svg' }, }, - { - id: 'pickAFolderTask-Mac', - title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"), - description: localize('gettingStarted.setup.OpenFolder.description.interpolated', "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", Button(localize('pickFolder', "Pick a Folder"), 'command:workbench.action.files.openFileFolder')), - when: '!config.chat.experimental.offerSetup && isMac && workspaceFolderCount == 0', - media: { - type: 'svg', altText: 'Explorer view showing buttons for opening folder and cloning repository.', path: 'openFolder.svg' - } - }, - { - id: 'pickAFolderTask-Other', - title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"), - description: localize('gettingStarted.setup.OpenFolder.description.interpolated', "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", Button(localize('pickFolder', "Pick a Folder"), 'command:workbench.action.files.openFolder')), - when: '!config.chat.experimental.offerSetup && !isMac && workspaceFolderCount == 0', - media: { - type: 'svg', altText: 'Explorer view showing buttons for opening folder and cloning repository.', path: 'openFolder.svg' - } - }, + // Hidden in favor of copilot entry (to be revisited when copilot entry moves, if at all) + // { + // id: 'pickAFolderTask-Mac', + // title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"), + // description: localize('gettingStarted.setup.OpenFolder.description.interpolated', "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", Button(localize('pickFolder', "Pick a Folder"), 'command:workbench.action.files.openFileFolder')), + // when: '!config.chat.experimental.offerSetup && isMac && workspaceFolderCount == 0', + // media: { + // type: 'svg', altText: 'Explorer view showing buttons for opening folder and cloning repository.', path: 'openFolder.svg' + // } + // }, + // { + // id: 'pickAFolderTask-Other', + // title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"), + // description: localize('gettingStarted.setup.OpenFolder.description.interpolated', "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", Button(localize('pickFolder', "Pick a Folder"), 'command:workbench.action.files.openFolder')), + // when: '!config.chat.experimental.offerSetup && !isMac && workspaceFolderCount == 0', + // media: { + // type: 'svg', altText: 'Explorer view showing buttons for opening folder and cloning repository.', path: 'openFolder.svg' + // } + // }, { id: 'quickOpen', title: localize('gettingStarted.quickOpen.title', "Quickly navigate between your files"), From 3daa33e93a945f4f6cab577f424e1862c00846c1 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:31:29 +0100 Subject: [PATCH 0200/3587] Restore close commands for keybinding support (#236446) * bring back the close commands * improve discoverability --- .../browser/actions/layoutActions.ts | 22 +++---------------- .../parts/auxiliarybar/auxiliaryBarActions.ts | 18 +++++++++++++++ .../browser/parts/panel/panelActions.ts | 18 +++++++++++++++ .../browser/parts/sidebar/sidebarActions.ts | 18 +++++++++++++++ 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index c3ba2ab9907d..75253e235de8 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -53,25 +53,6 @@ const fullscreenIcon = registerIcon('fullscreen', Codicon.screenFull, localize(' const centerLayoutIcon = registerIcon('centerLayoutIcon', Codicon.layoutCentered, localize('centerLayoutIcon', "Represents centered layout mode")); const zenModeIcon = registerIcon('zenMode', Codicon.target, localize('zenModeIcon', "Represents zen mode")); - -// --- Close Side Bar - -registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.action.closeSidebar', - title: localize2('closeSidebar', 'Close Primary Side Bar'), - category: Categories.View, - f1: true - }); - } - - run(accessor: ServicesAccessor): void { - accessor.get(IWorkbenchLayoutService).setPartHidden(true, Parts.SIDEBAR_PART); - } -}); - export const ToggleActivityBarVisibilityActionId = 'workbench.action.toggleActivityBarVisibility'; // --- Toggle Centered Layout @@ -323,6 +304,9 @@ class ToggleSidebarVisibilityAction extends Action2 { title: localize('primary sidebar', "Primary Side Bar"), mnemonicTitle: localize({ key: 'primary sidebar mnemonic', comment: ['&& denotes a mnemonic'] }, "&&Primary Side Bar"), }, + metadata: { + description: localize('openAndCloseSidebar', 'Open/Show and Close/Hide Sidebar'), + }, category: Categories.View, f1: true, keybinding: { diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts index 3bea858332e9..e564a8d49d77 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts @@ -41,6 +41,9 @@ export class ToggleAuxiliaryBarAction extends Action2 { }, icon: closeIcon, // Ensures no flickering when using toggled.icon category: Categories.View, + metadata: { + description: localize('openAndCloseAuxiliaryBar', 'Open/Show and Close/Hide Secondary Side Bar'), + }, f1: true, keybinding: { weight: KeybindingWeight.WorkbenchContrib, @@ -74,6 +77,21 @@ export class ToggleAuxiliaryBarAction extends Action2 { registerAction2(ToggleAuxiliaryBarAction); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.closeAuxiliaryBar', + title: localize2('closeSecondarySideBar', 'Hide Secondary Side Bar'), + category: Categories.View, + precondition: AuxiliaryBarVisibleContext, + f1: true, + }); + } + run(accessor: ServicesAccessor) { + accessor.get(IWorkbenchLayoutService).setPartHidden(true, Parts.AUXILIARYBAR_PART); + } +}); + registerAction2(class FocusAuxiliaryBarAction extends Action2 { static readonly ID = 'workbench.action.focusAuxiliaryBar'; diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index e8fbbacb68a4..b3be54f49d78 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -46,6 +46,9 @@ export class TogglePanelAction extends Action2 { icon: closeIcon, // Ensures no flickering when using toggled.icon f1: true, category: Categories.View, + metadata: { + description: localize('openAndClosePanel', 'Open/Show and Close/Hide Panel'), + }, keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyJ, weight: KeybindingWeight.WorkbenchContrib }, menu: [ { @@ -73,6 +76,21 @@ export class TogglePanelAction extends Action2 { registerAction2(TogglePanelAction); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.closePanel', + title: localize2('closePanel', 'Hide Panel'), + category: Categories.View, + precondition: PanelVisibleContext, + f1: true, + }); + } + run(accessor: ServicesAccessor) { + accessor.get(IWorkbenchLayoutService).setPartHidden(true, Parts.PANEL_PART); + } +}); + registerAction2(class extends Action2 { static readonly ID = 'workbench.action.focusPanel'; diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarActions.ts b/src/vs/workbench/browser/parts/sidebar/sidebarActions.ts index 408bb3e4eea0..4256ecb53524 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarActions.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarActions.ts @@ -13,6 +13,24 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; import { ViewContainerLocation } from '../../../common/views.js'; +import { SideBarVisibleContext } from '../../../common/contextkeys.js'; + +registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.action.closeSidebar', + title: localize2('closeSidebar', 'Close Primary Side Bar'), + category: Categories.View, + f1: true, + precondition: SideBarVisibleContext + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(IWorkbenchLayoutService).setPartHidden(true, Parts.SIDEBAR_PART); + } +}); export class FocusSideBarAction extends Action2 { From 224ade93d0510c71cbd362e7a8974b2e3df471d6 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:35:16 +0100 Subject: [PATCH 0201/3587] Git - don't use look-behind regex (#236447) --- extensions/git/src/git.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 4a968792a772..f2b9100d79a1 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -502,7 +502,7 @@ export class Git { const repoUri = Uri.file(repositoryRootPath); const pathUri = Uri.file(pathInsidePossibleRepository); if (repoUri.authority.length !== 0 && pathUri.authority.length === 0) { - const match = /(?<=^\/?)([a-zA-Z])(?=:\/)/.exec(pathUri.path); + const match = /^[\/]?([a-zA-Z])[:\/]/.exec(pathUri.path); if (match !== null) { const [, letter] = match; From 29a39607d85dba549907a12b4ceece574d6dce51 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:40:30 +0100 Subject: [PATCH 0202/3587] Align panel badge style with activity bar (#236448) closes #195998 --- src/vs/workbench/browser/parts/panel/panelPart.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 6363f2663846..d93cfd11b3de 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -16,7 +16,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { TogglePanelAction } from './panelActions.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_TITLE_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_DRAG_AND_DROP_BORDER, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND } from '../../../common/theme.js'; -import { badgeBackground, badgeForeground, contrastBorder } from '../../../../platform/theme/common/colorRegistry.js'; +import { contrastBorder } from '../../../../platform/theme/common/colorRegistry.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { Dimension } from '../../../../base/browser/dom.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -130,13 +130,12 @@ export class PanelPart extends AbstractPaneCompositePart { } protected getCompositeBarOptions(): IPaneCompositeBarOptions { - const showIcons = this.configurationService.getValue('workbench.panel.showLabels') === false; return { partContainerClass: 'panel', pinnedViewContainersKey: 'workbench.panel.pinnedPanels', placeholderViewContainersKey: 'workbench.panel.placeholderPanels', viewContainersWorkspaceStateKey: 'workbench.panel.viewContainersWorkspaceState', - icon: showIcons, + icon: this.configurationService.getValue('workbench.panel.showLabels') === false, orientation: ActionsOrientation.HORIZONTAL, recomputeSizes: true, activityHoverOptions: { @@ -153,8 +152,8 @@ export class PanelPart extends AbstractPaneCompositePart { activeBorderBottomColor: theme.getColor(PANEL_ACTIVE_TITLE_BORDER), activeForegroundColor: theme.getColor(PANEL_ACTIVE_TITLE_FOREGROUND), inactiveForegroundColor: theme.getColor(PANEL_INACTIVE_TITLE_FOREGROUND), - badgeBackground: theme.getColor(showIcons ? ACTIVITY_BAR_BADGE_BACKGROUND : badgeBackground), - badgeForeground: theme.getColor(showIcons ? ACTIVITY_BAR_BADGE_FOREGROUND : badgeForeground), + badgeBackground: theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND), + badgeForeground: theme.getColor(ACTIVITY_BAR_BADGE_FOREGROUND), dragAndDropBorder: theme.getColor(PANEL_DRAG_AND_DROP_BORDER) }) }; From 777fd07cccc3de449e529c9f701c2cfdd36ecb3e Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:20:00 +0100 Subject: [PATCH 0203/3587] Git - adopt #private in Git extension API (#236444) * Git - adopt #private in Git extension API * Fix post commit command provider --- extensions/git/src/api/api1.ts | 236 +++++++++++++---------- extensions/git/src/main.ts | 2 +- extensions/git/src/postCommitCommands.ts | 14 +- 3 files changed, 142 insertions(+), 110 deletions(-) diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index a8a8fb694b16..8cc0b99f1132 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/* eslint-disable local/code-no-native-private */ + import { Model } from '../model'; import { Repository as BaseRepository, Resource } from '../repository'; import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions } from './git'; @@ -15,222 +17,242 @@ import { PickRemoteSourceOptions } from './git-base'; import { OperationKind, OperationResult } from '../operation'; class ApiInputBox implements InputBox { - set value(value: string) { this._inputBox.value = value; } - get value(): string { return this._inputBox.value; } - constructor(private _inputBox: SourceControlInputBox) { } + #inputBox: SourceControlInputBox; + + constructor(inputBox: SourceControlInputBox) { this.#inputBox = inputBox; } + + set value(value: string) { this.#inputBox.value = value; } + get value(): string { return this.#inputBox.value; } } export class ApiChange implements Change { + #resource: Resource; + constructor(resource: Resource) { this.#resource = resource; } - get uri(): Uri { return this.resource.resourceUri; } - get originalUri(): Uri { return this.resource.original; } - get renameUri(): Uri | undefined { return this.resource.renameResourceUri; } - get status(): Status { return this.resource.type; } - - constructor(private readonly resource: Resource) { } + get uri(): Uri { return this.#resource.resourceUri; } + get originalUri(): Uri { return this.#resource.original; } + get renameUri(): Uri | undefined { return this.#resource.renameResourceUri; } + get status(): Status { return this.#resource.type; } } export class ApiRepositoryState implements RepositoryState { + #repository: BaseRepository; + readonly onDidChange: Event; - get HEAD(): Branch | undefined { return this._repository.HEAD; } + constructor(repository: BaseRepository) { + this.#repository = repository; + this.onDidChange = this.#repository.onDidRunGitStatus; + } + + get HEAD(): Branch | undefined { return this.#repository.HEAD; } /** * @deprecated Use ApiRepository.getRefs() instead. */ get refs(): Ref[] { console.warn('Deprecated. Use ApiRepository.getRefs() instead.'); return []; } - get remotes(): Remote[] { return [...this._repository.remotes]; } - get submodules(): Submodule[] { return [...this._repository.submodules]; } - get rebaseCommit(): Commit | undefined { return this._repository.rebaseCommit; } - - get mergeChanges(): Change[] { return this._repository.mergeGroup.resourceStates.map(r => new ApiChange(r)); } - get indexChanges(): Change[] { return this._repository.indexGroup.resourceStates.map(r => new ApiChange(r)); } - get workingTreeChanges(): Change[] { return this._repository.workingTreeGroup.resourceStates.map(r => new ApiChange(r)); } - get untrackedChanges(): Change[] { return this._repository.untrackedGroup.resourceStates.map(r => new ApiChange(r)); } - - readonly onDidChange: Event = this._repository.onDidRunGitStatus; - - constructor(private _repository: BaseRepository) { } + get remotes(): Remote[] { return [...this.#repository.remotes]; } + get submodules(): Submodule[] { return [...this.#repository.submodules]; } + get rebaseCommit(): Commit | undefined { return this.#repository.rebaseCommit; } + + get mergeChanges(): Change[] { return this.#repository.mergeGroup.resourceStates.map(r => new ApiChange(r)); } + get indexChanges(): Change[] { return this.#repository.indexGroup.resourceStates.map(r => new ApiChange(r)); } + get workingTreeChanges(): Change[] { return this.#repository.workingTreeGroup.resourceStates.map(r => new ApiChange(r)); } + get untrackedChanges(): Change[] { return this.#repository.untrackedGroup.resourceStates.map(r => new ApiChange(r)); } } export class ApiRepositoryUIState implements RepositoryUIState { + #sourceControl: SourceControl; + readonly onDidChange: Event; - get selected(): boolean { return this._sourceControl.selected; } - - readonly onDidChange: Event = mapEvent(this._sourceControl.onDidChangeSelection, () => null); + constructor(sourceControl: SourceControl) { + this.#sourceControl = sourceControl; + this.onDidChange = mapEvent(this.#sourceControl.onDidChangeSelection, () => null); + } - constructor(private _sourceControl: SourceControl) { } + get selected(): boolean { return this.#sourceControl.selected; } } export class ApiRepository implements Repository { - readonly rootUri: Uri = Uri.file(this.repository.root); - readonly inputBox: InputBox = new ApiInputBox(this.repository.inputBox); - readonly state: RepositoryState = new ApiRepositoryState(this.repository); - readonly ui: RepositoryUIState = new ApiRepositoryUIState(this.repository.sourceControl); + #repository: BaseRepository; + + readonly rootUri: Uri; + readonly inputBox: InputBox; + readonly state: RepositoryState; + readonly ui: RepositoryUIState; - readonly onDidCommit: Event = mapEvent( - filterEvent(this.repository.onDidRunOperation, e => e.operation.kind === OperationKind.Commit), () => null); + readonly onDidCommit: Event; + readonly onDidCheckout: Event; - readonly onDidCheckout: Event = mapEvent( - filterEvent(this.repository.onDidRunOperation, e => e.operation.kind === OperationKind.Checkout || e.operation.kind === OperationKind.CheckoutTracking), () => null); + constructor(repository: BaseRepository) { + this.#repository = repository; - constructor(readonly repository: BaseRepository) { } + this.rootUri = Uri.file(this.#repository.root); + this.inputBox = new ApiInputBox(this.#repository.inputBox); + this.state = new ApiRepositoryState(this.#repository); + this.ui = new ApiRepositoryUIState(this.#repository.sourceControl); + + this.onDidCommit = mapEvent( + filterEvent(this.#repository.onDidRunOperation, e => e.operation.kind === OperationKind.Commit), () => null); + this.onDidCheckout = mapEvent( + filterEvent(this.#repository.onDidRunOperation, e => e.operation.kind === OperationKind.Checkout || e.operation.kind === OperationKind.CheckoutTracking), () => null); + } apply(patch: string, reverse?: boolean): Promise { - return this.repository.apply(patch, reverse); + return this.#repository.apply(patch, reverse); } getConfigs(): Promise<{ key: string; value: string }[]> { - return this.repository.getConfigs(); + return this.#repository.getConfigs(); } getConfig(key: string): Promise { - return this.repository.getConfig(key); + return this.#repository.getConfig(key); } setConfig(key: string, value: string): Promise { - return this.repository.setConfig(key, value); + return this.#repository.setConfig(key, value); } getGlobalConfig(key: string): Promise { - return this.repository.getGlobalConfig(key); + return this.#repository.getGlobalConfig(key); } getObjectDetails(treeish: string, path: string): Promise<{ mode: string; object: string; size: number }> { - return this.repository.getObjectDetails(treeish, path); + return this.#repository.getObjectDetails(treeish, path); } detectObjectType(object: string): Promise<{ mimetype: string; encoding?: string }> { - return this.repository.detectObjectType(object); + return this.#repository.detectObjectType(object); } buffer(ref: string, filePath: string): Promise { - return this.repository.buffer(ref, filePath); + return this.#repository.buffer(ref, filePath); } show(ref: string, path: string): Promise { - return this.repository.show(ref, path); + return this.#repository.show(ref, path); } getCommit(ref: string): Promise { - return this.repository.getCommit(ref); + return this.#repository.getCommit(ref); } add(paths: string[]) { - return this.repository.add(paths.map(p => Uri.file(p))); + return this.#repository.add(paths.map(p => Uri.file(p))); } revert(paths: string[]) { - return this.repository.revert(paths.map(p => Uri.file(p))); + return this.#repository.revert(paths.map(p => Uri.file(p))); } clean(paths: string[]) { - return this.repository.clean(paths.map(p => Uri.file(p))); + return this.#repository.clean(paths.map(p => Uri.file(p))); } diff(cached?: boolean) { - return this.repository.diff(cached); + return this.#repository.diff(cached); } diffWithHEAD(): Promise; diffWithHEAD(path: string): Promise; diffWithHEAD(path?: string): Promise { - return this.repository.diffWithHEAD(path); + return this.#repository.diffWithHEAD(path); } diffWith(ref: string): Promise; diffWith(ref: string, path: string): Promise; diffWith(ref: string, path?: string): Promise { - return this.repository.diffWith(ref, path); + return this.#repository.diffWith(ref, path); } diffIndexWithHEAD(): Promise; diffIndexWithHEAD(path: string): Promise; diffIndexWithHEAD(path?: string): Promise { - return this.repository.diffIndexWithHEAD(path); + return this.#repository.diffIndexWithHEAD(path); } diffIndexWith(ref: string): Promise; diffIndexWith(ref: string, path: string): Promise; diffIndexWith(ref: string, path?: string): Promise { - return this.repository.diffIndexWith(ref, path); + return this.#repository.diffIndexWith(ref, path); } diffBlobs(object1: string, object2: string): Promise { - return this.repository.diffBlobs(object1, object2); + return this.#repository.diffBlobs(object1, object2); } diffBetween(ref1: string, ref2: string): Promise; diffBetween(ref1: string, ref2: string, path: string): Promise; diffBetween(ref1: string, ref2: string, path?: string): Promise { - return this.repository.diffBetween(ref1, ref2, path); + return this.#repository.diffBetween(ref1, ref2, path); } hashObject(data: string): Promise { - return this.repository.hashObject(data); + return this.#repository.hashObject(data); } createBranch(name: string, checkout: boolean, ref?: string | undefined): Promise { - return this.repository.branch(name, checkout, ref); + return this.#repository.branch(name, checkout, ref); } deleteBranch(name: string, force?: boolean): Promise { - return this.repository.deleteBranch(name, force); + return this.#repository.deleteBranch(name, force); } getBranch(name: string): Promise { - return this.repository.getBranch(name); + return this.#repository.getBranch(name); } getBranches(query: BranchQuery, cancellationToken?: CancellationToken): Promise { - return this.repository.getBranches(query, cancellationToken); + return this.#repository.getBranches(query, cancellationToken); } getBranchBase(name: string): Promise { - return this.repository.getBranchBase(name); + return this.#repository.getBranchBase(name); } setBranchUpstream(name: string, upstream: string): Promise { - return this.repository.setBranchUpstream(name, upstream); + return this.#repository.setBranchUpstream(name, upstream); } getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise { - return this.repository.getRefs(query, cancellationToken); + return this.#repository.getRefs(query, cancellationToken); } checkIgnore(paths: string[]): Promise> { - return this.repository.checkIgnore(paths); + return this.#repository.checkIgnore(paths); } getMergeBase(ref1: string, ref2: string): Promise { - return this.repository.getMergeBase(ref1, ref2); + return this.#repository.getMergeBase(ref1, ref2); } tag(name: string, message: string, ref?: string | undefined): Promise { - return this.repository.tag({ name, message, ref }); + return this.#repository.tag({ name, message, ref }); } deleteTag(name: string): Promise { - return this.repository.deleteTag(name); + return this.#repository.deleteTag(name); } status(): Promise { - return this.repository.status(); + return this.#repository.status(); } checkout(treeish: string): Promise { - return this.repository.checkout(treeish); + return this.#repository.checkout(treeish); } addRemote(name: string, url: string): Promise { - return this.repository.addRemote(name, url); + return this.#repository.addRemote(name, url); } removeRemote(name: string): Promise { - return this.repository.removeRemote(name); + return this.#repository.removeRemote(name); } renameRemote(name: string, newName: string): Promise { - return this.repository.renameRemote(name, newName); + return this.#repository.renameRemote(name, newName); } fetch(arg0?: FetchOptions | string | undefined, @@ -239,86 +261,92 @@ export class ApiRepository implements Repository { prune?: boolean | undefined ): Promise { if (arg0 !== undefined && typeof arg0 !== 'string') { - return this.repository.fetch(arg0); + return this.#repository.fetch(arg0); } - return this.repository.fetch({ remote: arg0, ref, depth, prune }); + return this.#repository.fetch({ remote: arg0, ref, depth, prune }); } pull(unshallow?: boolean): Promise { - return this.repository.pull(undefined, unshallow); + return this.#repository.pull(undefined, unshallow); } push(remoteName?: string, branchName?: string, setUpstream: boolean = false, force?: ForcePushMode): Promise { - return this.repository.pushTo(remoteName, branchName, setUpstream, force); + return this.#repository.pushTo(remoteName, branchName, setUpstream, force); } blame(path: string): Promise { - return this.repository.blame(path); + return this.#repository.blame(path); } log(options?: LogOptions): Promise { - return this.repository.log(options); + return this.#repository.log(options); } commit(message: string, opts?: CommitOptions): Promise { - return this.repository.commit(message, { ...opts, postCommitCommand: null }); + return this.#repository.commit(message, { ...opts, postCommitCommand: null }); } merge(ref: string): Promise { - return this.repository.merge(ref); + return this.#repository.merge(ref); } mergeAbort(): Promise { - return this.repository.mergeAbort(); + return this.#repository.mergeAbort(); } applyStash(index?: number): Promise { - return this.repository.applyStash(index); + return this.#repository.applyStash(index); } popStash(index?: number): Promise { - return this.repository.popStash(index); + return this.#repository.popStash(index); } dropStash(index?: number): Promise { - return this.repository.dropStash(index); + return this.#repository.dropStash(index); } } export class ApiGit implements Git { + #model: Model; - get path(): string { return this._model.git.path; } + constructor(model: Model) { this.#model = model; } - constructor(private _model: Model) { } + get path(): string { return this.#model.git.path; } } export class ApiImpl implements API { + #model: Model; + readonly git: ApiGit; - readonly git = new ApiGit(this._model); + constructor(model: Model) { + this.#model = model; + this.git = new ApiGit(this.#model); + } get state(): APIState { - return this._model.state; + return this.#model.state; } get onDidChangeState(): Event { - return this._model.onDidChangeState; + return this.#model.onDidChangeState; } get onDidPublish(): Event { - return this._model.onDidPublish; + return this.#model.onDidPublish; } get onDidOpenRepository(): Event { - return mapEvent(this._model.onDidOpenRepository, r => new ApiRepository(r)); + return mapEvent(this.#model.onDidOpenRepository, r => new ApiRepository(r)); } get onDidCloseRepository(): Event { - return mapEvent(this._model.onDidCloseRepository, r => new ApiRepository(r)); + return mapEvent(this.#model.onDidCloseRepository, r => new ApiRepository(r)); } get repositories(): Repository[] { - return this._model.repositories.map(r => new ApiRepository(r)); + return this.#model.repositories.map(r => new ApiRepository(r)); } toGitUri(uri: Uri, ref: string): Uri { @@ -326,14 +354,14 @@ export class ApiImpl implements API { } getRepository(uri: Uri): Repository | null { - const result = this._model.getRepository(uri); + const result = this.#model.getRepository(uri); return result ? new ApiRepository(result) : null; } async init(root: Uri, options?: InitOptions): Promise { const path = root.fsPath; - await this._model.git.init(path, options); - await this._model.openRepository(path); + await this.#model.git.init(path, options); + await this.#model.openRepository(path); return this.getRepository(root) || null; } @@ -342,7 +370,7 @@ export class ApiImpl implements API { return null; } - await this._model.openRepository(root.fsPath); + await this.#model.openRepository(root.fsPath); return this.getRepository(root) || null; } @@ -350,7 +378,7 @@ export class ApiImpl implements API { const disposables: Disposable[] = []; if (provider.publishRepository) { - disposables.push(this._model.registerRemoteSourcePublisher(provider as RemoteSourcePublisher)); + disposables.push(this.#model.registerRemoteSourcePublisher(provider as RemoteSourcePublisher)); } disposables.push(GitBaseApi.getAPI().registerRemoteSourceProvider(provider)); @@ -358,26 +386,24 @@ export class ApiImpl implements API { } registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable { - return this._model.registerRemoteSourcePublisher(publisher); + return this.#model.registerRemoteSourcePublisher(publisher); } registerCredentialsProvider(provider: CredentialsProvider): Disposable { - return this._model.registerCredentialsProvider(provider); + return this.#model.registerCredentialsProvider(provider); } registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable { - return this._model.registerPostCommitCommandsProvider(provider); + return this.#model.registerPostCommitCommandsProvider(provider); } registerPushErrorHandler(handler: PushErrorHandler): Disposable { - return this._model.registerPushErrorHandler(handler); + return this.#model.registerPushErrorHandler(handler); } registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable { - return this._model.registerBranchProtectionProvider(root, provider); + return this.#model.registerBranchProtectionProvider(root, provider); } - - constructor(private _model: Model) { } } function getRefType(type: RefType): string { diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 7180890ad846..515f57c12cf1 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -121,7 +121,7 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, new TerminalShellExecutionManager(model, logger) ); - const postCommitCommandsProvider = new GitPostCommitCommandsProvider(); + const postCommitCommandsProvider = new GitPostCommitCommandsProvider(model); model.registerPostCommitCommandsProvider(postCommitCommandsProvider); const diagnosticsManager = new GitCommitInputBoxDiagnosticsManager(model); diff --git a/extensions/git/src/postCommitCommands.ts b/extensions/git/src/postCommitCommands.ts index d4e227b6db76..69a18114a41e 100644 --- a/extensions/git/src/postCommitCommands.ts +++ b/extensions/git/src/postCommitCommands.ts @@ -5,7 +5,7 @@ import { Command, commands, Disposable, Event, EventEmitter, Memento, Uri, workspace, l10n } from 'vscode'; import { PostCommitCommandsProvider } from './api/git'; -import { Repository } from './repository'; +import { IRepositoryResolver, Repository } from './repository'; import { ApiRepository } from './api/api1'; import { dispose } from './util'; import { OperationKind } from './operation'; @@ -18,17 +18,23 @@ export interface IPostCommitCommandsProviderRegistry { } export class GitPostCommitCommandsProvider implements PostCommitCommandsProvider { + constructor(private readonly _repositoryResolver: IRepositoryResolver) { } + getCommands(apiRepository: ApiRepository): Command[] { - const config = workspace.getConfiguration('git', Uri.file(apiRepository.repository.root)); + const repository = this._repositoryResolver.getRepository(apiRepository.rootUri); + if (!repository) { + return []; + } + + const config = workspace.getConfiguration('git', Uri.file(repository.root)); // Branch protection - const isBranchProtected = apiRepository.repository.isBranchProtected(); + const isBranchProtected = repository.isBranchProtected(); const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!; const alwaysPrompt = isBranchProtected && branchProtectionPrompt === 'alwaysPrompt'; const alwaysCommitToNewBranch = isBranchProtected && branchProtectionPrompt === 'alwaysCommitToNewBranch'; // Icon - const repository = apiRepository.repository; const isCommitInProgress = repository.operations.isRunning(OperationKind.Commit) || repository.operations.isRunning(OperationKind.PostCommitCommand); const icon = isCommitInProgress ? '$(sync~spin)' : alwaysPrompt ? '$(lock)' : alwaysCommitToNewBranch ? '$(git-branch)' : undefined; From 9fb1d574701dd41b6e5014e1c20f3cc7c4e4f353 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 18 Dec 2024 11:34:24 +0100 Subject: [PATCH 0204/3587] Reapply "debt: clean up obsolete file usage" (#236453) * Reapply "debt: clean up obsolete file usage" (#236433) This reverts commit cf2ebd91b8e42e3bc5ab0e85e3323c886a977ffe. * Fix extension deletion logic to ensure proper removal and handle file not found errors --- .../common/extensionManagement.ts | 4 +- .../common/extensionsScannerService.ts | 51 ++---- .../node/extensionManagementService.ts | 171 +++++++++--------- .../node/extensionsWatcher.ts | 20 +- .../node/extensionsScannerService.test.ts | 33 +--- 5 files changed, 122 insertions(+), 157 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 155079831fcc..f08b46ae65b3 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -456,8 +456,8 @@ export const enum ExtensionManagementErrorCode { Extract = 'Extract', Scanning = 'Scanning', ScanningExtension = 'ScanningExtension', - ReadUninstalled = 'ReadUninstalled', - UnsetUninstalled = 'UnsetUninstalled', + ReadRemoved = 'ReadRemoved', + UnsetRemoved = 'UnsetRemoved', Delete = 'Delete', Rename = 'Rename', IntializeDefaultProfile = 'IntializeDefaultProfile', diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 99868cddb5d6..a186b6fa0456 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -7,7 +7,6 @@ import { coalesce } from '../../../base/common/arrays.js'; import { ThrottledDelayer } from '../../../base/common/async.js'; import * as objects from '../../../base/common/objects.js'; import { VSBuffer } from '../../../base/common/buffer.js'; -import { IStringDictionary } from '../../../base/common/collections.js'; import { getErrorMessage } from '../../../base/common/errors.js'; import { getNodeType, parse, ParseError } from '../../../base/common/json.js'; import { getParseErrorMessage } from '../../../base/common/jsonErrorMessages.js'; @@ -18,12 +17,11 @@ import * as platform from '../../../base/common/platform.js'; import { basename, isEqual, joinPath } from '../../../base/common/resources.js'; import * as semver from '../../../base/common/semver/semver.js'; import Severity from '../../../base/common/severity.js'; -import { isEmptyObject } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; import { IProductVersion, Metadata } from './extensionManagement.js'; -import { areSameExtensions, computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from './extensionManagementUtil.js'; +import { areSameExtensions, computeTargetPlatform, getExtensionId, getGalleryExtensionId } from './extensionManagementUtil.js'; import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, ExtensionIdentifierMap, parseEnabledApiProposalNames } from '../../extensions/common/extensions.js'; import { validateExtensionManifest } from '../../extensions/common/extensionValidator.js'; import { FileOperationResult, IFileService, toFileOperationResult } from '../../files/common/files.js'; @@ -106,7 +104,6 @@ export type ScanOptions = { readonly profileLocation?: URI; readonly includeInvalid?: boolean; readonly includeAllVersions?: boolean; - readonly includeUninstalled?: boolean; readonly checkControlFile?: boolean; readonly language?: string; readonly useCache?: boolean; @@ -145,10 +142,9 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem private readonly _onDidChangeCache = this._register(new Emitter()); readonly onDidChangeCache = this._onDidChangeCache.event; - private readonly obsoleteFile = joinPath(this.userExtensionsLocation, '.obsolete'); - private readonly systemExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile, this.obsoleteFile)); - private readonly userExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile, this.obsoleteFile)); - private readonly extensionsScanner = this._register(this.instantiationService.createInstance(ExtensionsScanner, this.obsoleteFile)); + private readonly systemExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile)); + private readonly userExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile)); + private readonly extensionsScanner = this._register(this.instantiationService.createInstance(ExtensionsScanner)); constructor( readonly systemExtensionsLocation: URI, @@ -199,8 +195,8 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem const location = scanOptions.profileLocation ?? this.userExtensionsLocation; this.logService.trace('Started scanning user extensions', location); const profileScanOptions: IProfileExtensionsScanOptions | undefined = this.uriIdentityService.extUri.isEqual(scanOptions.profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource) ? { bailOutWhenFileNotFound: true } : undefined; - const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion()); - const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode && extensionsScannerInput.excludeObsolete ? this.userExtensionsCachedScanner : this.extensionsScanner; + const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode ? this.userExtensionsCachedScanner : this.extensionsScanner; let extensions: IRelaxedScannedExtension[]; try { extensions = await extensionsScanner.scanExtensions(extensionsScannerInput); @@ -221,7 +217,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionDevelopmentLocationURI) { const extensions = (await Promise.all(this.environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file) .map(async extensionDevelopmentLocationURI => { - const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, true, scanOptions.language, false /* do not validate */, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, scanOptions.language, false /* do not validate */, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(input); return extensions.map(extension => { // Override the extension type from the existing extensions @@ -237,7 +233,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extension = await this.extensionsScanner.scanExtension(extensionsScannerInput); if (!extension) { return null; @@ -249,7 +245,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(extensionsScannerInput); return this.applyScanOptions(extensions, extensionType, scanOptions, true); } @@ -405,7 +401,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem private async scanDefaultSystemExtensions(useCache: boolean, language: string | undefined): Promise { this.logService.trace('Started scanning system extensions'); - const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, true, language, true, undefined, this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, language, true, undefined, this.getProductVersion()); const extensionsScanner = useCache && !extensionsScannerInput.devMode ? this.systemExtensionsCachedScanner : this.extensionsScanner; const result = await extensionsScanner.scanExtensions(extensionsScannerInput); this.logService.trace('Scanned system extensions:', result.length); @@ -435,7 +431,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem break; } } - const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, true, language, true, undefined, this.getProductVersion()))))); + const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, language, true, undefined, this.getProductVersion()))))); this.logService.trace('Scanned dev system extensions:', result.length); return coalesce(result); } @@ -449,7 +445,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } } - private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, excludeObsolete: boolean, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined, productVersion: IProductVersion): Promise { + private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined, productVersion: IProductVersion): Promise { const translations = await this.getTranslations(language ?? platform.language); const mtime = await this.getMtime(location); const applicationExtensionsLocation = profile && !this.uriIdentityService.extUri.isEqual(location, this.userDataProfilesService.defaultProfile.extensionsResource) ? this.userDataProfilesService.defaultProfile.extensionsResource : undefined; @@ -462,7 +458,6 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem profile, profileScanOptions, type, - excludeObsolete, validate, productVersion.version, productVersion.date, @@ -504,7 +499,6 @@ export class ExtensionScannerInput { public readonly profile: boolean, public readonly profileScanOptions: IProfileExtensionsScanOptions | undefined, public readonly type: ExtensionType, - public readonly excludeObsolete: boolean, public readonly validate: boolean, public readonly productVersion: string, public readonly productDate: string | undefined, @@ -534,7 +528,6 @@ export class ExtensionScannerInput { && a.profile === b.profile && objects.equals(a.profileScanOptions, b.profileScanOptions) && a.type === b.type - && a.excludeObsolete === b.excludeObsolete && a.validate === b.validate && a.productVersion === b.productVersion && a.productDate === b.productDate @@ -558,7 +551,6 @@ class ExtensionsScanner extends Disposable { private readonly extensionsEnabledWithApiProposalVersion: string[]; constructor( - private readonly obsoleteFile: URI, @IExtensionsProfileScannerService protected readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, @IFileService protected readonly fileService: IFileService, @@ -571,15 +563,9 @@ class ExtensionsScanner extends Disposable { } async scanExtensions(input: ExtensionScannerInput): Promise { - const extensions = input.profile ? await this.scanExtensionsFromProfile(input) : await this.scanExtensionsFromLocation(input); - let obsolete: IStringDictionary = {}; - if (input.excludeObsolete && input.type === ExtensionType.User) { - try { - const raw = (await this.fileService.readFile(this.obsoleteFile)).value.toString(); - obsolete = JSON.parse(raw); - } catch (error) { /* ignore */ } - } - return isEmptyObject(obsolete) ? extensions : extensions.filter(e => !obsolete[ExtensionKey.create(e).toString()]); + return input.profile + ? this.scanExtensionsFromProfile(input) + : this.scanExtensionsFromLocation(input); } private async scanExtensionsFromLocation(input: ExtensionScannerInput): Promise { @@ -596,7 +582,7 @@ class ExtensionsScanner extends Disposable { if (input.type === ExtensionType.User && basename(c.resource).indexOf('.') === 0) { return null; } - const extensionScannerInput = new ExtensionScannerInput(c.resource, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.excludeObsolete, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); + const extensionScannerInput = new ExtensionScannerInput(c.resource, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); return this.scanExtension(extensionScannerInput); })); return coalesce(extensions) @@ -622,7 +608,7 @@ class ExtensionsScanner extends Disposable { const extensions = await Promise.all( scannedProfileExtensions.map(async extensionInfo => { if (filter(extensionInfo)) { - const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.excludeObsolete, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); + const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); return this.scanExtension(extensionScannerInput, extensionInfo.metadata); } return null; @@ -891,7 +877,6 @@ class CachedExtensionsScanner extends ExtensionsScanner { constructor( private readonly currentProfile: IUserDataProfile, - obsoleteFile: URI, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IExtensionsProfileScannerService extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService uriIdentityService: IUriIdentityService, @@ -900,7 +885,7 @@ class CachedExtensionsScanner extends ExtensionsScanner { @IEnvironmentService environmentService: IEnvironmentService, @ILogService logService: ILogService ) { - super(obsoleteFile, extensionsProfileScannerService, uriIdentityService, fileService, productService, environmentService, logService); + super(extensionsProfileScannerService, uriIdentityService, fileService, productService, environmentService, logService); } override async scanExtensions(input: ExtensionScannerInput): Promise { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 92405eefb753..762da10967f2 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -60,7 +60,7 @@ export interface INativeServerExtensionManagementService extends IExtensionManag readonly _serviceBrand: undefined; scanAllUserInstalledExtensions(): Promise; scanInstalledExtensionAtLocation(location: URI): Promise; - markAsUninstalled(...extensions: IExtension[]): Promise; + deleteExtensions(...extensions: IExtension[]): Promise; } type ExtractExtensionResult = { readonly local: ILocalExtension; readonly verificationStatus?: ExtensionSignatureVerificationCode }; @@ -222,8 +222,8 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return this.extensionsScanner.copyExtensions(fromProfileLocation, toProfileLocation, { version: this.productService.version, date: this.productService.date }); } - markAsUninstalled(...extensions: IExtension[]): Promise { - return this.extensionsScanner.setUninstalled(...extensions); + deleteExtensions(...extensions: IExtension[]): Promise { + return this.extensionsScanner.setExtensionsForRemoval(...extensions); } async cleanUp(): Promise { @@ -480,8 +480,20 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi continue; } - // Check if this is a directory - if (!(await this.fileService.stat(resource)).isDirectory) { + // Ignore changes to the deleted folder + if (this.uriIdentityService.extUri.basename(resource).endsWith(DELETED_FOLDER_POSTFIX)) { + continue; + } + + try { + // Check if this is a directory + if (!(await this.fileService.stat(resource)).isDirectory) { + continue; + } + } catch (error) { + if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { + this.logService.error(error); + } continue; } @@ -502,23 +514,10 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi private async addExtensionsToProfile(extensions: [ILocalExtension, Metadata | undefined][], profileLocation: URI): Promise { const localExtensions = extensions.map(e => e[0]); - await this.setInstalled(localExtensions); + await this.extensionsScanner.unsetExtensionsForRemoval(...localExtensions.map(extension => ExtensionKey.create(extension))); await this.extensionsProfileScannerService.addExtensionsToProfile(extensions, profileLocation); this._onDidInstallExtensions.fire(localExtensions.map(local => ({ local, identifier: local.identifier, operation: InstallOperation.None, profileLocation }))); } - - private async setInstalled(extensions: ILocalExtension[]): Promise { - const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); - for (const extension of extensions) { - const extensionKey = ExtensionKey.create(extension); - if (!uninstalled[extensionKey.toString()]) { - continue; - } - this.logService.trace('Removing the extension from uninstalled list:', extensionKey.id); - await this.extensionsScanner.setInstalled(extensionKey); - this.logService.info('Removed the extension from uninstalled list:', extensionKey.id); - } - } } type UpdateMetadataErrorClassification = { @@ -536,8 +535,8 @@ type UpdateMetadataErrorEvent = { export class ExtensionsScanner extends Disposable { - private readonly uninstalledResource: URI; - private readonly uninstalledFileLimiter: Queue; + private readonly obsoletedResource: URI; + private readonly obsoleteFileLimiter: Queue; private readonly _onExtract = this._register(new Emitter()); readonly onExtract = this._onExtract.event; @@ -555,13 +554,13 @@ export class ExtensionsScanner extends Disposable { @ILogService private readonly logService: ILogService, ) { super(); - this.uninstalledResource = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete'); - this.uninstalledFileLimiter = new Queue(); + this.obsoletedResource = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete'); + this.obsoleteFileLimiter = new Queue(); } async cleanUp(): Promise { await this.removeTemporarilyDeletedFolders(); - await this.removeUninstalledExtensions(); + await this.deleteExtensionsMarkedForRemoval(); await this.initializeMetadata(); } @@ -720,42 +719,40 @@ export class ExtensionsScanner extends Disposable { return this.scanLocalExtension(local.location, local.type, profileLocation); } - async getUninstalledExtensions(): Promise> { - try { - return await this.withUninstalledExtensions(); - } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.ReadUninstalled); - } - } - - async setUninstalled(...extensions: IExtension[]): Promise { + async setExtensionsForRemoval(...extensions: IExtension[]): Promise { const extensionKeys: ExtensionKey[] = extensions.map(e => ExtensionKey.create(e)); - await this.withUninstalledExtensions(uninstalled => + await this.withRemovedExtensions(removedExtensions => extensionKeys.forEach(extensionKey => { - uninstalled[extensionKey.toString()] = true; - this.logService.info('Marked extension as uninstalled', extensionKey.toString()); + removedExtensions[extensionKey.toString()] = true; + this.logService.info('Marked extension as removed', extensionKey.toString()); })); } - async setInstalled(extensionKey: ExtensionKey): Promise { + async unsetExtensionsForRemoval(...extensionKeys: ExtensionKey[]): Promise { try { - await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); + const results: boolean[] = []; + await this.withRemovedExtensions(removedExtensions => + extensionKeys.forEach(extensionKey => { + if (removedExtensions[extensionKey.toString()]) { + results.push(true); + delete removedExtensions[extensionKey.toString()]; + } else { + results.push(false); + } + })); + return results; } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.UnsetUninstalled); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UnsetRemoved); } } - async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { + async deleteExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { if (this.uriIdentityService.extUri.isEqualOrParent(extension.location, this.extensionsScannerService.userExtensionsLocation)) { - return this.deleteExtensionFromLocation(extension.identifier.id, extension.location, type); + await this.deleteExtensionFromLocation(extension.identifier.id, extension.location, type); + await this.unsetExtensionsForRemoval(ExtensionKey.create(extension)); } } - async removeUninstalledExtension(extension: ILocalExtension | IScannedExtension): Promise { - await this.removeExtension(extension, 'uninstalled'); - await this.withUninstalledExtensions(uninstalled => delete uninstalled[ExtensionKey.create(extension).toString()]); - } - async copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata: Partial): Promise { const source = await this.getScannedExtension(extension, fromProfileLocation); const target = await this.getScannedExtension(extension, toProfileLocation); @@ -792,11 +789,11 @@ export class ExtensionsScanner extends Disposable { this.logService.info(`Deleted ${type} extension from disk`, id, location.fsPath); } - private withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { - return this.uninstalledFileLimiter.queue(async () => { + private withRemovedExtensions(updateFn?: (removed: IStringDictionary) => void): Promise> { + return this.obsoleteFileLimiter.queue(async () => { let raw: string | undefined; try { - const content = await this.fileService.readFile(this.uninstalledResource, 'utf8'); + const content = await this.fileService.readFile(this.obsoletedResource, 'utf8'); raw = content.value.toString(); } catch (error) { if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { @@ -804,23 +801,29 @@ export class ExtensionsScanner extends Disposable { } } - let uninstalled = {}; + let removed = {}; if (raw) { try { - uninstalled = JSON.parse(raw); + removed = JSON.parse(raw); } catch (e) { /* ignore */ } } if (updateFn) { - updateFn(uninstalled); - if (Object.keys(uninstalled).length) { - await this.fileService.writeFile(this.uninstalledResource, VSBuffer.fromString(JSON.stringify(uninstalled))); + updateFn(removed); + if (Object.keys(removed).length) { + await this.fileService.writeFile(this.obsoletedResource, VSBuffer.fromString(JSON.stringify(removed))); } else { - await this.fileService.del(this.uninstalledResource); + try { + await this.fileService.del(this.obsoletedResource); + } catch (error) { + if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { + throw error; + } + } } } - return uninstalled; + return removed; }); } @@ -898,19 +901,25 @@ export class ExtensionsScanner extends Disposable { })); } - private async removeUninstalledExtensions(): Promise { - const uninstalled = await this.getUninstalledExtensions(); - if (Object.keys(uninstalled).length === 0) { - this.logService.debug(`No uninstalled extensions found.`); + private async deleteExtensionsMarkedForRemoval(): Promise { + let removed: IStringDictionary; + try { + removed = await this.withRemovedExtensions(); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.ReadRemoved); + } + + if (Object.keys(removed).length === 0) { + this.logService.debug(`No extensions are marked as removed.`); return; } - this.logService.debug(`Removing uninstalled extensions:`, Object.keys(uninstalled)); + this.logService.debug(`Deleting extensions marked as removed:`, Object.keys(removed)); - const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions + const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeInvalid: true }); // All user extensions const installed: Set = new Set(); for (const e of extensions) { - if (!uninstalled[ExtensionKey.create(e).toString()]) { + if (!removed[ExtensionKey.create(e).toString()]) { installed.add(e.identifier.id.toLowerCase()); } } @@ -928,8 +937,8 @@ export class ExtensionsScanner extends Disposable { this.logService.error(error); } - const toRemove = extensions.filter(e => e.metadata /* Installed by System */ && uninstalled[ExtensionKey.create(e).toString()]); - await Promise.allSettled(toRemove.map(e => this.removeUninstalledExtension(e))); + const toRemove = extensions.filter(e => e.metadata /* Installed by System */ && removed[ExtensionKey.create(e).toString()]); + await Promise.allSettled(toRemove.map(e => this.deleteExtension(e, 'marked for removal'))); } private async removeTemporarilyDeletedFolders(): Promise { @@ -1021,7 +1030,7 @@ class InstallExtensionInProfileTask extends AbstractExtensionTask { - const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); - if (!uninstalled[extensionKey.toString()]) { - return undefined; + private async unsetIfRemoved(extensionKey: ExtensionKey): Promise { + // If the same version of extension is marked as removed, remove it from there and return the local. + const [removed] = await this.extensionsScanner.unsetExtensionsForRemoval(extensionKey); + if (removed) { + this.logService.info('Removed the extension from removed list:', extensionKey.id); + const userExtensions = await this.extensionsScanner.scanAllUserExtensions(true); + return userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)); } - - this.logService.trace('Removing the extension from uninstalled list:', extensionKey.id); - // If the same version of extension is marked as uninstalled, remove it from there and return the local. - await this.extensionsScanner.setInstalled(extensionKey); - this.logService.info('Removed the extension from uninstalled list:', extensionKey.id); - - const userExtensions = await this.extensionsScanner.scanAllUserExtensions(true); - return userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)); + return undefined; } private async updateMetadata(extension: ILocalExtension, token: CancellationToken): Promise { @@ -1149,8 +1154,8 @@ class UninstallExtensionInProfileTask extends AbstractExtensionTask implem super(); } - protected async doRun(token: CancellationToken): Promise { - await this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension, this.options.profileLocation); + protected doRun(token: CancellationToken): Promise { + return this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension, this.options.profileLocation); } } diff --git a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts index d2b65eaea553..2c4e976a5a64 100644 --- a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts +++ b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts @@ -48,7 +48,7 @@ export class ExtensionsWatcher extends Disposable { await this.extensionsScannerService.initializeDefaultProfileExtensions(); await this.onDidChangeProfiles(this.userDataProfilesService.profiles); this.registerListeners(); - await this.uninstallExtensionsNotInProfiles(); + await this.deleteExtensionsNotInProfiles(); } private registerListeners(): void { @@ -102,7 +102,7 @@ export class ExtensionsWatcher extends Disposable { } private async onDidRemoveExtensions(e: DidRemoveProfileExtensionsEvent): Promise { - const extensionsToUninstall: IExtension[] = []; + const extensionsToDelete: IExtension[] = []; const promises: Promise[] = []; for (const extension of e.extensions) { const key = this.getKey(extension.identifier, extension.version); @@ -115,7 +115,7 @@ export class ExtensionsWatcher extends Disposable { promises.push(this.extensionManagementService.scanInstalledExtensionAtLocation(extension.location) .then(result => { if (result) { - extensionsToUninstall.push(result); + extensionsToDelete.push(result); } else { this.logService.info('Extension not found at the location', extension.location.toString()); } @@ -125,8 +125,8 @@ export class ExtensionsWatcher extends Disposable { } try { await Promise.all(promises); - if (extensionsToUninstall.length) { - await this.uninstallExtensionsNotInProfiles(extensionsToUninstall); + if (extensionsToDelete.length) { + await this.deleteExtensionsNotInProfiles(extensionsToDelete); } } catch (error) { this.logService.error(error); @@ -180,13 +180,13 @@ export class ExtensionsWatcher extends Disposable { } } - private async uninstallExtensionsNotInProfiles(toUninstall?: IExtension[]): Promise { - if (!toUninstall) { + private async deleteExtensionsNotInProfiles(toDelete?: IExtension[]): Promise { + if (!toDelete) { const installed = await this.extensionManagementService.scanAllUserInstalledExtensions(); - toUninstall = installed.filter(installedExtension => !this.allExtensions.has(this.getKey(installedExtension.identifier, installedExtension.manifest.version))); + toDelete = installed.filter(installedExtension => !this.allExtensions.has(this.getKey(installedExtension.identifier, installedExtension.manifest.version))); } - if (toUninstall.length) { - await this.extensionManagementService.markAsUninstalled(...toUninstall); + if (toDelete.length) { + await this.extensionManagementService.deleteExtensions(...toDelete); } } diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts index 74d3ffcd738f..551ba576d445 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -224,31 +224,6 @@ suite('NativeExtensionsScanerService Test', () => { assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); }); - test('scan exclude uninstalled extensions', async () => { - await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); - await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); - await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true }))); - const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - - const actual = await testObject.scanUserExtensions({}); - - assert.deepStrictEqual(actual.length, 1); - assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); - }); - - test('scan include uninstalled extensions', async () => { - await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); - await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); - await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true }))); - const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - - const actual = await testObject.scanUserExtensions({ includeUninstalled: true }); - - assert.deepStrictEqual(actual.length, 2); - assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); - assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' }); - }); - test('scan include invalid extensions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } })); @@ -351,7 +326,7 @@ suite('ExtensionScannerInput', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('compare inputs - location', () => { - const anInput = (location: URI, mtime: number | undefined) => new ExtensionScannerInput(location, mtime, undefined, undefined, false, undefined, ExtensionType.User, true, true, '1.1.1', undefined, undefined, true, undefined, {}); + const anInput = (location: URI, mtime: number | undefined) => new ExtensionScannerInput(location, mtime, undefined, undefined, false, undefined, ExtensionType.User, true, '1.1.1', undefined, undefined, true, undefined, {}); assert.strictEqual(ExtensionScannerInput.equals(anInput(ROOT, undefined), anInput(ROOT, undefined)), true); assert.strictEqual(ExtensionScannerInput.equals(anInput(ROOT, 100), anInput(ROOT, 100)), true); @@ -361,7 +336,7 @@ suite('ExtensionScannerInput', () => { }); test('compare inputs - application location', () => { - const anInput = (location: URI, mtime: number | undefined) => new ExtensionScannerInput(ROOT, undefined, location, mtime, false, undefined, ExtensionType.User, true, true, '1.1.1', undefined, undefined, true, undefined, {}); + const anInput = (location: URI, mtime: number | undefined) => new ExtensionScannerInput(ROOT, undefined, location, mtime, false, undefined, ExtensionType.User, true, '1.1.1', undefined, undefined, true, undefined, {}); assert.strictEqual(ExtensionScannerInput.equals(anInput(ROOT, undefined), anInput(ROOT, undefined)), true); assert.strictEqual(ExtensionScannerInput.equals(anInput(ROOT, 100), anInput(ROOT, 100)), true); @@ -371,7 +346,7 @@ suite('ExtensionScannerInput', () => { }); test('compare inputs - profile', () => { - const anInput = (profile: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined) => new ExtensionScannerInput(ROOT, undefined, undefined, undefined, profile, profileScanOptions, ExtensionType.User, true, true, '1.1.1', undefined, undefined, true, undefined, {}); + const anInput = (profile: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined) => new ExtensionScannerInput(ROOT, undefined, undefined, undefined, profile, profileScanOptions, ExtensionType.User, true, '1.1.1', undefined, undefined, true, undefined, {}); assert.strictEqual(ExtensionScannerInput.equals(anInput(true, { bailOutWhenFileNotFound: true }), anInput(true, { bailOutWhenFileNotFound: true })), true); assert.strictEqual(ExtensionScannerInput.equals(anInput(false, { bailOutWhenFileNotFound: true }), anInput(false, { bailOutWhenFileNotFound: true })), true); @@ -384,7 +359,7 @@ suite('ExtensionScannerInput', () => { }); test('compare inputs - extension type', () => { - const anInput = (type: ExtensionType) => new ExtensionScannerInput(ROOT, undefined, undefined, undefined, false, undefined, type, true, true, '1.1.1', undefined, undefined, true, undefined, {}); + const anInput = (type: ExtensionType) => new ExtensionScannerInput(ROOT, undefined, undefined, undefined, false, undefined, type, true, '1.1.1', undefined, undefined, true, undefined, {}); assert.strictEqual(ExtensionScannerInput.equals(anInput(ExtensionType.System), anInput(ExtensionType.System)), true); assert.strictEqual(ExtensionScannerInput.equals(anInput(ExtensionType.User), anInput(ExtensionType.User)), true); From 41be1c6b69760824c9fa42d4bcec5a406c41e9bb Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 18 Dec 2024 11:49:08 +0100 Subject: [PATCH 0205/3587] handle invalid withProgress invocation as external error (#236454) fixes https://github.com/Microsoft/vscode/issues/134892 --- src/vs/workbench/api/browser/mainThreadProgress.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/browser/mainThreadProgress.ts b/src/vs/workbench/api/browser/mainThreadProgress.ts index 6606b0d64bd5..e6f4632b61bf 100644 --- a/src/vs/workbench/api/browser/mainThreadProgress.ts +++ b/src/vs/workbench/api/browser/mainThreadProgress.ts @@ -9,6 +9,7 @@ import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions import { Action } from '../../../base/common/actions.js'; import { ICommandService } from '../../../platform/commands/common/commands.js'; import { localize } from '../../../nls.js'; +import { onUnexpectedExternalError } from '../../../base/common/errors.js'; class ManageExtensionAction extends Action { constructor(extensionId: string, label: string, commandService: ICommandService) { @@ -52,7 +53,13 @@ export class MainThreadProgress implements MainThreadProgressShape { options = notificationOptions; } - this._progressService.withProgress(options, task, () => this._proxy.$acceptProgressCanceled(handle)); + try { + this._progressService.withProgress(options, task, () => this._proxy.$acceptProgressCanceled(handle)); + } catch (err) { + // the withProgress-method will throw synchronously when invoked with bad options + // which is then an enternal/extension error + onUnexpectedExternalError(err); + } } $progressReport(handle: number, message: IProgressStep): void { From 86ac7d1154717865aec7b87d86c22fce047e6b1a Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 18 Dec 2024 12:01:58 +0100 Subject: [PATCH 0206/3587] Test commit --- src/vs/workbench/browser/parts/views/treeView.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 3384ee711fa1..9734a53c4366 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import { DataTransfers, IDragAndDropData } from '../../../../base/browser/dnd.js'; import * as DOM from '../../../../base/browser/dom.js'; import * as cssJs from '../../../../base/browser/cssValue.js'; From fca8bb2388430e67b42253199e87ed7fa325442a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Dec 2024 12:18:50 +0100 Subject: [PATCH 0207/3587] Deduplicate untitled workspaces with workspaces that restore (fix #234232) (#236457) --- .../electron-main/windowsMainService.ts | 58 ++++++++++++++----- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index f441f7d5fb76..d403852efcc2 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -57,6 +57,7 @@ import { ILoggerMainService } from '../../log/electron-main/loggerService.js'; import { IAuxiliaryWindowsMainService } from '../../auxiliaryWindow/electron-main/auxiliaryWindows.js'; import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryWindow.js'; import { ICSSDevelopmentService } from '../../cssDev/node/cssDevService.js'; +import { ResourceSet } from '../../../base/common/map.js'; //#region Helper Interfaces @@ -729,7 +730,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private async getPathsToOpen(openConfig: IOpenConfiguration): Promise { let pathsToOpen: IPathToOpen[]; let isCommandLineOrAPICall = false; - let restoredWindows = false; + let isRestoringPaths = false; // Extract paths: from API if (openConfig.urisToOpen && openConfig.urisToOpen.length > 0) { @@ -759,19 +760,26 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic pathsToOpen.push(Object.create(null)); // add an empty window if we did not have windows to restore } - restoredWindows = true; + isRestoringPaths = true; } - // Convert multiple folders into workspace (if opened via API or CLI) - // This will ensure to open these folders in one window instead of multiple - // If we are in `addMode`, we should not do this because in that case all - // folders should be added to the existing window. + // Handle the case of multiple folders being opened from CLI while we are + // not in `--add` mode by creating an untitled workspace, only if: + // - they all share the same remote authority + // - there is no existing workspace to open that matches these folders if (!openConfig.addMode && isCommandLineOrAPICall) { - const foldersToOpen = pathsToOpen.filter(path => isSingleFolderWorkspacePathToOpen(path)) as ISingleFolderWorkspacePathToOpen[]; + const foldersToOpen = pathsToOpen.filter(path => isSingleFolderWorkspacePathToOpen(path)); if (foldersToOpen.length > 1) { const remoteAuthority = foldersToOpen[0].remoteAuthority; - if (foldersToOpen.every(folderToOpen => isEqualAuthority(folderToOpen.remoteAuthority, remoteAuthority))) { // only if all folder have the same authority - const workspace = await this.workspacesManagementMainService.createUntitledWorkspace(foldersToOpen.map(folder => ({ uri: folder.workspace.uri }))); + if (foldersToOpen.every(folderToOpen => isEqualAuthority(folderToOpen.remoteAuthority, remoteAuthority))) { + let workspace: IWorkspaceIdentifier | undefined; + + const lastSessionWorkspaceMatchingFolders = await this.doGetWorkspaceMatchingFoldersFromLastSession(remoteAuthority, foldersToOpen); + if (lastSessionWorkspaceMatchingFolders) { + workspace = lastSessionWorkspaceMatchingFolders; + } else { + workspace = await this.workspacesManagementMainService.createUntitledWorkspace(foldersToOpen.map(folder => ({ uri: folder.workspace.uri }))); + } // Add workspace and remove folders thereby pathsToOpen.push({ workspace, remoteAuthority }); @@ -780,12 +788,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } } - // Check for `window.startup` setting to include all windows + // Check for `window.restoreWindows` setting to include all windows // from the previous session if this is the initial startup and we have // not restored windows already otherwise. - // Use `unshift` to ensure any new window to open comes last - // for proper focus treatment. - if (openConfig.initialStartup && !restoredWindows && this.configurationService.getValue('window')?.restoreWindows === 'preserve') { + // Use `unshift` to ensure any new window to open comes last for proper + // focus treatment. + if (openConfig.initialStartup && !isRestoringPaths && this.configurationService.getValue('window')?.restoreWindows === 'preserve') { const lastSessionPaths = await this.doGetPathsFromLastSession(); pathsToOpen.unshift(...lastSessionPaths.filter(path => isWorkspacePathToOpen(path) || isSingleFolderWorkspacePathToOpen(path) || path.backupPath)); } @@ -974,6 +982,30 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return restoreWindows; } + private async doGetWorkspaceMatchingFoldersFromLastSession(remoteAuthority: string | undefined, folders: ISingleFolderWorkspacePathToOpen[]): Promise { + const workspaces = (await this.doGetPathsFromLastSession()).filter(path => isWorkspacePathToOpen(path)); + const folderUris = folders.map(folder => folder.workspace.uri); + + for (const { workspace } of workspaces) { + const resolvedWorkspace = await this.workspacesManagementMainService.resolveLocalWorkspace(workspace.configPath); + if ( + !resolvedWorkspace || + resolvedWorkspace.remoteAuthority !== remoteAuthority || + resolvedWorkspace.transient || + resolvedWorkspace.folders.length !== folders.length + ) { + continue; + } + + const folderSet = new ResourceSet(folderUris, uri => extUriBiasedIgnorePathCase.getComparisonKey(uri)); + if (resolvedWorkspace.folders.every(folder => folderSet.has(folder.uri))) { + return resolvedWorkspace; + } + } + + return undefined; + } + private async resolveOpenable(openable: IWindowOpenable, options: IPathResolveOptions = Object.create(null)): Promise { // handle file:// openables with some extra validation From 2f7beb56ff7319edc20b8f81c965da85738475e4 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Dec 2024 12:46:34 +0100 Subject: [PATCH 0208/3587] Support for code page `1125` aka `cp866u` (fix #230438 (#236461) ) --- .../services/textfile/common/encoding.ts | 60 ++++++++++--------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/services/textfile/common/encoding.ts b/src/vs/workbench/services/textfile/common/encoding.ts index b653dd7a8478..599f2309e6a5 100644 --- a/src/vs/workbench/services/textfile/common/encoding.ts +++ b/src/vs/workbench/services/textfile/common/encoding.ts @@ -615,151 +615,157 @@ export const SUPPORTED_ENCODINGS: EncodingsMap = { order: 20, guessableName: 'IBM866' }, + cp1125: { + labelLong: 'Cyrillic (CP 1125)', + labelShort: 'CP 1125', + order: 21, + guessableName: 'IBM1125' + }, iso88595: { labelLong: 'Cyrillic (ISO 8859-5)', labelShort: 'ISO 8859-5', - order: 21, + order: 22, guessableName: 'ISO-8859-5' }, koi8r: { labelLong: 'Cyrillic (KOI8-R)', labelShort: 'KOI8-R', - order: 22, + order: 23, guessableName: 'KOI8-R' }, koi8u: { labelLong: 'Cyrillic (KOI8-U)', labelShort: 'KOI8-U', - order: 23 + order: 24 }, iso885913: { labelLong: 'Estonian (ISO 8859-13)', labelShort: 'ISO 8859-13', - order: 24 + order: 25 }, windows1253: { labelLong: 'Greek (Windows 1253)', labelShort: 'Windows 1253', - order: 25, + order: 26, guessableName: 'windows-1253' }, iso88597: { labelLong: 'Greek (ISO 8859-7)', labelShort: 'ISO 8859-7', - order: 26, + order: 27, guessableName: 'ISO-8859-7' }, windows1255: { labelLong: 'Hebrew (Windows 1255)', labelShort: 'Windows 1255', - order: 27, + order: 28, guessableName: 'windows-1255' }, iso88598: { labelLong: 'Hebrew (ISO 8859-8)', labelShort: 'ISO 8859-8', - order: 28, + order: 29, guessableName: 'ISO-8859-8' }, iso885910: { labelLong: 'Nordic (ISO 8859-10)', labelShort: 'ISO 8859-10', - order: 29 + order: 30 }, iso885916: { labelLong: 'Romanian (ISO 8859-16)', labelShort: 'ISO 8859-16', - order: 30 + order: 31 }, windows1254: { labelLong: 'Turkish (Windows 1254)', labelShort: 'Windows 1254', - order: 31 + order: 32 }, iso88599: { labelLong: 'Turkish (ISO 8859-9)', labelShort: 'ISO 8859-9', - order: 32 + order: 33 }, windows1258: { labelLong: 'Vietnamese (Windows 1258)', labelShort: 'Windows 1258', - order: 33 + order: 34 }, gbk: { labelLong: 'Simplified Chinese (GBK)', labelShort: 'GBK', - order: 34 + order: 35 }, gb18030: { labelLong: 'Simplified Chinese (GB18030)', labelShort: 'GB18030', - order: 35 + order: 36 }, cp950: { labelLong: 'Traditional Chinese (Big5)', labelShort: 'Big5', - order: 36, + order: 37, guessableName: 'Big5' }, big5hkscs: { labelLong: 'Traditional Chinese (Big5-HKSCS)', labelShort: 'Big5-HKSCS', - order: 37 + order: 38 }, shiftjis: { labelLong: 'Japanese (Shift JIS)', labelShort: 'Shift JIS', - order: 38, + order: 39, guessableName: 'SHIFT_JIS' }, eucjp: { labelLong: 'Japanese (EUC-JP)', labelShort: 'EUC-JP', - order: 39, + order: 40, guessableName: 'EUC-JP' }, euckr: { labelLong: 'Korean (EUC-KR)', labelShort: 'EUC-KR', - order: 40, + order: 41, guessableName: 'EUC-KR' }, windows874: { labelLong: 'Thai (Windows 874)', labelShort: 'Windows 874', - order: 41 + order: 42 }, iso885911: { labelLong: 'Latin/Thai (ISO 8859-11)', labelShort: 'ISO 8859-11', - order: 42 + order: 43 }, koi8ru: { labelLong: 'Cyrillic (KOI8-RU)', labelShort: 'KOI8-RU', - order: 43 + order: 44 }, koi8t: { labelLong: 'Tajik (KOI8-T)', labelShort: 'KOI8-T', - order: 44 + order: 45 }, gb2312: { labelLong: 'Simplified Chinese (GB 2312)', labelShort: 'GB 2312', - order: 45, + order: 46, guessableName: 'GB2312' }, cp865: { labelLong: 'Nordic DOS (CP 865)', labelShort: 'CP 865', - order: 46 + order: 47 }, cp850: { labelLong: 'Western European DOS (CP 850)', labelShort: 'CP 850', - order: 47 + order: 48 } }; From 6c2faf1c5db3a438360b44a9f7b83bafac8efa0b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Dec 2024 13:06:31 +0100 Subject: [PATCH 0209/3587] chat - fix setup removal key (#236463) --- .../contrib/chat/browser/actions/chatActions.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 92f6c42dd3df..4cb15f40d68e 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -531,13 +531,7 @@ MenuRegistry.appendMenuItem(MenuId.CommandCenter, { submenu: MenuId.ChatCommandCenter, title: localize('title4', "Chat"), icon: Codicon.copilot, - when: ContextKeyExpr.and( - ContextKeyExpr.has('config.chat.commandCenter.enabled'), - ContextKeyExpr.or( - ChatContextKeys.Setup.installed, - ChatContextKeys.panelParticipantRegistered - ) - ), + when: ContextKeyExpr.has('config.chat.commandCenter.enabled'), order: 10001, }); @@ -547,13 +541,7 @@ registerAction2(class ToggleCopilotControl extends ToggleTitleBarConfigAction { 'chat.commandCenter.enabled', localize('toggle.chatControl', 'Copilot Controls'), localize('toggle.chatControlsDescription', "Toggle visibility of the Copilot Controls in title bar"), 4, false, - ContextKeyExpr.and( - ContextKeyExpr.has('config.window.commandCenter'), - ContextKeyExpr.or( - ChatContextKeys.Setup.installed, - ChatContextKeys.panelParticipantRegistered - ) - ) + ContextKeyExpr.has('config.window.commandCenter') ); } }); From 7579838b39047efb0f22236fea8e6d94ca6ea5e1 Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Wed, 18 Dec 2024 14:12:34 +0100 Subject: [PATCH 0210/3587] Render the a background cover up only if really necessary (#236460) --- .../view/inlineEdits/sideBySideDiff.ts | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index a64c52dcd90c..5f36bec5b336 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -14,6 +14,7 @@ import { ICommandService } from '../../../../../../platform/commands/common/comm import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { diffInserted, diffRemoved } from '../../../../../../platform/theme/common/colorRegistry.js'; import { darken, lighten, registerColor } from '../../../../../../platform/theme/common/colorUtils.js'; +import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { Point } from '../../../../../browser/point.js'; @@ -108,7 +109,8 @@ export class InlineEditsSideBySideDiff extends Disposable { originalDisplayRange: LineRange; } | undefined>, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ICommandService private readonly _commandService: ICommandService + @ICommandService private readonly _commandService: ICommandService, + @IThemeService private readonly _themeService: IThemeService, ) { super(); @@ -487,19 +489,33 @@ export class InlineEditsSideBySideDiff extends Disposable { return extendedModifiedPathBuilder.build(); }); + private readonly _originalBackgroundColor = observableFromEvent(this, this._themeService.onDidColorThemeChange, () => { + return this._themeService.getColorTheme().getColor(originalBackgroundColor) ?? Color.transparent; + }); + private readonly _backgroundSvg = n.svg({ transform: 'translate(-0.5 -0.5)', style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, }, [ n.svgElem('path', { class: 'rightOfModifiedBackgroundCoverUp', - d: this._previewEditorLayoutInfo.map(layoutInfo => layoutInfo && new PathBuilder() - .moveTo(layoutInfo.code1) - .lineTo(layoutInfo.code1.deltaX(1000)) - .lineTo(layoutInfo.code2.deltaX(1000)) - .lineTo(layoutInfo.code2) - .build() - ), + d: derived(reader => { + const layoutInfo = this._previewEditorLayoutInfo.read(reader); + if (!layoutInfo) { + return undefined; + } + const originalBackgroundColor = this._originalBackgroundColor.read(reader); + if (originalBackgroundColor.isTransparent()) { + return undefined; + } + + return new PathBuilder() + .moveTo(layoutInfo.code1) + .lineTo(layoutInfo.code1.deltaX(1000)) + .lineTo(layoutInfo.code2.deltaX(1000)) + .lineTo(layoutInfo.code2) + .build(); + }), style: { fill: 'var(--vscode-editor-background, transparent)', } From 473620cdd2f4f38e3e4f409d96aeeb02d81db5dc Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Dec 2024 14:14:46 +0100 Subject: [PATCH 0211/3587] Wrong use of disable store in searchResultsView.ts (fix #236456) (#236469) --- .../workbench/contrib/search/browser/searchResultsView.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index 40df97b78cbb..a4705ea7f15a 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -192,7 +192,7 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree SearchContext.FileFocusKey.bindTo(contextKeyServiceMain).set(false); SearchContext.FolderFocusKey.bindTo(contextKeyServiceMain).set(true); - const instantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyServiceMain]))); + const instantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyServiceMain]))); const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, { menuOptions: { shouldForwardArgs: true @@ -292,7 +292,7 @@ export class FileMatchRenderer extends Disposable implements ICompressibleTreeRe SearchContext.FileFocusKey.bindTo(contextKeyServiceMain).set(true); SearchContext.FolderFocusKey.bindTo(contextKeyServiceMain).set(false); - const instantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyServiceMain]))); + const instantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyServiceMain]))); const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, { menuOptions: { shouldForwardArgs: true @@ -384,7 +384,7 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender SearchContext.FileFocusKey.bindTo(contextKeyServiceMain).set(false); SearchContext.FolderFocusKey.bindTo(contextKeyServiceMain).set(false); - const instantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyServiceMain]))); + const instantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyServiceMain]))); const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, { menuOptions: { shouldForwardArgs: true From b274aac2ca213a2ca9b9626b7060328f31cfffaf Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:46:43 +0100 Subject: [PATCH 0212/3587] Allow center layout with vertical groups (#236471) allow center layout with vertical groups --- src/vs/workbench/browser/layout.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 84e2eaadb3c3..592bb4146af1 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -23,14 +23,14 @@ import { getMenuBarVisibility, IPath, hasNativeTitlebar, hasCustomTitlebar, Titl import { IHostService } from '../services/host/browser/host.js'; import { IBrowserWorkbenchEnvironmentService } from '../services/environment/browser/environmentService.js'; import { IEditorService } from '../services/editor/common/editorService.js'; -import { EditorGroupLayout, GroupsOrder, IEditorGroupsService } from '../services/editor/common/editorGroupsService.js'; +import { EditorGroupLayout, GroupOrientation, GroupsOrder, IEditorGroupsService } from '../services/editor/common/editorGroupsService.js'; import { SerializableGrid, ISerializableView, ISerializedGrid, Orientation, ISerializedNode, ISerializedLeafNode, Direction, IViewSize, Sizing } from '../../base/browser/ui/grid/grid.js'; import { Part } from './part.js'; import { IStatusbarService } from '../services/statusbar/browser/statusbar.js'; import { IFileService } from '../../platform/files/common/files.js'; import { isCodeEditor } from '../../editor/browser/editorBrowser.js'; import { coalesce } from '../../base/common/arrays.js'; -import { assertIsDefined } from '../../base/common/types.js'; +import { assertIsDefined, isDefined } from '../../base/common/types.js'; import { INotificationService, NotificationsFilter } from '../../platform/notification/common/notification.js'; import { IThemeService } from '../../platform/theme/common/themeService.js'; import { WINDOW_ACTIVE_BORDER, WINDOW_INACTIVE_BORDER } from '../common/theme.js'; @@ -1610,19 +1610,28 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi centerMainEditorLayout(active: boolean, skipLayout?: boolean): void { this.stateModel.setRuntimeValue(LayoutStateKeys.MAIN_EDITOR_CENTERED, active); - const activeMainEditor = this.mainPartEditorService.activeEditor; + const mainVisibleEditors = this.editorGroupService.mainPart.groups.map(g => g.activeEditor).filter(isDefined); + const isEditorComplex = mainVisibleEditors.some(editor => { + if (editor instanceof DiffEditorInput) { + return this.configurationService.getValue('diffEditor.renderSideBySide'); + } else if (editor?.hasCapability(EditorInputCapabilities.MultipleEditors)) { + return true; + } + return false; + }); - let isEditorComplex = false; - if (activeMainEditor instanceof DiffEditorInput) { - isEditorComplex = this.configurationService.getValue('diffEditor.renderSideBySide'); - } else if (activeMainEditor?.hasCapability(EditorInputCapabilities.MultipleEditors)) { - isEditorComplex = true; + const layout = this.editorGroupService.getLayout(); + let hasMoreThanOneColumn = false; + if (layout.orientation === GroupOrientation.HORIZONTAL) { + hasMoreThanOneColumn = layout.groups.length > 1; + } else { + hasMoreThanOneColumn = layout.groups.some(g => g.groups && g.groups.length > 1); } const isCenteredLayoutAutoResizing = this.configurationService.getValue('workbench.editor.centeredLayoutAutoResize'); if ( isCenteredLayoutAutoResizing && - ((this.editorGroupService.mainPart.groups.length > 1 && !this.editorGroupService.mainPart.hasMaximizedGroup()) || isEditorComplex) + ((hasMoreThanOneColumn && !this.editorGroupService.mainPart.hasMaximizedGroup()) || isEditorComplex) ) { active = false; // disable centered layout for complex editors or when there is more than one group } From 1efa0d219662095a77139bb0d18d664ee6230f3b Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 18 Dec 2024 14:59:40 +0100 Subject: [PATCH 0213/3587] refactor: enhance sync method and reuse for preview and some clean up (#236470) * refactor: enhance sync method and reuse for preview and some clean up * fix tests --- .../common/abstractSynchronizer.ts | 99 +++--- .../userDataSync/common/extensionsSync.ts | 3 +- .../userDataSync/common/globalStateSync.ts | 30 +- .../userDataSync/common/userDataSync.ts | 8 +- .../common/userDataSyncService.ts | 41 +-- .../test/common/keybindingsSync.test.ts | 6 +- .../test/common/settingsSync.test.ts | 2 +- .../test/common/snippetsSync.test.ts | 155 ++------- .../test/common/synchronizer.test.ts | 305 ++---------------- .../test/common/tasksSync.test.ts | 2 +- .../browser/editSessions.contribution.ts | 8 +- .../editSessions/common/workspaceStateSync.ts | 9 +- 12 files changed, 166 insertions(+), 502 deletions(-) diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index dc3b9950ad7d..11c0315468aa 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -26,7 +26,14 @@ import { getServiceMachineId } from '../../externalServices/common/serviceMachin import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; -import { Change, getLastSyncResourceUri, IRemoteUserData, IResourcePreview as IBaseResourcePreview, ISyncData, IUserDataSyncResourcePreview as IBaseSyncResourcePreview, IUserData, IUserDataSyncResourceInitializer, IUserDataSyncLocalStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, MergeState, PREVIEW_DIR_NAME, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_CONFIGURATION_SCOPE, USER_DATA_SYNC_SCHEME, IUserDataResourceManifest, getPathSegments, IUserDataSyncResourceConflicts, IUserDataSyncResource } from './userDataSync.js'; +import { + Change, getLastSyncResourceUri, IRemoteUserData, IResourcePreview as IBaseResourcePreview, ISyncData, + IUserDataSyncResourcePreview as IBaseSyncResourcePreview, IUserData, IUserDataSyncResourceInitializer, IUserDataSyncLocalStoreService, + IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, + IUserDataSyncUtilService, MergeState, PREVIEW_DIR_NAME, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, + USER_DATA_SYNC_CONFIGURATION_SCOPE, USER_DATA_SYNC_SCHEME, IUserDataResourceManifest, getPathSegments, IUserDataSyncResourceConflicts, + IUserDataSyncResource, IUserDataSyncResourcePreview +} from './userDataSync.js'; import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; type IncompatibleSyncSourceClassification = { @@ -114,6 +121,12 @@ interface ILastSyncUserDataState { [key: string]: any; } +export const enum SyncStrategy { + Preview = 'preview', // Merge the local and remote data without applying. + Merge = 'merge', // Merge the local and remote data and apply. + PullOrPush = 'pull-push', // Pull the remote data or push the local data. +} + export abstract class AbstractSynchroniser extends Disposable implements IUserDataSynchroniser { private syncPreviewPromise: CancelablePromise | null = null; @@ -180,7 +193,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa this.logService.info(`${this.syncResourceLogLabel}: In conflicts state and local change detected. Syncing again...`); const preview = await this.syncPreviewPromise!; this.syncPreviewPromise = null; - const status = await this.performSync(preview.remoteUserData, preview.lastSyncUserData, true, this.getUserDataSyncConfiguration()); + const status = await this.performSync(preview.remoteUserData, preview.lastSyncUserData, SyncStrategy.Merge, this.getUserDataSyncConfiguration()); this.setStatus(status); } @@ -202,28 +215,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa } } - async sync(manifest: IUserDataResourceManifest | null, headers: IHeaders = {}): Promise { - await this._sync(manifest, true, this.getUserDataSyncConfiguration(), headers); - } - - async preview(manifest: IUserDataResourceManifest | null, userDataSyncConfiguration: IUserDataSyncConfiguration, headers: IHeaders = {}): Promise { - return this._sync(manifest, false, userDataSyncConfiguration, headers); - } - - async apply(force: boolean, headers: IHeaders = {}): Promise { - try { - this.syncHeaders = { ...headers }; - - const status = await this.doApply(force); - this.setStatus(status); - - return this.syncPreviewPromise; - } finally { - this.syncHeaders = {}; - } - } - - private async _sync(manifest: IUserDataResourceManifest | null, apply: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration, headers: IHeaders): Promise { + async sync(manifest: IUserDataResourceManifest | null, preview: boolean = false, userDataSyncConfiguration: IUserDataSyncConfiguration = this.getUserDataSyncConfiguration(), headers: IHeaders = {}): Promise { try { this.syncHeaders = { ...headers }; @@ -244,7 +236,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa try { const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData); - status = await this.performSync(remoteUserData, lastSyncUserData, apply, userDataSyncConfiguration); + status = await this.performSync(remoteUserData, lastSyncUserData, preview ? SyncStrategy.Preview : SyncStrategy.Merge, userDataSyncConfiguration); if (status === SyncStatus.HasConflicts) { this.logService.info(`${this.syncResourceLogLabel}: Detected conflicts while synchronizing ${this.resource.toLowerCase()}.`); } else if (status === SyncStatus.Idle) { @@ -259,6 +251,19 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa } } + async apply(force: boolean, headers: IHeaders = {}): Promise { + try { + this.syncHeaders = { ...headers }; + + const status = await this.doApply(force); + this.setStatus(status); + + return this.syncPreviewPromise; + } finally { + this.syncHeaders = {}; + } + } + async replace(content: string): Promise { const syncData = this.parseSyncData(content); if (!syncData) { @@ -318,7 +323,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa return this.getRemoteUserData(lastSyncUserData); } - private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise { + private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, strategy: SyncStrategy, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise { if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) { // current version is not compatible with cloud version this.telemetryService.publicLog2<{ source: string }, IncompatibleSyncSourceClassification>('sync/incompatible', { source: this.resource }); @@ -326,7 +331,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa } try { - return await this.doSync(remoteUserData, lastSyncUserData, apply, userDataSyncConfiguration); + return await this.doSync(remoteUserData, lastSyncUserData, strategy, userDataSyncConfiguration); } catch (e) { if (e instanceof UserDataSyncError) { switch (e.code) { @@ -334,7 +339,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa case UserDataSyncErrorCode.LocalPreconditionFailed: // Rejected as there is a new local version. Syncing again... this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize ${this.syncResourceLogLabel} as there is a new local version available. Synchronizing again...`); - return this.performSync(remoteUserData, lastSyncUserData, apply, userDataSyncConfiguration); + return this.performSync(remoteUserData, lastSyncUserData, strategy, userDataSyncConfiguration); case UserDataSyncErrorCode.Conflict: case UserDataSyncErrorCode.PreconditionFailed: @@ -348,19 +353,20 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa // and one of them successfully updated remote and last sync state. lastSyncUserData = await this.getLastSyncUserData(); - return this.performSync(remoteUserData, lastSyncUserData, apply, userDataSyncConfiguration); + return this.performSync(remoteUserData, lastSyncUserData, SyncStrategy.Merge, userDataSyncConfiguration); } } throw e; } } - protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise { + protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, strategy: SyncStrategy, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise { try { const isRemoteDataFromCurrentMachine = await this.isRemoteDataFromCurrentMachine(remoteUserData); const acceptRemote = !isRemoteDataFromCurrentMachine && lastSyncUserData === null && this.getStoredLastSyncUserDataStateContent() !== undefined; - const merge = apply && !acceptRemote; + const merge = strategy === SyncStrategy.Preview || (strategy === SyncStrategy.Merge && !acceptRemote); + const apply = strategy === SyncStrategy.Merge || strategy === SyncStrategy.PullOrPush; // generate or use existing preview if (!this.syncPreviewPromise) { @@ -369,13 +375,26 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa let preview = await this.syncPreviewPromise; - if (apply && acceptRemote) { + if (strategy === SyncStrategy.Merge && acceptRemote) { this.logService.info(`${this.syncResourceLogLabel}: Accepting remote because it was synced before and the last sync data is not available.`); for (const resourcePreview of preview.resourcePreviews) { preview = (await this.accept(resourcePreview.remoteResource)) || preview; } } + else if (strategy === SyncStrategy.PullOrPush) { + for (const resourcePreview of preview.resourcePreviews) { + if (resourcePreview.mergeState === MergeState.Accepted) { + continue; + } + if (remoteUserData.ref === lastSyncUserData?.ref || isRemoteDataFromCurrentMachine) { + preview = (await this.accept(resourcePreview.localResource)) ?? preview; + } else { + preview = (await this.accept(resourcePreview.remoteResource)) ?? preview; + } + } + } + this.updateConflicts(preview.resourcePreviews); if (preview.resourcePreviews.some(({ mergeState }) => mergeState === MergeState.Conflict)) { return SyncStatus.HasConflicts; @@ -396,22 +415,6 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa } } - async merge(resource: URI): Promise { - await this.updateSyncResourcePreview(resource, async (resourcePreview) => { - const mergeResult = await this.getMergeResult(resourcePreview, CancellationToken.None); - await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(mergeResult?.content || '')); - const acceptResult: IAcceptResult | undefined = mergeResult && !mergeResult.hasConflicts - ? await this.getAcceptResult(resourcePreview, resourcePreview.previewResource, undefined, CancellationToken.None) - : undefined; - resourcePreview.acceptResult = acceptResult; - resourcePreview.mergeState = mergeResult.hasConflicts ? MergeState.Conflict : acceptResult ? MergeState.Accepted : MergeState.Preview; - resourcePreview.localChange = acceptResult ? acceptResult.localChange : mergeResult.localChange; - resourcePreview.remoteChange = acceptResult ? acceptResult.remoteChange : mergeResult.remoteChange; - return resourcePreview; - }); - return this.syncPreviewPromise; - } - async accept(resource: URI, content?: string | null): Promise { await this.updateSyncResourcePreview(resource, async (resourcePreview) => { const acceptResult = await this.getAcceptResult(resourcePreview, resource, content, CancellationToken.None); diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 4054babf6f05..2269f4e3c58d 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -242,7 +242,8 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse private async acceptLocal(resourcePreview: IExtensionResourcePreview): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(undefined, this.syncResource.profile.extensionsResource); const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); - const mergeResult = merge(resourcePreview.localExtensions, null, null, resourcePreview.skippedExtensions, ignoredExtensions, resourcePreview.builtinExtensions); + const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null; + const mergeResult = merge(resourcePreview.localExtensions, remoteExtensions, remoteExtensions, resourcePreview.skippedExtensions, ignoredExtensions, resourcePreview.builtinExtensions); const { local, remote } = mergeResult; return { content: resourcePreview.localContent, diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 75ea24464388..e37893825af1 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -194,25 +194,37 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs } private async acceptLocal(resourcePreview: IGlobalStateResourcePreview): Promise { - return { - content: resourcePreview.localContent, - local: { added: {}, removed: [], updated: {} }, - remote: { added: Object.keys(resourcePreview.localUserData.storage), removed: [], updated: [], all: resourcePreview.localUserData.storage }, - localChange: Change.None, - remoteChange: Change.Modified, - }; + if (resourcePreview.remoteContent !== null) { + const remoteGlobalState: IGlobalState = JSON.parse(resourcePreview.remoteContent); + const { local, remote } = merge(resourcePreview.localUserData.storage, remoteGlobalState.storage, remoteGlobalState.storage, resourcePreview.storageKeys, this.logService); + return { + content: resourcePreview.remoteContent, + local, + remote, + localChange: Change.None, + remoteChange: remote.all !== null ? Change.Modified : Change.None, + }; + } else { + return { + content: resourcePreview.localContent, + local: { added: {}, removed: [], updated: {} }, + remote: { added: Object.keys(resourcePreview.localUserData.storage), removed: [], updated: [], all: resourcePreview.localUserData.storage }, + localChange: Change.None, + remoteChange: Change.Modified, + }; + } } private async acceptRemote(resourcePreview: IGlobalStateResourcePreview): Promise { if (resourcePreview.remoteContent !== null) { const remoteGlobalState: IGlobalState = JSON.parse(resourcePreview.remoteContent); - const { local, remote } = merge(resourcePreview.localUserData.storage, remoteGlobalState.storage, null, resourcePreview.storageKeys, this.logService); + const { local, remote } = merge(resourcePreview.localUserData.storage, remoteGlobalState.storage, resourcePreview.localUserData.storage, resourcePreview.storageKeys, this.logService); return { content: resourcePreview.remoteContent, local, remote, localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None, - remoteChange: remote !== null ? Change.Modified : Change.None, + remoteChange: Change.None, }; } else { return { diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index c3224e18bddb..02824fa34676 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -498,14 +498,10 @@ export interface IUserDataSynchroniser { readonly onDidChangeLocal: Event; - sync(manifest: IUserDataResourceManifest | null, headers: IHeaders): Promise; - stop(): Promise; - - preview(manifest: IUserDataResourceManifest | null, userDataSyncConfiguration: IUserDataSyncConfiguration, headers: IHeaders): Promise; + sync(manifest: IUserDataResourceManifest | null, preview: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration, headers: IHeaders): Promise; accept(resource: URI, content?: string | null): Promise; - merge(resource: URI): Promise; - discard(resource: URI): Promise; apply(force: boolean, headers: IHeaders): Promise; + stop(): Promise; hasPreviouslySynced(): Promise; hasLocalData(): Promise; diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index a18be8e39969..46a443581b99 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -28,10 +28,10 @@ import { SnippetsSynchroniser } from './snippetsSync.js'; import { TasksSynchroniser } from './tasksSync.js'; import { UserDataProfilesManifestSynchroniser } from './userDataProfilesManifestSync.js'; import { - ALL_SYNC_RESOURCES, Change, createSyncHeaders, IUserDataManualSyncTask, IUserDataSyncResourceConflicts, IUserDataSyncResourceError, + ALL_SYNC_RESOURCES, createSyncHeaders, IUserDataManualSyncTask, IUserDataSyncResourceConflicts, IUserDataSyncResourceError, IUserDataSyncResource, ISyncResourceHandle, IUserDataSyncTask, ISyncUserDataProfile, IUserDataManifest, IUserDataResourceManifest, IUserDataSyncConfiguration, IUserDataSyncEnablementService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncService, IUserDataSyncStoreManagementService, IUserDataSyncStoreService, - MergeState, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, UserDataSyncStoreError, USER_DATA_SYNC_CONFIGURATION_SCOPE, IUserDataSyncResourceProviderService, IUserDataActivityData, IUserDataSyncLocalStoreService + SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, UserDataSyncStoreError, USER_DATA_SYNC_CONFIGURATION_SCOPE, IUserDataSyncResourceProviderService, IUserDataActivityData, IUserDataSyncLocalStoreService, } from './userDataSync.js'; type SyncErrorClassification = { @@ -200,7 +200,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ }; } - private async sync(manifest: IUserDataManifest | null, merge: boolean, executionId: string, token: CancellationToken): Promise { + private async sync(manifest: IUserDataManifest | null, preview: boolean, executionId: string, token: CancellationToken): Promise { this._syncErrors = []; try { if (this.status !== SyncStatus.HasConflicts) { @@ -209,7 +209,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ // Sync Default Profile First const defaultProfileSynchronizer = this.getOrCreateActiveProfileSynchronizer(this.userDataProfilesService.defaultProfile, undefined); - this._syncErrors.push(...await this.syncProfile(defaultProfileSynchronizer, manifest, merge, executionId, token)); + this._syncErrors.push(...await this.syncProfile(defaultProfileSynchronizer, manifest, preview, executionId, token)); // Sync other profiles const userDataProfileManifestSynchronizer = defaultProfileSynchronizer.enabled.find(s => s.resource === SyncResource.Profiles); @@ -218,7 +218,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (token.isCancellationRequested) { return; } - await this.syncRemoteProfiles(syncProfiles, manifest, merge, executionId, token); + await this.syncRemoteProfiles(syncProfiles, manifest, preview, executionId, token); } } finally { if (this.status !== SyncStatus.HasConflicts) { @@ -228,7 +228,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } } - private async syncRemoteProfiles(remoteProfiles: ISyncUserDataProfile[], manifest: IUserDataManifest | null, merge: boolean, executionId: string, token: CancellationToken): Promise { + private async syncRemoteProfiles(remoteProfiles: ISyncUserDataProfile[], manifest: IUserDataManifest | null, preview: boolean, executionId: string, token: CancellationToken): Promise { for (const syncProfile of remoteProfiles) { if (token.isCancellationRequested) { return; @@ -240,7 +240,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } this.logService.info('Syncing profile.', syncProfile.name); const profileSynchronizer = this.getOrCreateActiveProfileSynchronizer(profile, syncProfile); - this._syncErrors.push(...await this.syncProfile(profileSynchronizer, manifest, merge, executionId, token)); + this._syncErrors.push(...await this.syncProfile(profileSynchronizer, manifest, preview, executionId, token)); } // Dispose & Delete profile synchronizers which do not exist anymore for (const [key, profileSynchronizerItem] of this.activeProfileSynchronizers.entries()) { @@ -285,8 +285,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } } - private async syncProfile(profileSynchronizer: ProfileSynchronizer, manifest: IUserDataManifest | null, merge: boolean, executionId: string, token: CancellationToken): Promise { - const errors = await profileSynchronizer.sync(manifest, merge, executionId, token); + private async syncProfile(profileSynchronizer: ProfileSynchronizer, manifest: IUserDataManifest | null, preview: boolean, executionId: string, token: CancellationToken): Promise { + const errors = await profileSynchronizer.sync(manifest, preview, executionId, token); return errors.map(([syncResource, error]) => ({ profile: profileSynchronizer.profile, syncResource, error })); } @@ -715,7 +715,7 @@ class ProfileSynchronizer extends Disposable { } } - async sync(manifest: IUserDataManifest | null, merge: boolean, executionId: string, token: CancellationToken): Promise<[SyncResource, UserDataSyncError][]> { + async sync(manifest: IUserDataManifest | null, preview: boolean, executionId: string, token: CancellationToken): Promise<[SyncResource, UserDataSyncError][]> { // Return if cancellation is requested if (token.isCancellationRequested) { @@ -731,7 +731,7 @@ class ProfileSynchronizer extends Disposable { const syncErrors: [SyncResource, UserDataSyncError][] = []; const syncHeaders = createSyncHeaders(executionId); const resourceManifest: IUserDataResourceManifest | null = (this.collection ? manifest?.collections?.[this.collection]?.latest : manifest?.latest) ?? null; - const userDataSyncConfiguration = merge ? await this.getUserDataSyncConfiguration(resourceManifest) : {}; + const userDataSyncConfiguration = preview ? await this.getUserDataSyncConfiguration(resourceManifest) : this.getLocalUserDataSyncConfiguration(); for (const synchroniser of synchronizers) { // Return if cancellation is requested if (token.isCancellationRequested) { @@ -744,18 +744,7 @@ class ProfileSynchronizer extends Disposable { } try { - if (merge) { - const preview = await synchroniser.preview(resourceManifest, userDataSyncConfiguration, syncHeaders); - if (preview) { - for (const resourcePreview of preview.resourcePreviews) { - if ((resourcePreview.localChange !== Change.None || resourcePreview.remoteChange !== Change.None) && resourcePreview.mergeState === MergeState.Preview) { - await synchroniser.merge(resourcePreview.previewResource); - } - } - } - } else { - await synchroniser.sync(resourceManifest, syncHeaders); - } + await synchroniser.sync(resourceManifest, preview, userDataSyncConfiguration, syncHeaders); } catch (e) { const userDataSyncError = UserDataSyncError.toUserDataSyncError(e); reportUserDataSyncError(userDataSyncError, executionId, this.userDataSyncStoreManagementService, this.telemetryService); @@ -825,7 +814,7 @@ class ProfileSynchronizer extends Disposable { if (!this.profile.isDefault) { return {}; } - const local = this.configurationService.getValue(USER_DATA_SYNC_CONFIGURATION_SCOPE); + const local = this.getLocalUserDataSyncConfiguration(); const settingsSynchronizer = this.enabled.find(synchronizer => synchronizer instanceof SettingsSynchroniser); if (settingsSynchronizer) { const remote = await (settingsSynchronizer).getRemoteUserDataSyncConfiguration(manifest); @@ -834,6 +823,10 @@ class ProfileSynchronizer extends Disposable { return local; } + private getLocalUserDataSyncConfiguration(): IUserDataSyncConfiguration { + return this.configurationService.getValue(USER_DATA_SYNC_CONFIGURATION_SCOPE); + } + private setStatus(status: SyncStatus): void { if (this._status !== status) { this._status = status; diff --git a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts index 00841916bbe3..c0103898ddcd 100644 --- a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts @@ -196,11 +196,11 @@ suite('KeybindingsSync', () => { await fileService.del(keybindingsResource); } - const preview = (await testObject.preview(await client.getResourceManifest(), {}))!; + const preview = await testObject.sync(await client.getResourceManifest(), true); server.reset(); - const content = await testObject.resolveContent(preview.resourcePreviews[0].remoteResource); - await testObject.accept(preview.resourcePreviews[0].remoteResource, content); + const content = await testObject.resolveContent(preview!.resourcePreviews[0].remoteResource); + await testObject.accept(preview!.resourcePreviews[0].remoteResource, content); await testObject.apply(false); assert.deepStrictEqual(server.requests, []); }); diff --git a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts index 914ae83a5d47..b3c6eee9926d 100644 --- a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts @@ -569,7 +569,7 @@ suite('SettingsSync - Manual', () => { }`; await updateSettings(settingsContent, client); - let preview = await testObject.preview(await client.getResourceManifest(), {}); + let preview = await testObject.sync(await client.getResourceManifest(), true); assert.strictEqual(testObject.status, SyncStatus.Syncing); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.apply(false); diff --git a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts index 89e9c59c48be..0e37d04d493c 100644 --- a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts @@ -674,49 +674,12 @@ suite('SnippetsSync', () => { assert.ok(!await fileService.exists(dirname(conflicts[0].previewResource))); }); - test('merge when there are multiple snippets and only one snippet is merged', async () => { - const environmentService = testClient.instantiationService.get(IEnvironmentService); - - await updateSnippet('html.json', htmlSnippet2, testClient); - await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.getResourceManifest(), {}); - - assert.strictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, - [ - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'), - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'), - ]); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); - - preview = await testObject.merge(preview!.resourcePreviews[0].localResource); - - assert.strictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, - [ - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'), - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'), - ]); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); - }); - test('merge when there are multiple snippets and all snippets are merged', async () => { const environmentService = testClient.instantiationService.get(IEnvironmentService); await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.getResourceManifest(), {}); - - assert.strictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, - [ - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'), - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'), - ]); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); - - preview = await testObject.merge(preview!.resourcePreviews[0].localResource); - preview = await testObject.merge(preview!.resourcePreviews[1].localResource); + const preview = await testObject.sync(await testClient.getResourceManifest(), true); assert.strictEqual(testObject.status, SyncStatus.Syncing); assertPreviews(preview!.resourcePreviews, @@ -728,22 +691,9 @@ suite('SnippetsSync', () => { }); test('merge when there are multiple snippets and all snippets are merged and applied', async () => { - const environmentService = testClient.instantiationService.get(IEnvironmentService); - await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.getResourceManifest(), {}); - - assert.strictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, - [ - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'), - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'), - ]); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); - - preview = await testObject.merge(preview!.resourcePreviews[0].localResource); - preview = await testObject.merge(preview!.resourcePreviews[1].localResource); + let preview = await testObject.sync(await testClient.getResourceManifest(), true); preview = await testObject.apply(false); assert.strictEqual(testObject.status, SyncStatus.Idle); @@ -759,17 +709,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.getResourceManifest(), {}); - - assert.strictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, - [ - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'), - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'), - ]); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); - - preview = await testObject.merge(preview!.resourcePreviews[0].localResource); + const preview = await testObject.sync(await testClient.getResourceManifest(), true); assert.strictEqual(testObject.status, SyncStatus.Syncing); assertPreviews(preview!.resourcePreviews, @@ -780,25 +720,14 @@ suite('SnippetsSync', () => { assert.deepStrictEqual(testObject.conflicts.conflicts, []); }); - test('merge when there are multiple snippets and one snippet has no changes and one snippet is merged and applied', async () => { - const environmentService = testClient.instantiationService.get(IEnvironmentService); - + test('merge when there are multiple snippets and one snippet has no changes and snippets is merged and applied', async () => { await updateSnippet('html.json', htmlSnippet1, client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet1, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.getResourceManifest(), {}); - - assert.strictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, - [ - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'), - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'), - ]); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); + let preview = await testObject.sync(await testClient.getResourceManifest(), true); - preview = await testObject.merge(preview!.resourcePreviews[0].localResource); preview = await testObject.apply(false); assert.strictEqual(testObject.status, SyncStatus.Idle); @@ -806,7 +735,7 @@ suite('SnippetsSync', () => { assert.deepStrictEqual(testObject.conflicts.conflicts, []); }); - test('merge when there are multiple snippets with conflicts and only one snippet is merged', async () => { + test('merge when there are multiple snippets with conflicts and all snippets are merged', async () => { const environmentService = testClient.instantiationService.get(IEnvironmentService); await updateSnippet('html.json', htmlSnippet1, client2); @@ -815,17 +744,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.getResourceManifest(), {}); - - assert.strictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, - [ - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'), - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'), - ]); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); - - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + const preview = await testObject.sync(await testClient.getResourceManifest(), true); assert.strictEqual(testObject.status, SyncStatus.HasConflicts); assertPreviews(preview!.resourcePreviews, @@ -836,10 +755,11 @@ suite('SnippetsSync', () => { assertPreviews(testObject.conflicts.conflicts, [ joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'), + joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'), ]); }); - test('merge when there are multiple snippets with conflicts and all snippets are merged', async () => { + test('accept when there are multiple snippets with conflicts and only one snippet is accepted', async () => { const environmentService = testClient.instantiationService.get(IEnvironmentService); await updateSnippet('html.json', htmlSnippet1, client2); @@ -848,18 +768,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.getResourceManifest(), {}); - - assert.strictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, - [ - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'), - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'), - ]); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); - - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.merge(preview!.resourcePreviews[1].previewResource); + let preview = await testObject.sync(await testClient.getResourceManifest(), true); assert.strictEqual(testObject.status, SyncStatus.HasConflicts); assertPreviews(preview!.resourcePreviews, @@ -872,36 +781,19 @@ suite('SnippetsSync', () => { joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'), joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'), ]); - }); - test('accept when there are multiple snippets with conflicts and only one snippet is accepted', async () => { - const environmentService = testClient.instantiationService.get(IEnvironmentService); - - await updateSnippet('html.json', htmlSnippet1, client2); - await updateSnippet('typescript.json', tsSnippet1, client2); - await client2.sync(); - - await updateSnippet('html.json', htmlSnippet2, testClient); - await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.getResourceManifest(), {}); + preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, htmlSnippet2); - assert.strictEqual(testObject.status, SyncStatus.Syncing); + assert.strictEqual(testObject.status, SyncStatus.HasConflicts); assertPreviews(preview!.resourcePreviews, [ joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'), joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'), ]); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); - - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, htmlSnippet2); - - assert.strictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, + assertPreviews(testObject.conflicts.conflicts, [ - joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'), joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'), ]); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); }); test('accept when there are multiple snippets with conflicts and all snippets are accepted', async () => { @@ -913,15 +805,19 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.getResourceManifest(), {}); + let preview = await testObject.sync(await testClient.getResourceManifest(), true); - assert.strictEqual(testObject.status, SyncStatus.Syncing); + assert.strictEqual(testObject.status, SyncStatus.HasConflicts); assertPreviews(preview!.resourcePreviews, [ joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'), joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'), ]); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); + assertPreviews(testObject.conflicts.conflicts, + [ + joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'), + joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'), + ]); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, htmlSnippet2); preview = await testObject.accept(preview!.resourcePreviews[1].previewResource, tsSnippet2); @@ -937,22 +833,25 @@ suite('SnippetsSync', () => { test('accept when there are multiple snippets with conflicts and all snippets are accepted and applied', async () => { const environmentService = testClient.instantiationService.get(IEnvironmentService); - await updateSnippet('html.json', htmlSnippet1, client2); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.getResourceManifest(), {}); + let preview = await testObject.sync(await testClient.getResourceManifest(), true); - assert.strictEqual(testObject.status, SyncStatus.Syncing); + assert.strictEqual(testObject.status, SyncStatus.HasConflicts); assertPreviews(preview!.resourcePreviews, [ joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'), joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'), ]); - assert.deepStrictEqual(testObject.conflicts.conflicts, []); + assertPreviews(testObject.conflicts.conflicts, + [ + joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'), + joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'), + ]); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, htmlSnippet2); preview = await testObject.accept(preview!.resourcePreviews[1].previewResource, tsSnippet2); diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 4dfec2707115..31a558036605 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -15,7 +15,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c import { IFileService } from '../../../files/common/files.js'; import { IStorageService, StorageScope } from '../../../storage/common/storage.js'; import { IUserDataProfilesService } from '../../../userDataProfile/common/userDataProfile.js'; -import { AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from '../../common/abstractSynchronizer.js'; +import { AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview, SyncStrategy } from '../../common/abstractSynchronizer.js'; import { Change, IRemoteUserData, IResourcePreview as IBaseResourcePreview, IUserDataResourceManifest, IUserDataSyncConfiguration, IUserDataSyncStoreService, MergeState, SyncResource, SyncStatus, USER_DATA_SYNC_SCHEME } from '../../common/userDataSync.js'; import { UserDataSyncClient, UserDataSyncTestServer } from './userDataSyncClient.js'; @@ -45,7 +45,7 @@ class TestSynchroniser extends AbstractSynchroniser { return super.getLatestRemoteUserData(manifest, lastSyncUserData); } - protected override async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise { + protected override async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, strategy: SyncStrategy, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise { this.cancelled = false; this.onDoSyncCall.fire(); await this.syncBarrier.wait(); @@ -54,7 +54,7 @@ class TestSynchroniser extends AbstractSynchroniser { return SyncStatus.Idle; } - return super.doSync(remoteUserData, lastSyncUserData, apply, userDataSyncConfiguration); + return super.doSync(remoteUserData, lastSyncUserData, strategy, userDataSyncConfiguration); } protected override async generateSyncPreview(remoteUserData: IRemoteUserData): Promise { @@ -536,22 +536,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - const preview = await testObject.preview(await client.getResourceManifest(), {}); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assertConflicts(testObject.conflicts.conflicts, []); - }); - }); - - test('preview -> merge', async () => { - await runWithFakedTimers({}, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + const preview = await testObject.sync(await client.getResourceManifest(), true); assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); assertPreviews(preview!.resourcePreviews, [testObject.localResource]); @@ -566,24 +551,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); - assertConflicts(testObject.conflicts.conflicts, []); - }); - }); - - test('preview -> merge -> accept', async () => { - await runWithFakedTimers({}, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + let preview = await testObject.sync(await client.getResourceManifest(), true); preview = await testObject.accept(preview!.resourcePreviews[0].localResource); assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); @@ -601,8 +569,7 @@ suite('TestSynchronizer - Manual Sync', () => { await testObject.sync(await client.getResourceManifest()); const manifest = await client.getResourceManifest(); - let preview = await testObject.preview(manifest, {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + let preview = await testObject.sync(manifest, true); preview = await testObject.apply(false); assert.deepStrictEqual(testObject.status, SyncStatus.Idle); @@ -624,7 +591,7 @@ suite('TestSynchronizer - Manual Sync', () => { const manifest = await client.getResourceManifest(); const expectedContent = manifest![testObject.resource]; - let preview = await testObject.preview(manifest, {}); + let preview = await testObject.sync(manifest, true); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.apply(false); @@ -637,36 +604,13 @@ suite('TestSynchronizer - Manual Sync', () => { }); }); - test('preview -> merge -> accept -> apply', async () => { + test('preivew -> discard', async () => { await runWithFakedTimers({}, async () => { const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].localResource); - preview = await testObject.apply(false); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual(preview, null); - assertConflicts(testObject.conflicts.conflicts, []); - - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); - assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); - }); - }); - - test('preivew -> merge -> discard', async () => { - await runWithFakedTimers({}, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + let preview = await testObject.sync(await client.getResourceManifest(), true); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); @@ -676,14 +620,13 @@ suite('TestSynchronizer - Manual Sync', () => { }); }); - test('preivew -> merge -> discard -> accept', async () => { + test('preivew -> discard -> accept', async () => { await runWithFakedTimers({}, async () => { const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + let preview = await testObject.sync(await client.getResourceManifest(), true); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); @@ -694,30 +637,13 @@ suite('TestSynchronizer - Manual Sync', () => { }); }); - test('preivew -> accept -> discard', async () => { - await runWithFakedTimers({}, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Preview); - assertConflicts(testObject.conflicts.conflicts, []); - }); - }); - test('preivew -> accept -> discard -> accept', async () => { await runWithFakedTimers({}, async () => { const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); + let preview = await testObject.sync(await client.getResourceManifest(), true); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); @@ -729,32 +655,13 @@ suite('TestSynchronizer - Manual Sync', () => { }); }); - test('preivew -> accept -> discard -> merge', async () => { - await runWithFakedTimers({}, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Accepted); - assertConflicts(testObject.conflicts.conflicts, []); - }); - }); - - test('preivew -> merge -> accept -> discard', async () => { + test('preivew -> accept -> discard', async () => { await runWithFakedTimers({}, async () => { const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + let preview = await testObject.sync(await client.getResourceManifest(), true); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); @@ -765,29 +672,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); }); - test('preivew -> merge -> discard -> accept -> apply', async () => { - await runWithFakedTimers({}, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - - const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].localResource); - preview = await testObject.apply(false); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual(preview, null); - assertConflicts(testObject.conflicts.conflicts, []); - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); - assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); - }); - }); - - test('preivew -> accept -> discard -> accept -> apply', async () => { + test('preivew -> discard -> accept -> apply', async () => { await runWithFakedTimers({}, async () => { const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); testObject.syncResult = { hasConflicts: false, hasError: false }; @@ -795,8 +680,7 @@ suite('TestSynchronizer - Manual Sync', () => { await testObject.sync(await client.getResourceManifest()); const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + let preview = await testObject.sync(await client.getResourceManifest(), true); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].localResource); @@ -810,53 +694,13 @@ suite('TestSynchronizer - Manual Sync', () => { }); }); - test('preivew -> accept -> discard -> merge -> apply', async () => { - await runWithFakedTimers({}, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - - const manifest = await client.getResourceManifest(); - const expectedContent = manifest![testObject.resource]; - let preview = await testObject.preview(manifest, {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.merge(preview!.resourcePreviews[0].localResource); - preview = await testObject.apply(false); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual(preview, null); - assertConflicts(testObject.conflicts.conflicts, []); - - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); - assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); - }); - }); - test('conflicts: preview', async () => { await runWithFakedTimers({}, async () => { const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - const preview = await testObject.preview(await client.getResourceManifest(), {}); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assertConflicts(testObject.conflicts.conflicts, []); - }); - }); - - test('conflicts: preview -> merge', async () => { - await runWithFakedTimers({}, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + const preview = await testObject.sync(await client.getResourceManifest(), true); assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); assertPreviews(preview!.resourcePreviews, [testObject.localResource]); @@ -865,14 +709,13 @@ suite('TestSynchronizer - Manual Sync', () => { }); }); - test('conflicts: preview -> merge -> discard', async () => { + test('conflicts: preview -> discard', async () => { await runWithFakedTimers({}, async () => { const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - const preview = await testObject.preview(await client.getResourceManifest(), {}); - await testObject.merge(preview!.resourcePreviews[0].previewResource); + const preview = await testObject.sync(await client.getResourceManifest(), true); await testObject.discard(preview!.resourcePreviews[0].previewResource); assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); @@ -888,8 +731,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - await testObject.merge(preview!.resourcePreviews[0].previewResource); + let preview = await testObject.sync(await client.getResourceManifest(), true); const content = await testObject.resolveContent(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, content); @@ -899,38 +741,13 @@ suite('TestSynchronizer - Manual Sync', () => { }); }); - test('conflicts: preview -> merge -> accept -> apply', async () => { - await runWithFakedTimers({}, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: false, hasError: false }; - testObject.syncBarrier.open(); - await testObject.sync(await client.getResourceManifest()); - - testObject.syncResult = { hasConflicts: true, hasError: false }; - const manifest = await client.getResourceManifest(); - const expectedContent = manifest![testObject.resource]; - let preview = await testObject.preview(manifest, {}); - - await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); - preview = await testObject.apply(false); - - assert.deepStrictEqual(testObject.status, SyncStatus.Idle); - assert.strictEqual(preview, null); - assertConflicts(testObject.conflicts.conflicts, []); - - assert.strictEqual((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent); - assert.strictEqual((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent); - }); - }); - test('conflicts: preview -> accept 2', async () => { await runWithFakedTimers({}, async () => { const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); + let preview = await testObject.sync(await client.getResourceManifest(), true); const content = await testObject.resolveContent(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, content); @@ -950,7 +767,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: true, hasError: false }; const manifest = await client.getResourceManifest(); const expectedContent = manifest![testObject.resource]; - let preview = await testObject.preview(manifest, {}); + let preview = await testObject.sync(manifest, true); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.apply(false); @@ -964,14 +781,13 @@ suite('TestSynchronizer - Manual Sync', () => { }); }); - test('conflicts: preivew -> merge -> discard', async () => { + test('conflicts: preivew -> discard', async () => { await runWithFakedTimers({}, async () => { const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + let preview = await testObject.sync(await client.getResourceManifest(), true); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); @@ -981,14 +797,13 @@ suite('TestSynchronizer - Manual Sync', () => { }); }); - test('conflicts: preivew -> merge -> discard -> accept', async () => { + test('conflicts: preivew -> discard -> accept', async () => { await runWithFakedTimers({}, async () => { const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + let preview = await testObject.sync(await client.getResourceManifest(), true); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); @@ -999,30 +814,13 @@ suite('TestSynchronizer - Manual Sync', () => { }); }); - test('conflicts: preivew -> accept -> discard', async () => { - await runWithFakedTimers({}, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Preview); - assertConflicts(testObject.conflicts.conflicts, []); - }); - }); - test('conflicts: preivew -> accept -> discard -> accept', async () => { await runWithFakedTimers({}, async () => { const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); + let preview = await testObject.sync(await client.getResourceManifest(), true); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); @@ -1034,50 +832,13 @@ suite('TestSynchronizer - Manual Sync', () => { }); }); - test('conflicts: preivew -> accept -> discard -> merge', async () => { - await runWithFakedTimers({}, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Conflict); - assertConflicts(testObject.conflicts.conflicts, [preview!.resourcePreviews[0].localResource]); - }); - }); - - test('conflicts: preivew -> merge -> discard -> merge', async () => { - await runWithFakedTimers({}, async () => { - const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); - testObject.syncResult = { hasConflicts: true, hasError: false }; - testObject.syncBarrier.open(); - - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); - preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); - preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource); - - assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); - assertPreviews(preview!.resourcePreviews, [testObject.localResource]); - assert.strictEqual(preview!.resourcePreviews[0].mergeState, MergeState.Conflict); - assertConflicts(testObject.conflicts.conflicts, [preview!.resourcePreviews[0].localResource]); - }); - }); - - test('conflicts: preivew -> merge -> accept -> discard', async () => { + test('conflicts: preivew -> accept -> discard', async () => { await runWithFakedTimers({}, async () => { const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + let preview = await testObject.sync(await client.getResourceManifest(), true); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); @@ -1088,7 +849,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); }); - test('conflicts: preivew -> merge -> discard -> accept -> apply', async () => { + test('conflicts: preivew -> discard -> accept -> apply', async () => { await runWithFakedTimers({}, async () => { const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined)); testObject.syncResult = { hasConflicts: false, hasError: false }; @@ -1096,8 +857,7 @@ suite('TestSynchronizer - Manual Sync', () => { await testObject.sync(await client.getResourceManifest()); const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + let preview = await testObject.sync(await client.getResourceManifest(), true); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].localResource); preview = await testObject.apply(false); @@ -1118,8 +878,7 @@ suite('TestSynchronizer - Manual Sync', () => { await testObject.sync(await client.getResourceManifest()); const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); - let preview = await testObject.preview(await client.getResourceManifest(), {}); - preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); + let preview = await testObject.sync(await client.getResourceManifest(), true); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].localResource); diff --git a/src/vs/platform/userDataSync/test/common/tasksSync.test.ts b/src/vs/platform/userDataSync/test/common/tasksSync.test.ts index 26fa39aeeb2b..e875a66455da 100644 --- a/src/vs/platform/userDataSync/test/common/tasksSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/tasksSync.test.ts @@ -540,7 +540,7 @@ suite('TasksSync', () => { await fileService.del(tasksResource); } - const preview = (await testObject.preview(await client.getResourceManifest(), {}))!; + const preview = (await testObject.sync(await client.getResourceManifest(), true))!; server.reset(); const content = await testObject.resolveContent(preview.resourcePreviews[0].remoteResource); diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index d8ed8a95c5fb..cb2e6e815551 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -21,7 +21,7 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { IProgress, IProgressService, IProgressStep, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { EditSessionsWorkbenchService } from './editSessionsStorageService.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { UserDataSyncErrorCode, UserDataSyncStoreError, IUserDataSynchroniser } from '../../../../platform/userDataSync/common/userDataSync.js'; +import { UserDataSyncErrorCode, UserDataSyncStoreError } from '../../../../platform/userDataSync/common/userDataSync.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { getFileNamesMessage, IDialogService, IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; @@ -124,7 +124,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo private registeredCommands = new Set(); - private workspaceStateSynchronizer: IUserDataSynchroniser | undefined; + private workspaceStateSynchronizer: WorkspaceStateSynchroniser | undefined; private editSessionsStorageClient: EditSessionsStoreClient | undefined; constructor( @@ -565,7 +565,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo } } - await this.workspaceStateSynchronizer?.apply(false, {}); + await this.workspaceStateSynchronizer?.apply(); this.logService.info(`Deleting edit session with ref ${ref} after successfully applying it to current workspace...`); await this.editSessionsStorageService.delete('editSessions', ref); @@ -743,7 +743,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo } // Store contributed workspace state - await this.workspaceStateSynchronizer?.sync(null, {}); + await this.workspaceStateSynchronizer?.sync(); if (!hasEdits) { this.logService.info('Skipped storing working changes in the cloud as there are no edits to store.'); diff --git a/src/vs/workbench/contrib/editSessions/common/workspaceStateSync.ts b/src/vs/workbench/contrib/editSessions/common/workspaceStateSync.ts index dd6f61459019..0068c223d2b3 100644 --- a/src/vs/workbench/contrib/editSessions/common/workspaceStateSync.ts +++ b/src/vs/workbench/contrib/editSessions/common/workspaceStateSync.ts @@ -16,7 +16,7 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IUserDataProfile } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview, ISyncResourcePreview } from '../../../../platform/userDataSync/common/abstractSynchronizer.js'; -import { IRemoteUserData, IResourceRefHandle, IUserDataSyncLocalStoreService, IUserDataSyncConfiguration, IUserDataSyncEnablementService, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSynchroniser, IWorkspaceState, SyncResource } from '../../../../platform/userDataSync/common/userDataSync.js'; +import { IRemoteUserData, IResourceRefHandle, IUserDataSyncLocalStoreService, IUserDataSyncConfiguration, IUserDataSyncEnablementService, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSynchroniser, IWorkspaceState, SyncResource, IUserDataSyncResourcePreview } from '../../../../platform/userDataSync/common/userDataSync.js'; import { EditSession, IEditSessionsStorageService } from './editSessions.js'; import { IWorkspaceIdentityService } from '../../../services/workspaces/common/workspaceIdentityService.js'; @@ -75,11 +75,11 @@ export class WorkspaceStateSynchroniser extends AbstractSynchroniser implements super({ syncResource: SyncResource.WorkspaceState, profile }, collection, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncLocalStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService, uriIdentityService); } - override async sync(): Promise { + override async sync(): Promise { const cancellationTokenSource = new CancellationTokenSource(); const folders = await this.workspaceIdentityService.getWorkspaceStateFolders(cancellationTokenSource.token); if (!folders.length) { - return; + return null; } // Ensure we have latest state by sending out onWillSaveState event @@ -87,7 +87,7 @@ export class WorkspaceStateSynchroniser extends AbstractSynchroniser implements const keys = this.storageService.keys(StorageScope.WORKSPACE, StorageTarget.USER); if (!keys.length) { - return; + return null; } const contributedData: IStringDictionary = {}; @@ -100,6 +100,7 @@ export class WorkspaceStateSynchroniser extends AbstractSynchroniser implements const content: IWorkspaceState = { folders, storage: contributedData, version: this.version }; await this.editSessionsStorageService.write('workspaceState', stringify(content)); + return null; } override async apply(): Promise { From 3a4f4164c7ec15e5607c47c7666c4bde99a21958 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:49:37 +0100 Subject: [PATCH 0214/3587] Git - fix context key for unstageSelectedRanges (#236476) --- extensions/git/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 08349fd47837..d63027619d18 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1054,7 +1054,7 @@ }, { "command": "git.unstageSelectedRanges", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && resourceScheme == git" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme == git" }, { "command": "git.clean", From a02cb4218adce96301f7561d7ce585cdeb84598f Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 18 Dec 2024 09:10:16 -0600 Subject: [PATCH 0215/3587] add `when` for terminal chat keybinding (#236480) fix #236474 --- .../terminalContrib/chat/browser/terminalChatActions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index deeb48f48c79..0d6fdd353616 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -224,7 +224,8 @@ registerActiveXtermAction({ ), keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KeyR + primary: KeyMod.CtrlCmd | KeyCode.KeyR, + when: TerminalChatContextKeys.focused }, menu: { id: MENU_TERMINAL_CHAT_WIDGET_STATUS, From cec5112d1172e69cee13d729f5bc21ceaad36519 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 18 Dec 2024 16:40:44 +0100 Subject: [PATCH 0216/3587] Fixes https://github.com/microsoft/vscode-copilot/issues/9818 (#236486) --- .../browser/controller/inlineCompletionsController.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index f3720436c2f6..7c7e44957197 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -181,14 +181,16 @@ export class InlineCompletionsController extends Disposable { return; } - if (this.model.get()?.inlineEditAvailable.get()) { - // dont hide inline edits on blur + const model = this.model.get(); + if (!model) { return; } + if (model.state.get()?.inlineCompletion?.request.isExplicitRequest && model.inlineEditAvailable.get()) { + // dont hide inline edits on blur when requested explicitly return; } transaction(tx => { /** @description InlineCompletionsController.onDidBlurEditorWidget */ - this.model.get()?.stop('automatic', tx); + model.stop('automatic', tx); }); })); From 929ce7c1fa6bd6acccf703b5b1c491659b63ef11 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 18 Dec 2024 16:45:47 +0100 Subject: [PATCH 0217/3587] fixes a random bunch of leaking disposables (#236487) Adds (disabled for now) disposable tracker that is based on the finalization registry, gist is that every GC'ed disposable that isn't disposed is a bug. --- .../ui/breadcrumbs/breadcrumbsWidget.ts | 1 + src/vs/base/common/async.ts | 2 +- src/vs/base/common/lifecycle.ts | 28 +++++++++++++++++++ .../services/hoverService/hoverService.ts | 2 +- .../codeAction/browser/codeActionModel.ts | 1 + .../chat/browser/actions/chatActions.ts | 4 +-- .../debug/common/debugContentProvider.ts | 9 ++++-- .../browser/performance.contribution.ts | 25 ++++++++++++++++- .../browser/searchTreeModel/fileMatch.ts | 12 ++++---- .../common/notificationService.ts | 2 +- .../progress/browser/progressService.ts | 5 +++- 11 files changed, 75 insertions(+), 16 deletions(-) diff --git a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts index b87e883bd3a0..a4cb3361f9df 100644 --- a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts +++ b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts @@ -282,6 +282,7 @@ export class BreadcrumbsWidget { removed = this._items.splice(prefix, this._items.length - prefix, ...items.slice(prefix)); this._render(prefix); dispose(removed); + dispose(items.slice(0, prefix)); this._focus(-1, undefined); } catch (e) { const newError = new Error(`BreadcrumbsItem#setItems: newItems: ${items.length}, prefix: ${prefix}, removed: ${removed.length}`); diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index bd76ba77e4ad..84feeb777048 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -557,7 +557,7 @@ export function disposableTimeout(handler: () => void, timeout = 0, store?: Disp }, timeout); const disposable = toDisposable(() => { clearTimeout(timer); - store?.deleteAndLeak(disposable); + store?.delete(disposable); }); store?.add(disposable); return disposable; diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index 4cc2d172bf4f..e04e62f76d38 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -44,6 +44,34 @@ export interface IDisposableTracker { markAsSingleton(disposable: IDisposable): void; } +export class GCBasedDisposableTracker implements IDisposableTracker { + + private readonly _registry = new FinalizationRegistry(heldValue => { + console.warn(`[LEAKED DISPOSABLE] ${heldValue}`); + }); + + trackDisposable(disposable: IDisposable): void { + const stack = new Error('CREATED via:').stack!; + this._registry.register(disposable, stack, disposable); + } + + setParent(child: IDisposable, parent: IDisposable | null): void { + if (parent) { + this._registry.unregister(child); + } else { + this.trackDisposable(child); + } + } + + markAsDisposed(disposable: IDisposable): void { + this._registry.unregister(disposable); + } + + markAsSingleton(disposable: IDisposable): void { + this._registry.unregister(disposable); + } +} + export interface DisposableInfo { value: IDisposable; source: string | null; diff --git a/src/vs/editor/browser/services/hoverService/hoverService.ts b/src/vs/editor/browser/services/hoverService/hoverService.ts index b4164c04d1d9..873ece14a4f8 100644 --- a/src/vs/editor/browser/services/hoverService/hoverService.ts +++ b/src/vs/editor/browser/services/hoverService/hoverService.ts @@ -51,7 +51,7 @@ export class HoverService extends Disposable implements IHoverService { ) { super(); - contextMenuService.onDidShowContextMenu(() => this.hideHover()); + this._register(contextMenuService.onDidShowContextMenu(() => this.hideHover())); this._contextViewHandler = this._register(new ContextViewHandler(this._layoutService)); } diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts b/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts index a909d559ed83..10381ff4ffce 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts @@ -235,6 +235,7 @@ export class CodeActionModel extends Disposable { const codeActionSet = await getCodeActions(this._registry, model, trigger.selection, trigger.trigger, Progress.None, token); const allCodeActions = [...codeActionSet.allActions]; if (token.isCancellationRequested) { + codeActionSet.dispose(); return emptyCodeActionSet; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 4cb15f40d68e..a0bb888eb80f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -561,7 +561,7 @@ export class ChatCommandCenterRendering extends Disposable implements IWorkbench const contextKeySet = new Set([ChatContextKeys.Setup.signedOut.key]); - actionViewItemService.register(MenuId.CommandCenter, MenuId.ChatCommandCenter, (action, options) => { + this._store.add(actionViewItemService.register(MenuId.CommandCenter, MenuId.ChatCommandCenter, (action, options) => { if (!(action instanceof SubmenuItemAction)) { return undefined; } @@ -607,6 +607,6 @@ export class ChatCommandCenterRendering extends Disposable implements IWorkbench agentService.onDidChangeAgents, chatQuotasService.onDidChangeQuotas, Event.filter(contextKeyService.onDidChangeContext, e => e.affectsSome(contextKeySet)) - )); + ))); } } diff --git a/src/vs/workbench/contrib/debug/common/debugContentProvider.ts b/src/vs/workbench/contrib/debug/common/debugContentProvider.ts index 935d2c3db2d8..5ad05e1cb7db 100644 --- a/src/vs/workbench/contrib/debug/common/debugContentProvider.ts +++ b/src/vs/workbench/contrib/debug/common/debugContentProvider.ts @@ -19,6 +19,7 @@ import { Range } from '../../../../editor/common/core/range.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { PLAINTEXT_LANGUAGE_ID } from '../../../../editor/common/languages/modesRegistry.js'; import { ErrorNoTelemetry } from '../../../../base/common/errors.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; /** * Debug URI format @@ -33,7 +34,7 @@ import { ErrorNoTelemetry } from '../../../../base/common/errors.js'; * the arbitrary_path and the session id are encoded with 'encodeURIComponent' * */ -export class DebugContentProvider implements IWorkbenchContribution, ITextModelContentProvider { +export class DebugContentProvider extends Disposable implements IWorkbenchContribution, ITextModelContentProvider { private static INSTANCE: DebugContentProvider; @@ -46,12 +47,14 @@ export class DebugContentProvider implements IWorkbenchContribution, ITextModelC @ILanguageService private readonly languageService: ILanguageService, @IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService ) { - textModelResolverService.registerTextModelContentProvider(DEBUG_SCHEME, this); + super(); + this._store.add(textModelResolverService.registerTextModelContentProvider(DEBUG_SCHEME, this)); DebugContentProvider.INSTANCE = this; } - dispose(): void { + override dispose(): void { this.pendingUpdates.forEach(cancellationSource => cancellationSource.dispose()); + super.dispose(); } provideTextContent(resource: uri): Promise | null { diff --git a/src/vs/workbench/contrib/performance/browser/performance.contribution.ts b/src/vs/workbench/contrib/performance/browser/performance.contribution.ts index a4b1a39c3e45..bd69320c3584 100644 --- a/src/vs/workbench/contrib/performance/browser/performance.contribution.ts +++ b/src/vs/workbench/contrib/performance/browser/performance.contribution.ts @@ -9,13 +9,15 @@ import { IInstantiationService, ServicesAccessor } from '../../../../platform/in import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; -import { Extensions, IWorkbenchContributionsRegistry, registerWorkbenchContribution2 } from '../../../common/contributions.js'; +import { Extensions, IWorkbenchContributionsRegistry, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { EditorExtensions, IEditorSerializer, IEditorFactoryRegistry } from '../../../common/editor.js'; import { PerfviewContrib, PerfviewInput } from './perfviewEditor.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { InstantiationService, Trace } from '../../../../platform/instantiation/common/instantiationService.js'; import { EventProfiling } from '../../../../base/common/event.js'; import { InputLatencyContrib } from './inputLatencyContrib.js'; +import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; +import { GCBasedDisposableTracker, setDisposableTracker } from '../../../../base/common/lifecycle.js'; // -- startup performance view @@ -136,3 +138,24 @@ Registry.as(Extensions.Workbench).registerWorkb InputLatencyContrib, LifecyclePhase.Eventually ); + + +// -- track leaking disposables, those that get GC'ed before having been disposed + +// this is currently disabled because there is too many leaks and some false positives, e.g disposables from registers +// like MenuRegistry, CommandsRegistery etc should be marked as singleton + +const _enableLeakDetection = false + // || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this + ; + +class DisposableTracking { + static readonly Id = 'perf.disposableTracking'; + constructor(@IEnvironmentService envService: IEnvironmentService) { + if (!envService.isBuilt && _enableLeakDetection) { + setDisposableTracker(new GCBasedDisposableTracker()); + } + } +} + +registerWorkbenchContribution2(DisposableTracking.Id, DisposableTracking, WorkbenchPhase.Eventually); diff --git a/src/vs/workbench/contrib/search/browser/searchTreeModel/fileMatch.ts b/src/vs/workbench/contrib/search/browser/searchTreeModel/fileMatch.ts index 1b5d2fa717bb..dfaf589263b1 100644 --- a/src/vs/workbench/contrib/search/browser/searchTreeModel/fileMatch.ts +++ b/src/vs/workbench/contrib/search/browser/searchTreeModel/fileMatch.ts @@ -5,7 +5,7 @@ import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { Lazy } from '../../../../../base/common/lazy.js'; -import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { themeColorFromId } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { TrackedRangeStickiness, MinimapPosition, ITextModel, FindMatch, IModelDeltaDecoration } from '../../../../../editor/common/model.js'; @@ -69,7 +69,7 @@ export class FileMatchImpl extends Disposable implements ISearchTreeFileMatch { protected _resource: URI; private _fileStat?: IFileStatWithPartialMetadata; private _model: ITextModel | null = null; - private _modelListener: IDisposable | null = null; + private _modelListener: DisposableStore | null = null; protected _textMatches: Map; private _removedTextMatches: Set; @@ -132,10 +132,11 @@ export class FileMatchImpl extends Disposable implements ISearchTreeFileMatch { } bindModel(model: ITextModel): void { this._model = model; - this._modelListener = this._model.onDidChangeContent(() => { + this._modelListener = new DisposableStore(); + this._modelListener.add(this._model.onDidChangeContent(() => { this._updateScheduler.schedule(); - }); - this._model.onWillDispose(() => this.onModelWillDispose()); + })); + this._modelListener.add(this._model.onWillDispose(() => this.onModelWillDispose())); this.updateHighlights(); } @@ -360,4 +361,3 @@ export class FileMatchImpl extends Disposable implements ISearchTreeFileMatch { //#endregion } - diff --git a/src/vs/workbench/services/notification/common/notificationService.ts b/src/vs/workbench/services/notification/common/notificationService.ts index 151edfccf9fb..f557bc098c13 100644 --- a/src/vs/workbench/services/notification/common/notificationService.ts +++ b/src/vs/workbench/services/notification/common/notificationService.ts @@ -272,7 +272,6 @@ export class NotificationService extends Disposable implements INotificationServ } prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle { - const toDispose = new DisposableStore(); // Handle neverShowAgain option accordingly if (options?.neverShowAgain) { @@ -300,6 +299,7 @@ export class NotificationService extends Disposable implements INotificationServ } let choiceClicked = false; + const toDispose = new DisposableStore(); // Convert choices into primary/secondary actions diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 872300d3937b..73b4c71daba1 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -356,7 +356,10 @@ export class ProgressService extends Disposable implements IProgressService { } // Clear upon dispose - Event.once(notification.onDidClose)(() => notificationDisposables.dispose()); + Event.once(notification.onDidClose)(() => { + notificationDisposables.dispose(); + dispose(windowProgressDisposable); + }); return notification; }; From 148a1ca4bd890aa4743327988070da0546180dd2 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 18 Dec 2024 17:41:28 +0100 Subject: [PATCH 0218/3587] TreeView: MaxCallStackError - Nesting (#236493) Part of #233056 --- src/vs/base/browser/ui/list/listView.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index e149029df4f5..00a19352e91a 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -9,7 +9,7 @@ import { DomEmitter } from '../../event.js'; import { IMouseWheelEvent } from '../../mouseEvent.js'; import { EventType as TouchEventType, Gesture, GestureEvent } from '../../touch.js'; import { SmoothScrollableElement } from '../scrollbar/scrollableElement.js'; -import { distinct, equals } from '../../../common/arrays.js'; +import { distinct, equals, splice } from '../../../common/arrays.js'; import { Delayer, disposableTimeout } from '../../../common/async.js'; import { memoize } from '../../../common/decorators.js'; import { Emitter, Event, IValueWithChangeEvent } from '../../../common/event.js'; @@ -643,7 +643,7 @@ export class ListView implements IListView { this.items = inserted; } else { this.rangeMap.splice(start, deleteCount, inserted); - deleted = this.items.splice(start, deleteCount, ...inserted); + deleted = splice(this.items, start, deleteCount, inserted); } const delta = elements.length - deleteCount; From 7d0efabd3b29e465be0c76c50ffedec391de3379 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 18 Dec 2024 17:47:19 +0100 Subject: [PATCH 0219/3587] Adds single observable logging (#236481) * Adds single observable logging * Improved observePosition * Fixes CI * Fixes tests --- src/vs/base/common/observableInternal/base.ts | 13 +++- .../base/common/observableInternal/derived.ts | 10 +++ .../observableInternal/lazyObservableValue.ts | 7 +- .../base/common/observableInternal/logging.ts | 69 +++++++++++++++---- src/vs/base/test/common/observable.test.ts | 10 +-- src/vs/editor/browser/observableCodeEditor.ts | 6 +- 6 files changed, 89 insertions(+), 26 deletions(-) diff --git a/src/vs/base/common/observableInternal/base.ts b/src/vs/base/common/observableInternal/base.ts index 3ce43ead2aea..c28e773b2688 100644 --- a/src/vs/base/common/observableInternal/base.ts +++ b/src/vs/base/common/observableInternal/base.ts @@ -6,7 +6,7 @@ import { DebugNameData, DebugOwner, getFunctionName } from './debugName.js'; import { DisposableStore, EqualityComparer, IDisposable, strictEquals } from './commonFacade/deps.js'; import type { derivedOpts } from './derived.js'; -import { getLogger } from './logging.js'; +import { getLogger, logObservable } from './logging.js'; import { keepObserved, recomputeInitiallyAndOnChange } from './utils.js'; /** @@ -67,6 +67,12 @@ export interface IObservable { flatten(this: IObservable>): IObservable; + /** + * ONLY FOR DEBUGGING! + * Logs computations of this derived. + */ + log(): IObservable; + /** * Makes sure this value is computed eagerly. */ @@ -233,6 +239,11 @@ export abstract class ConvenientObservable implements IObservable { + logObservable(this); + return this; + } + /** * @sealed * Converts an observable of an observable value into a direct observable of the value. diff --git a/src/vs/base/common/observableInternal/derived.ts b/src/vs/base/common/observableInternal/derived.ts index 80cc9a721247..ba018041799d 100644 --- a/src/vs/base/common/observableInternal/derived.ts +++ b/src/vs/base/common/observableInternal/derived.ts @@ -459,6 +459,16 @@ export class Derived extends BaseObservable im } super.removeObserver(observer); } + + public override log(): IObservable { + if (!getLogger()) { + super.log(); + getLogger()?.handleDerivedCreated(this); + } else { + super.log(); + } + return this; + } } diff --git a/src/vs/base/common/observableInternal/lazyObservableValue.ts b/src/vs/base/common/observableInternal/lazyObservableValue.ts index 8a3f63c05d7a..6c0f85aa8767 100644 --- a/src/vs/base/common/observableInternal/lazyObservableValue.ts +++ b/src/vs/base/common/observableInternal/lazyObservableValue.ts @@ -6,6 +6,7 @@ import { EqualityComparer } from './commonFacade/deps.js'; import { BaseObservable, IObserver, ISettableObservable, ITransaction, TransactionImpl } from './base.js'; import { DebugNameData } from './debugName.js'; +import { getLogger } from './logging.js'; /** * Holds off updating observers until the value is actually read. @@ -42,13 +43,15 @@ export class LazyObservableValue this._isUpToDate = true; if (this._deltas.length > 0) { - for (const observer of this.observers) { - for (const change of this._deltas) { + for (const change of this._deltas) { + getLogger()?.handleObservableChanged(this, { change, didChange: true, oldValue: '(unknown)', newValue: this._value, hadValue: true }); + for (const observer of this.observers) { observer.handleChange(this, change); } } this._deltas.length = 0; } else { + getLogger()?.handleObservableChanged(this, { change: undefined, didChange: true, oldValue: '(unknown)', newValue: this._value, hadValue: true }); for (const observer of this.observers) { observer.handleChange(this, undefined); } diff --git a/src/vs/base/common/observableInternal/logging.ts b/src/vs/base/common/observableInternal/logging.ts index 0de37a858cb7..0c343b548e47 100644 --- a/src/vs/base/common/observableInternal/logging.ts +++ b/src/vs/base/common/observableInternal/logging.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { AutorunObserver } from './autorun.js'; -import { IObservable, ObservableValue, TransactionImpl } from './base.js'; +import { IObservable, TransactionImpl } from './base.js'; import { Derived } from './derived.js'; import { FromEventObservable } from './utils.js'; @@ -18,6 +18,18 @@ export function getLogger(): IObservableLogger | undefined { return globalObservableLogger; } +export function logObservable(obs: IObservable): void { + if (!globalObservableLogger) { + const l = new ConsoleObservableLogger(); + l.addFilteredObj(obs); + setLogger(l); + } else { + if (globalObservableLogger instanceof ConsoleObservableLogger) { + (globalObservableLogger as ConsoleObservableLogger).addFilteredObj(obs); + } + } +} + interface IChangeInformation { oldValue: unknown; newValue: unknown; @@ -27,7 +39,7 @@ interface IChangeInformation { } export interface IObservableLogger { - handleObservableChanged(observable: ObservableValue, info: IChangeInformation): void; + handleObservableChanged(observable: IObservable, info: IChangeInformation): void; handleFromEventObservableTriggered(observable: FromEventObservable, info: IChangeInformation): void; handleAutorunCreated(autorun: AutorunObserver): void; @@ -44,6 +56,19 @@ export interface IObservableLogger { export class ConsoleObservableLogger implements IObservableLogger { private indentation = 0; + private _filteredObjects: Set | undefined; + + public addFilteredObj(obj: unknown): void { + if (!this._filteredObjects) { + this._filteredObjects = new Set(); + } + this._filteredObjects.add(obj); + } + + private _isIncluded(obj: unknown): boolean { + return this._filteredObjects?.has(obj) ?? true; + } + private textToConsoleArgs(text: ConsoleText): unknown[] { return consoleTextToArgs([ normalText(repeat('| ', this.indentation)), @@ -77,6 +102,7 @@ export class ConsoleObservableLogger implements IObservableLogger { } handleObservableChanged(observable: IObservable, info: IChangeInformation): void { + if (!this._isIncluded(observable)) { return; } console.log(...this.textToConsoleArgs([ formatKind('observable value changed'), styled(observable.debugName, { color: 'BlueViolet' }), @@ -130,7 +156,10 @@ export class ConsoleObservableLogger implements IObservableLogger { } handleDerivedRecomputed(derived: Derived, info: IChangeInformation): void { - const changedObservables = this.changedObservablesSets.get(derived)!; + if (!this._isIncluded(derived)) { return; } + + const changedObservables = this.changedObservablesSets.get(derived); + if (!changedObservables) { return; } console.log(...this.textToConsoleArgs([ formatKind('derived recomputed'), styled(derived.debugName, { color: 'BlueViolet' }), @@ -142,6 +171,8 @@ export class ConsoleObservableLogger implements IObservableLogger { } handleFromEventObservableTriggered(observable: FromEventObservable, info: IChangeInformation): void { + if (!this._isIncluded(observable)) { return; } + console.log(...this.textToConsoleArgs([ formatKind('observable from event triggered'), styled(observable.debugName, { color: 'BlueViolet' }), @@ -151,6 +182,8 @@ export class ConsoleObservableLogger implements IObservableLogger { } handleAutorunCreated(autorun: AutorunObserver): void { + if (!this._isIncluded(autorun)) { return; } + const existingHandleChange = autorun.handleChange; this.changedObservablesSets.set(autorun, new Set()); autorun.handleChange = (observable, change) => { @@ -160,13 +193,17 @@ export class ConsoleObservableLogger implements IObservableLogger { } handleAutorunTriggered(autorun: AutorunObserver): void { - const changedObservables = this.changedObservablesSets.get(autorun)!; - console.log(...this.textToConsoleArgs([ - formatKind('autorun'), - styled(autorun.debugName, { color: 'BlueViolet' }), - this.formatChanges(changedObservables), - { data: [{ fn: autorun._debugNameData.referenceFn ?? autorun._runFn }] } - ])); + const changedObservables = this.changedObservablesSets.get(autorun); + if (!changedObservables) { return; } + + if (this._isIncluded(autorun)) { + console.log(...this.textToConsoleArgs([ + formatKind('autorun'), + styled(autorun.debugName, { color: 'BlueViolet' }), + this.formatChanges(changedObservables), + { data: [{ fn: autorun._debugNameData.referenceFn ?? autorun._runFn }] } + ])); + } changedObservables.clear(); this.indentation++; } @@ -180,11 +217,13 @@ export class ConsoleObservableLogger implements IObservableLogger { if (transactionName === undefined) { transactionName = ''; } - console.log(...this.textToConsoleArgs([ - formatKind('transaction'), - styled(transactionName, { color: 'BlueViolet' }), - { data: [{ fn: transaction._fn }] } - ])); + if (this._isIncluded(transaction)) { + console.log(...this.textToConsoleArgs([ + formatKind('transaction'), + styled(transactionName, { color: 'BlueViolet' }), + { data: [{ fn: transaction._fn }] } + ])); + } this.indentation++; } diff --git a/src/vs/base/test/common/observable.test.ts b/src/vs/base/test/common/observable.test.ts index 2f8979243232..3d7ed472fcdb 100644 --- a/src/vs/base/test/common/observable.test.ts +++ b/src/vs/base/test/common/observable.test.ts @@ -1507,21 +1507,21 @@ export class LoggingObservableValue implements ISettableObservable { private value: T; - constructor(public readonly debugName: string, initialValue: T, private readonly log: Log) { + constructor(public readonly debugName: string, initialValue: T, private readonly logger: Log) { super(); this.value = initialValue; } protected override onFirstObserverAdded(): void { - this.log.log(`${this.debugName}.firstObserverAdded`); + this.logger.log(`${this.debugName}.firstObserverAdded`); } protected override onLastObserverRemoved(): void { - this.log.log(`${this.debugName}.lastObserverRemoved`); + this.logger.log(`${this.debugName}.lastObserverRemoved`); } public get(): T { - this.log.log(`${this.debugName}.get`); + this.logger.log(`${this.debugName}.get`); return this.value; } @@ -1537,7 +1537,7 @@ export class LoggingObservableValue return; } - this.log.log(`${this.debugName}.set (value ${value})`); + this.logger.log(`${this.debugName}.set (value ${value})`); this.value = value; diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts index 404f10dbe460..781a9895212f 100644 --- a/src/vs/editor/browser/observableCodeEditor.ts +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -266,13 +266,13 @@ export class ObservableCodeEditor extends Disposable { } public observePosition(position: IObservable, store: DisposableStore): IObservable { - const result = observableValueOpts({ owner: this, equalsFn: equalsIfDefined(Point.equals) }, new Point(0, 0)); + let pos = position.get(); + const result = observableValueOpts({ owner: this, debugName: () => `topLeftOfPosition${pos?.toString()}`, equalsFn: equalsIfDefined(Point.equals) }, new Point(0, 0)); const contentWidgetId = `observablePositionWidget` + (this._widgetCounter++); const domNode = document.createElement('div'); const w: IContentWidget = { getDomNode: () => domNode, getPosition: () => { - const pos = position.get(); return pos ? { preference: [ContentWidgetPositionPreference.EXACT], position: position.get() } : null; }, getId: () => contentWidgetId, @@ -283,7 +283,7 @@ export class ObservableCodeEditor extends Disposable { }; this.editor.addContentWidget(w); store.add(autorun(reader => { - position.read(reader); + pos = position.read(reader); this.editor.layoutContentWidget(w); })); store.add(toDisposable(() => { From aaa5982ec9d269d9c63d0a5e762c7c307b8e5033 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 18 Dec 2024 09:50:52 -0800 Subject: [PATCH 0220/3587] debug: fix underflow/overflow in breakpoint edit widget (#236428) Fixes #233819 --- src/vs/editor/contrib/hover/browser/glyphHoverWidget.ts | 1 + .../workbench/contrib/debug/browser/media/breakpointWidget.css | 1 + 2 files changed, 2 insertions(+) diff --git a/src/vs/editor/contrib/hover/browser/glyphHoverWidget.ts b/src/vs/editor/contrib/hover/browser/glyphHoverWidget.ts index a6a120b040ab..d40ff7189c58 100644 --- a/src/vs/editor/contrib/hover/browser/glyphHoverWidget.ts +++ b/src/vs/editor/contrib/hover/browser/glyphHoverWidget.ts @@ -180,6 +180,7 @@ export class GlyphHoverWidget extends Disposable implements IOverlayWidget, IHov const left = editorLayout.glyphMarginLeft + editorLayout.glyphMarginWidth + (laneOrLine === 'lineNo' ? editorLayout.lineNumbersWidth : 0); this._hover.containerDomNode.style.left = `${left}px`; this._hover.containerDomNode.style.top = `${Math.max(Math.round(top), 0)}px`; + this._hover.containerDomNode.style.zIndex = '11'; // 1 more than the zone widget at 10 (#233819) } private _onMouseLeave(e: MouseEvent): void { diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpointWidget.css b/src/vs/workbench/contrib/debug/browser/media/breakpointWidget.css index 4bbf07f03c77..27b11b0a3923 100644 --- a/src/vs/workbench/contrib/debug/browser/media/breakpointWidget.css +++ b/src/vs/workbench/contrib/debug/browser/media/breakpointWidget.css @@ -6,6 +6,7 @@ .monaco-editor .zone-widget .zone-widget-container.breakpoint-widget { display: flex; border-color: #007ACC; + background: var(--vscode-editor-background); .breakpoint-select-container { display: flex; From 4fcae8834d70e4beadbeceeedd62d1e12484fe3e Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 18 Dec 2024 12:38:58 -0600 Subject: [PATCH 0221/3587] do not show no suggestions widget unless it was explicitly invoked (#236505) --- .../browser/terminal.suggest.contribution.ts | 2 +- .../suggest/browser/terminalSuggestAddon.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 82b9dc7d5149..96676a5a12c1 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -192,7 +192,7 @@ registerActiveInstanceAction({ weight: KeybindingWeight.WorkbenchContrib + 1, when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.terminalShellIntegrationEnabled, ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.Enabled}`, true)) }, - run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.requestCompletions() + run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.requestCompletions(true) }); registerActiveInstanceAction({ diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 331d36a2e58a..9f173cfc15a6 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -129,7 +129,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest })); } - private async _handleCompletionProviders(terminal: Terminal | undefined, token: CancellationToken, triggerCharacter?: boolean): Promise { + private async _handleCompletionProviders(terminal: Terminal | undefined, token: CancellationToken, explicitlyInvoked?: boolean): Promise { // Nothing to handle if the terminal is not attached if (!terminal?.element || !this._enableWidget || !this._promptInputModel) { return; @@ -156,7 +156,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest await this._extensionService.activateByEvent('onTerminalCompletionsRequested'); } - const providedCompletions = await this._terminalCompletionService.provideCompletions(this._promptInputModel.prefix, this._promptInputModel.cursorIndex, this._shellType, token, triggerCharacter, doNotRequestExtensionCompletions); + const providedCompletions = await this._terminalCompletionService.provideCompletions(this._promptInputModel.prefix, this._promptInputModel.cursorIndex, this._shellType, token, doNotRequestExtensionCompletions); if (!providedCompletions?.length || token.isCancellationRequested) { return; } @@ -220,7 +220,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest if (token.isCancellationRequested) { return; } - this._showCompletions(model); + this._showCompletions(model, explicitlyInvoked); } setContainerWithOverflow(container: HTMLElement): void { @@ -231,7 +231,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._screen = screen; } - async requestCompletions(triggerCharacter?: boolean): Promise { + async requestCompletions(explicitlyInvoked?: boolean): Promise { if (!this._promptInputModel) { return; } @@ -245,7 +245,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } this._cancellationTokenSource = new CancellationTokenSource(); const token = this._cancellationTokenSource.token; - await this._handleCompletionProviders(this._terminal, token, triggerCharacter); + await this._handleCompletionProviders(this._terminal, token, explicitlyInvoked); } private _sync(promptInputState: IPromptInputModelState): void { @@ -292,7 +292,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } for (const char of provider.triggerCharacters) { if (prefix?.endsWith(char)) { - this.requestCompletions(true); + this.requestCompletions(); sent = true; break; } @@ -359,13 +359,13 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest }; } - private _showCompletions(model: SimpleCompletionModel): void { + private _showCompletions(model: SimpleCompletionModel, explicitlyInvoked?: boolean): void { if (!this._terminal?.element) { return; } const suggestWidget = this._ensureSuggestWidget(this._terminal); suggestWidget.setCompletionModel(model); - if (!this._promptInputModel) { + if (!this._promptInputModel || !explicitlyInvoked && model.items.length === 0) { return; } this._model = model; From 4c3f5de78914fd38b4e3c3d46ccd7f777e233c39 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 18 Dec 2024 10:40:34 -0800 Subject: [PATCH 0222/3587] debug: make ctrl+c copy value(s) in debug views (#236501) Not sure how this ever worked, but it was reported as a bug, and this makes it work. Fixes #232767 --- .../debug/browser/debug.contribution.ts | 12 +++++++ .../contrib/debug/browser/variablesView.ts | 35 ++++++++++++++++--- .../debug/browser/watchExpressionsView.ts | 8 +++-- .../workbench/contrib/debug/common/debug.ts | 5 +++ 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 7c501db354ff..6033b0cf9b53 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -63,6 +63,8 @@ import { ReplAccessibilityHelp } from './replAccessibilityHelp.js'; import { ReplAccessibilityAnnouncer } from '../common/replAccessibilityAnnouncer.js'; import { RunAndDebugAccessibilityHelp } from './runAndDebugAccessibilityHelp.js'; import { DebugWatchAccessibilityAnnouncer } from '../common/debugAccessibilityAnnouncer.js'; +import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { FocusedViewContext } from '../../../common/contextkeys.js'; const debugCategory = nls.localize('debugCategory', "Debug"); registerColors(); @@ -206,6 +208,16 @@ registerDebugViewMenuItem(MenuId.DebugWatchContext, REMOVE_WATCH_EXPRESSIONS_COM registerDebugViewMenuItem(MenuId.NotebookVariablesContext, COPY_NOTEBOOK_VARIABLE_VALUE_ID, COPY_NOTEBOOK_VARIABLE_VALUE_LABEL, 20, CONTEXT_VARIABLE_VALUE); +KeybindingsRegistry.registerKeybindingRule({ + id: COPY_VALUE_ID, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.or( + FocusedViewContext.isEqualTo(WATCH_VIEW_ID), + FocusedViewContext.isEqualTo(VARIABLES_VIEW_ID), + ), + primary: KeyMod.CtrlCmd | KeyCode.KeyC +}); + // Touch Bar if (isMacintosh) { diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 222e0a7b57b6..16d53c4f87ee 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -40,8 +40,9 @@ import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.j import { IViewDescriptorService } from '../../../common/views.js'; import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; +import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; -import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLES_FOCUSED, DataBreakpointSetType, DebugVisualizationType, IDataBreakpointInfoResponse, IDebugService, IExpression, IScope, IStackFrame, IViewModel, VARIABLES_VIEW_ID } from '../common/debug.js'; +import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLES_FOCUSED, DataBreakpointSetType, DebugVisualizationType, IDataBreakpointInfoResponse, IDebugService, IDebugViewWithVariables, IExpression, IScope, IStackFrame, IViewModel, VARIABLES_VIEW_ID, WATCH_VIEW_ID } from '../common/debug.js'; import { getContextForVariable } from '../common/debugContext.js'; import { ErrorScope, Expression, Scope, StackFrame, Variable, VisualizedExpression, getUriForDebugMemory } from '../common/debugModel.js'; import { DebugVisualizer, IDebugVisualizerService } from '../common/debugVisualizers.js'; @@ -61,7 +62,7 @@ interface IVariablesContext { variable: DebugProtocol.Variable; } -export class VariablesView extends ViewPane { +export class VariablesView extends ViewPane implements IDebugViewWithVariables { private updateTreeScheduler: RunOnceScheduler; private needsRefresh = false; @@ -69,6 +70,10 @@ export class VariablesView extends ViewPane { private savedViewState = new Map(); private autoExpandedScopes = new Set(); + public get treeSelection() { + return this.tree.getSelection(); + } + constructor( options: IViewletViewOptions, @IContextMenuService contextMenuService: IContextMenuService, @@ -653,12 +658,34 @@ CommandsRegistry.registerCommand({ description: COPY_VALUE_LABEL, }, id: COPY_VALUE_ID, - handler: async (accessor: ServicesAccessor, arg: Variable | Expression | IVariablesContext, ctx?: (Variable | Expression)[]) => { + handler: async (accessor: ServicesAccessor, arg: Variable | Expression | IVariablesContext | undefined, ctx?: (Variable | Expression)[]) => { + if (!arg) { + const viewService = accessor.get(IViewsService); + const view = viewService.getActiveViewWithId(WATCH_VIEW_ID) || viewService.getActiveViewWithId(VARIABLES_VIEW_ID); + if (view) { + + } + } const debugService = accessor.get(IDebugService); const clipboardService = accessor.get(IClipboardService); let elementContext = ''; let elements: (Variable | Expression)[]; - if (arg instanceof Variable || arg instanceof Expression) { + if (!arg) { + const viewService = accessor.get(IViewsService); + const focusedView = viewService.getFocusedView(); + let view: IDebugViewWithVariables | null | undefined; + if (focusedView?.id === WATCH_VIEW_ID) { + view = viewService.getActiveViewWithId(WATCH_VIEW_ID); + elementContext = 'watch'; + } else if (focusedView?.id === VARIABLES_VIEW_ID) { + view = viewService.getActiveViewWithId(VARIABLES_VIEW_ID); + elementContext = 'variables'; + } + if (!view) { + return; + } + elements = view.treeSelection.filter(e => e instanceof Expression || e instanceof Variable); + } else if (arg instanceof Variable || arg instanceof Expression) { elementContext = 'watch'; elements = ctx ? ctx : []; } else { diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index b1b75a73bc0f..b325c138b0a3 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -29,7 +29,7 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { ViewAction, ViewPane } from '../../../browser/parts/views/viewPane.js'; import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; import { IViewDescriptorService } from '../../../common/views.js'; -import { CONTEXT_CAN_VIEW_MEMORY, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_WATCH_EXPRESSIONS_EXIST, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_WATCH_ITEM_TYPE, IDebugConfiguration, IDebugService, IExpression, WATCH_VIEW_ID } from '../common/debug.js'; +import { CONTEXT_CAN_VIEW_MEMORY, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_WATCH_EXPRESSIONS_EXIST, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_WATCH_ITEM_TYPE, IDebugConfiguration, IDebugService, IDebugViewWithVariables, IExpression, WATCH_VIEW_ID } from '../common/debug.js'; import { Expression, Variable, VisualizedExpression } from '../common/debugModel.js'; import { AbstractExpressionDataSource, AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderViewTree } from './baseDebugView.js'; import { DebugExpressionRenderer } from './debugExpressionRenderer.js'; @@ -40,7 +40,7 @@ const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; let ignoreViewUpdates = false; let useCachedEvaluation = false; -export class WatchExpressionsView extends ViewPane { +export class WatchExpressionsView extends ViewPane implements IDebugViewWithVariables { private watchExpressionsUpdatedScheduler: RunOnceScheduler; private needsRefresh = false; @@ -51,6 +51,10 @@ export class WatchExpressionsView extends ViewPane { private menu: IMenu; private expressionRenderer: DebugExpressionRenderer; + public get treeSelection() { + return this.tree.getSelection(); + } + constructor( options: IViewletViewOptions, @IContextMenuService contextMenuService: IContextMenuService, diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index d5387b10408e..c05015544729 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -29,6 +29,7 @@ import { Source } from './debugSource.js'; import { ITaskIdentifier } from '../../tasks/common/tasks.js'; import { LiveTestResult } from '../../testing/common/testResult.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IView } from '../../../common/views.js'; export const VIEWLET_ID = 'workbench.view.debug'; @@ -115,6 +116,10 @@ export const INTERNAL_CONSOLE_OPTIONS_SCHEMA = { description: nls.localize('internalConsoleOptions', "Controls when the internal Debug Console should open.") }; +export interface IDebugViewWithVariables extends IView { + readonly treeSelection: IExpression[]; +} + // raw export interface IRawModelUpdate { From d6d5ebe048e65abd1377b76bdb21935c9bd5d283 Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Wed, 18 Dec 2024 10:49:25 -0800 Subject: [PATCH 0223/3587] Splitting cells should maintain the current edit state (#236507) * split cell command should be in editing mode * stay in preview if splitting from that state * writing the PR description made me realize that this was way simpler --- .../notebook/browser/contrib/cellCommands/cellCommands.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts index 261c80215623..2abc28493e91 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts @@ -205,6 +205,8 @@ registerAction2(class extends NotebookCellAction { ], { quotableLabel: 'Split Notebook Cell' } ); + + context.notebookEditor.cellAt(index + 1)?.updateEditState(cell.getEditState(), 'splitCell'); } } } From 91581cab6f837fb31161322a910f4c414aa40300 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 18 Dec 2024 12:53:00 -0600 Subject: [PATCH 0224/3587] focus debug console when it becomes visible (#236502) fix #236499 --- src/vs/workbench/contrib/debug/browser/repl.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index d09086152d66..e9ffbe5a57c2 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -214,6 +214,7 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this.tree?.updateChildren(undefined, true, false); this.onDidStyleChange(); } + this.focus(); } })); this._register(this.configurationService.onDidChangeConfiguration(e => { From 7f9c7a41873b88861744d06aed33d2dbcaa3a92e Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 18 Dec 2024 11:03:57 -0800 Subject: [PATCH 0225/3587] testing: avoid profiles dropdown when there's a single profile (#236509) Fixes #232767 --- .../testing/browser/testingExplorerView.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 5e84db11b6af..dea0b909d049 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -346,6 +346,7 @@ export class TestingExplorerView extends ViewPane { const profileActions: IAction[] = []; let participatingGroups = 0; + let participatingProfiles = 0; let hasConfigurable = false; const defaults = this.testProfileService.getGroupDefaultProfiles(group); for (const { profiles, controller } of this.testProfileService.all()) { @@ -363,6 +364,7 @@ export class TestingExplorerView extends ViewPane { } hasConfigurable = hasConfigurable || profile.hasConfigurationHandler; + participatingProfiles++; profileActions.push(new Action( `${controller.id}.${profile.profileId}`, defaults.includes(profile) ? localize('defaultTestProfile', '{0} (Default)', profile.label) : profile.label, @@ -402,7 +404,7 @@ export class TestingExplorerView extends ViewPane { const menuActions = getFlatContextMenuActions(menu); const postActions: IAction[] = []; - if (profileActions.length > 1) { + if (participatingProfiles > 1) { postActions.push(new Action( 'selectDefaultTestConfigurations', localize('selectDefaultConfigs', 'Select Default Profile'), @@ -423,9 +425,12 @@ export class TestingExplorerView extends ViewPane { } // show menu actions if there are any otherwise don't - return menuActions.length > 0 - ? Separator.join(profileActions, menuActions, postActions) - : Separator.join(profileActions, postActions); + return { + numberOfProfiles: participatingProfiles, + actions: menuActions.length > 0 + ? Separator.join(profileActions, menuActions, postActions) + : Separator.join(profileActions, postActions), + }; } /** @@ -438,7 +443,7 @@ export class TestingExplorerView extends ViewPane { private getRunGroupDropdown(group: TestRunProfileBitset, defaultAction: IAction, options: IActionViewItemOptions) { const dropdownActions = this.getTestConfigGroupActions(group); - if (dropdownActions.length < 2) { + if (dropdownActions.numberOfProfiles < 2) { return super.getActionViewItem(defaultAction, options); } @@ -452,7 +457,7 @@ export class TestingExplorerView extends ViewPane { return this.instantiationService.createInstance( DropdownWithPrimaryActionViewItem, - primaryAction, this.getDropdownAction(), dropdownActions, + primaryAction, this.getDropdownAction(), dropdownActions.actions, '', options ); From 9fc5861de2162b2f6ab6f316c655679fdaf61eb0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:51:32 -0800 Subject: [PATCH 0226/3587] Speculative fix when pwsh is 'powershell' on mac/linux Fixes #219583 --- src/vs/platform/shell/node/shellEnv.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/shell/node/shellEnv.ts b/src/vs/platform/shell/node/shellEnv.ts index 2d3ef3ead0d4..31a1ebe1ecf1 100644 --- a/src/vs/platform/shell/node/shellEnv.ts +++ b/src/vs/platform/shell/node/shellEnv.ts @@ -129,7 +129,7 @@ async function doResolveUnixShellEnv(logService: ILogService, token: Cancellatio const name = basename(systemShellUnix); let command: string, shellArgs: Array; const extraArgs = ''; - if (/^pwsh(-preview)?$/.test(name)) { + if (/^(?:pwsh(?:-preview)|powershell)$/.test(name)) { // Older versions of PowerShell removes double quotes sometimes so we use "double single quotes" which is how // you escape single quotes inside of a single quoted string. command = `& '${process.execPath}' ${extraArgs} -p '''${mark}'' + JSON.stringify(process.env) + ''${mark}'''`; From c88542ef6216f589c6675b2aac96447b284708ef Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 18 Dec 2024 21:21:30 +0100 Subject: [PATCH 0227/3587] Fix navigation to single match in tree find (#236515) fixes #236478 --- src/vs/base/browser/ui/tree/abstractTree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 38570ae882de..b8144e58c23f 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -1177,7 +1177,7 @@ export class FindController extends AbstractFindController this.shouldAllowFocus(node)); + this.tree.focusNext(0, true, undefined, (node) => !FuzzyScore.isDefault(node.filterData as any as FuzzyScore)); } const focus = this.tree.getFocus(); From 1147139fbb8174119528d71a99cb9a0688170999 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Wed, 18 Dec 2024 12:27:49 -0800 Subject: [PATCH 0228/3587] fix: copy from context menu for chat references does not work (#236518) --- .../chatReferencesContentPart.ts | 30 +++++++++++++++++-- .../files/browser/fileActions.contribution.ts | 2 +- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts index 3b71a48f0cc9..a06c21d966b0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -19,7 +19,8 @@ import { localize, localize2 } from '../../../../../nls.js'; import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; import { Action2, IMenuService, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; +import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { FileKind } from '../../../../../platform/files/common/files.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -476,7 +477,7 @@ registerAction2(class AddToChatAction extends Action2 { id: MenuId.ChatAttachmentsContext, group: 'chat', order: 1, - when: ExplorerFolderContext.negate(), + when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResource, ExplorerFolderContext.negate()), }] }); } @@ -498,4 +499,29 @@ registerAction2(class AddToChatAction extends Action2 { } }); +registerAction2(class OpenChatReferenceLinkAction extends Action2 { + + static readonly id = 'workbench.action.chat.copyLink'; + + constructor() { + super({ + id: OpenChatReferenceLinkAction.id, + title: { + ...localize2('copyLink', "Copy Link"), + }, + f1: false, + menu: [{ + id: MenuId.ChatAttachmentsContext, + group: 'chat', + order: 0, + when: ContextKeyExpr.or(ResourceContextKey.Scheme.isEqualTo(Schemas.http), ResourceContextKey.Scheme.isEqualTo(Schemas.https)), + }] + }); + } + + override async run(accessor: ServicesAccessor, resource: URI): Promise { + await accessor.get(IClipboardService).writeResources([resource]); + } +}); + //#endregion diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 9da55d34ce37..be7dddf26ff9 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -770,7 +770,7 @@ MenuRegistry.appendMenuItem(MenuId.ChatAttachmentsContext, { group: 'navigation', order: 10, command: openToSideCommand, - when: ContextKeyExpr.and(ResourceContextKey.HasResource, ExplorerFolderContext.toNegated()) + when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResource, ExplorerFolderContext.toNegated()) }); MenuRegistry.appendMenuItem(MenuId.ChatAttachmentsContext, { From e6e5856995525c808ad52547beb8fe19802207ea Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 18 Dec 2024 12:39:57 -0800 Subject: [PATCH 0229/3587] debug: fix unexpected behaviors with duplicate `name`s in `launch.json` (#236513) Suffix duplicated launch configs with a config at the time they're read. In debug we assume the names are unique, so this should fix #231377 and probably other hidden issues as well. --- .../browser/debugConfigurationManager.ts | 61 ++++++++++++------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index 0cc18f401f1a..89edaf1e668f 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -10,7 +10,6 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import * as json from '../../../../base/common/json.js'; import { IJSONSchema } from '../../../../base/common/jsonSchema.js'; import { DisposableStore, IDisposable, dispose } from '../../../../base/common/lifecycle.js'; -import * as objects from '../../../../base/common/objects.js'; import * as resources from '../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI as uri } from '../../../../base/common/uri.js'; @@ -27,16 +26,16 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; import { IEditorPane } from '../../../common/editor.js'; -import { debugConfigure } from './debugIcons.js'; -import { CONTEXT_DEBUG_CONFIGURATION_TYPE, DebugConfigurationProviderTriggerKind, IAdapterManager, ICompound, IConfig, IConfigPresentation, IConfigurationManager, IDebugConfigurationProvider, IGlobalConfig, IGuessedDebugger, ILaunch } from '../common/debug.js'; -import { launchSchema } from '../common/debugSchemas.js'; -import { getVisibleAndSorted } from '../common/debugUtils.js'; import { launchSchemaId } from '../../../services/configuration/common/configuration.js'; import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IHistoryService } from '../../../services/history/common/history.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; +import { CONTEXT_DEBUG_CONFIGURATION_TYPE, DebugConfigurationProviderTriggerKind, IAdapterManager, ICompound, IConfig, IConfigPresentation, IConfigurationManager, IDebugConfigurationProvider, IGlobalConfig, IGuessedDebugger, ILaunch } from '../common/debug.js'; +import { launchSchema } from '../common/debugSchemas.js'; +import { getVisibleAndSorted } from '../common/debugUtils.js'; +import { debugConfigure } from './debugIcons.js'; const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); jsonRegistry.registerSchema(launchSchemaId, launchSchema); @@ -509,7 +508,7 @@ abstract class AbstractLaunch implements ILaunch { ) { } getCompound(name: string): ICompound | undefined { - const config = this.getConfig(); + const config = this.getDeduplicatedConfig(); if (!config || !config.compounds) { return undefined; } @@ -518,7 +517,7 @@ abstract class AbstractLaunch implements ILaunch { } getConfigurationNames(ignoreCompoundsAndPresentation = false): string[] { - const config = this.getConfig(); + const config = this.getDeduplicatedConfig(); if (!config || (!Array.isArray(config.configurations) && !Array.isArray(config.compounds))) { return []; } else { @@ -540,21 +539,22 @@ abstract class AbstractLaunch implements ILaunch { getConfiguration(name: string): IConfig | undefined { // We need to clone the configuration in order to be able to make changes to it #42198 - const config = objects.deepClone(this.getConfig()); + const config = this.getDeduplicatedConfig(); if (!config || !config.configurations) { return undefined; } const configuration = config.configurations.find(config => config && config.name === name); - if (configuration) { - if (this instanceof UserLaunch) { - configuration.__configurationTarget = ConfigurationTarget.USER; - } else if (this instanceof WorkspaceLaunch) { - configuration.__configurationTarget = ConfigurationTarget.WORKSPACE; - } else { - configuration.__configurationTarget = ConfigurationTarget.WORKSPACE_FOLDER; - } + if (!configuration) { + return; + } + + if (this instanceof UserLaunch) { + return { ...configuration, __configurationTarget: ConfigurationTarget.USER }; + } else if (this instanceof WorkspaceLaunch) { + return { ...configuration, __configurationTarget: ConfigurationTarget.WORKSPACE }; + } else { + return { ...configuration, __configurationTarget: ConfigurationTarget.WORKSPACE_FOLDER }; } - return configuration; } async getInitialConfigurationContent(folderUri?: uri, type?: string, useInitialConfigs?: boolean, token?: CancellationToken): Promise { @@ -575,9 +575,28 @@ abstract class AbstractLaunch implements ILaunch { return content; } + get hidden(): boolean { return false; } + + private getDeduplicatedConfig(): IGlobalConfig | undefined { + const original = this.getConfig(); + return original && { + version: original.version, + compounds: original.compounds && distinguishConfigsByName(original.compounds), + configurations: original.configurations && distinguishConfigsByName(original.configurations), + }; + } +} + +function distinguishConfigsByName(things: readonly T[]): T[] { + const seen = new Map(); + return things.map(thing => { + const no = seen.get(thing.name) || 0; + seen.set(thing.name, no + 1); + return no === 0 ? thing : { ...thing, name: `${thing.name} (${no})` }; + }); } class Launch extends AbstractLaunch implements ILaunch { @@ -655,11 +674,9 @@ class Launch extends AbstractLaunch implements ILaunch { } async writeConfiguration(configuration: IConfig): Promise { - const fullConfig = objects.deepClone(this.getConfig()!); - if (!fullConfig.configurations) { - fullConfig.configurations = []; - } - fullConfig.configurations.push(configuration); + // note: we don't get the deduplicated config since we don't want that to 'leak' into the file + const fullConfig: Partial = this.getConfig() || {}; + fullConfig.configurations = [...fullConfig.configurations || [], configuration]; await this.configurationService.updateValue('launch', fullConfig, { resource: this.workspace.uri }, ConfigurationTarget.WORKSPACE_FOLDER); } } From 3fd6eef7b9683b6581c0111fd0304ca8e1423035 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 18 Dec 2024 13:04:11 -0800 Subject: [PATCH 0230/3587] fix: workbench.debug.action.focusRepl resolving before focus is given (#236520) Note: I intentionally did not keep this registered under the command palette because there is a similar duplicate command provided automatically from the views service. Fixes #228852 --- .../debug/browser/debug.contribution.ts | 3 +-- .../contrib/debug/browser/debugCommands.ts | 8 ------- .../workbench/contrib/debug/browser/repl.ts | 24 ++++++++++++++++--- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 6033b0cf9b53..fe281b6c1b14 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -31,7 +31,7 @@ import { CallStackEditorContribution } from './callStackEditorContribution.js'; import { CallStackView } from './callStackView.js'; import { ReplAccessibleView } from './replAccessibleView.js'; import { registerColors } from './debugColors.js'; -import { ADD_CONFIGURATION_ID, ADD_TO_WATCH_ID, ADD_TO_WATCH_LABEL, CALLSTACK_BOTTOM_ID, CALLSTACK_BOTTOM_LABEL, CALLSTACK_DOWN_ID, CALLSTACK_DOWN_LABEL, CALLSTACK_TOP_ID, CALLSTACK_TOP_LABEL, CALLSTACK_UP_ID, CALLSTACK_UP_LABEL, CONTINUE_ID, CONTINUE_LABEL, COPY_EVALUATE_PATH_ID, COPY_EVALUATE_PATH_LABEL, COPY_STACK_TRACE_ID, COPY_VALUE_ID, COPY_VALUE_LABEL, DEBUG_COMMAND_CATEGORY, DEBUG_CONSOLE_QUICK_ACCESS_PREFIX, DEBUG_QUICK_ACCESS_PREFIX, DEBUG_RUN_COMMAND_ID, DEBUG_RUN_LABEL, DEBUG_START_COMMAND_ID, DEBUG_START_LABEL, DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, EDIT_EXPRESSION_COMMAND_ID, FOCUS_REPL_ID, JUMP_TO_CURSOR_ID, NEXT_DEBUG_CONSOLE_ID, NEXT_DEBUG_CONSOLE_LABEL, OPEN_LOADED_SCRIPTS_LABEL, PAUSE_ID, PAUSE_LABEL, PREV_DEBUG_CONSOLE_ID, PREV_DEBUG_CONSOLE_LABEL, REMOVE_EXPRESSION_COMMAND_ID, RESTART_FRAME_ID, RESTART_LABEL, RESTART_SESSION_ID, SELECT_AND_START_ID, SELECT_AND_START_LABEL, SELECT_DEBUG_CONSOLE_ID, SELECT_DEBUG_CONSOLE_LABEL, SELECT_DEBUG_SESSION_ID, SELECT_DEBUG_SESSION_LABEL, SET_EXPRESSION_COMMAND_ID, SHOW_LOADED_SCRIPTS_ID, STEP_INTO_ID, STEP_INTO_LABEL, STEP_INTO_TARGET_ID, STEP_INTO_TARGET_LABEL, STEP_OUT_ID, STEP_OUT_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STOP_ID, STOP_LABEL, TERMINATE_THREAD_ID, TOGGLE_INLINE_BREAKPOINT_ID } from './debugCommands.js'; +import { ADD_CONFIGURATION_ID, ADD_TO_WATCH_ID, ADD_TO_WATCH_LABEL, CALLSTACK_BOTTOM_ID, CALLSTACK_BOTTOM_LABEL, CALLSTACK_DOWN_ID, CALLSTACK_DOWN_LABEL, CALLSTACK_TOP_ID, CALLSTACK_TOP_LABEL, CALLSTACK_UP_ID, CALLSTACK_UP_LABEL, CONTINUE_ID, CONTINUE_LABEL, COPY_EVALUATE_PATH_ID, COPY_EVALUATE_PATH_LABEL, COPY_STACK_TRACE_ID, COPY_VALUE_ID, COPY_VALUE_LABEL, DEBUG_COMMAND_CATEGORY, DEBUG_CONSOLE_QUICK_ACCESS_PREFIX, DEBUG_QUICK_ACCESS_PREFIX, DEBUG_RUN_COMMAND_ID, DEBUG_RUN_LABEL, DEBUG_START_COMMAND_ID, DEBUG_START_LABEL, DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, EDIT_EXPRESSION_COMMAND_ID, JUMP_TO_CURSOR_ID, NEXT_DEBUG_CONSOLE_ID, NEXT_DEBUG_CONSOLE_LABEL, OPEN_LOADED_SCRIPTS_LABEL, PAUSE_ID, PAUSE_LABEL, PREV_DEBUG_CONSOLE_ID, PREV_DEBUG_CONSOLE_LABEL, REMOVE_EXPRESSION_COMMAND_ID, RESTART_FRAME_ID, RESTART_LABEL, RESTART_SESSION_ID, SELECT_AND_START_ID, SELECT_AND_START_LABEL, SELECT_DEBUG_CONSOLE_ID, SELECT_DEBUG_CONSOLE_LABEL, SELECT_DEBUG_SESSION_ID, SELECT_DEBUG_SESSION_LABEL, SET_EXPRESSION_COMMAND_ID, SHOW_LOADED_SCRIPTS_ID, STEP_INTO_ID, STEP_INTO_LABEL, STEP_INTO_TARGET_ID, STEP_INTO_TARGET_LABEL, STEP_OUT_ID, STEP_OUT_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STOP_ID, STOP_LABEL, TERMINATE_THREAD_ID, TOGGLE_INLINE_BREAKPOINT_ID } from './debugCommands.js'; import { DebugConsoleQuickAccess } from './debugConsoleQuickAccess.js'; import { RunToCursorAction, SelectionToReplAction, SelectionToWatchExpressionsAction } from './debugEditorActions.js'; import { DebugEditorContribution } from './debugEditorContribution.js'; @@ -133,7 +133,6 @@ registerDebugCommandPaletteItem(DISCONNECT_ID, DISCONNECT_LABEL, CONTEXT_IN_DEBU registerDebugCommandPaletteItem(DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL, CONTEXT_IN_DEBUG_MODE, ContextKeyExpr.or(CONTEXT_FOCUSED_SESSION_IS_ATTACH, ContextKeyExpr.and(CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED))); registerDebugCommandPaletteItem(STOP_ID, STOP_LABEL, CONTEXT_IN_DEBUG_MODE, ContextKeyExpr.or(CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED)); registerDebugCommandPaletteItem(CONTINUE_ID, CONTINUE_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugCommandPaletteItem(FOCUS_REPL_ID, nls.localize2({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugFocusConsole' }, "Focus on Debug Console View")); registerDebugCommandPaletteItem(JUMP_TO_CURSOR_ID, nls.localize2('jumpToCursor', "Jump to Cursor"), CONTEXT_JUMP_TO_CURSOR_SUPPORTED); registerDebugCommandPaletteItem(JUMP_TO_CURSOR_ID, nls.localize2('SetNextStatement', "Set Next Statement"), CONTEXT_JUMP_TO_CURSOR_SUPPORTED); registerDebugCommandPaletteItem(RunToCursorAction.ID, RunToCursorAction.LABEL, CONTEXT_DEBUGGERS_AVAILABLE); diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index c6d45bda9626..2d3cb0c46c83 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -679,14 +679,6 @@ CommandsRegistry.registerCommand({ } }); -CommandsRegistry.registerCommand({ - id: FOCUS_REPL_ID, - handler: async (accessor) => { - const viewsService = accessor.get(IViewsService); - await viewsService.openView(REPL_VIEW_ID, true); - } -}); - CommandsRegistry.registerCommand({ id: 'debug.startFromConfig', handler: async (accessor, config: IConfig) => { diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index e9ffbe5a57c2..295049420458 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -11,7 +11,7 @@ import * as aria from '../../../../base/browser/ui/aria/aria.js'; import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from '../../../../base/browser/ui/mouseCursor/mouseCursor.js'; import { IAsyncDataSource, ITreeContextMenuEvent, ITreeNode } from '../../../../base/browser/ui/tree/tree.js'; import { IAction } from '../../../../base/common/actions.js'; -import { RunOnceScheduler } from '../../../../base/common/async.js'; +import { RunOnceScheduler, timeout } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { memoize } from '../../../../base/common/decorators.js'; @@ -71,6 +71,7 @@ import { CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_REPL, CONTEXT_MULTI_SESSION_REPL, import { Variable } from '../common/debugModel.js'; import { ReplEvaluationResult, ReplGroup } from '../common/replModel.js'; import { FocusSessionActionViewItem } from './debugActionViewItems.js'; +import { DEBUG_COMMAND_CATEGORY, FOCUS_REPL_ID } from './debugCommands.js'; import { DebugExpressionRenderer } from './debugExpressionRenderer.js'; import { debugConsoleClearAll, debugConsoleEvaluationPrompt } from './debugIcons.js'; import './media/repl.css'; @@ -554,9 +555,10 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this.tree?.domFocus(); } - override focus(): void { + override async focus(): Promise { super.focus(); - setTimeout(() => this.replInput.focus(), 0); + await timeout(0); // wait a task for the repl to get attached to the DOM, #83387 + this.replInput.focus(); } override getActionViewItem(action: IAction): IActionViewItem | undefined { @@ -1209,3 +1211,19 @@ registerAction2(class extends Action2 { } } }); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: FOCUS_REPL_ID, + category: DEBUG_COMMAND_CATEGORY, + title: localize2({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugFocusConsole' }, "Focus on Debug Console View"), + }); + } + + override async run(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const repl = await viewsService.openView(REPL_VIEW_ID); + await repl?.focus(); + } +}); From 37c543ba48a33b8113f514951a8b3e39a04fd6bc Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 18 Dec 2024 22:26:30 +0100 Subject: [PATCH 0231/3587] Implements gutter indicator (off by default). (#236522) * Implements gutter indicator (off by default). * Adds isSingleLine method to monaco Range * update --- src/vs/editor/browser/observableCodeEditor.ts | 16 ++ src/vs/editor/browser/rect.ts | 135 +++++++++++++ src/vs/editor/common/config/editorOptions.ts | 8 + src/vs/editor/common/core/offsetRange.ts | 4 + .../browser/model/inlineCompletionsModel.ts | 2 +- .../view/inlineEdits/gutterIndicatorView.ts | 189 ++++++++++++++++++ .../browser/view/inlineEdits/indicatorView.ts | 4 +- .../browser/view/inlineEdits/utils.ts | 10 +- .../browser/view/inlineEdits/view.ts | 39 ++-- src/vs/monaco.d.ts | 1 + 10 files changed, 388 insertions(+), 20 deletions(-) create mode 100644 src/vs/editor/browser/rect.ts create mode 100644 src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts index 781a9895212f..d8607ae319aa 100644 --- a/src/vs/editor/browser/observableCodeEditor.ts +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -7,6 +7,8 @@ import { equalsIfDefined, itemsEquals } from '../../base/common/equals.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../base/common/lifecycle.js'; import { IObservable, ITransaction, TransactionImpl, autorun, autorunOpts, derived, derivedOpts, derivedWithSetter, observableFromEvent, observableSignal, observableValue, observableValueOpts } from '../../base/common/observable.js'; import { EditorOption, FindComputedEditorOptionValueById } from '../common/config/editorOptions.js'; +import { LineRange } from '../common/core/lineRange.js'; +import { OffsetRange } from '../common/core/offsetRange.js'; import { Position } from '../common/core/position.js'; import { Selection } from '../common/core/selection.js'; import { ICursorSelectionChangedEvent } from '../common/cursorEvents.js'; @@ -265,6 +267,20 @@ export class ObservableCodeEditor extends Disposable { }); } + public observeLineOffsetRange(lineRange: IObservable, store: DisposableStore): IObservable { + const start = this.observePosition(lineRange.map(r => new Position(r.startLineNumber, 1)), store); + const end = this.observePosition(lineRange.map(r => new Position(r.endLineNumberExclusive + 1, 1)), store); + + return derived(reader => { + start.read(reader); + end.read(reader); + const range = lineRange.read(reader); + const s = this.editor.getTopForLineNumber(range.startLineNumber) - this.scrollTop.read(reader); + const e = range.isEmpty ? s : (this.editor.getBottomForLineNumber(range.endLineNumberExclusive - 1) - this.scrollTop.read(reader)); + return new OffsetRange(s, e); + }); + } + public observePosition(position: IObservable, store: DisposableStore): IObservable { let pos = position.get(); const result = observableValueOpts({ owner: this, debugName: () => `topLeftOfPosition${pos?.toString()}`, equalsFn: equalsIfDefined(Point.equals) }, new Point(0, 0)); diff --git a/src/vs/editor/browser/rect.ts b/src/vs/editor/browser/rect.ts new file mode 100644 index 000000000000..b990944eb676 --- /dev/null +++ b/src/vs/editor/browser/rect.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { OffsetRange } from '../common/core/offsetRange.js'; +import { Point } from './point.js'; + +export class Rect { + public static fromPoint(point: Point): Rect { + return new Rect(point.x, point.y, point.x, point.y); + } + + public static fromLeftTopRightBottom(left: number, top: number, right: number, bottom: number): Rect { + return new Rect(left, top, right, bottom); + } + + public static fromLeftTopWidthHeight(left: number, top: number, width: number, height: number): Rect { + return new Rect(left, top, left + width, top + height); + } + + public static fromRanges(leftRight: OffsetRange, topBottom: OffsetRange): Rect { + return new Rect(leftRight.start, topBottom.start, leftRight.endExclusive, topBottom.endExclusive); + } + + public static hull(rects: Rect[]): Rect { + let left = Number.MAX_VALUE; + let top = Number.MAX_VALUE; + let right = Number.MIN_VALUE; + let bottom = Number.MIN_VALUE; + + for (const rect of rects) { + left = Math.min(left, rect.left); + top = Math.min(top, rect.top); + right = Math.max(right, rect.right); + bottom = Math.max(bottom, rect.bottom); + } + + return new Rect(left, top, right, bottom); + } + + public readonly width = this.right - this.left; + public readonly height = this.bottom - this.top; + + constructor( + public readonly left: number, + public readonly top: number, + public readonly right: number, + public readonly bottom: number, + ) { + if (left > right || top > bottom) { + throw new Error('Invalid arguments'); + } + } + + withMargin(margin: number): Rect { + return new Rect(this.left - margin, this.top - margin, this.right + margin, this.bottom + margin); + } + + intersectVertical(range: OffsetRange): Rect { + return new Rect( + this.left, + Math.max(this.top, range.start), + this.right, + Math.min(this.bottom, range.endExclusive), + ); + } + + toString(): string { + return `Rect{(${this.left},${this.top}), (${this.right},${this.bottom})}`; + } + + intersect(parent: Rect): Rect | undefined { + const left = Math.max(this.left, parent.left); + const right = Math.min(this.right, parent.right); + const top = Math.max(this.top, parent.top); + const bottom = Math.min(this.bottom, parent.bottom); + + if (left > right || top > bottom) { + return undefined; + } + + return new Rect(left, top, right, bottom); + } + + union(other: Rect): Rect { + return new Rect( + Math.min(this.left, other.left), + Math.min(this.top, other.top), + Math.max(this.right, other.right), + Math.max(this.bottom, other.bottom), + ); + } + + containsRect(other: Rect): boolean { + return this.left <= other.left + && this.top <= other.top + && this.right >= other.right + && this.bottom >= other.bottom; + } + + moveToBeContainedIn(parent: Rect): Rect { + const width = this.width; + const height = this.height; + + let left = this.left; + let top = this.top; + + if (left < parent.left) { + left = parent.left; + } else if (left + width > parent.right) { + left = parent.right - width; + } + + if (top < parent.top) { + top = parent.top; + } else if (top + height > parent.bottom) { + top = parent.bottom - height; + } + + return new Rect(left, top, left + width, top + height); + } + + withWidth(width: number): Rect { + return new Rect(this.left, this.top, this.left + width, this.bottom); + } + + withHeight(height: number): Rect { + return new Rect(this.left, this.top, this.right, this.top + height); + } + + withTop(top: number): Rect { + return new Rect(this.left, top, this.right, this.bottom); + } +} diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 0c2559635819..c7481985f2f7 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -4198,6 +4198,7 @@ export interface IInlineSuggestOptions { useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; onlyShowWhenCloseToCursor?: boolean; + useGutterIndicator?: boolean; }; }; } @@ -4230,6 +4231,7 @@ class InlineEditorSuggest extends BaseEditorOption { await this._deltaSelectedInlineCompletionIndex(-1); } - public async accept(editor: ICodeEditor): Promise { + public async accept(editor: ICodeEditor = this._editor): Promise { if (editor.getModel() !== this.textModel) { throw new BugIndicatingError(); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts new file mode 100644 index 000000000000..936ab8b56b3b --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -0,0 +1,189 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { IObservable, IReader, constObservable, derived, observableFromEvent } from '../../../../../../base/common/observable.js'; +import { buttonBackground, buttonForeground, buttonSecondaryBackground, buttonSecondaryForeground } from '../../../../../../platform/theme/common/colorRegistry.js'; +import { registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; +import { ObservableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; +import { Rect } from '../../../../../browser/rect.js'; +import { EditorOption } from '../../../../../common/config/editorOptions.js'; +import { LineRange } from '../../../../../common/core/lineRange.js'; +import { OffsetRange } from '../../../../../common/core/offsetRange.js'; +import { StickyScrollController } from '../../../../stickyScroll/browser/stickyScrollController.js'; +import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; +import { mapOutFalsy, n } from './utils.js'; + +export const inlineEditIndicatorPrimaryForeground = registerColor('inlineEdit.gutterIndicator.primaryForeground', buttonForeground, 'Foreground color for the primary inline edit gutter indicator.'); +export const inlineEditIndicatorPrimaryBackground = registerColor('inlineEdit.gutterIndicator.primaryBackground', buttonBackground, 'Background color for the primary inline edit gutter indicator.'); + +export const inlineEditIndicatorSecondaryForeground = registerColor('inlineEdit.gutterIndicator.secondaryForeground', buttonSecondaryForeground, 'Foreground color for the secondary inline edit gutter indicator.'); +export const inlineEditIndicatorSecondaryBackground = registerColor('inlineEdit.gutterIndicator.secondaryBackground', buttonSecondaryBackground, 'Background color for the secondary inline edit gutter indicator.'); + +export const inlineEditIndicatorsuccessfulForeground = registerColor('inlineEdit.gutterIndicator.successfulForeground', buttonForeground, 'Foreground color for the successful inline edit gutter indicator.'); +export const inlineEditIndicatorsuccessfulBackground = registerColor('inlineEdit.gutterIndicator.successfulBackground', { light: '#2e825c', dark: '#2e825c', hcLight: '#2e825c', hcDark: '#2e825c' }, 'Background color for the successful inline edit gutter indicator.'); + +export const inlineEditIndicatorBackground = registerColor( + 'inlineEdit.gutterIndicator.background', + { + hcDark: transparent('tab.inactiveBackground', 0.5), + hcLight: transparent('tab.inactiveBackground', 0.5), + dark: transparent('tab.inactiveBackground', 0.5), + light: '#5f5f5f18', + }, + 'Background color for the inline edit gutter indicator.' +); + + +export class InlineEditsGutterIndicator extends Disposable { + private readonly _state = derived(reader => { + const range = mapOutFalsy(this._originalRange).read(reader); + if (!range) { + return undefined; + } + + return { + range, + lineOffsetRange: this._editorObs.observeLineOffsetRange(range, this._store), + }; + }); + + private _stickyScrollController = StickyScrollController.get(this._editorObs.editor); + private readonly _stickyScrollHeight = this._stickyScrollController ? observableFromEvent(this._stickyScrollController.onDidChangeStickyScrollHeight, () => this._stickyScrollController!.stickyScrollWidgetHeight) : constObservable(0); + + + private readonly _layout = derived(reader => { + const s = this._state.read(reader); + if (!s) { return undefined; } + + const layout = this._editorObs.layoutInfo.read(reader); + + const fullViewPort = Rect.fromLeftTopRightBottom(0, 0, layout.width, layout.height); + const viewPortWithStickyScroll = fullViewPort.withTop(this._stickyScrollHeight.read(reader)); + + const targetVertRange = s.lineOffsetRange.read(reader); + + const space = 1; + + const targetRect = Rect.fromRanges(OffsetRange.fromTo(space, layout.lineNumbersLeft + layout.lineNumbersWidth + 4), targetVertRange); + + + const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); + const pillRect = targetRect.withHeight(lineHeight).withWidth(22); + const pillRectMoved = pillRect.moveToBeContainedIn(viewPortWithStickyScroll); + + const rect = targetRect; + + const iconRect = (targetRect.containsRect(pillRectMoved)) + ? pillRectMoved + : pillRectMoved.moveToBeContainedIn(fullViewPort.intersect(targetRect.union(fullViewPort.withHeight(lineHeight)))!); //viewPortWithStickyScroll.intersect(rect)!; + + return { + rect, + iconRect, + mode: (iconRect.top === targetRect.top ? 'right' as const + : iconRect.top > targetRect.top ? 'top' as const : 'bottom' as const), + docked: rect.containsRect(iconRect) && viewPortWithStickyScroll.containsRect(iconRect), + }; + }); + + private readonly _mode = derived(this, reader => { + const m = this._model.read(reader); + if (m && m.tabShouldAcceptInlineEdit.read(reader)) { return 'accept' as const; } + if (m && m.tabShouldJumpToInlineEdit.read(reader)) { return 'jump' as const; } + return 'inactive' as const; + }); + + private readonly _onClickAction = derived(this, reader => { + if (this._layout.map(d => d && d.docked).read(reader)) { + return { + label: 'Click to accept inline edit', + action: () => { this._model.get()?.accept(); } + }; + } else { + return { + label: 'Click to jump to inline edit', + action: () => { this._model.get()?.jump(); } + }; + } + }); + + private readonly _indicator = n.div({ + class: 'inline-edits-view-gutter-indicator', + onclick: () => this._onClickAction.get().action(), + title: this._onClickAction.map(a => a.label), + style: { + position: 'absolute', + overflow: 'visible', + }, + }, mapOutFalsy(this._layout).map(l => !l ? [] : [ + n.div({ + style: { + position: 'absolute', + background: 'var(--vscode-inlineEdit-gutterIndicator-background)', + borderRadius: '4px', + ...rectToProps(reader => l.read(reader).rect), + } + }), + n.div({ + class: 'icon', + style: { + cursor: 'pointer', + zIndex: '1000', + position: 'absolute', + backgroundColor: this._mode.map(v => ({ + inactive: 'var(--vscode-inlineEdit-gutterIndicator-secondaryBackground)', + jump: 'var(--vscode-inlineEdit-gutterIndicator-primaryBackground)', + accept: 'var(--vscode-inlineEdit-gutterIndicator-successfulBackground)', + }[v])), + '--vscodeIconForeground': this._mode.map(v => ({ + inactive: 'var(--vscode-inlineEdit-gutterIndicator-secondaryForeground)', + jump: 'var(--vscode-inlineEdit-gutterIndicator-primaryForeground)', + accept: 'var(--vscode-inlineEdit-gutterIndicator-successfulForeground)', + }[v])), + borderRadius: '4px', + display: 'flex', + justifyContent: 'center', + transition: 'background-color 0.2s ease-in-out', + ...rectToProps(reader => l.read(reader).iconRect), + } + }, [ + n.div({ + style: { + rotate: l.map(l => ({ right: '0deg', bottom: '90deg', top: '-90deg' }[l.mode])), + transition: 'rotate 0.2s ease-in-out', + } + }, [ + renderIcon(Codicon.arrowRight), + ]) + ]), + ])).keepUpdated(this._store); + + constructor( + private readonly _editorObs: ObservableCodeEditor, + private readonly _originalRange: IObservable, + private readonly _model: IObservable, + ) { + super(); + + this._register(this._editorObs.createOverlayWidget({ + domNode: this._indicator.element, + position: constObservable(null), + allowEditorOverflow: false, + minContentWidthInPx: constObservable(0), + })); + } +} + +function rectToProps(fn: (reader: IReader) => Rect): any { + return { + left: derived(reader => fn(reader).left), + top: derived(reader => fn(reader).top), + width: derived(reader => fn(reader).right - fn(reader).left), + height: derived(reader => fn(reader).bottom - fn(reader).top), + }; +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/indicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/indicatorView.ts index 052451f326dc..f58456636583 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/indicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/indicatorView.ts @@ -19,11 +19,9 @@ export interface IInlineEditsIndicatorState { showAlways: boolean; } -// editorHoverForeground + export const inlineEditIndicatorForeground = registerColor('inlineEdit.indicator.foreground', buttonForeground, ''); -// editorHoverBackground export const inlineEditIndicatorBackground = registerColor('inlineEdit.indicator.background', buttonBackground, ''); -// editorHoverBorder export const inlineEditIndicatorBorder = registerColor('inlineEdit.indicator.border', buttonSeparator, ''); export class InlineEditsIndicator extends Disposable { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts index d9928a05b2eb..73b9cbb56186 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts @@ -9,7 +9,7 @@ import { numberComparator } from '../../../../../../base/common/arrays.js'; import { findFirstMin } from '../../../../../../base/common/arraysFind.js'; import { BugIndicatingError } from '../../../../../../base/common/errors.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; -import { derived, IObservable, IReader, observableValue, transaction } from '../../../../../../base/common/observable.js'; +import { derived, derivedObservableWithCache, IObservable, IReader, observableValue, transaction } from '../../../../../../base/common/observable.js'; import { OS } from '../../../../../../base/common/platform.js'; import { getIndentationLength, splitLines } from '../../../../../../base/common/strings.js'; import { URI } from '../../../../../../base/common/uri.js'; @@ -329,6 +329,8 @@ export abstract class ObserverNode extends Disposab } else { this._element.tabIndex = value; } + } else if (key.startsWith('on')) { + (this._element as any)[key] = value; } else { if (isObservable(value)) { this._deriveds.push(derived(this, reader => { @@ -425,12 +427,16 @@ type ElementAttributeKeys = Partial<{ }>; export function mapOutFalsy(obs: IObservable): IObservable | undefined | null | false> { + const nonUndefinedObs = derivedObservableWithCache(undefined, (reader, lastValue) => obs.read(reader) || lastValue); + return derived(reader => { + nonUndefinedObs.read(reader); const val = obs.read(reader); if (!val) { return undefined; } - return obs as IObservable; + + return nonUndefinedObs as IObservable; }); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 945c91b9e130..68bc963e374a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { derived, IObservable, IReader } from '../../../../../../base/common/observable.js'; +import { autorunWithStore, derived, IObservable, IReader } from '../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; @@ -15,6 +15,7 @@ import { StringText } from '../../../../../common/core/textEdit.js'; import { DetailedLineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../../../../../common/diff/rangeMapping.js'; import { TextModel } from '../../../../../common/model/textModel.js'; import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; +import { InlineEditsGutterIndicator } from './gutterIndicatorView.js'; import { IInlineEditsIndicatorState, InlineEditsIndicator } from './indicatorView.js'; import { IOriginalEditorInlineDiffViewState, OriginalEditorInlineDiffView } from './inlineDiffView.js'; import { InlineEditsSideBySideDiff } from './sideBySideDiff.js'; @@ -118,19 +119,29 @@ export class InlineEditsView extends Disposable { protected readonly _inlineDiffView = this._register(new OriginalEditorInlineDiffView(this._editor, this._inlineDiffViewState, this._previewTextModel)); - protected readonly _indicator = this._register(new InlineEditsIndicator( - this._editorObs, - derived(reader => { - const state = this._uiState.read(reader); - if (!state) { return undefined; } - - const range = state.originalDisplayRange; - const top = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); - - return { editTop: top, showAlways: state.state !== 'sideBySide' }; - }), - this._model, - )); + private readonly _useGutterIndicator = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useGutterIndicator); + + protected readonly _indicator = this._register(autorunWithStore((reader, store) => { + if (this._useGutterIndicator.read(reader)) { + store.add(new InlineEditsGutterIndicator( + this._editorObs, + this._uiState.map(s => s && s.originalDisplayRange), + this._model, + )); + } else { + store.add(new InlineEditsIndicator( + this._editorObs, + derived(reader => { + const state = this._uiState.read(reader); + if (!state) { return undefined; } + const range = state.originalDisplayRange; + const top = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); + return { editTop: top, showAlways: state.state !== 'sideBySide' }; + }), + this._model, + )); + } + })); private determinRenderState(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[]) { if (edit.isCollapsed) { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 77cd7ae49a53..7b6d78e85528 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4605,6 +4605,7 @@ declare namespace monaco.editor { useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; onlyShowWhenCloseToCursor?: boolean; + useGutterIndicator?: boolean; }; }; } From 0900a621135265c95c0a499b5123e2cae848ae1f Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 18 Dec 2024 15:40:29 -0600 Subject: [PATCH 0232/3587] get terminal completions to work for screen reader users (#236516) fix #235022 --- .../suggest/browser/simpleSuggestWidget.ts | 72 ++++++++++++++++++- .../browser/simpleSuggestWidgetRenderer.ts | 2 +- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index 61fb7437cea9..bc9c9ea0cba9 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -10,9 +10,9 @@ import { List } from '../../../../base/browser/ui/list/listWidget.js'; import { ResizableHTMLElement } from '../../../../base/browser/ui/resizable/resizable.js'; import { SimpleCompletionItem } from './simpleCompletionItem.js'; import { LineContext, SimpleCompletionModel } from './simpleCompletionModel.js'; -import { SimpleSuggestWidgetItemRenderer, type ISimpleSuggestWidgetFontInfo } from './simpleSuggestWidgetRenderer.js'; +import { getAriaId, SimpleSuggestWidgetItemRenderer, type ISimpleSuggestWidgetFontInfo } from './simpleSuggestWidgetRenderer.js'; import { TimeoutTimer } from '../../../../base/common/async.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; +import { Emitter, Event, PauseableEmitter } from '../../../../base/common/event.js'; import { MutableDisposable, Disposable } from '../../../../base/common/lifecycle.js'; import { clamp } from '../../../../base/common/numbers.js'; import { localize } from '../../../../nls.js'; @@ -68,7 +68,9 @@ export class SimpleSuggestWidget extends Disposable { private _forceRenderingAbove: boolean = false; private _preference?: WidgetPositionPreference; private readonly _pendingLayout = this._register(new MutableDisposable()); - + // private _currentSuggestionDetails?: CancelablePromise; + private _focusedItem?: SimpleCompletionItem; + private _ignoreFocusEvents: boolean = false; readonly element: ResizableHTMLElement; private readonly _messageElement: HTMLElement; private readonly _listElement: HTMLElement; @@ -83,6 +85,8 @@ export class SimpleSuggestWidget extends Disposable { readonly onDidHide: Event = this._onDidHide.event; private readonly _onDidShow = this._register(new Emitter()); readonly onDidShow: Event = this._onDidShow.event; + private readonly _onDidFocus = new PauseableEmitter(); + readonly onDidFocus: Event = this._onDidFocus.event; get list(): List { return this._list; } @@ -206,6 +210,7 @@ export class SimpleSuggestWidget extends Disposable { this._register(this._list.onMouseDown(e => this._onListMouseDownOrTap(e))); this._register(this._list.onTap(e => this._onListMouseDownOrTap(e))); + this._register(this._list.onDidChangeFocus(e => this._onListFocus(e))); this._register(this._list.onDidChangeSelection(e => this._onListSelection(e))); this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('editor.suggest.showIcons')) { @@ -214,6 +219,67 @@ export class SimpleSuggestWidget extends Disposable { })); } + private _onListFocus(e: IListEvent): void { + if (this._ignoreFocusEvents) { + return; + } + + if (this._state === State.Details) { + // This can happen when focus is in the details-panel and when + // arrow keys are pressed to select next/prev items + this._setState(State.Open); + } + + if (!e.elements.length) { + // if (this._currentSuggestionDetails) { + // this._currentSuggestionDetails.cancel(); + // this._currentSuggestionDetails = undefined; + // this._focusedItem = undefined; + // } + this._clearAriaActiveDescendant(); + return; + } + + if (!this._completionModel) { + return; + } + + // this._ctxSuggestWidgetHasFocusedSuggestion.set(true); + const item = e.elements[0]; + const index = e.indexes[0]; + + if (item !== this._focusedItem) { + + // this._currentSuggestionDetails?.cancel(); + // this._currentSuggestionDetails = undefined; + + this._focusedItem = item; + + this._list.reveal(index); + const id = getAriaId(index); + const node = dom.getActiveWindow().document.activeElement; + if (node && id) { + node.setAttribute('aria-haspopup', 'true'); + node.setAttribute('aria-autocomplete', 'list'); + node.setAttribute('aria-activedescendant', id); + } else { + this._clearAriaActiveDescendant(); + } + } + // emit an event + this._onDidFocus.fire({ item, index, model: this._completionModel }); + } + + private _clearAriaActiveDescendant(): void { + const node = dom.getActiveWindow().document.activeElement; + if (!node) { + return; + } + node.setAttribute('aria-haspopup', 'false'); + node.setAttribute('aria-autocomplete', 'both'); + node.removeAttribute('aria-activedescendant'); + } + private _cursorPosition?: { top: number; left: number; height: number }; setCompletionModel(completionModel: SimpleCompletionModel) { diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts index 86e0f510b0f3..83605e2156de 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts @@ -14,7 +14,7 @@ import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; export function getAriaId(index: number): string { - return `simple-suggest-aria-id:${index}`; + return `simple-suggest-aria-id-${index}`; } export interface ISimpleSuggestionTemplateData { From 4572abc566c41ae926fcd01c979f9e312c69a7ca Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 18 Dec 2024 13:51:49 -0800 Subject: [PATCH 0233/3587] debug: fix REPL input not resizing when pasting content that wraps (#236519) Fixes #229541 --- src/vs/workbench/contrib/debug/browser/repl.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 295049420458..4bcfe48be3e0 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -109,7 +109,6 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { private replInput!: CodeEditorWidget; private replInputContainer!: HTMLElement; private bodyContentDimension: dom.Dimension | undefined; - private replInputLineCount = 1; private model: ITextModel | undefined; private setHistoryNavigationEnablement!: (enabled: boolean) => void; private scopedInstantiationService!: IInstantiationService; @@ -476,9 +475,7 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { revealLastElement(this.tree!); this.history.add(this.replInput.getValue()); this.replInput.setValue(''); - const shouldRelayout = this.replInputLineCount > 1; - this.replInputLineCount = 1; - if (shouldRelayout && this.bodyContentDimension) { + if (this.bodyContentDimension) { // Trigger a layout to shrink a potential multi line input this.layoutBodyContent(this.bodyContentDimension.height, this.bodyContentDimension.width); } @@ -737,12 +734,14 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this.replInput = this.scopedInstantiationService.createInstance(CodeEditorWidget, this.replInputContainer, options, getSimpleCodeEditorWidgetOptions()); + let lastContentHeight = -1; this._register(this.replInput.onDidChangeModelContent(() => { const model = this.replInput.getModel(); this.setHistoryNavigationEnablement(!!model && model.getValue() === ''); - const lineCount = model ? Math.min(10, model.getLineCount()) : 1; - if (lineCount !== this.replInputLineCount) { - this.replInputLineCount = lineCount; + + const contentHeight = this.replInput.getContentHeight(); + if (contentHeight !== lastContentHeight) { + lastContentHeight = contentHeight; if (this.bodyContentDimension) { this.layoutBodyContent(this.bodyContentDimension.height, this.bodyContentDimension.width); } From a7d5ca7e7d5d97e6cefa9fc9f4d16950829bdfd7 Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Wed, 18 Dec 2024 14:12:51 -0800 Subject: [PATCH 0234/3587] Fix logic for notebook outline data source isEmpty() fn (#236525) amend logic for notebook outline data source isEmpty() --- .../contrib/outline/notebookOutline.ts | 61 ++++++++++++------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index 7df78960c43d..0e3812f9f8be 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -346,6 +346,28 @@ export class NotebookQuickPickProvider implements IQuickPickDataSource NotebookOutlineConstants.NonHeaderOutlineLevel) // show symbols off + cell is code + is level >7 (nb symbol levels) + ) { + return true; + } + + return false; +} + export class NotebookOutlinePaneProvider implements IDataSource { private readonly _disposables = new DisposableStore(); @@ -381,14 +403,14 @@ export class NotebookOutlinePaneProvider implements IDataSource NotebookOutlineConstants.NonHeaderOutlineLevel) // show symbols off + cell is code + is level >7 (nb symbol levels) - ) { - return true; - } - - return false; - } - *getChildren(element: NotebookCellOutline | OutlineEntry): Iterable { const isOutline = element instanceof NotebookCellOutline; const entries = isOutline ? this.outlineDataSourceRef?.object?.entries ?? [] : element.children; @@ -518,7 +521,9 @@ export class NotebookCellOutline implements IOutline { private _breadcrumbsDataSource!: IBreadcrumbsDataSource; // view settings + private outlineShowCodeCells: boolean; private outlineShowCodeCellSymbols: boolean; + private outlineShowMarkdownHeadersOnly: boolean; // getters get activeElement(): OutlineEntry | undefined { @@ -538,7 +543,13 @@ export class NotebookCellOutline implements IOutline { return this._outlineDataSourceReference?.object?.uri; } get isEmpty(): boolean { - return this._outlineDataSourceReference?.object?.isEmpty ?? true; + if (!this._outlineDataSourceReference?.object?.entries) { + return true; + } + + return !this._outlineDataSourceReference.object.entries.some(entry => { + return !filterEntry(entry, this.outlineShowMarkdownHeadersOnly, this.outlineShowCodeCells, this.outlineShowCodeCellSymbols); + }); } private checkDelayer() { @@ -558,7 +569,9 @@ export class NotebookCellOutline implements IOutline { @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, ) { + this.outlineShowCodeCells = this._configurationService.getValue(NotebookSetting.outlineShowCodeCells); this.outlineShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); + this.outlineShowMarkdownHeadersOnly = this._configurationService.getValue(NotebookSetting.outlineShowMarkdownHeadersOnly); this.initializeOutline(); @@ -615,6 +628,10 @@ export class NotebookCellOutline implements IOutline { e.affectsConfiguration(NotebookSetting.outlineShowCodeCellSymbols) || e.affectsConfiguration(NotebookSetting.breadcrumbsShowCodeCells) ) { + this.outlineShowCodeCells = this._configurationService.getValue(NotebookSetting.outlineShowCodeCells); + this.outlineShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); + this.outlineShowMarkdownHeadersOnly = this._configurationService.getValue(NotebookSetting.outlineShowMarkdownHeadersOnly); + this.delayedRecomputeState(); } })); From 6869b35a03738298550ab62968cc817ab15828e3 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 18 Dec 2024 14:16:19 -0800 Subject: [PATCH 0235/3587] Show chat participants and tools on extension features page (#236526) * Show chat participants and tools on extension features page * Remove "ID" --- .../platform/extensions/common/extensions.ts | 19 ++++++- .../browser/chatParticipant.contribution.ts | 54 +++++++++++++++++- .../common/chatParticipantContribTypes.ts | 1 - .../tools/languageModelToolsContribution.ts | 55 ++++++++++++++++++- 4 files changed, 122 insertions(+), 7 deletions(-) diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 687fd50dc360..d6758a9b54df 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -167,6 +167,22 @@ export interface ILocalizationContribution { minimalTranslations?: { [key: string]: string }; } +export interface IChatParticipantContribution { + id: string; + name: string; + fullName: string; + description?: string; + isDefault?: boolean; + commands?: { name: string }[]; +} + +export interface IToolContribution { + name: string; + displayName: string; + modelDescription: string; + userDescription?: string; +} + export interface IExtensionContributions { commands?: ICommand[]; configuration?: any; @@ -192,7 +208,8 @@ export interface IExtensionContributions { readonly notebooks?: INotebookEntry[]; readonly notebookRenderer?: INotebookRendererContribution[]; readonly debugVisualizers?: IDebugVisualizationContribution[]; - readonly chatParticipants?: ReadonlyArray<{ id: string }>; + readonly chatParticipants?: ReadonlyArray; + readonly languageModelTools?: ReadonlyArray; } export interface IExtensionCapabilities { diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts index 6326c643d9c7..3d16ee993b75 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts @@ -7,12 +7,13 @@ import { coalesce, isNonEmptyArray } from '../../../../base/common/arrays.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { Event } from '../../../../base/common/event.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import * as strings from '../../../../base/common/strings.js'; import { localize, localize2 } from '../../../../nls.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; +import { ExtensionIdentifier, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; @@ -20,6 +21,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation, Extensions as ViewExtensions } from '../../../common/views.js'; +import { IExtensionFeatureTableRenderer, IRenderedData, ITableData, IRowData, IExtensionFeaturesRegistry, Extensions } from '../../../services/extensionManagement/common/extensionFeatures.js'; import { isProposedApiEnabled } from '../../../services/extensions/common/extensions.js'; import * as extensionsRegistry from '../../../services/extensions/common/extensionsRegistry.js'; import { showExtensionsWithIdsCommandId } from '../../extensions/browser/extensionsActions.js'; @@ -202,7 +204,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { continue; } - if ((providerDescriptor.defaultImplicitVariables || providerDescriptor.locations) && !isProposedApiEnabled(extension.description, 'chatParticipantAdditions')) { + if (providerDescriptor.locations && !isProposedApiEnabled(extension.description, 'chatParticipantAdditions')) { this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: chatParticipantAdditions.`); continue; } @@ -423,3 +425,51 @@ export class ChatCompatibilityNotifier extends Disposable implements IWorkbenchC })); } } + +class ChatParticipantDataRenderer extends Disposable implements IExtensionFeatureTableRenderer { + readonly type = 'table'; + + shouldRender(manifest: IExtensionManifest): boolean { + return !!manifest.contributes?.chatParticipants; + } + + render(manifest: IExtensionManifest): IRenderedData { + const nonDefaultContributions = manifest.contributes?.chatParticipants?.filter(c => !c.isDefault) ?? []; + if (!nonDefaultContributions.length) { + return { data: { headers: [], rows: [] }, dispose: () => { } }; + } + + const headers = [ + localize('participantName', "Name"), + localize('participantFullName', "Full Name"), + localize('participantDescription', "Description"), + localize('participantCommands', "Commands"), + ]; + + const rows: IRowData[][] = nonDefaultContributions.map(d => { + return [ + '@' + d.name, + d.fullName, + d.description ?? '-', + d.commands?.length ? new MarkdownString(d.commands.map(c => `- /` + c.name).join('\n')) : '-' + ]; + }); + + return { + data: { + headers, + rows + }, + dispose: () => { } + }; + } +} + +Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: 'chatParticipants', + label: localize('chatParticipants', "Chat Participants"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(ChatParticipantDataRenderer), +}); diff --git a/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts b/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts index e17e9a1322f1..211d7e7e9c9f 100644 --- a/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts @@ -25,7 +25,6 @@ export interface IRawChatParticipantContribution { isSticky?: boolean; sampleRequest?: string; commands?: IRawChatCommandContribution[]; - defaultImplicitVariables?: string[]; locations?: RawChatParticipantLocation[]; disambiguation?: { category: string; categoryName?: string /** Deprecated */; description: string; examples: string[] }[]; } diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts index de36f417e6a4..e78ef045c614 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts @@ -3,19 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - import { IJSONSchema } from '../../../../../base/common/jsonSchema.js'; -import { DisposableMap } from '../../../../../base/common/lifecycle.js'; +import { DisposableMap, Disposable } from '../../../../../base/common/lifecycle.js'; import { joinPath } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { localize } from '../../../../../nls.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; +import { ExtensionIdentifier, IExtensionManifest } from '../../../../../platform/extensions/common/extensions.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { ILanguageModelToolsService, IToolData } from '../languageModelToolsService.js'; import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js'; import { toolsParametersSchemaSchemaId } from './languageModelToolsParametersSchema.js'; +import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; +import { Registry } from '../../../../../platform/registry/common/platform.js'; +import { IExtensionFeatureTableRenderer, IRenderedData, ITableData, IRowData, IExtensionFeaturesRegistry, Extensions } from '../../../../services/extensionManagement/common/extensionFeatures.js'; export interface IRawToolContribution { name: string; @@ -192,3 +195,49 @@ export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContri }); } } + +class LanguageModelToolDataRenderer extends Disposable implements IExtensionFeatureTableRenderer { + readonly type = 'table'; + + shouldRender(manifest: IExtensionManifest): boolean { + return !!manifest.contributes?.languageModelTools; + } + + render(manifest: IExtensionManifest): IRenderedData { + const contribs = manifest.contributes?.languageModelTools ?? []; + if (!contribs.length) { + return { data: { headers: [], rows: [] }, dispose: () => { } }; + } + + const headers = [ + localize('toolTableName', "Name"), + localize('toolTableDisplayName', "Display Name"), + localize('toolTableDescription', "Description"), + ]; + + const rows: IRowData[][] = contribs.map(t => { + return [ + new MarkdownString(`\`${t.name}\``), + t.displayName, + t.userDescription ?? t.modelDescription, + ]; + }); + + return { + data: { + headers, + rows + }, + dispose: () => { } + }; + } +} + +Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: 'languageModelTools', + label: localize('langModelTools', "Language Model Tools"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(LanguageModelToolDataRenderer), +}); From 37b1016c80c5e6cd667c382765651055ad9f0ba8 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 18 Dec 2024 15:04:42 -0800 Subject: [PATCH 0236/3587] Lock some SCM strings (#236531) Fixes https://github.com/microsoft/vscode/issues/236530 --- extensions/git/package.nls.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index d706b650c8a0..6db253137e15 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -310,6 +310,8 @@ "message": "[Download Git for Windows](https://git-scm.com/download/win)\nAfter installing, please [reload](command:workbench.action.reloadWindow) (or [troubleshoot](command:git.showOutput)). Additional source control providers can be installed [from the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22).", "comment": [ "{Locked='](command:workbench.action.reloadWindow'}", + "{Locked='](command:git.showOutput'}", + "{Locked='](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22'}", "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" ] @@ -318,6 +320,8 @@ "message": "[Download Git for macOS](https://git-scm.com/download/mac)\nAfter installing, please [reload](command:workbench.action.reloadWindow) (or [troubleshoot](command:git.showOutput)). Additional source control providers can be installed [from the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22).", "comment": [ "{Locked='](command:workbench.action.reloadWindow'}", + "{Locked='](command:git.showOutput'}", + "{Locked='](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22'}", "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" ] @@ -326,11 +330,19 @@ "message": "Source control depends on Git being installed.\n[Download Git for Linux](https://git-scm.com/download/linux)\nAfter installing, please [reload](command:workbench.action.reloadWindow) (or [troubleshoot](command:git.showOutput)). Additional source control providers can be installed [from the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22).", "comment": [ "{Locked='](command:workbench.action.reloadWindow'}", + "{Locked='](command:git.showOutput'}", + "{Locked='](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22'}", "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" ] }, - "view.workbench.scm.missing": "Install Git, a popular source control system, to track code changes and collaborate with others. Learn more in our [Git guides](https://aka.ms/vscode-scm).", + "view.workbench.scm.missing": { + "message": "Install Git, a popular source control system, to track code changes and collaborate with others. Learn more in our [Git guides](https://aka.ms/vscode-scm).", + "comment": [ + "{Locked='](https://aka.ms/vscode-scm'}", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, "view.workbench.scm.disabled": { "message": "If you would like to use Git features, please enable Git in your [settings](command:workbench.action.openSettings?%5B%22git.enabled%22%5D).\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", "comment": [ From 03e9e31965788888ef75b7b5b5baa73897f2db0a Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Wed, 18 Dec 2024 17:25:00 -0800 Subject: [PATCH 0237/3587] Await remote extensions before checking enablement (#236538) await remote extensions before checking enablement --- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index aab4103ddac9..f42fd26ada52 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -60,6 +60,7 @@ import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { URI } from '../../../../base/common/uri.js'; import { IHostService } from '../../../services/host/browser/host.js'; import Severity from '../../../../base/common/severity.js'; +import { IRemoteExtensionsScannerService } from '../../../../platform/remote/common/remoteExtensionsScanner.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -1022,6 +1023,7 @@ class ChatSetupContext extends Disposable { @IExtensionService private readonly extensionService: IExtensionService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @IRemoteExtensionsScannerService private readonly remoteExtensionsScannerService: IRemoteExtensionsScannerService, @ILogService private readonly logService: ILogService ) { super(); @@ -1047,6 +1049,7 @@ class ChatSetupContext extends Disposable { } })); + await this.remoteExtensionsScannerService.whenExtensionsReady(); const extensions = await this.extensionManagementService.getInstalled(); const defaultChatExtension = extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); this.update({ installed: !!defaultChatExtension && this.extensionEnablementService.isEnabled(defaultChatExtension) }); From dd86be1a95485a849a2003721b6e08235c46b031 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 18 Dec 2024 17:33:45 -0800 Subject: [PATCH 0238/3587] lists: support user selection in lists, and adopt it in the debug console (#236534) - Adds a `userSelection` option on lists that can be used to control behavior - Uses the DND logic for handling scrolling near the top and bottom - Preserved selected list elements while they're selected to ensure they can be accurately copied. - The DOM events we get around selection are pretty poor. I support mouse here but I'm unclear if/how touch events should be handled. ![](https://memes.peet.io/img/24-12-f06c680d-f209-476b-8945-b6fc33efe502.mp4) Fixes #228432, cc @joaomoreno --- src/vs/base/browser/ui/list/listView.ts | 98 +++++++++++++++++-- src/vs/base/browser/ui/list/listWidget.ts | 1 + .../workbench/contrib/debug/browser/repl.ts | 1 + 3 files changed, 94 insertions(+), 6 deletions(-) diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 00a19352e91a..36355a377ba5 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { DataTransfers, IDragAndDropData } from '../../dnd.js'; -import { $, addDisposableListener, animate, Dimension, getContentHeight, getContentWidth, getTopLeftOffset, getWindow, isAncestor, isHTMLElement, isSVGElement, scheduleAtNextAnimationFrame } from '../../dom.js'; +import { $, addDisposableListener, animate, Dimension, getContentHeight, getContentWidth, getDocument, getTopLeftOffset, getWindow, isAncestor, isHTMLElement, isSVGElement, scheduleAtNextAnimationFrame } from '../../dom.js'; import { DomEmitter } from '../../event.js'; import { IMouseWheelEvent } from '../../mouseEvent.js'; import { EventType as TouchEventType, Gesture, GestureEvent } from '../../touch.js'; @@ -82,6 +82,7 @@ export interface IListViewOptions extends IListViewOptionsUpdate { readonly setRowHeight?: boolean; readonly supportDynamicHeights?: boolean; readonly mouseSupport?: boolean; + readonly userSelection?: boolean; readonly accessibilityProvider?: IListViewAccessibilityProvider; readonly transformOptimization?: boolean; readonly alwaysConsumeMouseWheel?: boolean; @@ -105,7 +106,7 @@ const DefaultOptions = { horizontalScrolling: false, transformOptimization: true, alwaysConsumeMouseWheel: true, -}; +} satisfies IListViewOptions; export class ElementsDragAndDropData implements IDragAndDropData { @@ -321,6 +322,8 @@ export class ListView implements IListView { private currentDragFeedbackPosition: ListDragOverEffectPosition | undefined; private currentDragFeedbackDisposable: IDisposable = Disposable.None; private onDragLeaveTimeout: IDisposable = Disposable.None; + private currentSelectionDisposable: IDisposable = Disposable.None; + private currentSelectionBounds: IRange | undefined; private readonly disposables: DisposableStore = new DisposableStore(); @@ -369,7 +372,7 @@ export class ListView implements IListView { container: HTMLElement, private virtualDelegate: IListVirtualDelegate, renderers: IListRenderer[], - options: IListViewOptions = DefaultOptions as IListViewOptions + options: IListViewOptions = DefaultOptions ) { if (options.horizontalScrolling && options.supportDynamicHeights) { throw new Error('Horizontal scrolling and dynamic heights not supported simultaneously'); @@ -444,6 +447,12 @@ export class ListView implements IListView { this.disposables.add(addDisposableListener(this.domNode, 'drop', e => this.onDrop(this.toDragEvent(e)))); this.disposables.add(addDisposableListener(this.domNode, 'dragleave', e => this.onDragLeave(this.toDragEvent(e)))); this.disposables.add(addDisposableListener(this.domNode, 'dragend', e => this.onDragEnd(e))); + if (options.userSelection) { + if (options.dnd) { + throw new Error('DND and user selection cannot be used simultaneously'); + } + this.disposables.add(addDisposableListener(this.domNode, 'mousedown', e => this.onPotentialSelectionStart(e))); + } this.setRowLineHeight = options.setRowLineHeight ?? DefaultOptions.setRowLineHeight; this.setRowHeight = options.setRowHeight ?? DefaultOptions.setRowHeight; @@ -768,7 +777,7 @@ export class ListView implements IListView { } get firstVisibleIndex(): number { - const range = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); + const range = this.getVisibleRange(this.lastRenderTop, this.lastRenderHeight); return range.start; } @@ -1168,6 +1177,73 @@ export class ListView implements IListView { this.dnd.onDragStart?.(this.currentDragData, event); } + private onPotentialSelectionStart(e: MouseEvent) { + this.currentSelectionDisposable.dispose(); + const doc = getDocument(this.domNode); + + // Set up both the 'movement store' for watching the mouse, and the + // 'selection store' which lasts as long as there's a selection, even + // after the usr has stopped modifying it. + const selectionStore = this.currentSelectionDisposable = new DisposableStore(); + const movementStore = selectionStore.add(new DisposableStore()); + + // The selection events we get from the DOM are fairly limited and we lack a 'selection end' event. + // Selection events also don't tell us where the input doing the selection is. So, make a poor + // assumption that a user is using the mouse, and base our events on that. + movementStore.add(addDisposableListener(this.domNode, 'selectstart', () => { + this.setupDragAndDropScrollTopAnimation(e); + + movementStore.add(addDisposableListener(doc, 'mousemove', e => this.setupDragAndDropScrollTopAnimation(e))); + + // The selection is cleared either on mouseup if there's no selection, or on next mousedown + // when `this.currentSelectionDisposable` is reset. + selectionStore.add(toDisposable(() => { + const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); + this.currentSelectionBounds = undefined; + this.render(previousRenderRange, this.lastRenderTop, this.lastRenderHeight, undefined, undefined); + })); + selectionStore.add(addDisposableListener(doc, 'selectionchange', () => { + const selection = doc.getSelection(); + if (!selection) { + return; + } + + let start = this.getIndexOfListElement(selection.anchorNode as HTMLElement); + let end = this.getIndexOfListElement(selection.focusNode as HTMLElement); + if (start !== undefined && end !== undefined) { + if (end < start) { + [start, end] = [end, start]; + } + this.currentSelectionBounds = { start, end }; + } + })); + })); + + movementStore.add(addDisposableListener(doc, 'mouseup', () => { + movementStore.dispose(); + + if (doc.getSelection()?.isCollapsed !== false) { + selectionStore.dispose(); + } + })); + } + + private getIndexOfListElement(element: HTMLElement | null): number | undefined { + if (!element || !this.domNode.contains(element)) { + return undefined; + } + + while (element && element !== this.domNode) { + if (element.dataset?.index) { + return Number(element.dataset.index); + } + + element = element.parentElement; + } + + return undefined; + } + private onDragOver(event: IListDragEvent): boolean { event.browserEvent.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome) @@ -1327,7 +1403,7 @@ export class ListView implements IListView { // DND scroll top animation - private setupDragAndDropScrollTopAnimation(event: DragEvent): void { + private setupDragAndDropScrollTopAnimation(event: DragEvent | MouseEvent): void { if (!this.dragOverAnimationDisposable) { const viewTop = getTopLeftOffset(this.domNode).top; this.dragOverAnimationDisposable = animate(getWindow(this.domNode), this.animateDragAndDropScrollTop.bind(this, viewTop)); @@ -1401,13 +1477,23 @@ export class ListView implements IListView { return undefined; } - protected getRenderRange(renderTop: number, renderHeight: number): IRange { + private getVisibleRange(renderTop: number, renderHeight: number): IRange { return { start: this.rangeMap.indexAt(renderTop), end: this.rangeMap.indexAfter(renderTop + renderHeight - 1) }; } + protected getRenderRange(renderTop: number, renderHeight: number): IRange { + const range = this.getVisibleRange(renderTop, renderHeight); + if (this.currentSelectionBounds) { + range.start = Math.min(range.start, this.currentSelectionBounds.start); + range.end = Math.max(range.end, this.currentSelectionBounds.end + 1); + } + + return range; + } + /** * Given a stable rendered state, checks every rendered element whether it needs * to be probed for dynamic height. Adjusts scroll height and top if necessary. diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index ae9d5a46fb47..78932044f96e 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -1056,6 +1056,7 @@ export interface IListOptions extends IListOptionsUpdate { readonly setRowHeight?: boolean; readonly supportDynamicHeights?: boolean; readonly mouseSupport?: boolean; + readonly userSelection?: boolean; readonly horizontalScrolling?: boolean; readonly scrollByPage?: boolean; readonly transformOptimization?: boolean; diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 4bcfe48be3e0..a9959c5d1c53 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -667,6 +667,7 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { filter: this.filter, accessibilityProvider: new ReplAccessibilityProvider(), identityProvider, + userSelection: true, mouseSupport: false, findWidgetEnabled: true, keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IReplElement) => e.toString(true) }, From acd32b17b837b05a64275c297949753df46dbe6d Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 18 Dec 2024 20:17:02 -0800 Subject: [PATCH 0239/3587] Use IExtensionsWorkbenchService instead (#236540) Use IExtensionsWorkbenchService --- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index f42fd26ada52..a325d5526f15 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -26,7 +26,6 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -60,7 +59,6 @@ import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { URI } from '../../../../base/common/uri.js'; import { IHostService } from '../../../services/host/browser/host.js'; import Severity from '../../../../base/common/severity.js'; -import { IRemoteExtensionsScannerService } from '../../../../platform/remote/common/remoteExtensionsScanner.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -1021,10 +1019,9 @@ class ChatSetupContext extends Disposable { @IStorageService private readonly storageService: IStorageService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IExtensionService private readonly extensionService: IExtensionService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @IRemoteExtensionsScannerService private readonly remoteExtensionsScannerService: IRemoteExtensionsScannerService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, ) { super(); @@ -1049,10 +1046,9 @@ class ChatSetupContext extends Disposable { } })); - await this.remoteExtensionsScannerService.whenExtensionsReady(); - const extensions = await this.extensionManagementService.getInstalled(); + const extensions = await this.extensionsWorkbenchService.queryLocal(); const defaultChatExtension = extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); - this.update({ installed: !!defaultChatExtension && this.extensionEnablementService.isEnabled(defaultChatExtension) }); + this.update({ installed: !!defaultChatExtension?.local && this.extensionEnablementService.isEnabled(defaultChatExtension.local) }); } update(context: { installed: boolean }): Promise; From d47f63e5dcde69292e29018bd6fbb4bc93ccaaf1 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:35:34 +0100 Subject: [PATCH 0240/3587] Revert focus behavior fix for views/panels (#236556) Revert "Fix inconsistent focus behavior when toggling views/panels (#235622)" This reverts commit d5746c5593f6afe15e44a9f7877a064df6605d11. --- src/vs/workbench/browser/layout.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 592bb4146af1..bf1f54199d13 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -1783,9 +1783,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi else if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { const viewletToOpen = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.Sidebar); if (viewletToOpen) { - const viewlet = this.paneCompositeService.openPaneComposite(viewletToOpen, ViewContainerLocation.Sidebar); + const viewlet = this.paneCompositeService.openPaneComposite(viewletToOpen, ViewContainerLocation.Sidebar, true); if (!viewlet) { - this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id, ViewContainerLocation.Sidebar); + this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id, ViewContainerLocation.Sidebar, true); } } } @@ -1931,7 +1931,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } if (panelToOpen) { - this.paneCompositeService.openPaneComposite(panelToOpen, ViewContainerLocation.Panel); + const focus = !skipLayout; + this.paneCompositeService.openPaneComposite(panelToOpen, ViewContainerLocation.Panel, focus); } } @@ -2030,7 +2031,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } if (panelToOpen) { - this.paneCompositeService.openPaneComposite(panelToOpen, ViewContainerLocation.AuxiliaryBar); + const focus = !skipLayout; + this.paneCompositeService.openPaneComposite(panelToOpen, ViewContainerLocation.AuxiliaryBar, focus); } } From d74499bdb99fd78fb4c6ef819e9c76fa766cae1e Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Thu, 19 Dec 2024 08:23:06 +0100 Subject: [PATCH 0241/3587] @vscode/proxy-agent 0.28.0 --- package-lock.json | 8 ++++---- package.json | 2 +- remote/package-lock.json | 8 ++++---- remote/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 60dc545ca375..5f2b14c3e397 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.8", - "@vscode/proxy-agent": "^0.27.0", + "@vscode/proxy-agent": "^0.28.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", @@ -2848,9 +2848,9 @@ } }, "node_modules/@vscode/proxy-agent": { - "version": "0.27.0", - "resolved": "git+ssh://git@github.com/microsoft/vscode-proxy-agent.git#0803e0a7d1249bcb64ae9d256d0ee732b7246ea5", - "integrity": "sha512-ID1MOlynRkVN121n85Hs1enjW16B5Z1O6bMDkSPqvAMVD6eHUOqmgPZuEkBbRI43PfA6boUEY9Ndjp+0wPKtsg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.28.0.tgz", + "integrity": "sha512-7rYF8ju0dP/ASpjjnuOCvzRosGLoKz0WOyNohREUskRdrvMEnYuEUXy84lHlH+4+MD8CZZjw2SUzhjHaJK1hxg==", "license": "MIT", "dependencies": { "@tootallnate/once": "^3.0.0", diff --git a/package.json b/package.json index 8dd5e0a86a37..3316ae225ea8 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.8", - "@vscode/proxy-agent": "^0.27.0", + "@vscode/proxy-agent": "^0.28.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", diff --git a/remote/package-lock.json b/remote/package-lock.json index dfbff101a88f..3cf229398615 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -13,7 +13,7 @@ "@parcel/watcher": "2.5.0", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.27.0", + "@vscode/proxy-agent": "^0.28.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/tree-sitter-wasm": "^0.0.4", @@ -418,9 +418,9 @@ "integrity": "sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg==" }, "node_modules/@vscode/proxy-agent": { - "version": "0.27.0", - "resolved": "git+ssh://git@github.com/microsoft/vscode-proxy-agent.git#0803e0a7d1249bcb64ae9d256d0ee732b7246ea5", - "integrity": "sha512-ID1MOlynRkVN121n85Hs1enjW16B5Z1O6bMDkSPqvAMVD6eHUOqmgPZuEkBbRI43PfA6boUEY9Ndjp+0wPKtsg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.28.0.tgz", + "integrity": "sha512-7rYF8ju0dP/ASpjjnuOCvzRosGLoKz0WOyNohREUskRdrvMEnYuEUXy84lHlH+4+MD8CZZjw2SUzhjHaJK1hxg==", "license": "MIT", "dependencies": { "@tootallnate/once": "^3.0.0", diff --git a/remote/package.json b/remote/package.json index b5ebb761e093..5a61a15c0513 100644 --- a/remote/package.json +++ b/remote/package.json @@ -8,7 +8,7 @@ "@parcel/watcher": "2.5.0", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.27.0", + "@vscode/proxy-agent": "^0.28.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/tree-sitter-wasm": "^0.0.4", From 7e000daa484bbf6e434c9942c39bb2eee569619c Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 19 Dec 2024 10:55:10 +0100 Subject: [PATCH 0242/3587] recovery fix: fixing installing extensions everywhere when it is already installed locally (#236562) * recovery fix: fixing installing extensions everywhere when it is already installed locally * clean up --- .../extensions/browser/extensionsWorkbenchService.ts | 8 +++++--- .../common/extensionManagementService.ts | 8 ++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index ae7df7d09b38..0967e7d34c7f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -2368,8 +2368,9 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extension?.isMalicious) { throw new Error(nls.localize('malicious', "This extension is reported to be problematic.")); } + // TODO: @sandy081 - Install the extension only on servers where it is not installed // Do not install if requested to enable and extension is already installed - if (!(installOptions.enable && extension?.local)) { + if (installOptions.installEverywhere || !(installOptions.enable && extension?.local)) { if (!installable) { if (!gallery) { const id = isString(arg) ? arg : (arg).identifier.id; @@ -2730,10 +2731,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return this.extensionManagementService.installVSIX(vsix, manifest, installOptions); } - private installFromGallery(extension: IExtension, gallery: IGalleryExtension, installOptions?: InstallOptions): Promise { + private installFromGallery(extension: IExtension, gallery: IGalleryExtension, installOptions?: InstallExtensionOptions): Promise { installOptions = installOptions ?? {}; installOptions.pinned = extension.local?.pinned || !this.shouldAutoUpdateExtension(extension); - if (extension.local) { + // TODO: @sandy081 - Install the extension only on servers where it is not installed + if (!installOptions.installEverywhere && extension.local) { installOptions.productVersion = this.getProductVersion(); installOptions.operation = InstallOperation.Update; return this.extensionManagementService.updateFromGallery(gallery, extension.local, installOptions); diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index d82368283243..bc0a4687128c 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -466,7 +466,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench installOptions = { ...(installOptions || {}), isMachineScoped }; } - if (installOptions.installEverywhere || (!installOptions.isMachineScoped && this.isExtensionsSyncEnabled())) { + if (!installOptions.isMachineScoped && this.isExtensionsSyncEnabled()) { if (this.extensionManagementServerService.localExtensionManagementServer && !servers.includes(this.extensionManagementServerService.localExtensionManagementServer) && await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.canInstall(gallery) === true) { @@ -597,7 +597,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } } - private async validateAndGetExtensionManagementServersToInstall(gallery: IGalleryExtension, installOptions?: InstallOptions): Promise { + private async validateAndGetExtensionManagementServersToInstall(gallery: IGalleryExtension, installOptions?: IWorkbenchInstallOptions): Promise { const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None); if (!manifest) { @@ -606,8 +606,8 @@ export class ExtensionManagementService extends Disposable implements IWorkbench const servers: IExtensionManagementServer[] = []; - // Install Language pack on local and remote servers - if (isLanguagePackExtension(manifest)) { + // Install everywhere if asked to install everywhere or if the extension is a language pack + if (installOptions?.installEverywhere || isLanguagePackExtension(manifest)) { servers.push(...this.servers.filter(server => server !== this.extensionManagementServerService.webExtensionManagementServer)); } else { const server = this.getExtensionManagementServerToInstall(manifest); From 7efdaa5e8eadfdb97428e86c0ade801d22ab5868 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 19 Dec 2024 11:34:50 +0100 Subject: [PATCH 0243/3587] don't show inline chat hint when line has too many comments or strings. (#236567) The limit is 25% strings, comments, or regex token and (as before) lines ending in comments https://github.com/microsoft/vscode-copilot-release/issues/3009 --- .../browser/inlineChatCurrentLine.ts | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts index 43e73454efd1..08c9911fdb80 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts @@ -140,14 +140,32 @@ export class ShowInlineChatHintAction extends EditorAction2 { model.tokenization.forceTokenization(position.lineNumber); const tokens = model.tokenization.getLineTokens(position.lineNumber); - const tokenIndex = tokens.findTokenIndexAtOffset(position.column - 1); - const tokenType = tokens.getStandardTokenType(tokenIndex); - if (tokenType === StandardTokenType.Comment) { + let totalLength = 0; + let specialLength = 0; + let lastTokenType: StandardTokenType | undefined; + + tokens.forEach(idx => { + const tokenType = tokens.getStandardTokenType(idx); + const startOffset = tokens.getStartOffset(idx); + const endOffset = tokens.getEndOffset(idx); + totalLength += endOffset - startOffset; + + if (tokenType !== StandardTokenType.Other) { + specialLength += endOffset - startOffset; + } + lastTokenType = tokenType; + }); + + if (specialLength / totalLength > 0.25) { ctrl.hide(); - } else { - ctrl.show(); + return; + } + if (lastTokenType === StandardTokenType.Comment) { + ctrl.hide(); + return; } + ctrl.show(); } } From cbfd8ab51359eafff1b5258f25dc334b2d9e26e5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 19 Dec 2024 02:50:00 -0800 Subject: [PATCH 0244/3587] Cache builtin commands and get all global commands for pwsh This should make things quite a bit faster for expensive fetching of commands. This also caches empty builtin commands so it's not attempted every time. Fixes #236097 Part of #235024 --- .../src/terminalSuggestMain.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 4acd5b59f8a0..13ff032037ac 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -12,14 +12,14 @@ import codeCompletionSpec from './completions/code'; import cdSpec from './completions/cd'; let cachedAvailableCommands: Set | undefined; -let cachedBuiltinCommands: Map | undefined; +const cachedBuiltinCommands: Map = new Map(); export const availableSpecs = [codeCompletionSpec, codeInsidersCompletionSpec, cdSpec]; function getBuiltinCommands(shell: string): string[] | undefined { try { const shellType = path.basename(shell, path.extname(shell)); - const cachedCommands = cachedBuiltinCommands?.get(shellType); + const cachedCommands = cachedBuiltinCommands.get(shellType); if (cachedCommands) { return cachedCommands; } @@ -38,14 +38,14 @@ function getBuiltinCommands(shell: string): string[] | undefined { break; } case 'fish': { - // TODO: ghost text in the command line prevents - // completions from working ATM for fish + // TODO: Ghost text in the command line prevents completions from working ATM for fish const fishOutput = execSync('functions -n', options); commands = fishOutput.split(', ').filter(filter); break; } case 'pwsh': { - const output = execSync('Get-Command | Select-Object Name, CommandType, DisplayName | ConvertTo-Json', options); + // TODO: Select `CommandType, DisplayName` and map to a rich type with kind and detail + const output = execSync('Get-Command -All | Select-Object Name | ConvertTo-Json', options); let json: any; try { json = JSON.parse(output); @@ -53,17 +53,12 @@ function getBuiltinCommands(shell: string): string[] | undefined { console.error('Error parsing pwsh output:', e); return []; } - // TODO: Return a rich type with kind and detail commands = (json as any[]).map(e => e.Name); break; } } - // TODO: Cache failure results too - if (commands?.length) { - cachedBuiltinCommands?.set(shellType, commands); - return commands; - } - return; + cachedBuiltinCommands.set(shellType, commands); + return commands; } catch (error) { console.error('Error fetching builtin commands:', error); From 011e8ec6df9cfd2aef2da84a617d4489fb9f372a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Dec 2024 11:56:01 +0100 Subject: [PATCH 0245/3587] chat setup - use 1 service as source of truth for extensions (#236564) --- .../contrib/chat/browser/chatSetup.ts | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index a325d5526f15..3e93dbf6375b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -43,7 +43,6 @@ import { IViewDescriptorService, ViewContainerLocation } from '../../../common/v import { IActivityService, ProgressBadge } from '../../../services/activity/common/activity.js'; import { AuthenticationSession, IAuthenticationExtensionsService, IAuthenticationService } from '../../../services/authentication/common/authentication.js'; import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; -import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; @@ -1018,7 +1017,6 @@ class ChatSetupContext extends Disposable { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IStorageService private readonly storageService: IStorageService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IExtensionService private readonly extensionService: IExtensionService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @ILogService private readonly logService: ILogService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @@ -1030,25 +1028,19 @@ class ChatSetupContext extends Disposable { } private async checkExtensionInstallation(): Promise { - this._register(this.extensionService.onDidChangeExtensions(result => { - for (const extension of result.removed) { - if (ExtensionIdentifier.equals(defaultChat.extensionId, extension.identifier)) { - this.update({ installed: false }); - break; - } - } - for (const extension of result.added) { - if (ExtensionIdentifier.equals(defaultChat.extensionId, extension.identifier)) { - this.update({ installed: true }); - break; - } + // Await extensions to be ready to be queries + await this.extensionsWorkbenchService.queryLocal(); + + // Listen to change and process extensions once + this._register(Event.runAndSubscribe(this.extensionsWorkbenchService.onChange, (e) => { + if (e && !ExtensionIdentifier.equals(e.identifier.id, defaultChat.extensionId)) { + return; // unrelated event } - })); - const extensions = await this.extensionsWorkbenchService.queryLocal(); - const defaultChatExtension = extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); - this.update({ installed: !!defaultChatExtension?.local && this.extensionEnablementService.isEnabled(defaultChatExtension.local) }); + const defaultChatExtension = this.extensionsWorkbenchService.local.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); + this.update({ installed: !!defaultChatExtension?.local && this.extensionEnablementService.isEnabled(defaultChatExtension.local) }); + })); } update(context: { installed: boolean }): Promise; From 7f512c528d36154fe0cab63dcd75b505013fff44 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Dec 2024 12:13:57 +0100 Subject: [PATCH 0246/3587] chat - fix setup in web based on remote connection --- .../chat/browser/actions/chatActions.ts | 19 ++++++++++++++----- .../contrib/chat/browser/chat.contribution.ts | 2 ++ .../contrib/chat/browser/chatSetup.ts | 10 ++++++++-- .../contrib/chat/common/chatContextKeys.ts | 4 ++++ .../electron-sandbox/chat.contribution.ts | 2 -- .../common/gettingStartedContent.ts | 7 +++---- 6 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index a0bb888eb80f..d1d7d951559f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -9,7 +9,7 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { fromNowByDay } from '../../../../../base/common/date.js'; import { Event } from '../../../../../base/common/event.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, markAsSingleton } from '../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; @@ -531,7 +531,10 @@ MenuRegistry.appendMenuItem(MenuId.CommandCenter, { submenu: MenuId.ChatCommandCenter, title: localize('title4', "Chat"), icon: Codicon.copilot, - when: ContextKeyExpr.has('config.chat.commandCenter.enabled'), + when: ContextKeyExpr.and( + ChatContextKeys.supported, + ContextKeyExpr.has('config.chat.commandCenter.enabled') + ), order: 10001, }); @@ -541,7 +544,10 @@ registerAction2(class ToggleCopilotControl extends ToggleTitleBarConfigAction { 'chat.commandCenter.enabled', localize('toggle.chatControl', 'Copilot Controls'), localize('toggle.chatControlsDescription', "Toggle visibility of the Copilot Controls in title bar"), 4, false, - ContextKeyExpr.has('config.window.commandCenter') + ContextKeyExpr.and( + ChatContextKeys.supported, + ContextKeyExpr.has('config.window.commandCenter') + ) ); } }); @@ -561,7 +567,7 @@ export class ChatCommandCenterRendering extends Disposable implements IWorkbench const contextKeySet = new Set([ChatContextKeys.Setup.signedOut.key]); - this._store.add(actionViewItemService.register(MenuId.CommandCenter, MenuId.ChatCommandCenter, (action, options) => { + const disposable = actionViewItemService.register(MenuId.CommandCenter, MenuId.ChatCommandCenter, (action, options) => { if (!(action instanceof SubmenuItemAction)) { return undefined; } @@ -607,6 +613,9 @@ export class ChatCommandCenterRendering extends Disposable implements IWorkbench agentService.onDidChangeAgents, chatQuotasService.onDidChangeQuotas, Event.filter(contextKeyService.onDidChangeContext, e => e.affectsSome(contextKeySet)) - ))); + )); + + // Reduces flicker a bit on reload/restart + markAsSingleton(disposable); } } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index b03d4605d160..456c8817cd46 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -81,6 +81,7 @@ import { Extensions, IConfigurationMigrationRegistry } from '../../../common/con import { ChatEditorOverlayController } from './chatEditorOverlay.js'; import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesContrib.js'; import { ChatQuotasService, ChatQuotasStatusBarEntry, IChatQuotasService } from './chatQuotasService.js'; +import { ChatSetupContribution } from './chatSetup.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -316,6 +317,7 @@ registerWorkbenchContribution2(ChatEditorSaving.ID, ChatEditorSaving, WorkbenchP registerWorkbenchContribution2(ChatEditorAutoSaveDisabler.ID, ChatEditorAutoSaveDisabler, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatViewsWelcomeHandler.ID, ChatViewsWelcomeHandler, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(ChatGettingStartedContribution.ID, ChatGettingStartedContribution, WorkbenchPhase.Eventually); +registerWorkbenchContribution2(ChatSetupContribution.ID, ChatSetupContribution, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatQuotasStatusBarEntry.ID, ChatQuotasStatusBarEntry, WorkbenchPhase.Eventually); registerChatActions(); diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 3e93dbf6375b..53a86ec5245a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -58,6 +58,8 @@ import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { URI } from '../../../../base/common/uri.js'; import { IHostService } from '../../../services/host/browser/host.js'; import Severity from '../../../../base/common/severity.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; +import { isWeb } from '../../../../base/common/platform.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -106,11 +108,15 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr constructor( @IProductService private readonly productService: IProductService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { super(); - if (!this.productService.defaultChatAgent) { + if ( + !this.productService.defaultChatAgent || // needs product config + (isWeb && !this.environmentService.remoteAuthority) // only enabled locally or a remote backend + ) { return; } diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index f615233de505..be03126dd2db 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -5,6 +5,8 @@ import { localize } from '../../../../nls.js'; import { ContextKeyExpr, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js'; +import { RemoteNameContext } from '../../../common/contextkeys.js'; import { ChatAgentLocation } from './chatAgents.js'; export namespace ChatContextKeys { @@ -27,7 +29,9 @@ export namespace ChatContextKeys { export const inChatInput = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); export const inChatSession = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); + export const supported = ContextKeyExpr.or(IsWebContext.toNegated(), RemoteNameContext.notEqualsTo('')); // supported on desktop and in web only with a remote connection export const enabled = new RawContextKey('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is activated with an implementation.") }); + export const panelParticipantRegistered = new RawContextKey('chatPanelParticipantRegistered', false, { type: 'boolean', description: localize('chatParticipantRegistered', "True when a default chat participant is registered for the panel.") }); export const editingParticipantRegistered = new RawContextKey('chatEditingParticipantRegistered', false, { type: 'boolean', description: localize('chatEditingParticipantRegistered', "True when a default chat participant is registered for editing.") }); export const chatEditingCanUndo = new RawContextKey('chatEditingCanUndo', false, { type: 'boolean', description: localize('chatEditingCanUndo', "True when it is possible to undo an interaction in the editing panel.") }); diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts index e71f4342b157..0e888b7f8644 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts @@ -6,7 +6,6 @@ import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallSpeechProviderForVoiceChatAction, HoldToVoiceChatInChatViewAction, ReadChatResponseAloud, StopReadAloud, StopReadChatItemAloud } from './actions/voiceChatActions.js'; import { registerAction2 } from '../../../../platform/actions/common/actions.js'; import { WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; -import { ChatSetupContribution } from '../browser/chatSetup.js'; registerAction2(StartVoiceChatAction); registerAction2(InstallSpeechProviderForVoiceChatAction); @@ -24,4 +23,3 @@ registerAction2(StopReadChatItemAloud); registerAction2(StopReadAloud); registerWorkbenchContribution2(KeywordActivationContribution.ID, KeywordActivationContribution, WorkbenchPhase.AfterRestored); -registerWorkbenchContribution2(ChatSetupContribution.ID, ChatSetupContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index 7fd3e30163f3..53c8ae805113 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -282,7 +282,6 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ // id: 'settings', // title: localize('gettingStarted.settings.title', "Tune your settings"), // description: localize('gettingStarted.settings.description.interpolated', "Customize every aspect of VS Code and your extensions to your liking. Commonly used settings are listed first to get you started.\n{0}", Button(localize('tweakSettings', "Open Settings"), 'command:toSide:workbench.action.openSettings')), - // when: '!config.chat.experimental.offerSetup', // media: { // type: 'svg', altText: 'VS Code Settings', path: 'settings.svg' // }, @@ -291,7 +290,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ // id: 'settingsSync', // title: localize('gettingStarted.settingsSync.title', "Sync settings across devices"), // description: localize('gettingStarted.settingsSync.description.interpolated', "Keep your essential customizations backed up and updated across all your devices.\n{0}", Button(localize('enableSync', "Backup and Sync Settings"), 'command:workbench.userDataSync.actions.turnOn')), - // when: '!config.chat.experimental.offerSetup && syncStatus != uninitialized', + // when: 'syncStatus != uninitialized', // completionEvents: ['onEvent:sync-enabled'], // media: { // type: 'svg', altText: 'The "Turn on Sync" entry in the settings gear menu.', path: 'settingsSync.svg' @@ -318,7 +317,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ // id: 'pickAFolderTask-Mac', // title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"), // description: localize('gettingStarted.setup.OpenFolder.description.interpolated', "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", Button(localize('pickFolder', "Pick a Folder"), 'command:workbench.action.files.openFileFolder')), - // when: '!config.chat.experimental.offerSetup && isMac && workspaceFolderCount == 0', + // when: 'isMac && workspaceFolderCount == 0', // media: { // type: 'svg', altText: 'Explorer view showing buttons for opening folder and cloning repository.', path: 'openFolder.svg' // } @@ -327,7 +326,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ // id: 'pickAFolderTask-Other', // title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"), // description: localize('gettingStarted.setup.OpenFolder.description.interpolated', "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", Button(localize('pickFolder', "Pick a Folder"), 'command:workbench.action.files.openFolder')), - // when: '!config.chat.experimental.offerSetup && !isMac && workspaceFolderCount == 0', + // when: '!isMac && workspaceFolderCount == 0', // media: { // type: 'svg', altText: 'Explorer view showing buttons for opening folder and cloning repository.', path: 'openFolder.svg' // } From 22959031c000aa613a72c6336dda2487e4d514a2 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:23:45 +0100 Subject: [PATCH 0247/3587] Enable dragging of editor from breadcrumbs in single tabs (#236571) drag editor from single tab breadcrumbs --- .../browser/parts/editor/breadcrumbsControl.ts | 16 +++++++++++----- .../browser/parts/editor/breadcrumbsModel.ts | 2 +- .../browser/parts/editor/editorTitleControl.ts | 3 ++- .../parts/editor/singleEditorTabsControl.ts | 3 ++- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 51ca71f42d14..9a2581907c41 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -25,7 +25,7 @@ import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/c import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; -import { fillInSymbolsDragData } from '../../../../platform/dnd/browser/dnd.js'; +import { fillInSymbolsDragData, LocalSelectionTransfer } from '../../../../platform/dnd/browser/dnd.js'; import { FileKind, IFileService, IFileStat } from '../../../../platform/files/common/files.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { InstantiationService } from '../../../../platform/instantiation/common/instantiationService.js'; @@ -39,7 +39,7 @@ import { EditorResourceAccessor, IEditorPartOptions, SideBySideEditor } from '.. import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { ACTIVE_GROUP, ACTIVE_GROUP_TYPE, IEditorService, SIDE_GROUP, SIDE_GROUP_TYPE } from '../../../services/editor/common/editorService.js'; import { IOutline } from '../../../services/outline/browser/outline.js'; -import { fillEditorsDragData } from '../../dnd.js'; +import { DraggedEditorIdentifier, fillEditorsDragData } from '../../dnd.js'; import { DEFAULT_LABELS_CONTAINER, ResourceLabels } from '../../labels.js'; import { BreadcrumbsConfig, IBreadcrumbsService } from './breadcrumbs.js'; import { BreadcrumbsModel, FileElement, OutlineElement2 } from './breadcrumbsModel.js'; @@ -105,7 +105,7 @@ class OutlineItem extends BreadcrumbsItem { this._disposables.add(toDisposable(() => { renderer.disposeTemplate(template); })); if (element instanceof OutlineElement && outline.uri) { - this._disposables.add(this._instantiationService.invokeFunction(accessor => createBreadcrumbDndObserver(accessor, container, element.symbol.name, { symbol: element.symbol, uri: outline.uri! }))); + this._disposables.add(this._instantiationService.invokeFunction(accessor => createBreadcrumbDndObserver(accessor, container, element.symbol.name, { symbol: element.symbol, uri: outline.uri! }, this.model, this.options.dragEditor))); } } } @@ -151,12 +151,12 @@ class FileItem extends BreadcrumbsItem { container.classList.add(FileKind[this.element.kind].toLowerCase()); this._disposables.add(label); - this._disposables.add(this._instantiationService.invokeFunction(accessor => createBreadcrumbDndObserver(accessor, container, basename(this.element.uri), this.element.uri))); + this._disposables.add(this._instantiationService.invokeFunction(accessor => createBreadcrumbDndObserver(accessor, container, basename(this.element.uri), this.element.uri, this.model, this.options.dragEditor))); } } -function createBreadcrumbDndObserver(accessor: ServicesAccessor, container: HTMLElement, label: string, item: URI | { symbol: DocumentSymbol; uri: URI }): IDisposable { +function createBreadcrumbDndObserver(accessor: ServicesAccessor, container: HTMLElement, label: string, item: URI | { symbol: DocumentSymbol; uri: URI }, model: BreadcrumbsModel, dragEditor: boolean): IDisposable { const instantiationService = accessor.get(IInstantiationService); container.draggable = true; @@ -183,6 +183,11 @@ function createBreadcrumbDndObserver(accessor: ServicesAccessor, container: HTML kind: item.symbol.kind }], event); } + + if (dragEditor && model.editor && model.editor?.input) { + const editorTransfer = LocalSelectionTransfer.getInstance(); + editorTransfer.setData([new DraggedEditorIdentifier({ editor: model.editor.input, groupId: model.editor.group.id })], DraggedEditorIdentifier.prototype); + } }); // Create drag image and remove when dropped @@ -209,6 +214,7 @@ export interface IBreadcrumbsControlOptions { readonly showSymbolIcons: boolean; readonly showDecorationColors: boolean; readonly showPlaceholder: boolean; + readonly dragEditor: boolean; readonly widgetStyles?: IBreadcrumbsWidgetStyles; } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts index f49b2301ea50..1c4aaef090ab 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -49,7 +49,7 @@ export class BreadcrumbsModel { constructor( readonly resource: URI, - editor: IEditorPane | undefined, + readonly editor: IEditorPane | undefined, @IConfigurationService configurationService: IConfigurationService, @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, @IOutlineService private readonly _outlineService: IOutlineService, diff --git a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts index ac74f9b77284..65ef9fca411f 100644 --- a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts @@ -90,7 +90,8 @@ export class EditorTitleControl extends Themable { showFileIcons: true, showSymbolIcons: true, showDecorationColors: false, - showPlaceholder: true + showPlaceholder: true, + dragEditor: false, })); // Breadcrumbs enablement & visibility change have an impact on layout diff --git a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts index b8d31c0d171a..a3b16b2d7fcc 100644 --- a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts @@ -60,7 +60,8 @@ export class SingleEditorTabsControl extends EditorTabsControl { showSymbolIcons: true, showDecorationColors: false, widgetStyles: { ...defaultBreadcrumbsWidgetStyles, breadcrumbsBackground: Color.transparent.toString() }, - showPlaceholder: false + showPlaceholder: false, + dragEditor: true, })); this._register(this.breadcrumbsControlFactory.onDidEnablementChange(() => this.handleBreadcrumbsEnablementChange())); titleContainer.classList.toggle('breadcrumbs', Boolean(this.breadcrumbsControl)); From 1b078e1a20c122f11d4df60dd836f9ee2f87c2f6 Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Thu, 19 Dec 2024 12:42:58 +0100 Subject: [PATCH 0248/3587] Disable word wrap in preview editor (#236574) --- .../browser/view/inlineEdits/sideBySideDiff.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index 5f36bec5b336..b34dc44ac1df 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -268,6 +268,8 @@ export class InlineEditsSideBySideDiff extends Disposable { }, readOnly: true, wordWrap: 'off', + wordWrapOverride1: 'off', + wordWrapOverride2: 'off', }, { contributions: [], }, this._editor From b392dd9337e935d639d6a7b18ae02164eda7c283 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 19 Dec 2024 12:51:27 +0100 Subject: [PATCH 0249/3587] simplification: tokenizeLineWithEdit -> tokenizeLinesAt (#236577) --- src/vs/editor/common/core/offsetEdit.ts | 12 ++++++ src/vs/editor/common/model/textModelTokens.ts | 38 ++++++------------- .../common/model/tokenizationTextModelPart.ts | 13 ++++--- src/vs/editor/common/model/tokens.ts | 4 +- .../editor/common/model/treeSitterTokens.ts | 5 +-- .../common/tokenizationTextModelPart.ts | 31 +-------------- src/vs/editor/common/tokens/lineTokens.ts | 4 ++ src/vs/editor/common/tokens/tokenArray.ts | 25 ++++++++++++ .../browser/model/provideInlineCompletions.ts | 18 ++++----- .../browser/view/ghostText/ghostTextView.ts | 11 ++---- 10 files changed, 77 insertions(+), 84 deletions(-) diff --git a/src/vs/editor/common/core/offsetEdit.ts b/src/vs/editor/common/core/offsetEdit.ts index 426f90d22b65..d6886ee5fae2 100644 --- a/src/vs/editor/common/core/offsetEdit.ts +++ b/src/vs/editor/common/core/offsetEdit.ts @@ -228,6 +228,10 @@ export class SingleOffsetEdit { return new SingleOffsetEdit(OffsetRange.emptyAt(offset), text); } + public static replace(range: OffsetRange, text: string): SingleOffsetEdit { + return new SingleOffsetEdit(range, text); + } + constructor( public readonly replaceRange: OffsetRange, public readonly newText: string, @@ -240,6 +244,14 @@ export class SingleOffsetEdit { get isEmpty() { return this.newText.length === 0 && this.replaceRange.length === 0; } + + apply(str: string): string { + return str.substring(0, this.replaceRange.start) + this.newText + str.substring(this.replaceRange.endExclusive); + } + + getRangeAfterApply(): OffsetRange { + return new OffsetRange(this.replaceRange.start, this.replaceRange.start + this.newText.length); + } } /** diff --git a/src/vs/editor/common/model/textModelTokens.ts b/src/vs/editor/common/model/textModelTokens.ts index 85068d38984c..7dbbc93a1310 100644 --- a/src/vs/editor/common/model/textModelTokens.ts +++ b/src/vs/editor/common/model/textModelTokens.ts @@ -17,7 +17,6 @@ import { nullTokenizeEncoded } from '../languages/nullTokenize.js'; import { ITextModel } from '../model.js'; import { FixedArray } from './fixedArray.js'; import { IModelContentChange } from '../textModelEvents.js'; -import { ITokenizeLineWithEditResult, LineEditWithAdditionalLines } from '../tokenizationTextModelPart.js'; import { ContiguousMultilineTokensBuilder } from '../tokens/contiguousMultilineTokensBuilder.js'; import { LineTokens } from '../tokens/lineTokens.js'; @@ -102,38 +101,23 @@ export class TokenizerWithStateStoreAndTextModel } /** assumes state is up to date */ - public tokenizeLineWithEdit(lineNumber: number, edit: LineEditWithAdditionalLines): ITokenizeLineWithEditResult { - const lineStartState = this.getStartState(lineNumber); + public tokenizeLinesAt(lineNumber: number, lines: string[]): LineTokens[] | null { + const lineStartState: IState | null = this.getStartState(lineNumber); if (!lineStartState) { - return { mainLineTokens: null, additionalLines: null }; + return null; } - const curLineContent = this._textModel.getLineContent(lineNumber); - const newLineContent = edit.lineEdit.apply(curLineContent); - - const languageId = this._textModel.getLanguageIdAtPosition(lineNumber, 0); - const result = safeTokenize( - this._languageIdCodec, - languageId, - this.tokenizationSupport, - newLineContent, - true, - lineStartState - ); + const languageId = this._textModel.getLanguageId(); + const result: LineTokens[] = []; - let additionalLines: LineTokens[] | null = null; - if (edit.additionalLines) { - additionalLines = []; - let state = result.endState; - for (const line of edit.additionalLines) { - const r = safeTokenize(this._languageIdCodec, languageId, this.tokenizationSupport, line, true, state); - additionalLines.push(new LineTokens(r.tokens, line, this._languageIdCodec)); - state = r.endState; - } + let state = lineStartState; + for (const line of lines) { + const r = safeTokenize(this._languageIdCodec, languageId, this.tokenizationSupport, line, true, state); + result.push(new LineTokens(r.tokens, line, this._languageIdCodec)); + state = r.endState; } - const mainLineTokens = new LineTokens(result.tokens, newLineContent, this._languageIdCodec); - return { mainLineTokens, additionalLines }; + return result; } public hasAccurateTokensForLine(lineNumber: number): boolean { diff --git a/src/vs/editor/common/model/tokenizationTextModelPart.ts b/src/vs/editor/common/model/tokenizationTextModelPart.ts index 811c24b72561..f51bf1b98bcb 100644 --- a/src/vs/editor/common/model/tokenizationTextModelPart.ts +++ b/src/vs/editor/common/model/tokenizationTextModelPart.ts @@ -25,7 +25,7 @@ import { AbstractTokens, AttachedViewHandler, AttachedViews } from './tokens.js' import { TreeSitterTokens } from './treeSitterTokens.js'; import { ITreeSitterParserService } from '../services/treeSitterParserService.js'; import { IModelContentChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelTokensChangedEvent } from '../textModelEvents.js'; -import { BackgroundTokenizationState, ITokenizationTextModelPart, ITokenizeLineWithEditResult, LineEditWithAdditionalLines } from '../tokenizationTextModelPart.js'; +import { BackgroundTokenizationState, ITokenizationTextModelPart } from '../tokenizationTextModelPart.js'; import { ContiguousMultilineTokens } from '../tokens/contiguousMultilineTokens.js'; import { ContiguousMultilineTokensBuilder } from '../tokens/contiguousMultilineTokensBuilder.js'; import { ContiguousTokensStore } from '../tokens/contiguousTokensStore.js'; @@ -202,8 +202,8 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz return this._tokens.getTokenTypeIfInsertingCharacter(lineNumber, column, character); } - public tokenizeLineWithEdit(lineNumber: number, edit: LineEditWithAdditionalLines): ITokenizeLineWithEditResult { - return this._tokens.tokenizeLineWithEdit(lineNumber, edit); + public tokenizeLinesAt(lineNumber: number, lines: string[]): LineTokens[] | null { + return this._tokens.tokenizeLinesAt(lineNumber, lines); } // #endregion @@ -648,12 +648,13 @@ class GrammarTokens extends AbstractTokens { return this._tokenizer.getTokenTypeIfInsertingCharacter(position, character); } - public tokenizeLineWithEdit(lineNumber: number, edit: LineEditWithAdditionalLines): ITokenizeLineWithEditResult { + + public tokenizeLinesAt(lineNumber: number, lines: string[]): LineTokens[] | null { if (!this._tokenizer) { - return { mainLineTokens: null, additionalLines: null }; + return null; } this.forceTokenization(lineNumber); - return this._tokenizer.tokenizeLineWithEdit(lineNumber, edit); + return this._tokenizer.tokenizeLinesAt(lineNumber, lines); } public get hasTokens(): boolean { diff --git a/src/vs/editor/common/model/tokens.ts b/src/vs/editor/common/model/tokens.ts index 0e4ed56480cf..493cd2d0662a 100644 --- a/src/vs/editor/common/model/tokens.ts +++ b/src/vs/editor/common/model/tokens.ts @@ -13,7 +13,7 @@ import { ILanguageIdCodec } from '../languages.js'; import { IAttachedView } from '../model.js'; import { TextModel } from './textModel.js'; import { IModelContentChangedEvent, IModelTokensChangedEvent } from '../textModelEvents.js'; -import { BackgroundTokenizationState, ITokenizeLineWithEditResult, LineEditWithAdditionalLines } from '../tokenizationTextModelPart.js'; +import { BackgroundTokenizationState } from '../tokenizationTextModelPart.js'; import { LineTokens } from '../tokens/lineTokens.js'; /** @@ -131,7 +131,7 @@ export abstract class AbstractTokens extends Disposable { public abstract getTokenTypeIfInsertingCharacter(lineNumber: number, column: number, character: string): StandardTokenType; - public abstract tokenizeLineWithEdit(lineNumber: number, edit: LineEditWithAdditionalLines): ITokenizeLineWithEditResult; + public abstract tokenizeLinesAt(lineNumber: number, lines: string[]): LineTokens[] | null; public abstract get hasTokens(): boolean; } diff --git a/src/vs/editor/common/model/treeSitterTokens.ts b/src/vs/editor/common/model/treeSitterTokens.ts index 43ab00f0f0f4..f4077388ef08 100644 --- a/src/vs/editor/common/model/treeSitterTokens.ts +++ b/src/vs/editor/common/model/treeSitterTokens.ts @@ -10,7 +10,6 @@ import { TextModel } from './textModel.js'; import { ITreeSitterParserService } from '../services/treeSitterParserService.js'; import { IModelContentChangedEvent } from '../textModelEvents.js'; import { AbstractTokens } from './tokens.js'; -import { ITokenizeLineWithEditResult, LineEditWithAdditionalLines } from '../tokenizationTextModelPart.js'; import { IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js'; export class TreeSitterTokens extends AbstractTokens { @@ -95,9 +94,9 @@ export class TreeSitterTokens extends AbstractTokens { // TODO @alexr00 implement once we have custom parsing and don't just feed in the whole text model value return StandardTokenType.Other; } - public override tokenizeLineWithEdit(lineNumber: number, edit: LineEditWithAdditionalLines): ITokenizeLineWithEditResult { + public override tokenizeLinesAt(lineNumber: number, lines: string[]): LineTokens[] | null { // TODO @alexr00 understand what this is for and implement - return { mainLineTokens: null, additionalLines: null }; + return null; } public override get hasTokens(): boolean { // TODO @alexr00 once we have a token store, implement properly diff --git a/src/vs/editor/common/tokenizationTextModelPart.ts b/src/vs/editor/common/tokenizationTextModelPart.ts index 25e7569b2f9a..6238c606552b 100644 --- a/src/vs/editor/common/tokenizationTextModelPart.ts +++ b/src/vs/editor/common/tokenizationTextModelPart.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { OffsetEdit } from './core/offsetEdit.js'; -import { OffsetRange } from './core/offsetRange.js'; import { Range } from './core/range.js'; import { StandardTokenType } from './encodedTokenAttributes.js'; import { LineTokens } from './tokens/lineTokens.js'; @@ -85,9 +83,10 @@ export interface ITokenizationTextModelPart { getTokenTypeIfInsertingCharacter(lineNumber: number, column: number, character: string): StandardTokenType; /** + * Tokens the lines as if they were inserted at [lineNumber, lineNumber). * @internal */ - tokenizeLineWithEdit(lineNumber: number, edit: LineEditWithAdditionalLines): ITokenizeLineWithEditResult; + tokenizeLinesAt(lineNumber: number, lines: string[]): LineTokens[] | null; getLanguageId(): string; getLanguageIdAtPosition(lineNumber: number, column: number): string; @@ -97,32 +96,6 @@ export interface ITokenizationTextModelPart { readonly backgroundTokenizationState: BackgroundTokenizationState; } -export class LineEditWithAdditionalLines { - public static replace(range: OffsetRange, text: string): LineEditWithAdditionalLines { - return new LineEditWithAdditionalLines( - OffsetEdit.replace(range, text), - null, - ); - } - - constructor( - /** - * The edit for the main line. - */ - readonly lineEdit: OffsetEdit, - - /** - * Full lines appended after the main line. - */ - readonly additionalLines: string[] | null, - ) { } -} - -export interface ITokenizeLineWithEditResult { - readonly mainLineTokens: LineTokens | null; - readonly additionalLines: LineTokens[] | null; -} - export const enum BackgroundTokenizationState { InProgress = 1, Completed = 2, diff --git a/src/vs/editor/common/tokens/lineTokens.ts b/src/vs/editor/common/tokens/lineTokens.ts index 7d2b07476d06..72d94d120d40 100644 --- a/src/vs/editor/common/tokens/lineTokens.ts +++ b/src/vs/editor/common/tokens/lineTokens.ts @@ -203,6 +203,10 @@ export class LineTokens implements IViewLineTokens { return new SliceLineTokens(this, startOffset, endOffset, deltaOffset); } + public sliceZeroCopy(range: OffsetRange): IViewLineTokens { + return this.sliceAndInflate(range.start, range.endExclusive, 0); + } + /** * @pure * @param insertTokens Must be sorted by offset. diff --git a/src/vs/editor/common/tokens/tokenArray.ts b/src/vs/editor/common/tokens/tokenArray.ts index 1b6891f223dd..bc089d1604ae 100644 --- a/src/vs/editor/common/tokens/tokenArray.ts +++ b/src/vs/editor/common/tokens/tokenArray.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { OffsetRange } from '../core/offsetRange.js'; +import { ILanguageIdCodec } from '../languages.js'; +import { LineTokens } from './lineTokens.js'; /** * This class represents a sequence of tokens. @@ -14,6 +16,14 @@ import { OffsetRange } from '../core/offsetRange.js'; * TODO: Make this class more efficient (e.g. by using a Int32Array). */ export class TokenArray { + public static fromLineTokens(lineTokens: LineTokens): TokenArray { + const tokenInfo: TokenInfo[] = []; + for (let i = 0; i < lineTokens.getCount(); i++) { + tokenInfo.push(new TokenInfo(lineTokens.getEndOffset(i) - lineTokens.getStartOffset(i), lineTokens.getMetadata(i))); + } + return TokenArray.create(tokenInfo); + } + public static create(tokenInfo: TokenInfo[]): TokenArray { return new TokenArray(tokenInfo); } @@ -22,6 +32,10 @@ export class TokenArray { private readonly _tokenInfo: TokenInfo[], ) { } + public toLineTokens(lineContent: string, decoder: ILanguageIdCodec): LineTokens { + return LineTokens.createFromTextAndMetadata(this.map((r, t) => ({ text: r.substring(lineContent), metadata: t.metadata })), decoder); + } + public forEach(cb: (range: OffsetRange, tokenInfo: TokenInfo) => void): void { let lengthSum = 0; for (const tokenInfo of this._tokenInfo) { @@ -31,6 +45,17 @@ export class TokenArray { } } + public map(cb: (range: OffsetRange, tokenInfo: TokenInfo) => T): T[] { + const result: T[] = []; + let lengthSum = 0; + for (const tokenInfo of this._tokenInfo) { + const range = new OffsetRange(lengthSum, lengthSum + tokenInfo.length); + result.push(cb(range, tokenInfo)); + lengthSum += tokenInfo.length; + } + return result; + } + public slice(range: OffsetRange): TokenArray { const result: TokenInfo[] = []; let lengthSum = 0; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index befcccca81a2..a240f67595dd 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -11,6 +11,7 @@ import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js import { SetMap } from '../../../../../base/common/map.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { ISingleEditOperation } from '../../../../common/core/editOperation.js'; +import { SingleOffsetEdit } from '../../../../common/core/offsetEdit.js'; import { OffsetRange } from '../../../../common/core/offsetRange.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; @@ -21,7 +22,6 @@ import { ILanguageConfigurationService } from '../../../../common/languages/lang import { ITextModel } from '../../../../common/model.js'; import { fixBracketsInLine } from '../../../../common/model/bracketPairsTextModelPart/fixBrackets.js'; import { TextModelText } from '../../../../common/model/textModelText.js'; -import { LineEditWithAdditionalLines } from '../../../../common/tokenizationTextModelPart.js'; import { SnippetParser, Text } from '../../../snippet/browser/snippetParser.js'; import { getReadonlyEmptyArray } from '../utils.js'; @@ -412,17 +412,15 @@ function getDefaultRange(position: Position, model: ITextModel): Range { } function closeBrackets(text: string, position: Position, model: ITextModel, languageConfigurationService: ILanguageConfigurationService): string { - const lineStart = model.getLineContent(position.lineNumber).substring(0, position.column - 1); - const newLine = lineStart + text; + const currentLine = model.getLineContent(position.lineNumber); + const edit = SingleOffsetEdit.replace(new OffsetRange(position.column - 1, currentLine.length), text); - const edit = LineEditWithAdditionalLines.replace(OffsetRange.ofStartAndLength(position.column - 1, newLine.length - (position.column - 1)), text); - const newTokens = model.tokenization.tokenizeLineWithEdit(position.lineNumber, edit); - const slicedTokens = newTokens?.mainLineTokens?.sliceAndInflate(position.column - 1, newLine.length, 0); - if (!slicedTokens) { + const proposedLineTokens = model.tokenization.tokenizeLinesAt(position.lineNumber, [edit.apply(currentLine)]); + const textTokens = proposedLineTokens?.[0].sliceZeroCopy(edit.getRangeAfterApply()); + if (!textTokens) { return text; } - const newText = fixBracketsInLine(slicedTokens, languageConfigurationService); - - return newText; + const fixedText = fixBracketsInLine(textTokens, languageConfigurationService); + return fixedText; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts index 8bff9b3efbaa..01df05929393 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts @@ -18,7 +18,6 @@ import { Range } from '../../../../../common/core/range.js'; import { StringBuilder } from '../../../../../common/core/stringBuilder.js'; import { ILanguageService } from '../../../../../common/languages/language.js'; import { IModelDeltaDecoration, ITextModel, InjectedTextCursorStops, PositionAffinity } from '../../../../../common/model.js'; -import { LineEditWithAdditionalLines } from '../../../../../common/tokenizationTextModelPart.js'; import { LineTokens } from '../../../../../common/tokens/lineTokens.js'; import { LineDecoration } from '../../../../../common/viewLayout/lineDecorations.js'; import { RenderLineInput, renderViewLine } from '../../../../../common/viewLayout/viewLineRenderer.js'; @@ -63,16 +62,14 @@ export class GhostTextView extends Disposable { const extraClassName = syntaxHighlightingEnabled ? ' syntax-highlighted' : ''; const { inlineTexts, additionalLines, hiddenRange } = computeGhostTextViewData(ghostText, textModel, 'ghost-text' + extraClassName); + const currentLine = textModel.getLineContent(ghostText.lineNumber); const edit = new OffsetEdit(inlineTexts.map(t => SingleOffsetEdit.insert(t.column - 1, t.text))); - const tokens = syntaxHighlightingEnabled ? textModel.tokenization.tokenizeLineWithEdit(ghostText.lineNumber, new LineEditWithAdditionalLines( - edit, - additionalLines.map(l => l.content) - )) : undefined; + const tokens = syntaxHighlightingEnabled ? textModel.tokenization.tokenizeLinesAt(ghostText.lineNumber, [edit.apply(currentLine), ...additionalLines.map(l => l.content)]) : undefined; const newRanges = edit.getNewTextRanges(); - const inlineTextsWithTokens = inlineTexts.map((t, idx) => ({ ...t, tokens: tokens?.mainLineTokens?.getTokensInRange(newRanges[idx]) })); + const inlineTextsWithTokens = inlineTexts.map((t, idx) => ({ ...t, tokens: tokens?.[0]?.getTokensInRange(newRanges[idx]) })); const tokenizedAdditionalLines: LineData[] = additionalLines.map((l, idx) => ({ - content: tokens?.additionalLines?.[idx] ?? LineTokens.createEmpty(l.content, this._languageService.languageIdCodec), + content: tokens?.[idx + 1] ?? LineTokens.createEmpty(l.content, this._languageService.languageIdCodec), decorations: l.decorations, })); From 41a58be380da2ab2211035600ed4859bce05a0bc Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 19 Dec 2024 12:52:16 +0100 Subject: [PATCH 0250/3587] Bug: The cursor must be within a commenting range to add a comment. (#236578) Fixes #236559 --- src/vs/workbench/contrib/comments/browser/commentNode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index f90a81288a3f..89418b813182 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -457,7 +457,7 @@ export class CommentNode extends Disposable { this._register(this._reactionsActionBar); const hasReactionHandler = this.commentService.hasReactionHandler(this.owner); - this.comment.commentReactions!.filter(reaction => !!reaction.count).map(reaction => { + this.comment.commentReactions?.filter(reaction => !!reaction.count).map(reaction => { const action = new ReactionAction(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && (reaction.canEdit || hasReactionHandler) ? 'active' : '', (reaction.canEdit || hasReactionHandler), async () => { try { await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread, this.comment, reaction); From 2ba0803abfb1b749c432e4dc7a8315f98cc799a7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Dec 2024 13:02:00 +0100 Subject: [PATCH 0251/3587] multi root - allow `--remove` for removal of workspace folders (fix #204770) (#236580) --- src/vs/platform/environment/common/argv.ts | 1 + src/vs/platform/environment/node/argv.ts | 1 + .../launch/electron-main/launchMainService.ts | 1 + .../electron-main/nativeHostMainService.ts | 1 + src/vs/platform/window/common/window.ts | 4 +- .../platform/windows/electron-main/windows.ts | 1 + .../electron-main/windowsMainService.ts | 39 ++++++----- src/vs/server/node/server.cli.ts | 1 + src/vs/workbench/api/node/extHostCLIServer.ts | 7 +- src/vs/workbench/electron-sandbox/window.ts | 67 +++++++++++-------- .../host/browser/browserHostService.ts | 21 ++++-- 11 files changed, 91 insertions(+), 53 deletions(-) diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 87825ee7d2c8..e0756ae89462 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -36,6 +36,7 @@ export interface NativeParsedArgs { diff?: boolean; merge?: boolean; add?: boolean; + remove?: boolean; goto?: boolean; 'new-window'?: boolean; 'reuse-window'?: boolean; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 4ef107c79bcd..606aa8b277fa 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -81,6 +81,7 @@ export const OPTIONS: OptionDescriptions> = { 'diff': { type: 'boolean', cat: 'o', alias: 'd', args: ['file', 'file'], description: localize('diff', "Compare two files with each other.") }, 'merge': { type: 'boolean', cat: 'o', alias: 'm', args: ['path1', 'path2', 'base', 'result'], description: localize('merge', "Perform a three-way merge by providing paths for two modified versions of a file, the common origin of both modified versions and the output file to save merge results.") }, 'add': { type: 'boolean', cat: 'o', alias: 'a', args: 'folder', description: localize('add', "Add folder(s) to the last active window.") }, + 'remove': { type: 'boolean', cat: 'o', args: 'folder', description: localize('remove', "Remove folder(s) from the last active window.") }, 'goto': { type: 'boolean', cat: 'o', alias: 'g', args: 'file:line[:character]', description: localize('goto', "Open a file at the path on the specified line and character position.") }, 'new-window': { type: 'boolean', cat: 'o', alias: 'n', description: localize('newWindow', "Force to open a new window.") }, 'reuse-window': { type: 'boolean', cat: 'o', alias: 'r', description: localize('reuseWindow', "Force to open a file or folder in an already opened window.") }, diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 36020f52cfcd..df93721b40e3 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -207,6 +207,7 @@ export class LaunchMainService implements ILaunchMainService { diffMode: args.diff, mergeMode: args.merge, addMode: args.add, + removeMode: args.remove, noRecentEntry: !!args['skip-add-to-recently-opened'], gotoLineMode: args.goto }); diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index c3dbf1ee3ea7..794c5aed5753 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -220,6 +220,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain diffMode: options.diffMode, mergeMode: options.mergeMode, addMode: options.addMode, + removeMode: options.removeMode, gotoLineMode: options.gotoLineMode, noRecentEntry: options.noRecentEntry, waitMarkerFileURI: options.waitMarkerFileURI, diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index d547c37bf50e..3a03481e55a5 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -63,6 +63,7 @@ export interface IOpenWindowOptions extends IBaseOpenWindowsOptions { readonly noRecentEntry?: boolean; readonly addMode?: boolean; + readonly removeMode?: boolean; readonly diffMode?: boolean; readonly mergeMode?: boolean; @@ -71,8 +72,9 @@ export interface IOpenWindowOptions extends IBaseOpenWindowsOptions { readonly waitMarkerFileURI?: URI; } -export interface IAddFoldersRequest { +export interface IAddRemoveFoldersRequest { readonly foldersToAdd: UriComponents[]; + readonly foldersToRemove: UriComponents[]; } interface IOpenedWindow { diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index dd5148103bde..6d14080654d4 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -103,6 +103,7 @@ export interface IOpenConfiguration extends IBaseOpenConfiguration { readonly diffMode?: boolean; readonly mergeMode?: boolean; addMode?: boolean; + removeMode?: boolean; readonly gotoLineMode?: boolean; readonly initialStartup?: boolean; readonly noRecentEntry?: boolean; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index d403852efcc2..8db5f04b7e3e 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -37,7 +37,7 @@ import product from '../../product/common/product.js'; import { IProtocolMainService } from '../../protocol/electron-main/protocol.js'; import { getRemoteAuthority } from '../../remote/common/remoteHosts.js'; import { IStateService } from '../../state/node/state.js'; -import { IAddFoldersRequest, INativeOpenFileRequest, INativeWindowConfiguration, IOpenEmptyWindowOptions, IPath, IPathsToWaitFor, isFileToOpen, isFolderToOpen, isWorkspaceToOpen, IWindowOpenable, IWindowSettings } from '../../window/common/window.js'; +import { IAddRemoveFoldersRequest, INativeOpenFileRequest, INativeWindowConfiguration, IOpenEmptyWindowOptions, IPath, IPathsToWaitFor, isFileToOpen, isFolderToOpen, isWorkspaceToOpen, IWindowOpenable, IWindowSettings } from '../../window/common/window.js'; import { CodeWindow } from './windowImpl.js'; import { IOpenConfiguration, IOpenEmptyConfiguration, IWindowsCountChangedEvent, IWindowsMainService, OpenContext, getLastFocused } from './windows.js'; import { findWindowOnExtensionDevelopmentPath, findWindowOnFile, findWindowOnWorkspaceOrFolder } from './windowsFinder.js'; @@ -287,11 +287,15 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic async open(openConfig: IOpenConfiguration): Promise { this.logService.trace('windowsManager#open'); - if (openConfig.addMode && (openConfig.initialStartup || !this.getLastActiveWindow())) { - openConfig.addMode = false; // Make sure addMode is only enabled if we have an active window + // Make sure addMode/removeMode is only enabled if we have an active window + if ((openConfig.addMode || openConfig.removeMode) && (openConfig.initialStartup || !this.getLastActiveWindow())) { + openConfig.addMode = false; + openConfig.removeMode = false; } const foldersToAdd: ISingleFolderWorkspacePathToOpen[] = []; + const foldersToRemove: ISingleFolderWorkspacePathToOpen[] = []; + const foldersToOpen: ISingleFolderWorkspacePathToOpen[] = []; const workspacesToOpen: IWorkspacePathToOpen[] = []; @@ -311,6 +315,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // When run with --add, take the folders that are to be opened as // folders that should be added to the currently active window. foldersToAdd.push(path); + } else if (openConfig.removeMode) { + // When run with --remove, take the folders that are to be opened as + // folders that should be removed from the currently active window. + foldersToRemove.push(path); } else { foldersToOpen.push(path); } @@ -360,7 +368,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // Open based on config - const { windows: usedWindows, filesOpenedInWindow } = await this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyWindowsWithBackupsToRestore, openOneEmptyWindow, filesToOpen, foldersToAdd); + const { windows: usedWindows, filesOpenedInWindow } = await this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyWindowsWithBackupsToRestore, openOneEmptyWindow, filesToOpen, foldersToAdd, foldersToRemove); this.logService.trace(`windowsManager#open used window count ${usedWindows.length} (workspacesToOpen: ${workspacesToOpen.length}, foldersToOpen: ${foldersToOpen.length}, emptyToRestore: ${emptyWindowsWithBackupsToRestore.length}, openOneEmptyWindow: ${openOneEmptyWindow})`); @@ -463,7 +471,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic emptyToRestore: IEmptyWindowBackupInfo[], openOneEmptyWindow: boolean, filesToOpen: IFilesToOpen | undefined, - foldersToAdd: ISingleFolderWorkspacePathToOpen[] + foldersToAdd: ISingleFolderWorkspacePathToOpen[], + foldersToRemove: ISingleFolderWorkspacePathToOpen[] ): Promise<{ windows: ICodeWindow[]; filesOpenedInWindow: ICodeWindow | undefined }> { // Keep track of used windows and remember @@ -482,12 +491,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Settings can decide if files/folders open in new window or not let { openFolderInNewWindow, openFilesInNewWindow } = this.shouldOpenNewWindow(openConfig); - // Handle folders to add by looking for the last active workspace (not on initial startup) - if (!openConfig.initialStartup && foldersToAdd.length > 0) { - const authority = foldersToAdd[0].remoteAuthority; + // Handle folders to add/remove by looking for the last active workspace (not on initial startup) + if (!openConfig.initialStartup && (foldersToAdd.length > 0 || foldersToRemove.length > 0)) { + const authority = foldersToAdd.at(0)?.remoteAuthority ?? foldersToRemove.at(0)?.remoteAuthority; const lastActiveWindow = this.getLastActiveWindowForAuthority(authority); if (lastActiveWindow) { - addUsedWindow(this.doAddFoldersToExistingWindow(lastActiveWindow, foldersToAdd.map(folderToAdd => folderToAdd.workspace.uri))); + addUsedWindow(this.doAddRemoveFoldersInExistingWindow(lastActiveWindow, foldersToAdd.map(folderToAdd => folderToAdd.workspace.uri), foldersToRemove.map(folderToRemove => folderToRemove.workspace.uri))); } } @@ -671,13 +680,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic windowToFocus.focus(); } - private doAddFoldersToExistingWindow(window: ICodeWindow, foldersToAdd: URI[]): ICodeWindow { - this.logService.trace('windowsManager#doAddFoldersToExistingWindow', { foldersToAdd }); + private doAddRemoveFoldersInExistingWindow(window: ICodeWindow, foldersToAdd: URI[], foldersToRemove: URI[]): ICodeWindow { + this.logService.trace('windowsManager#doAddRemoveFoldersToExistingWindow', { foldersToAdd, foldersToRemove }); window.focus(); // make sure window has focus - const request: IAddFoldersRequest = { foldersToAdd }; - window.sendWhenReady('vscode:addFolders', CancellationToken.None, request); + const request: IAddRemoveFoldersRequest = { foldersToAdd, foldersToRemove }; + window.sendWhenReady('vscode:addRemoveFolders', CancellationToken.None, request); return window; } @@ -764,10 +773,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // Handle the case of multiple folders being opened from CLI while we are - // not in `--add` mode by creating an untitled workspace, only if: + // not in `--add` or `--remove` mode by creating an untitled workspace, only if: // - they all share the same remote authority // - there is no existing workspace to open that matches these folders - if (!openConfig.addMode && isCommandLineOrAPICall) { + if (!openConfig.addMode && !openConfig.removeMode && isCommandLineOrAPICall) { const foldersToOpen = pathsToOpen.filter(path => isSingleFolderWorkspacePathToOpen(path)); if (foldersToOpen.length > 1) { const remoteAuthority = foldersToOpen[0].remoteAuthority; diff --git a/src/vs/server/node/server.cli.ts b/src/vs/server/node/server.cli.ts index 7f20588c3bd4..0535ddd998f7 100644 --- a/src/vs/server/node/server.cli.ts +++ b/src/vs/server/node/server.cli.ts @@ -337,6 +337,7 @@ export async function main(desc: ProductDescription, args: string[]): Promise { - const { fileURIs, folderURIs, forceNewWindow, diffMode, mergeMode, addMode, forceReuseWindow, gotoLineMode, waitMarkerFilePath, remoteAuthority } = data; + const { fileURIs, folderURIs, forceNewWindow, diffMode, mergeMode, addMode, removeMode, forceReuseWindow, gotoLineMode, waitMarkerFilePath, remoteAuthority } = data; const urisToOpen: IWindowOpenable[] = []; if (Array.isArray(folderURIs)) { for (const s of folderURIs) { @@ -144,8 +145,8 @@ export class CLIServerBase { } } const waitMarkerFileURI = waitMarkerFilePath ? URI.file(waitMarkerFilePath) : undefined; - const preferNewWindow = !forceReuseWindow && !waitMarkerFileURI && !addMode; - const windowOpenArgs: IOpenWindowOptions = { forceNewWindow, diffMode, mergeMode, addMode, gotoLineMode, forceReuseWindow, preferNewWindow, waitMarkerFileURI, remoteAuthority }; + const preferNewWindow = !forceReuseWindow && !waitMarkerFileURI && !addMode && !removeMode; + const windowOpenArgs: IOpenWindowOptions = { forceNewWindow, diffMode, mergeMode, addMode, removeMode, gotoLineMode, forceReuseWindow, preferNewWindow, waitMarkerFileURI, remoteAuthority }; this._commands.executeCommand('_remoteCLI.windowOpen', urisToOpen, windowOpenArgs); } diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index c186b45d2ba8..4babd90fe708 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -14,7 +14,7 @@ import { IFileService } from '../../platform/files/common/files.js'; import { EditorResourceAccessor, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors, IResourceDiffEditorInput, IUntypedEditorInput, IEditorPane, isResourceEditorInput, IResourceMergeEditorInput } from '../common/editor.js'; import { IEditorService } from '../services/editor/common/editorService.js'; import { ITelemetryService } from '../../platform/telemetry/common/telemetry.js'; -import { WindowMinimumSize, IOpenFileRequest, IAddFoldersRequest, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, INativeOpenFileRequest, hasNativeTitlebar } from '../../platform/window/common/window.js'; +import { WindowMinimumSize, IOpenFileRequest, IAddRemoveFoldersRequest, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, INativeOpenFileRequest, hasNativeTitlebar } from '../../platform/window/common/window.js'; import { ITitleService } from '../services/title/browser/titleService.js'; import { IWorkbenchThemeService } from '../services/themes/common/workbenchThemeService.js'; import { ApplyZoomTarget, applyZoom } from '../../platform/window/electron-sandbox/window.js'; @@ -84,8 +84,9 @@ export class NativeWindow extends BaseWindow { private readonly customTitleContextMenuDisposable = this._register(new DisposableStore()); - private readonly addFoldersScheduler = this._register(new RunOnceScheduler(() => this.doAddFolders(), 100)); + private readonly addRemoveFoldersScheduler = this._register(new RunOnceScheduler(() => this.doAddRemoveFolders(), 100)); private pendingFoldersToAdd: URI[] = []; + private pendingFoldersToRemove: URI[] = []; private isDocumentedEdited = false; @@ -209,11 +210,11 @@ export class NativeWindow extends BaseWindow { // Support openFiles event for existing and new files ipcRenderer.on('vscode:openFiles', (event: unknown, request: IOpenFileRequest) => { this.onOpenFiles(request); }); - // Support addFolders event if we have a workspace opened - ipcRenderer.on('vscode:addFolders', (event: unknown, request: IAddFoldersRequest) => { this.onAddFoldersRequest(request); }); + // Support addRemoveFolders event for workspace management + ipcRenderer.on('vscode:addRemoveFolders', (event: unknown, request: IAddRemoveFoldersRequest) => this.onAddRemoveFoldersRequest(request)); // Message support - ipcRenderer.on('vscode:showInfoMessage', (event: unknown, message: string) => { this.notificationService.info(message); }); + ipcRenderer.on('vscode:showInfoMessage', (event: unknown, message: string) => this.notificationService.info(message)); // Shell Environment Issue Notifications ipcRenderer.on('vscode:showResolveShellEnvError', (event: unknown, message: string) => { @@ -788,20 +789,6 @@ export class NativeWindow extends BaseWindow { }); } - private async openTunnel(address: string, port: number): Promise { - const remoteAuthority = this.environmentService.remoteAuthority; - const addressProvider: IAddressProvider | undefined = remoteAuthority ? { - getAddress: async (): Promise => { - return (await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority)).authority; - } - } : undefined; - const tunnel = await this.tunnelService.getExistingTunnel(address, port); - if (!tunnel || (typeof tunnel === 'string')) { - return this.tunnelService.openTunnel(addressProvider, address, port); - } - return tunnel; - } - async resolveExternalUri(uri: URI, options?: OpenOptions): Promise { let queryTunnel: RemoteTunnel | string | undefined; if (options?.allowTunneling) { @@ -826,6 +813,7 @@ export class NativeWindow extends BaseWindow { } } } + if (portMappingRequest) { const tunnel = await this.openTunnel(portMappingRequest.address, portMappingRequest.port); if (tunnel && (typeof tunnel !== 'string')) { @@ -861,6 +849,22 @@ export class NativeWindow extends BaseWindow { return undefined; } + private async openTunnel(address: string, port: number): Promise { + const remoteAuthority = this.environmentService.remoteAuthority; + const addressProvider: IAddressProvider | undefined = remoteAuthority ? { + getAddress: async (): Promise => { + return (await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority)).authority; + } + } : undefined; + + const tunnel = await this.tunnelService.getExistingTunnel(address, port); + if (!tunnel || (typeof tunnel === 'string')) { + return this.tunnelService.openTunnel(addressProvider, address, port); + } + + return tunnel; + } + private setupOpenHandlers(): void { // Handle external open() calls @@ -961,27 +965,32 @@ export class NativeWindow extends BaseWindow { //#endregion - private onAddFoldersRequest(request: IAddFoldersRequest): void { + private onAddRemoveFoldersRequest(request: IAddRemoveFoldersRequest): void { // Buffer all pending requests this.pendingFoldersToAdd.push(...request.foldersToAdd.map(folder => URI.revive(folder))); + this.pendingFoldersToRemove.push(...request.foldersToRemove.map(folder => URI.revive(folder))); // Delay the adding of folders a bit to buffer in case more requests are coming - if (!this.addFoldersScheduler.isScheduled()) { - this.addFoldersScheduler.schedule(); + if (!this.addRemoveFoldersScheduler.isScheduled()) { + this.addRemoveFoldersScheduler.schedule(); } } - private doAddFolders(): void { - const foldersToAdd: IWorkspaceFolderCreationData[] = []; - - for (const folder of this.pendingFoldersToAdd) { - foldersToAdd.push(({ uri: folder })); - } + private async doAddRemoveFolders(): Promise { + const foldersToAdd: IWorkspaceFolderCreationData[] = this.pendingFoldersToAdd.map(folder => ({ uri: folder })); + const foldersToRemove = this.pendingFoldersToRemove.slice(0); this.pendingFoldersToAdd = []; + this.pendingFoldersToRemove = []; + + if (foldersToAdd.length) { + await this.workspaceEditingService.addFolders(foldersToAdd); + } - this.workspaceEditingService.addFolders(foldersToAdd); + if (foldersToRemove.length) { + await this.workspaceEditingService.removeFolders(foldersToRemove); + } } private async onOpenFiles(request: INativeOpenFileRequest): Promise { diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index afd2a3d12673..57cb6e482cfc 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -40,6 +40,7 @@ import { coalesce } from '../../../../base/common/arrays.js'; import { mainWindow, isAuxiliaryWindow } from '../../../../base/browser/window.js'; import { isIOS, isMacintosh } from '../../../../base/common/platform.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; +import { URI } from '../../../../base/common/uri.js'; enum HostShutdownReason { @@ -238,7 +239,9 @@ export class BrowserHostService extends Disposable implements IHostService { private async doOpenWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise { const payload = this.preservePayload(false /* not an empty window */, options); const fileOpenables: IFileToOpen[] = []; + const foldersToAdd: IWorkspaceFolderCreationData[] = []; + const foldersToRemove: URI[] = []; for (const openable of toOpen) { openable.label = openable.label || this.getRecentLabel(openable); @@ -246,7 +249,9 @@ export class BrowserHostService extends Disposable implements IHostService { // Folder if (isFolderToOpen(openable)) { if (options?.addMode) { - foldersToAdd.push(({ uri: openable.folderUri })); + foldersToAdd.push({ uri: openable.folderUri }); + } else if (options?.removeMode) { + foldersToRemove.push(openable.folderUri); } else { this.doOpen({ folderUri: openable.folderUri }, { reuse: this.shouldReuse(options, false /* no file */), payload }); } @@ -263,11 +268,17 @@ export class BrowserHostService extends Disposable implements IHostService { } } - // Handle Folders to Add - if (foldersToAdd.length > 0) { - this.withServices(accessor => { + // Handle Folders to add or remove + if (foldersToAdd.length > 0 || foldersToRemove.length > 0) { + this.withServices(async accessor => { const workspaceEditingService: IWorkspaceEditingService = accessor.get(IWorkspaceEditingService); - workspaceEditingService.addFolders(foldersToAdd); + if (foldersToAdd.length > 0) { + await workspaceEditingService.addFolders(foldersToAdd); + } + + if (foldersToRemove.length > 0) { + await workspaceEditingService.removeFolders(foldersToRemove); + } }); } From 225d1ca870a984369bde1a7fcd75f863fc69fee1 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 19 Dec 2024 13:07:00 +0100 Subject: [PATCH 0252/3587] Improved value formatting in observable logging (#236579) * Improved value formatting in observable logging * substr -> substring --- src/vs/base/common/observableInternal/logging.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/vs/base/common/observableInternal/logging.ts b/src/vs/base/common/observableInternal/logging.ts index 0c343b548e47..a6a5bb78a069 100644 --- a/src/vs/base/common/observableInternal/logging.ts +++ b/src/vs/base/common/observableInternal/logging.ts @@ -356,6 +356,14 @@ function formatArray(value: unknown[], availableLen: number): string { } function formatObject(value: object, availableLen: number): string { + if (typeof value.toString === 'function' && value.toString !== Object.prototype.toString) { + const val = value.toString(); + if (val.length <= availableLen) { + return val; + } + return val.substring(0, availableLen - 3) + '...'; + } + let result = '{ '; let first = true; for (const [key, val] of Object.entries(value)) { From 9813e9a4a95b2c1c74fddf911ab09855ee6ba59c Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Thu, 19 Dec 2024 13:59:41 +0100 Subject: [PATCH 0253/3587] Revert "Pressing alt/opt makes the hover temporarily sticky (#236356)" This reverts commit 317d55da7b91e07131403663d8aa6f9499e3a112. --- .../hover/browser/contentHoverController.ts | 39 +++---------------- .../browser/contentHoverWidgetWrapper.ts | 26 +++---------- 2 files changed, 12 insertions(+), 53 deletions(-) diff --git a/src/vs/editor/contrib/hover/browser/contentHoverController.ts b/src/vs/editor/contrib/hover/browser/contentHoverController.ts index 09cb6efcab47..11ddb36fa838 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverController.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverController.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { DECREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID, SHOW_OR_FOCUS_HOVER_ACTION_ID } from './hoverActionIds.js'; -import { IKeyboardEvent, StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; -import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { IKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { ICodeEditor, IEditorMouseEvent, IPartialEditorMouseEvent } from '../../../browser/editorBrowser.js'; import { ConfigurationChangedEvent, EditorOption } from '../../../common/config/editorOptions.js'; import { Range } from '../../../common/core/range.js'; @@ -22,9 +22,6 @@ import { ContentHoverWidgetWrapper } from './contentHoverWidgetWrapper.js'; import './hover.css'; import { Emitter } from '../../../../base/common/event.js'; import { isOnColorDecorator } from '../../colorPicker/browser/hoverColorPicker/hoverColorPicker.js'; -import { KeyCode } from '../../../../base/common/keyCodes.js'; -import { EventType } from '../../../../base/browser/dom.js'; -import { mainWindow } from '../../../../base/browser/window.js'; // sticky hover widget which doesn't disappear on focus out and such const _sticky = false @@ -95,18 +92,11 @@ export class ContentHoverController extends Disposable implements IEditorContrib this._listenersStore.add(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(e))); this._listenersStore.add(this._editor.onMouseUp(() => this._onEditorMouseUp())); this._listenersStore.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(e))); + this._listenersStore.add(this._editor.onKeyDown((e: IKeyboardEvent) => this._onKeyDown(e))); this._listenersStore.add(this._editor.onMouseLeave((e) => this._onEditorMouseLeave(e))); this._listenersStore.add(this._editor.onDidChangeModel(() => this._cancelSchedulerAndHide())); this._listenersStore.add(this._editor.onDidChangeModelContent(() => this._cancelScheduler())); this._listenersStore.add(this._editor.onDidScrollChange((e: IScrollEvent) => this._onEditorScrollChanged(e))); - const keyDownListener = (e: KeyboardEvent) => this._onKeyDown(e); - const keyUpListener = (e: KeyboardEvent) => this._onKeyUp(e); - mainWindow.addEventListener(EventType.KEY_DOWN, keyDownListener); - mainWindow.addEventListener(EventType.KEY_UP, keyUpListener); - this._listenersStore.add(toDisposable(() => { - mainWindow.removeEventListener(EventType.KEY_DOWN, keyDownListener); - mainWindow.removeEventListener(EventType.KEY_UP, keyUpListener); - })); } private _unhookListeners(): void { @@ -165,9 +155,6 @@ export class ContentHoverController extends Disposable implements IEditorContrib if (_sticky) { return; } - if (this._contentWidget) { - this._contentWidget.temporarilySticky = false; - } this.hideContentHover(); } @@ -247,15 +234,11 @@ export class ContentHoverController extends Disposable implements IEditorContrib this.hideContentHover(); } - private _onKeyDown(e: KeyboardEvent): void { - if (!this._contentWidget) { + private _onKeyDown(e: IKeyboardEvent): void { + if (!this._editor.hasModel()) { return; } - const event = new StandardKeyboardEvent(e); - if (event.keyCode === KeyCode.Alt) { - this._contentWidget.temporarilySticky = true; - } - const isPotentialKeyboardShortcut = this._isPotentialKeyboardShortcut(event); + const isPotentialKeyboardShortcut = this._isPotentialKeyboardShortcut(e); if (isPotentialKeyboardShortcut) { return; } @@ -265,16 +248,6 @@ export class ContentHoverController extends Disposable implements IEditorContrib this.hideContentHover(); } - private _onKeyUp(e: KeyboardEvent): void { - if (!this._contentWidget) { - return; - } - const event = new StandardKeyboardEvent(e); - if (event.keyCode === KeyCode.Alt) { - this._contentWidget.temporarilySticky = false; - } - } - private _isPotentialKeyboardShortcut(e: IKeyboardEvent): boolean { if (!this._editor.hasModel() || !this._contentWidget) { return false; diff --git a/src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.ts b/src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.ts index 228e9533d299..ad9a99209670 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.ts @@ -27,7 +27,6 @@ export class ContentHoverWidgetWrapper extends Disposable implements IHoverWidge private _currentResult: ContentHoverResult | null = null; private _renderedContentHover: RenderedContentHover | undefined; - private _temporarilySticky: boolean = false; private readonly _contentHoverWidget: ContentHoverWidget; private readonly _participants: IEditorHoverParticipant[]; @@ -163,25 +162,11 @@ export class ContentHoverWidgetWrapper extends Disposable implements IHoverWidge if (currentHoverResultIsEmpty) { currentHoverResult = null; } - const hoverVisible = this._contentHoverWidget.isVisible; - if (!hoverVisible) { - this._renderResult(currentHoverResult); - } else { - if (this._temporarilySticky) { - return; - } else { - this._renderResult(currentHoverResult); - } - } - } - - private _renderResult(currentHoverResult: ContentHoverResult | null): void { this._currentResult = currentHoverResult; if (this._currentResult) { this._showHover(this._currentResult); } else { - this._contentHoverWidget.hide(); - this._participants.forEach(participant => participant.handleHide?.()); + this._hideHover(); } } @@ -230,6 +215,11 @@ export class ContentHoverWidgetWrapper extends Disposable implements IHoverWidge } } + private _hideHover(): void { + this._contentHoverWidget.hide(); + this._participants.forEach(participant => participant.handleHide?.()); + } + private _getHoverContext(): IEditorHoverContext { const hide = () => { this.hide(); @@ -303,10 +293,6 @@ export class ContentHoverWidgetWrapper extends Disposable implements IHoverWidge } } - public set temporarilySticky(value: boolean) { - this._temporarilySticky = value; - } - public startShowingAtRange(range: Range, mode: HoverStartMode, source: HoverStartSource, focus: boolean): void { this._startShowingOrUpdateHover(new HoverRangeAnchor(0, range, undefined, undefined), mode, source, focus, null); } From aea3ab47f5d820198b0f68d34b3846b483f9e809 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 19 Dec 2024 14:07:44 +0100 Subject: [PATCH 0254/3587] Fix compilation errors --- .../editor/contrib/hover/browser/contentHoverController.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/contrib/hover/browser/contentHoverController.ts b/src/vs/editor/contrib/hover/browser/contentHoverController.ts index 11ddb36fa838..acbabe6a83e8 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverController.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverController.ts @@ -22,6 +22,7 @@ import { ContentHoverWidgetWrapper } from './contentHoverWidgetWrapper.js'; import './hover.css'; import { Emitter } from '../../../../base/common/event.js'; import { isOnColorDecorator } from '../../colorPicker/browser/hoverColorPicker/hoverColorPicker.js'; +import { KeyCode } from '../../../../base/common/keyCodes.js'; // sticky hover widget which doesn't disappear on focus out and such const _sticky = false @@ -235,14 +236,14 @@ export class ContentHoverController extends Disposable implements IEditorContrib } private _onKeyDown(e: IKeyboardEvent): void { - if (!this._editor.hasModel()) { + if (!this._contentWidget) { return; } const isPotentialKeyboardShortcut = this._isPotentialKeyboardShortcut(e); if (isPotentialKeyboardShortcut) { return; } - if (this._contentWidget.isFocused && event.keyCode === KeyCode.Tab) { + if (this._contentWidget.isFocused && e.keyCode === KeyCode.Tab) { return; } this.hideContentHover(); From 5385e31b3f7883a5524deec90f1df5faa49c4287 Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Thu, 19 Dec 2024 14:10:37 +0100 Subject: [PATCH 0255/3587] Trigger inline edits on paste (#236584) --- src/vs/editor/browser/observableCodeEditor.ts | 13 ++++++++++++- .../controller/inlineCompletionsController.ts | 6 ++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts index d8607ae319aa..3633d4cac45c 100644 --- a/src/vs/editor/browser/observableCodeEditor.ts +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -14,7 +14,7 @@ import { Selection } from '../common/core/selection.js'; import { ICursorSelectionChangedEvent } from '../common/cursorEvents.js'; import { IModelDeltaDecoration, ITextModel } from '../common/model.js'; import { IModelContentChangedEvent } from '../common/textModelEvents.js'; -import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IOverlayWidget, IOverlayWidgetPosition } from './editorBrowser.js'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IOverlayWidget, IOverlayWidgetPosition, IPasteEvent } from './editorBrowser.js'; import { Point } from './point.js'; /** @@ -94,6 +94,16 @@ export class ObservableCodeEditor extends Disposable { } })); + this._register(this.editor.onDidPaste((e) => { + this._beginUpdate(); + try { + this._forceUpdate(); + this.onDidPaste.trigger(this._currentTransaction, e); + } finally { + this._endUpdate(); + } + })); + this._register(this.editor.onDidChangeModelContent(e => { this._beginUpdate(); try { @@ -213,6 +223,7 @@ export class ObservableCodeEditor extends Disposable { public readonly cursorLineNumber = derived(this, reader => this.cursorPosition.read(reader)?.lineNumber ?? null); public readonly onDidType = observableSignal(this); + public readonly onDidPaste = observableSignal(this); public readonly scrollTop = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollTop()); public readonly scrollLeft = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollLeft()); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index 7c7e44957197..79fdfb85afde 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -140,6 +140,12 @@ export class InlineCompletionsController extends Disposable { } })); + this._register(runOnChange(this._editorObs.onDidPaste, (_value, _changes) => { + if (this._enabled.get()) { + this.model.get()?.trigger(); + } + })); + this._register(this._commandService.onDidExecuteCommand((e) => { // These commands don't trigger onDidType. const commands = new Set([ From 05519998aeb60a492478948e6e196586a24c04c6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 19 Dec 2024 05:10:51 -0800 Subject: [PATCH 0256/3587] Ignore bg terminals for confirmOnExit Fixes #235575 --- .../contrib/terminal/browser/terminalService.ts | 10 +++++++--- .../contrib/terminal/common/terminalConfiguration.ts | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 70d2aa007ec2..698559d3b2ce 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -95,6 +95,10 @@ export class TerminalService extends Disposable implements ITerminalService { get instances(): ITerminalInstance[] { return this._terminalGroupService.instances.concat(this._terminalEditorService.instances).concat(this._backgroundedTerminalInstances); } + /** Gets all non-background terminals. */ + get foregroundInstances(): ITerminalInstance[] { + return this._terminalGroupService.instances.concat(this._terminalEditorService.instances); + } get detachedInstances(): Iterable { return this._detachedXterms; } @@ -417,7 +421,6 @@ export class TerminalService extends Disposable implements ITerminalService { if (instance.target !== TerminalLocation.Editor && instance.hasChildProcesses && (this._terminalConfigurationService.config.confirmOnKill === 'panel' || this._terminalConfigurationService.config.confirmOnKill === 'always')) { - const veto = await this._showTerminalCloseConfirmation(true); if (veto) { return; @@ -904,10 +907,11 @@ export class TerminalService extends Disposable implements ITerminalService { protected async _showTerminalCloseConfirmation(singleTerminal?: boolean): Promise { let message: string; - if (this.instances.length === 1 || singleTerminal) { + const foregroundInstances = this.foregroundInstances; + if (foregroundInstances.length === 1 || singleTerminal) { message = nls.localize('terminalService.terminalCloseConfirmationSingular', "Do you want to terminate the active terminal session?"); } else { - message = nls.localize('terminalService.terminalCloseConfirmationPlural', "Do you want to terminate the {0} active terminal sessions?", this.instances.length); + message = nls.localize('terminalService.terminalCloseConfirmationPlural', "Do you want to terminate the {0} active terminal sessions?", foregroundInstances.length); } const { confirmed } = await this._dialogService.confirm({ type: 'warning', diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index beea86681fbc..b90a67cbbc63 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -366,7 +366,7 @@ const terminalConfiguration: IConfigurationNode = { default: 'never' }, [TerminalSettingId.ConfirmOnKill]: { - description: localize('terminal.integrated.confirmOnKill', "Controls whether to confirm killing terminals when they have child processes. When set to editor, terminals in the editor area will be marked as changed when they have child processes. Note that child process detection may not work well for shells like Git Bash which don't run their processes as child processes of the shell."), + description: localize('terminal.integrated.confirmOnKill', "Controls whether to confirm killing terminals when they have child processes. When set to editor, terminals in the editor area will be marked as changed when they have child processes. Note that child process detection may not work well for shells like Git Bash which don't run their processes as child processes of the shell. Background terminals like those launched by some extensions will not trigger the confirmation."), type: 'string', enum: ['never', 'editor', 'panel', 'always'], enumDescriptions: [ From 4051adf0a86f9ec6543be6b27840614c816a7323 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:10:05 +0100 Subject: [PATCH 0257/3587] Fix TypeError for undefined isShowingFilterResults (#236590) fix #236521 --- src/vs/workbench/contrib/files/browser/views/explorerView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index e77296ffed93..1c7188375010 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -964,7 +964,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { } hasPhantomElements(): boolean { - return this.findProvider.isShowingFilterResults(); + return !!this.findProvider?.isShowingFilterResults(); } override dispose(): void { From 25b88b7e4a8f1a96487896ca8b5fe8aa045af54e Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:46:46 +0100 Subject: [PATCH 0258/3587] Git - fix encoding issue with stage selected ranges (#236484) --- extensions/git/src/git.ts | 4 ++-- extensions/git/src/repository.ts | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index f2b9100d79a1..d6a6bd52cfc3 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1657,9 +1657,9 @@ export class Repository { await this.exec(args); } - async stage(path: string, data: string): Promise { + async stage(path: string, data: string, encoding: string): Promise { const child = this.stream(['hash-object', '--stdin', '-w', '--path', sanitizePath(path)], { stdio: [null, null, null] }); - child.stdin!.end(data, 'utf8'); + child.stdin!.end(iconv.encode(data, encoding)); const { exitCode, stdout } = await exec(child); const hash = stdout.toString('utf8'); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index ac2649242f2f..0c4b50cd5e5c 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -7,6 +7,7 @@ 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'; @@ -24,6 +25,7 @@ import { StatusBarCommands } from './statusbar'; import { toGitUri } from './uri'; import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; +import { detectEncoding } from './encoding'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -1215,7 +1217,17 @@ export class Repository implements Disposable { async stage(resource: Uri, contents: string): Promise { const path = relativePath(this.repository.root, resource.fsPath).replace(/\\/g, '/'); await this.run(Operation.Stage, async () => { - await this.repository.stage(path, contents); + const configFiles = workspace.getConfiguration('files', Uri.file(resource.fsPath)); + let encoding = configFiles.get('encoding') ?? 'utf8'; + const autoGuessEncoding = configFiles.get('autoGuessEncoding') === true; + const candidateGuessEncodings = configFiles.get('candidateGuessEncodings') ?? []; + + if (autoGuessEncoding) { + encoding = detectEncoding(Buffer.from(contents), candidateGuessEncodings) ?? encoding; + } + + encoding = iconv.encodingExists(encoding) ? encoding : 'utf8'; + await this.repository.stage(path, contents, encoding); this._onDidChangeOriginalResource.fire(resource); this.closeDiffEditors([], [...resource.fsPath]); From 986871f71ae46e0b7b27362f1456986abc7a1fbe Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 19 Dec 2024 16:20:19 +0100 Subject: [PATCH 0259/3587] Proper fix for #236537 (#236596) - Support installEveryWhere option by installing the extension only on servers on which it is not installed --- .../browser/extensionsWorkbenchService.ts | 47 ++++++++++++++--- .../contrib/extensions/common/extensions.ts | 1 + .../common/extensionManagement.ts | 3 +- .../common/extensionManagementService.ts | 50 +++++++++++++++---- .../test/browser/workbenchTestServices.ts | 3 +- 5 files changed, 84 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 0967e7d34c7f..33692adf1485 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -2342,35 +2342,69 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } else { let installableInfo: IExtensionInfo | undefined; let gallery: IGalleryExtension | undefined; + + // Install by id if (isString(arg)) { extension = this.local.find(e => areSameExtensions(e.identifier, { id: arg })); if (!extension?.isBuiltin) { installableInfo = { id: arg, version: installOptions.version, preRelease: installOptions.installPreReleaseVersion ?? this.preferPreReleases }; } - } else if (arg.gallery) { + } + // Install by gallery + else if (arg.gallery) { extension = arg; gallery = arg.gallery; if (installOptions.version && installOptions.version !== gallery?.version) { installableInfo = { id: extension.identifier.id, version: installOptions.version }; } - } else if (arg.resourceExtension) { + } + // Install by resource + else if (arg.resourceExtension) { extension = arg; installable = arg.resourceExtension; } + if (installableInfo) { const targetPlatform = extension?.server ? await extension.server.extensionManagementService.getTargetPlatform() : undefined; gallery = (await this.galleryService.getExtensions([installableInfo], { targetPlatform }, CancellationToken.None)).at(0); } + if (!extension && gallery) { extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery, undefined); (extension).setExtensionsControlManifest(await this.extensionManagementService.getExtensionsControlManifest()); } + if (extension?.isMalicious) { throw new Error(nls.localize('malicious', "This extension is reported to be problematic.")); } - // TODO: @sandy081 - Install the extension only on servers where it is not installed - // Do not install if requested to enable and extension is already installed - if (installOptions.installEverywhere || !(installOptions.enable && extension?.local)) { + + if (gallery) { + // If requested to install everywhere + // then install the extension in all the servers where it is not installed + if (installOptions.installEverywhere) { + installOptions.servers = []; + const installableServers = await this.extensionManagementService.getInstallableServers(gallery); + for (const extensionsServer of this.extensionsServers) { + if (installableServers.includes(extensionsServer.server) && !extensionsServer.local.find(e => areSameExtensions(e.identifier, gallery.identifier))) { + installOptions.servers.push(extensionsServer.server); + } + } + } + // If requested to enable and extension is already installed + // Check if the extension is disabled because of extension kind + // If so, install the extension in the server that is compatible. + else if (installOptions.enable && extension?.local) { + installOptions.servers = []; + if (extension.enablementState === EnablementState.DisabledByExtensionKind) { + const [installableServer] = await this.extensionManagementService.getInstallableServers(gallery); + if (installableServer) { + installOptions.servers.push(installableServer); + } + } + } + } + + if (!installOptions.servers || installOptions.servers.length) { if (!installable) { if (!gallery) { const id = isString(arg) ? arg : (arg).identifier.id; @@ -2734,8 +2768,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private installFromGallery(extension: IExtension, gallery: IGalleryExtension, installOptions?: InstallExtensionOptions): Promise { installOptions = installOptions ?? {}; installOptions.pinned = extension.local?.pinned || !this.shouldAutoUpdateExtension(extension); - // TODO: @sandy081 - Install the extension only on servers where it is not installed - if (!installOptions.installEverywhere && extension.local) { + if (extension.local && !installOptions.servers) { installOptions.productVersion = this.getProductVersion(); installOptions.operation = InstallOperation.Update; return this.extensionManagementService.updateFromGallery(gallery, extension.local, installOptions); diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 4cd9d5ea1d4e..e8f26f634cab 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -112,6 +112,7 @@ export interface InstallExtensionOptions extends IWorkbenchInstallOptions { version?: string; justification?: string | { reason: string; action: string }; enable?: boolean; + installEverywhere?: boolean; } export interface IExtensionsNotification { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 2bc9bc6e7938..1deee908d89b 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -60,7 +60,7 @@ export type DidUninstallExtensionOnServerEvent = DidUninstallExtensionEvent & { export type DidChangeProfileForServerEvent = DidChangeProfileEvent & { server: IExtensionManagementServer }; export interface IWorkbenchInstallOptions extends InstallOptions { - readonly installEverywhere?: boolean; + servers?: IExtensionManagementServer[]; } export const IWorkbenchExtensionManagementService = refineServiceDecorator(IProfileAwareExtensionManagementService); @@ -84,6 +84,7 @@ export interface IWorkbenchExtensionManagementService extends IProfileAwareExten canInstall(extension: IGalleryExtension | IResourceExtension): Promise; + getInstallableServers(extension: IGalleryExtension): Promise; installVSIX(location: URI, manifest: IExtensionManifest, installOptions?: InstallOptions): Promise; installFromGallery(gallery: IGalleryExtension, installOptions?: IWorkbenchInstallOptions): Promise; installFromLocation(location: URI): Promise; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index bc0a4687128c..7bc18909d7f1 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -554,6 +554,14 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } } + async getInstallableServers(gallery: IGalleryExtension): Promise { + const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None); + if (!manifest) { + return Promise.reject(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", gallery.displayName || gallery.name)); + } + return this.getInstallableExtensionManagementServers(manifest); + } + private async uninstallExtensionFromWorkspace(extension: ILocalExtension): Promise { if (!extension.isWorkspaceScoped) { throw new Error('The extension is not a workspace extension'); @@ -606,11 +614,25 @@ export class ExtensionManagementService extends Disposable implements IWorkbench const servers: IExtensionManagementServer[] = []; - // Install everywhere if asked to install everywhere or if the extension is a language pack - if (installOptions?.installEverywhere || isLanguagePackExtension(manifest)) { + if (installOptions?.servers?.length) { + const installableServers = this.getInstallableExtensionManagementServers(manifest); + servers.push(...installOptions.servers); + for (const server of servers) { + if (!installableServers.includes(server)) { + const error = new Error(localize('cannot be installed in server', "Cannot install the '{0}' extension because it is not available in the '{1}' setup.", gallery.displayName || gallery.name, server.label)); + error.name = ExtensionManagementErrorCode.Unsupported; + throw error; + } + } + } + + // Language packs should be installed on both local and remote servers + else if (isLanguagePackExtension(manifest)) { servers.push(...this.servers.filter(server => server !== this.extensionManagementServerService.webExtensionManagementServer)); - } else { - const server = this.getExtensionManagementServerToInstall(manifest); + } + + else { + const [server] = this.getInstallableExtensionManagementServers(manifest); if (server) { servers.push(server); } @@ -633,27 +655,33 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return servers; } - private getExtensionManagementServerToInstall(manifest: IExtensionManifest): IExtensionManagementServer | null { + private getInstallableExtensionManagementServers(manifest: IExtensionManifest): IExtensionManagementServer[] { // Only local server if (this.servers.length === 1 && this.extensionManagementServerService.localExtensionManagementServer) { - return this.extensionManagementServerService.localExtensionManagementServer; + return [this.extensionManagementServerService.localExtensionManagementServer]; } + const servers: IExtensionManagementServer[] = []; + const extensionKind = this.extensionManifestPropertiesService.getExtensionKind(manifest); for (const kind of extensionKind) { if (kind === 'ui' && this.extensionManagementServerService.localExtensionManagementServer) { - return this.extensionManagementServerService.localExtensionManagementServer; + servers.push(this.extensionManagementServerService.localExtensionManagementServer); } if (kind === 'workspace' && this.extensionManagementServerService.remoteExtensionManagementServer) { - return this.extensionManagementServerService.remoteExtensionManagementServer; + servers.push(this.extensionManagementServerService.remoteExtensionManagementServer); } if (kind === 'web' && this.extensionManagementServerService.webExtensionManagementServer) { - return this.extensionManagementServerService.webExtensionManagementServer; + servers.push(this.extensionManagementServerService.webExtensionManagementServer); } } - // Local server can accept any extension. So return local server if not compatible server found. - return this.extensionManagementServerService.localExtensionManagementServer; + // Local server can accept any extension. + if (this.extensionManagementServerService.localExtensionManagementServer && !servers.includes(this.extensionManagementServerService.localExtensionManagementServer)) { + servers.push(this.extensionManagementServerService.localExtensionManagementServer); + } + + return servers; } private isExtensionsSyncEnabled(): boolean { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index a914f4586d4c..259de7b24b0b 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -160,7 +160,7 @@ import { ILayoutOffsetInfo } from '../../../platform/layout/browser/layoutServic import { IUserDataProfile, IUserDataProfilesService, toUserDataProfile, UserDataProfilesService } from '../../../platform/userDataProfile/common/userDataProfile.js'; import { UserDataProfileService } from '../../services/userDataProfile/common/userDataProfileService.js'; import { IUserDataProfileService } from '../../services/userDataProfile/common/userDataProfile.js'; -import { EnablementState, IResourceExtension, IScannedExtension, IWebExtensionsScannerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../services/extensionManagement/common/extensionManagement.js'; +import { EnablementState, IExtensionManagementServer, IResourceExtension, IScannedExtension, IWebExtensionsScannerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../services/extensionManagement/common/extensionManagement.js'; import { ILocalExtension, IGalleryExtension, InstallOptions, UninstallOptions, IExtensionsControlManifest, IGalleryMetadata, IExtensionManagementParticipant, Metadata, InstallExtensionResult, InstallExtensionInfo, UninstallExtensionInfo } from '../../../platform/extensionManagement/common/extensionManagement.js'; import { Codicon } from '../../../base/common/codicons.js'; import { IRemoteExtensionsScannerService } from '../../../platform/remote/common/remoteExtensionsScanner.js'; @@ -2246,6 +2246,7 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens installResourceExtension(): Promise { throw new Error('Method not implemented.'); } getExtensions(): Promise { throw new Error('Method not implemented.'); } resetPinnedStateForAllUserExtensions(pinned: boolean): Promise { throw new Error('Method not implemented.'); } + getInstallableServers(extension: IGalleryExtension): Promise { throw new Error('Method not implemented.'); } } export class TestUserDataProfileService implements IUserDataProfileService { From f990dfb385d69e035810e3bd3102d301dd2157b7 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:27:08 +0100 Subject: [PATCH 0260/3587] Git - expose `env` through the extension API (#236598) --- extensions/git/src/api/api1.ts | 11 ++++++++++- extensions/git/src/git.ts | 5 +++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 8cc0b99f1132..e559c0cb8073 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -72,7 +72,6 @@ export class ApiRepositoryUIState implements RepositoryUIState { } export class ApiRepository implements Repository { - #repository: BaseRepository; readonly rootUri: Uri; @@ -311,9 +310,19 @@ export class ApiRepository implements Repository { export class ApiGit implements Git { #model: Model; + private _env: { [key: string]: string } | undefined; + constructor(model: Model) { this.#model = model; } get path(): string { return this.#model.git.path; } + + get env(): { [key: string]: string } { + if (this._env === undefined) { + this._env = Object.freeze(this.#model.git.env); + } + + return this._env; + } } export class ApiImpl implements API { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index d6a6bd52cfc3..87ea23e3a853 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -315,7 +315,7 @@ export interface IGitOptions { gitPath: string; userAgent: string; version: string; - env?: any; + env?: { [key: string]: string }; } function getGitErrorCode(stderr: string): string | undefined { @@ -369,7 +369,8 @@ export class Git { readonly path: string; readonly userAgent: string; readonly version: string; - private env: any; + readonly env: { [key: string]: string }; + private commandsToLog: string[] = []; private _onOutput = new EventEmitter(); From ffbf5a7f28058787dc6476ad436b550412fce981 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:51:06 +0100 Subject: [PATCH 0261/3587] Use indices for queue processing instead of Array.shift() (#236601) * Use indices instead of Array.shift() * :lipstick: --- src/vs/base/browser/ui/tree/abstractTree.ts | 8 ++++---- src/vs/base/browser/ui/tree/asyncDataTree.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index b8144e58c23f..09ce418bda94 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -3080,16 +3080,16 @@ export abstract class AbstractTree implements IDisposable } const root = this.model.getNode(); - const queue = [root]; + const stack = [root]; - while (queue.length > 0) { - const node = queue.shift()!; + while (stack.length > 0) { + const node = stack.pop()!; if (node !== root && node.collapsible) { state.expanded[getId(node.element)] = node.collapsed ? 0 : 1; } - insertInto(queue, queue.length, node.children); + insertInto(stack, stack.length, node.children); } return state; diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 9d0d2cbf4b25..dd6059051f08 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -24,7 +24,7 @@ import { isIterable } from '../../../common/types.js'; import { CancellationToken, CancellationTokenSource } from '../../../common/cancellation.js'; import { IContextViewProvider } from '../contextview/contextview.js'; import { FuzzyScore } from '../../../common/filters.js'; -import { splice } from '../../../common/arrays.js'; +import { insertInto, splice } from '../../../common/arrays.js'; import { localize } from '../../../../nls.js'; interface IAsyncDataTreeNode { @@ -1350,7 +1350,7 @@ export class AsyncDataTree implements IDisposable expanded.push(getId(node.element!.element as T)); } - stack.push(...node.children); + insertInto(stack, stack.length, node.children); } return { focus, selection, expanded, scrollTop: this.scrollTop }; From 613ad56601c62ff8fea04a62444217470fbc496d Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 19 Dec 2024 17:03:58 +0100 Subject: [PATCH 0262/3587] code cleanup (#236595) * code cleanup * Fix CI --- .../view/inlineEdits/gutterIndicatorView.ts | 82 +++++++++++-------- .../browser/view/inlineEdits/utils.ts | 11 ++- 2 files changed, 53 insertions(+), 40 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index 936ab8b56b3b..bcfa7d7463b7 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -40,21 +40,36 @@ export const inlineEditIndicatorBackground = registerColor( export class InlineEditsGutterIndicator extends Disposable { - private readonly _state = derived(reader => { - const range = mapOutFalsy(this._originalRange).read(reader); - if (!range) { - return undefined; - } + constructor( + private readonly _editorObs: ObservableCodeEditor, + private readonly _originalRange: IObservable, + private readonly _model: IObservable, + ) { + super(); + + this._register(this._editorObs.createOverlayWidget({ + domNode: this._indicator.element, + position: constObservable(null), + allowEditorOverflow: false, + minContentWidthInPx: constObservable(0), + })); + } + + private readonly _originalRangeObs = mapOutFalsy(this._originalRange); + private readonly _state = derived(reader => { + const range = this._originalRangeObs.read(reader); + if (!range) { return undefined; } return { range, lineOffsetRange: this._editorObs.observeLineOffsetRange(range, this._store), }; }); - private _stickyScrollController = StickyScrollController.get(this._editorObs.editor); - private readonly _stickyScrollHeight = this._stickyScrollController ? observableFromEvent(this._stickyScrollController.onDidChangeStickyScrollHeight, () => this._stickyScrollController!.stickyScrollWidgetHeight) : constObservable(0); - + private readonly _stickyScrollController = StickyScrollController.get(this._editorObs.editor); + private readonly _stickyScrollHeight = this._stickyScrollController + ? observableFromEvent(this._stickyScrollController.onDidChangeStickyScrollHeight, () => this._stickyScrollController!.stickyScrollWidgetHeight) + : constObservable(0); private readonly _layout = derived(reader => { const s = this._state.read(reader); @@ -85,13 +100,13 @@ export class InlineEditsGutterIndicator extends Disposable { return { rect, iconRect, - mode: (iconRect.top === targetRect.top ? 'right' as const + arrowDirection: (iconRect.top === targetRect.top ? 'right' as const : iconRect.top > targetRect.top ? 'top' as const : 'bottom' as const), docked: rect.containsRect(iconRect) && viewPortWithStickyScroll.containsRect(iconRect), }; }); - private readonly _mode = derived(this, reader => { + private readonly _tabAction = derived(this, reader => { const m = this._model.read(reader); if (m && m.tabShouldAcceptInlineEdit.read(reader)) { return 'accept' as const; } if (m && m.tabShouldJumpToInlineEdit.read(reader)) { return 'jump' as const; } @@ -135,16 +150,20 @@ export class InlineEditsGutterIndicator extends Disposable { cursor: 'pointer', zIndex: '1000', position: 'absolute', - backgroundColor: this._mode.map(v => ({ - inactive: 'var(--vscode-inlineEdit-gutterIndicator-secondaryBackground)', - jump: 'var(--vscode-inlineEdit-gutterIndicator-primaryBackground)', - accept: 'var(--vscode-inlineEdit-gutterIndicator-successfulBackground)', - }[v])), - '--vscodeIconForeground': this._mode.map(v => ({ - inactive: 'var(--vscode-inlineEdit-gutterIndicator-secondaryForeground)', - jump: 'var(--vscode-inlineEdit-gutterIndicator-primaryForeground)', - accept: 'var(--vscode-inlineEdit-gutterIndicator-successfulForeground)', - }[v])), + backgroundColor: this._tabAction.map(v => { + switch (v) { + case 'inactive': return 'var(--vscode-inlineEdit-gutterIndicator-secondaryBackground)'; + case 'jump': return 'var(--vscode-inlineEdit-gutterIndicator-primaryBackground)'; + case 'accept': return 'var(--vscode-inlineEdit-gutterIndicator-successfulBackground)'; + } + }), + '--vscodeIconForeground': this._tabAction.map(v => { + switch (v) { + case 'inactive': return 'var(--vscode-inlineEdit-gutterIndicator-secondaryForeground)'; + case 'jump': return 'var(--vscode-inlineEdit-gutterIndicator-primaryForeground)'; + case 'accept': return 'var(--vscode-inlineEdit-gutterIndicator-successfulForeground)'; + } + }), borderRadius: '4px', display: 'flex', justifyContent: 'center', @@ -154,7 +173,13 @@ export class InlineEditsGutterIndicator extends Disposable { }, [ n.div({ style: { - rotate: l.map(l => ({ right: '0deg', bottom: '90deg', top: '-90deg' }[l.mode])), + rotate: l.map(l => { + switch (l.arrowDirection) { + case 'right': return '0deg'; + case 'bottom': return '90deg'; + case 'top': return '-90deg'; + } + }), transition: 'rotate 0.2s ease-in-out', } }, [ @@ -162,21 +187,6 @@ export class InlineEditsGutterIndicator extends Disposable { ]) ]), ])).keepUpdated(this._store); - - constructor( - private readonly _editorObs: ObservableCodeEditor, - private readonly _originalRange: IObservable, - private readonly _model: IObservable, - ) { - super(); - - this._register(this._editorObs.createOverlayWidget({ - domNode: this._indicator.element, - position: constObservable(null), - allowEditorOverflow: false, - minContentWidthInPx: constObservable(0), - })); - } } function rectToProps(fn: (reader: IReader) => Rect): any { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts index 73b9cbb56186..b99b0478bdcd 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts @@ -423,20 +423,23 @@ type ElementAttributeKeys = Partial<{ ? never : T[K] extends object ? ElementAttributeKeys - : Value + : Value }>; -export function mapOutFalsy(obs: IObservable): IObservable | undefined | null | false> { +type RemoveFalsy = T extends false | undefined | null ? never : T; +type Falsy = T extends false | undefined | null ? T : never; + +export function mapOutFalsy(obs: IObservable): IObservable> | Falsy> { const nonUndefinedObs = derivedObservableWithCache(undefined, (reader, lastValue) => obs.read(reader) || lastValue); return derived(reader => { nonUndefinedObs.read(reader); const val = obs.read(reader); if (!val) { - return undefined; + return undefined as Falsy; } - return nonUndefinedObs as IObservable; + return nonUndefinedObs as IObservable>; }); } From eb3b58cbdf14015a19c2b53516dffb50f06a2e00 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 19 Dec 2024 17:45:44 +0100 Subject: [PATCH 0263/3587] closes #232780 --- .../browser/actions/layoutActions.ts | 17 +------- .../browser/parts/titlebar/titlebarActions.ts | 41 ++++++++++++++++++- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 75253e235de8..cdd04eab0358 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -22,7 +22,7 @@ import { IPaneCompositePartService } from '../../services/panecomposite/browser/ import { ToggleAuxiliaryBarAction } from '../parts/auxiliarybar/auxiliaryBarActions.js'; import { TogglePanelAction } from '../parts/panel/panelActions.js'; import { ICommandService } from '../../../platform/commands/common/commands.js'; -import { AuxiliaryBarVisibleContext, PanelAlignmentContext, PanelVisibleContext, SideBarVisibleContext, FocusedViewContext, InEditorZenModeContext, IsMainEditorCenteredLayoutContext, MainEditorAreaVisibleContext, IsMainWindowFullscreenContext, PanelPositionContext, IsAuxiliaryWindowFocusedContext, TitleBarStyleContext } from '../../common/contextkeys.js'; +import { AuxiliaryBarVisibleContext, PanelAlignmentContext, PanelVisibleContext, SideBarVisibleContext, FocusedViewContext, InEditorZenModeContext, IsMainEditorCenteredLayoutContext, MainEditorAreaVisibleContext, IsMainWindowFullscreenContext, PanelPositionContext, IsAuxiliaryWindowFocusedContext } from '../../common/contextkeys.js'; import { Codicon } from '../../../base/common/codicons.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { DisposableStore } from '../../../base/common/lifecycle.js'; @@ -30,7 +30,6 @@ import { registerIcon } from '../../../platform/theme/common/iconRegistry.js'; import { ICommandActionTitle } from '../../../platform/action/common/action.js'; import { mainWindow } from '../../../base/browser/window.js'; import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js'; -import { TitlebarStyle } from '../../../platform/window/common/window.js'; import { IPreferencesService } from '../../services/preferences/common/preferences.js'; // Register Icons @@ -798,20 +797,6 @@ if (isWindows || isLinux || isWeb) { return accessor.get(IWorkbenchLayoutService).toggleMenuBar(); } }); - - // Add separately to title bar context menu so we can use a different title - for (const menuId of [MenuId.TitleBarContext, MenuId.TitleBarTitleContext]) { - MenuRegistry.appendMenuItem(menuId, { - command: { - id: 'workbench.action.toggleMenuBar', - title: localize('miMenuBarNoMnemonic', "Menu Bar"), - toggled: ContextKeyExpr.and(IsMacNativeContext.toNegated(), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'hidden'), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'toggle'), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'compact')) - }, - when: ContextKeyExpr.and(IsAuxiliaryWindowFocusedContext.toNegated(), ContextKeyExpr.notEquals(TitleBarStyleContext.key, TitlebarStyle.NATIVE), IsMainWindowFullscreenContext.negate()), - group: '2_config', - order: 0 - }); - } } // --- Reset View Locations diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts b/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts index f84bf3f06b73..7eba94561f09 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts @@ -8,13 +8,15 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { LayoutSettings } from '../../../services/layout/browser/layoutService.js'; -import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ACCOUNTS_ACTIVITY_ID, GLOBAL_ACTIVITY_ID } from '../../../common/activity.js'; import { IAction } from '../../../../base/common/actions.js'; import { IsAuxiliaryWindowFocusedContext, IsMainWindowFullscreenContext, TitleBarStyleContext, TitleBarVisibleContext } from '../../../common/contextkeys.js'; import { CustomTitleBarVisibility, TitleBarSetting, TitlebarStyle } from '../../../../platform/window/common/window.js'; import { isLinux, isNative } from '../../../../base/common/platform.js'; +import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; +import { IsMacNativeContext } from '../../../../platform/contextkey/common/contextkeys.js'; // --- Context Menu Actions --- // @@ -279,6 +281,43 @@ if (isLinux && isNative) { }); } +for (const menuId of [MenuId.TitleBarContext, MenuId.TitleBarTitleContext]) { + MenuRegistry.appendMenuItem(menuId, { + command: { + id: 'workbench.action.toggleMenuBar', + title: localize('miMenuBarNoMnemonic', "Menu Bar"), + toggled: ContextKeyExpr.and(IsMacNativeContext.toNegated(), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'hidden'), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'toggle'), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'compact')) + }, + when: ContextKeyExpr.and(IsAuxiliaryWindowFocusedContext.toNegated(), ContextKeyExpr.notEquals(TitleBarStyleContext.key, TitlebarStyle.NATIVE), IsMainWindowFullscreenContext.negate()), + group: '2_config', + order: 0 + }); +} + +registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.action.toggleMenuBarInFullScreen', + title: localize2('menuBar', "Menu Bar"), + category: Categories.View, + toggled: ContextKeyExpr.equals('config.window.menuBarVisibility', 'visible'), + menu: [{ + id: MenuId.TitleBarContext, + group: '2_config', + order: 0, + when: ContextKeyExpr.and(IsAuxiliaryWindowFocusedContext.toNegated(), ContextKeyExpr.notEquals(TitleBarStyleContext.key, TitlebarStyle.NATIVE), IsMainWindowFullscreenContext, IsMacNativeContext.negate()), + }] + }); + } + + run(accessor: ServicesAccessor): void { + const configurationService = accessor.get(IConfigurationService); + const isVisible = configurationService.getValue('window.menuBarVisibility') === 'visible'; + configurationService.updateValue('window.menuBarVisibility', isVisible ? 'classic' : 'visible'); + } +}); + // --- Toolbar actions --- // export const ACCOUNTS_ACTIVITY_TILE_ACTION: IAction = { From 9945f3b953f1c330bc4a9b1dd02b17304d56eead Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 19 Dec 2024 10:55:15 -0600 Subject: [PATCH 0264/3587] Revert "focus debug console when it becomes visible " (#236607) --- src/vs/workbench/contrib/debug/browser/repl.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index a9959c5d1c53..edef7e72b706 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -214,7 +214,6 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this.tree?.updateChildren(undefined, true, false); this.onDidStyleChange(); } - this.focus(); } })); this._register(this.configurationService.onDidChangeConfiguration(e => { From ceab34bc36825ebd163e625426c2d6518c177a0b Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 19 Dec 2024 18:10:29 +0100 Subject: [PATCH 0265/3587] icon themes: remove fontCharacterRegex (#236610) --- src/vs/platform/theme/common/iconRegistry.ts | 4 +--- .../services/themes/browser/fileIconThemeData.ts | 4 ++-- .../services/themes/browser/productIconThemeData.ts | 6 +++--- .../services/themes/common/fileIconThemeSchema.ts | 6 ++---- .../services/themes/common/iconExtensionPoint.ts | 10 ++-------- 5 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/vs/platform/theme/common/iconRegistry.ts b/src/vs/platform/theme/common/iconRegistry.ts index 775c1cf1d4a4..88cee9c3ddbe 100644 --- a/src/vs/platform/theme/common/iconRegistry.ts +++ b/src/vs/platform/theme/common/iconRegistry.ts @@ -152,10 +152,8 @@ export const fontStyleRegex = /^(normal|italic|(oblique[ \w\s-]+))$/; export const fontWeightRegex = /^(normal|bold|lighter|bolder|(\d{0-1000}))$/; export const fontSizeRegex = /^([\w_.%+-]+)$/; export const fontFormatRegex = /^woff|woff2|truetype|opentype|embedded-opentype|svg$/; -export const fontCharacterRegex = /^([^\\]|\\[a-fA-F0-9]+)$/u; export const fontColorRegex = /^#[0-9a-fA-F]{0,6}$/; -export const fontCharacterErrorMessage = localize('schema.fontCharacter.formatError', 'The fontCharacter must be a single letter or a backslash followed by unicode code points in hexadecimal.'); export const fontIdErrorMessage = localize('schema.fontId.formatError', 'The font ID must only contain letters, numbers, underscores and dashes.'); class IconRegistry implements IIconRegistry { @@ -170,7 +168,7 @@ class IconRegistry implements IIconRegistry { type: 'object', properties: { fontId: { type: 'string', description: localize('iconDefinition.fontId', 'The id of the font to use. If not set, the font that is defined first is used.'), pattern: fontIdRegex.source, patternErrorMessage: fontIdErrorMessage }, - fontCharacter: { type: 'string', description: localize('iconDefinition.fontCharacter', 'The font character associated with the icon definition.'), pattern: fontCharacterRegex.source, patternErrorMessage: fontCharacterErrorMessage } + fontCharacter: { type: 'string', description: localize('iconDefinition.fontCharacter', 'The font character associated with the icon definition.') } }, additionalProperties: false, defaultSnippets: [{ body: { fontCharacter: '\\\\e030' } }] diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index e1d349be4813..a6e4c965f495 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -13,7 +13,7 @@ import { getParseErrorMessage } from '../../../../base/common/jsonErrorMessages. import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IExtensionResourceLoaderService } from '../../../../platform/extensionResourceLoader/common/extensionResourceLoader.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; -import { fontCharacterRegex, fontColorRegex, fontSizeRegex } from '../../../../platform/theme/common/iconRegistry.js'; +import { fontColorRegex, fontSizeRegex } from '../../../../platform/theme/common/iconRegistry.js'; import * as css from '../../../../base/browser/cssValue.js'; import { fileIconSelectorEscape } from '../../../../editor/common/services/getIconClasses.js'; @@ -424,7 +424,7 @@ export class FileIconThemeLoader { if (definition.fontColor && definition.fontColor.match(fontColorRegex)) { body.push(css.inline`color: ${css.hexColorValue(definition.fontColor)};`); } - if (definition.fontCharacter && definition.fontCharacter.match(fontCharacterRegex)) { + if (definition.fontCharacter) { body.push(css.inline`content: ${css.stringValue(definition.fontCharacter)};`); } const fontSize = definition.fontSize ?? (definition.fontId ? fontSizes.get(definition.fontId) : undefined); diff --git a/src/vs/workbench/services/themes/browser/productIconThemeData.ts b/src/vs/workbench/services/themes/browser/productIconThemeData.ts index 1e0a9fa295cb..824a35ca9e9b 100644 --- a/src/vs/workbench/services/themes/browser/productIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/productIconThemeData.ts @@ -13,7 +13,7 @@ import { getParseErrorMessage } from '../../../../base/common/jsonErrorMessages. import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { isObject, isString } from '../../../../base/common/types.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { IconDefinition, getIconRegistry, IconContribution, IconFontDefinition, IconFontSource, fontIdRegex, fontWeightRegex, fontStyleRegex, fontFormatRegex, fontCharacterRegex, fontCharacterErrorMessage } from '../../../../platform/theme/common/iconRegistry.js'; +import { IconDefinition, getIconRegistry, IconContribution, IconFontDefinition, IconFontSource, fontIdRegex, fontWeightRegex, fontStyleRegex, fontFormatRegex } from '../../../../platform/theme/common/iconRegistry.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { IExtensionResourceLoaderService } from '../../../../platform/extensionResourceLoader/common/extensionResourceLoader.js'; @@ -244,7 +244,7 @@ function _loadProductIconThemeDocument(fileService: IExtensionResourceLoaderServ for (const iconId in contentValue.iconDefinitions) { const definition = contentValue.iconDefinitions[iconId]; - if (isString(definition.fontCharacter) && definition.fontCharacter.match(fontCharacterRegex)) { + if (isString(definition.fontCharacter)) { const fontId = definition.fontId ?? primaryFontId; const fontDefinition = sanitizedFonts.get(fontId); if (fontDefinition) { @@ -255,7 +255,7 @@ function _loadProductIconThemeDocument(fileService: IExtensionResourceLoaderServ warnings.push(nls.localize('error.icon.font', 'Skipping icon definition \'{0}\'. Unknown font.', iconId)); } } else { - warnings.push(nls.localize('error.icon.fontCharacter', 'Skipping icon definition \'{0}\': {1}', iconId, fontCharacterErrorMessage)); + warnings.push(nls.localize('error.icon.fontCharacter', 'Skipping icon definition \'{0}\': Needs to be defined', iconId)); } } return { iconDefinitions }; diff --git a/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts b/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts index f51191117c53..6a8ece75403b 100644 --- a/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts +++ b/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts @@ -7,7 +7,7 @@ import * as nls from '../../../../nls.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { Extensions as JSONExtensions, IJSONContributionRegistry } from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js'; import { IJSONSchema } from '../../../../base/common/jsonSchema.js'; -import { fontWeightRegex, fontStyleRegex, fontSizeRegex, fontIdRegex, fontCharacterRegex, fontColorRegex, fontCharacterErrorMessage, fontIdErrorMessage } from '../../../../platform/theme/common/iconRegistry.js'; +import { fontWeightRegex, fontStyleRegex, fontSizeRegex, fontIdRegex, fontColorRegex, fontIdErrorMessage } from '../../../../platform/theme/common/iconRegistry.js'; const schemaId = 'vscode://schemas/icon-theme'; const schema: IJSONSchema = { @@ -208,9 +208,7 @@ const schema: IJSONSchema = { }, fontCharacter: { type: 'string', - description: nls.localize('schema.fontCharacter', 'When using a glyph font: The character in the font to use.'), - pattern: fontCharacterRegex.source, - patternErrorMessage: fontCharacterErrorMessage + description: nls.localize('schema.fontCharacter', 'When using a glyph font: The character in the font to use.') }, fontColor: { type: 'string', diff --git a/src/vs/workbench/services/themes/common/iconExtensionPoint.ts b/src/vs/workbench/services/themes/common/iconExtensionPoint.ts index 9b554226c3f3..08a9b0591139 100644 --- a/src/vs/workbench/services/themes/common/iconExtensionPoint.ts +++ b/src/vs/workbench/services/themes/common/iconExtensionPoint.ts @@ -5,7 +5,7 @@ import * as nls from '../../../../nls.js'; import { ExtensionsRegistry } from '../../extensions/common/extensionsRegistry.js'; -import { IIconRegistry, Extensions as IconRegistryExtensions, fontCharacterErrorMessage, fontCharacterRegex } from '../../../../platform/theme/common/iconRegistry.js'; +import { IIconRegistry, Extensions as IconRegistryExtensions } from '../../../../platform/theme/common/iconRegistry.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import * as resources from '../../../../base/common/resources.js'; @@ -53,9 +53,7 @@ const iconConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint Date: Thu, 19 Dec 2024 19:15:05 +0100 Subject: [PATCH 0266/3587] Implements experimental word replacement and insertion views. (#236618) --- src/vs/editor/browser/rect.ts | 3 +- .../diffEditorViewZones/renderLines.ts | 33 +- src/vs/editor/common/config/editorOptions.ts | 19 ++ src/vs/editor/common/core/range.ts | 4 + .../defaultLinesDiffComputer.ts | 19 +- .../heuristicSequenceOptimizations.ts | 18 +- .../linesSliceCharSequence.ts | 29 ++ .../common/diff/documentDiffProvider.ts | 2 + .../editor/common/diff/linesDiffComputer.ts | 1 + .../view/inlineEdits/gutterIndicatorView.ts | 15 +- .../browser/view/inlineEdits/utils.ts | 14 + .../browser/view/inlineEdits/view.ts | 69 +++-- .../view/inlineEdits/viewAndDiffProducer.ts | 1 + .../view/inlineEdits/wordReplacementView.ts | 285 ++++++++++++++++++ src/vs/monaco.d.ts | 3 + 15 files changed, 467 insertions(+), 48 deletions(-) create mode 100644 src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts diff --git a/src/vs/editor/browser/rect.ts b/src/vs/editor/browser/rect.ts index b990944eb676..6de464d201af 100644 --- a/src/vs/editor/browser/rect.ts +++ b/src/vs/editor/browser/rect.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { BugIndicatingError } from '../../base/common/errors.js'; import { OffsetRange } from '../common/core/offsetRange.js'; import { Point } from './point.js'; @@ -49,7 +50,7 @@ export class Rect { public readonly bottom: number, ) { if (left > right || top > bottom) { - throw new Error('Invalid arguments'); + throw new BugIndicatingError('Invalid arguments'); } } diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.ts index d2ff0bf9fb8d..ce0eea0bcaaa 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.ts @@ -17,7 +17,7 @@ import { InlineDecoration, ViewLineRenderingData } from '../../../../../common/v const ttPolicy = createTrustedTypesPolicy('diffEditorWidget', { createHTML: value => value }); -export function renderLines(source: LineSource, options: RenderOptions, decorations: InlineDecoration[], domNode: HTMLElement): RenderLinesResult { +export function renderLines(source: LineSource, options: RenderOptions, decorations: InlineDecoration[], domNode: HTMLElement, noExtra = false): RenderLinesResult { applyFontInfo(domNode, options.fontInfo); const hasCharChanges = (decorations.length > 0); @@ -44,7 +44,8 @@ export function renderLines(source: LineSource, options: RenderOptions, decorati source.mightContainNonBasicASCII, source.mightContainRTL, options, - sb + sb, + noExtra, )); renderedLineCount++; lastBreakOffset = breakOffset; @@ -61,6 +62,7 @@ export function renderLines(source: LineSource, options: RenderOptions, decorati source.mightContainRTL, options, sb, + noExtra, )); renderedLineCount++; } @@ -125,7 +127,25 @@ export class RenderOptions { public readonly renderWhitespace: FindComputedEditorOptionValueById, public readonly renderControlCharacters: boolean, public readonly fontLigatures: FindComputedEditorOptionValueById, + public readonly setWidth = true, ) { } + + public withSetWidth(setWidth: boolean): RenderOptions { + return new RenderOptions( + this.tabSize, + this.fontInfo, + this.disableMonospaceOptimizations, + this.typicalHalfwidthCharacterWidth, + this.scrollBeyondLastColumn, + this.lineHeight, + this.lineDecorationsWidth, + this.stopRenderingLineAfter, + this.renderWhitespace, + this.renderControlCharacters, + this.fontLigatures, + setWidth, + ); + } } export interface RenderLinesResult { @@ -143,16 +163,21 @@ function renderOriginalLine( mightContainRTL: boolean, options: RenderOptions, sb: StringBuilder, + noExtra: boolean, ): number { sb.appendString('
'); + if (options.setWidth) { + sb.appendString('px;width:1000000px;">'); + } else { + sb.appendString('px;">'); + } const lineContent = lineTokens.getLineContent(); const isBasicASCII = ViewLineRenderingData.isBasicASCII(lineContent, mightContainNonBasicASCII); diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index c7481985f2f7..e355a960db99 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -4197,6 +4197,9 @@ export interface IInlineSuggestOptions { enabled?: boolean; useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; + useWordInsertionView?: 'never' | 'whenPossible'; + useWordReplacementView?: 'never' | 'whenPossible'; + onlyShowWhenCloseToCursor?: boolean; useGutterIndicator?: boolean; }; @@ -4230,6 +4233,8 @@ class InlineEditorSuggest extends BaseEditorOption seq.findWordContaining(idx)); if (check) { SequenceDiff.assertSorted(diffs); } + + if (options.extendToSubwords) { + diffs = extendDiffsToEntireWordIfAppropriate(slice1, slice2, diffs, (seq, idx) => seq.findSubWordContaining(idx), true); + if (check) { SequenceDiff.assertSorted(diffs); } + } + diffs = removeShortMatches(slice1, slice2, diffs); if (check) { SequenceDiff.assertSorted(diffs); } diffs = removeVeryShortMatchingTextBetweenLongDiffs(slice1, slice2, diffs); diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts index c205457ac534..20de8dd1ab0e 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts @@ -219,7 +219,13 @@ export function removeShortMatches(sequence1: ISequence, sequence2: ISequence, s return result; } -export function extendDiffsToEntireWordIfAppropriate(sequence1: LinesSliceCharSequence, sequence2: LinesSliceCharSequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] { +export function extendDiffsToEntireWordIfAppropriate( + sequence1: LinesSliceCharSequence, + sequence2: LinesSliceCharSequence, + sequenceDiffs: SequenceDiff[], + findParent: (seq: LinesSliceCharSequence, idx: number) => OffsetRange | undefined, + force: boolean = false, +): SequenceDiff[] { const equalMappings = SequenceDiff.invert(sequenceDiffs, sequence1.length); const additional: SequenceDiff[] = []; @@ -231,8 +237,8 @@ export function extendDiffsToEntireWordIfAppropriate(sequence1: LinesSliceCharSe return; } - const w1 = sequence1.findWordContaining(pair.offset1); - const w2 = sequence2.findWordContaining(pair.offset2); + const w1 = findParent(sequence1, pair.offset1); + const w2 = findParent(sequence2, pair.offset2); if (!w1 || !w2) { return; } @@ -252,8 +258,8 @@ export function extendDiffsToEntireWordIfAppropriate(sequence1: LinesSliceCharSe break; } - const v1 = sequence1.findWordContaining(next.seq1Range.start); - const v2 = sequence2.findWordContaining(next.seq2Range.start); + const v1 = findParent(sequence1, next.seq1Range.start); + const v2 = findParent(sequence2, next.seq2Range.start); // Because there is an intersection, we know that the words are not empty. const v = new SequenceDiff(v1!, v2!); const equalPart = v.intersect(next)!; @@ -271,7 +277,7 @@ export function extendDiffsToEntireWordIfAppropriate(sequence1: LinesSliceCharSe } } - if (equalChars1 + equalChars2 < (w.seq1Range.length + w.seq2Range.length) * 2 / 3) { + if ((force && equalChars1 + equalChars2 < w.seq1Range.length + w.seq2Range.length) || equalChars1 + equalChars2 < (w.seq1Range.length + w.seq2Range.length) * 2 / 3) { additional.push(w); } diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts index b56245bbc0f0..25b4a6127f8c 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts @@ -144,6 +144,31 @@ export class LinesSliceCharSequence implements ISequence { return new OffsetRange(start, end); } + /** fooBar has the two sub-words foo and bar */ + public findSubWordContaining(offset: number): OffsetRange | undefined { + if (offset < 0 || offset >= this.elements.length) { + return undefined; + } + + if (!isWordChar(this.elements[offset])) { + return undefined; + } + + // find start + let start = offset; + while (start > 0 && isWordChar(this.elements[start - 1]) && !isUpperCase(this.elements[start])) { + start--; + } + + // find end + let end = offset; + while (end < this.elements.length && isWordChar(this.elements[end]) && !isUpperCase(this.elements[end])) { + end++; + } + + return new OffsetRange(start, end); + } + public countLinesIn(range: OffsetRange): number { return this.translateOffset(range.endExclusive).lineNumber - this.translateOffset(range.start).lineNumber; } @@ -165,6 +190,10 @@ function isWordChar(charCode: number): boolean { || charCode >= CharCode.Digit0 && charCode <= CharCode.Digit9; } +function isUpperCase(charCode: number): boolean { + return charCode >= CharCode.A && charCode <= CharCode.Z; +} + const enum CharBoundaryCategory { WordLower, WordUpper, diff --git a/src/vs/editor/common/diff/documentDiffProvider.ts b/src/vs/editor/common/diff/documentDiffProvider.ts index da88be843cbe..6f6f06a9d738 100644 --- a/src/vs/editor/common/diff/documentDiffProvider.ts +++ b/src/vs/editor/common/diff/documentDiffProvider.ts @@ -45,6 +45,8 @@ export interface IDocumentDiffProviderOptions { * If set, the diff computation should compute moves in addition to insertions and deletions. */ computeMoves: boolean; + + extendToSubwords?: boolean; } /** diff --git a/src/vs/editor/common/diff/linesDiffComputer.ts b/src/vs/editor/common/diff/linesDiffComputer.ts index 054d1eebfdd6..6a384b6d8a56 100644 --- a/src/vs/editor/common/diff/linesDiffComputer.ts +++ b/src/vs/editor/common/diff/linesDiffComputer.ts @@ -13,6 +13,7 @@ export interface ILinesDiffComputerOptions { readonly ignoreTrimWhitespace: boolean; readonly maxComputationTimeMs: number; readonly computeMoves: boolean; + readonly extendToSubwords?: boolean; } export class LinesDiff { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index bcfa7d7463b7..b0f8b3dc54dc 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -6,7 +6,7 @@ import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { IObservable, IReader, constObservable, derived, observableFromEvent } from '../../../../../../base/common/observable.js'; +import { IObservable, constObservable, derived, observableFromEvent } from '../../../../../../base/common/observable.js'; import { buttonBackground, buttonForeground, buttonSecondaryBackground, buttonSecondaryForeground } from '../../../../../../platform/theme/common/colorRegistry.js'; import { registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; import { ObservableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; @@ -16,7 +16,7 @@ import { LineRange } from '../../../../../common/core/lineRange.js'; import { OffsetRange } from '../../../../../common/core/offsetRange.js'; import { StickyScrollController } from '../../../../stickyScroll/browser/stickyScrollController.js'; import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; -import { mapOutFalsy, n } from './utils.js'; +import { mapOutFalsy, n, rectToProps } from './utils.js'; export const inlineEditIndicatorPrimaryForeground = registerColor('inlineEdit.gutterIndicator.primaryForeground', buttonForeground, 'Foreground color for the primary inline edit gutter indicator.'); export const inlineEditIndicatorPrimaryBackground = registerColor('inlineEdit.gutterIndicator.primaryBackground', buttonBackground, 'Background color for the primary inline edit gutter indicator.'); @@ -157,7 +157,7 @@ export class InlineEditsGutterIndicator extends Disposable { case 'accept': return 'var(--vscode-inlineEdit-gutterIndicator-successfulBackground)'; } }), - '--vscodeIconForeground': this._tabAction.map(v => { + ['--vscodeIconForeground' as any]: this._tabAction.map(v => { switch (v) { case 'inactive': return 'var(--vscode-inlineEdit-gutterIndicator-secondaryForeground)'; case 'jump': return 'var(--vscode-inlineEdit-gutterIndicator-primaryForeground)'; @@ -188,12 +188,3 @@ export class InlineEditsGutterIndicator extends Disposable { ]), ])).keepUpdated(this._store); } - -function rectToProps(fn: (reader: IReader) => Rect): any { - return { - left: derived(reader => fn(reader).left), - top: derived(reader => fn(reader).top), - width: derived(reader => fn(reader).right - fn(reader).left), - height: derived(reader => fn(reader).bottom - fn(reader).top), - }; -} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts index b99b0478bdcd..a70a0721491a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts @@ -16,6 +16,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { MenuEntryActionViewItem } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { ObservableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { Point } from '../../../../../browser/point.js'; +import { Rect } from '../../../../../browser/rect.js'; import { EditorOption } from '../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../common/core/lineRange.js'; import { OffsetRange } from '../../../../../common/core/offsetRange.js'; @@ -173,9 +174,13 @@ type SVGElementTagNameMap2 = { width: number; height: number; transform: string; + viewBox: string; + fill: string; }; path: SVGElement & { d: string; + stroke: string; + fill: string; }; linearGradient: SVGElement & { id: string; @@ -465,3 +470,12 @@ export function observeElementPosition(element: HTMLElement, store: DisposableSt left }; } + +export function rectToProps(fn: (reader: IReader) => Rect) { + return { + left: derived(reader => fn(reader).left), + top: derived(reader => fn(reader).top), + width: derived(reader => fn(reader).right - fn(reader).left), + height: derived(reader => fn(reader).bottom - fn(reader).top), + }; +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 68bc963e374a..dfb11c8bebf2 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -4,14 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { autorunWithStore, derived, IObservable, IReader } from '../../../../../../base/common/observable.js'; +import { autorunWithStore, derived, IObservable, IReader, mapObservableArrayCached } from '../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { EditorOption } from '../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../common/core/lineRange.js'; import { Position } from '../../../../../common/core/position.js'; -import { StringText } from '../../../../../common/core/textEdit.js'; +import { SingleTextEdit, StringText } from '../../../../../common/core/textEdit.js'; +import { TextLength } from '../../../../../common/core/textLength.js'; import { DetailedLineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../../../../../common/diff/rangeMapping.js'; import { TextModel } from '../../../../../common/model/textModel.js'; import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; @@ -22,12 +23,15 @@ import { InlineEditsSideBySideDiff } from './sideBySideDiff.js'; import { applyEditToModifiedRangeMappings, createReindentEdit } from './utils.js'; import './view.css'; import { InlineEditWithChanges } from './viewAndDiffProducer.js'; +import { WordInsertView, WordReplacementView } from './wordReplacementView.js'; export class InlineEditsView extends Disposable { private readonly _editorObs = observableCodeEditor(this._editor); private readonly _useMixedLinesDiff = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useMixedLinesDiff); private readonly _useInterleavedLinesDiff = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useInterleavedLinesDiff); + private readonly _useWordReplacementView = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useWordReplacementView); + private readonly _useWordInsertionView = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useWordInsertionView); constructor( private readonly _editor: ICodeEditor, @@ -39,7 +43,7 @@ export class InlineEditsView extends Disposable { } private readonly _uiState = derived<{ - state: 'collapsed' | 'mixedLines' | 'ghostText' | 'interleavedLines' | 'sideBySide'; + state: ReturnType; diff: DetailedLineRangeMapping[]; edit: InlineEditWithChanges; newText: string; @@ -57,9 +61,9 @@ export class InlineEditsView extends Disposable { let newText = edit.edit.apply(edit.originalText); let diff = lineRangeMappingFromRangeMappings(mappings, edit.originalText, new StringText(newText)); - const state = this.determinRenderState(edit, reader, diff); + const state = this.determineRenderState(edit, reader, diff, new StringText(newText)); - if (state === 'sideBySide') { + if (state.kind === 'sideBySide') { const indentationAdjustmentEdit = createReindentEdit(newText, edit.modifiedLineRange); newText = indentationAdjustmentEdit.applyToString(newText); @@ -98,7 +102,7 @@ export class InlineEditsView extends Disposable { this._editor, this._edit, this._previewTextModel, - this._uiState.map(s => s && s.state === 'sideBySide' ? ({ + this._uiState.map(s => s && s.state.kind === 'sideBySide' ? ({ edit: s.edit, newTextLineCount: s.newTextLineCount, originalDisplayRange: s.originalDisplayRange, @@ -108,17 +112,27 @@ export class InlineEditsView extends Disposable { private readonly _inlineDiffViewState = derived(this, reader => { const e = this._uiState.read(reader); if (!e) { return undefined; } - + if (e.state.kind === 'wordReplacements') { + return undefined; + } return { modifiedText: new StringText(e.newText), diff: e.diff, - mode: e.state === 'collapsed' ? 'sideBySide' : e.state, + mode: e.state.kind === 'collapsed' ? 'sideBySide' : e.state.kind, modifiedCodeEditor: this._sideBySide.previewEditor, }; }); protected readonly _inlineDiffView = this._register(new OriginalEditorInlineDiffView(this._editor, this._inlineDiffViewState, this._previewTextModel)); + protected readonly _wordReplacementViews = mapObservableArrayCached(this, this._uiState.map(s => s?.state.kind === 'wordReplacements' ? s.state.replacements : []), (e, store) => { + if (e.range.isEmpty()) { + return store.add(this._instantiationService.createInstance(WordInsertView, this._editorObs, e)); + } else { + return store.add(this._instantiationService.createInstance(WordReplacementView, this._editorObs, e)); + } + }).recomputeInitiallyAndOnChange(this._store); + private readonly _useGutterIndicator = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useGutterIndicator); protected readonly _indicator = this._register(autorunWithStore((reader, store) => { @@ -136,37 +150,54 @@ export class InlineEditsView extends Disposable { if (!state) { return undefined; } const range = state.originalDisplayRange; const top = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); - return { editTop: top, showAlways: state.state !== 'sideBySide' }; + return { editTop: top, showAlways: state.state.kind !== 'sideBySide' }; }), this._model, )); } })); - private determinRenderState(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[]) { + private determineRenderState(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText) { if (edit.isCollapsed) { - return 'collapsed'; + return { kind: 'collapsed' as const }; } if ( - (this._useMixedLinesDiff.read(reader) === 'whenPossible' || (edit.userJumpedToIt && this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible')) - && diff.every(m => OriginalEditorInlineDiffView.supportsInlineDiffRendering(m)) + this._useMixedLinesDiff.read(reader) === 'forStableInsertions' + && isInsertionAfterPosition(diff, edit.cursorPosition) ) { - return 'mixedLines'; + return { kind: 'ghostText' as const }; + } + + if (diff.length === 1 && diff[0].original.length === 1 && diff[0].modified.length === 1) { + const inner = diff.flatMap(d => d.innerChanges!); + if (inner.every( + m => (m.originalRange.isEmpty() && this._useWordInsertionView.read(reader) === 'whenPossible' + || !m.originalRange.isEmpty() && this._useWordReplacementView.read(reader) === 'whenPossible') + && TextLength.ofRange(m.originalRange).columnCount < 100 + && TextLength.ofRange(m.modifiedRange).columnCount < 100 + )) { + return { + kind: 'wordReplacements' as const, + replacements: inner.map(i => + new SingleTextEdit(i.originalRange, newText.getValueOfRange(i.modifiedRange)) + ) + }; + } } if ( - this._useMixedLinesDiff.read(reader) === 'forStableInsertions' - && isInsertionAfterPosition(diff, edit.cursorPosition) + (this._useMixedLinesDiff.read(reader) === 'whenPossible' || (edit.userJumpedToIt && this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible')) + && diff.every(m => OriginalEditorInlineDiffView.supportsInlineDiffRendering(m)) ) { - return 'ghostText'; + return { kind: 'mixedLines' as const }; } if (this._useInterleavedLinesDiff.read(reader) === 'always' || (edit.userJumpedToIt && this._useInterleavedLinesDiff.read(reader) === 'afterJump')) { - return 'interleavedLines'; + return { kind: 'interleavedLines' as const }; } - return 'sideBySide'; + return { kind: 'sideBySide' as const }; } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts index ed40c6c0edbd..f56b4d2c9867 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts @@ -47,6 +47,7 @@ export class InlineEditsViewAndDiffProducer extends Disposable { computeMoves: false, ignoreTrimWhitespace: false, maxComputationTimeMs: 1000, + extendToSubwords: true, }, CancellationToken.None); return result; }); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts new file mode 100644 index 000000000000..6e6f213d38ae --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -0,0 +1,285 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { constObservable, derived } from '../../../../../../base/common/observable.js'; +import { editorHoverStatusBarBackground } from '../../../../../../platform/theme/common/colorRegistry.js'; +import { registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; +import { ObservableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; +import { Point } from '../../../../../browser/point.js'; +import { Rect } from '../../../../../browser/rect.js'; +import { LineSource, renderLines, RenderOptions } from '../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; +import { EditorOption } from '../../../../../common/config/editorOptions.js'; +import { SingleOffsetEdit } from '../../../../../common/core/offsetEdit.js'; +import { OffsetRange } from '../../../../../common/core/offsetRange.js'; +import { SingleTextEdit } from '../../../../../common/core/textEdit.js'; +import { ILanguageService } from '../../../../../common/languages/language.js'; +import { LineTokens } from '../../../../../common/tokens/lineTokens.js'; +import { TokenArray } from '../../../../../common/tokens/tokenArray.js'; +import { mapOutFalsy, n, rectToProps } from './utils.js'; + +export const transparentHoverBackground = registerColor( + 'inlineEdit.wordReplacementView.background', + { + light: transparent(editorHoverStatusBarBackground, 0.1), + dark: transparent(editorHoverStatusBarBackground, 0.5), + hcLight: transparent(editorHoverStatusBarBackground, 0.1), + hcDark: transparent(editorHoverStatusBarBackground, 0.1), + }, + 'Background color for the inline edit word replacement view.' +); + +export class WordReplacementView extends Disposable { + private readonly _start = this._editor.observePosition(constObservable(this._edit.range.getStartPosition()), this._store); + private readonly _end = this._editor.observePosition(constObservable(this._edit.range.getEndPosition()), this._store); + + private readonly _line = document.createElement('div'); + + private readonly _text = derived(reader => { + const tm = this._editor.model.get()!; + const origLine = tm.getLineContent(this._edit.range.startLineNumber); + + const edit = SingleOffsetEdit.replace(new OffsetRange(this._edit.range.startColumn - 1, this._edit.range.endColumn - 1), this._edit.text); + const lineToTokenize = edit.apply(origLine); + const t = tm.tokenization.tokenizeLinesAt(this._edit.range.startLineNumber, [lineToTokenize])?.[0]; + let tokens: LineTokens; + if (t) { + tokens = TokenArray.fromLineTokens(t).slice(edit.getRangeAfterApply()).toLineTokens(this._edit.text, this._languageService.languageIdCodec); + } else { + tokens = LineTokens.createEmpty(this._edit.text, this._languageService.languageIdCodec); + } + renderLines(new LineSource([tokens]), RenderOptions.fromEditor(this._editor.editor).withSetWidth(false), [], this._line, true); + }); + + private readonly _layout = derived(this, reader => { + this._text.read(reader); + const start = this._start.read(reader); + const end = this._end.read(reader); + if (!start || !end) { + return undefined; + } + const contentLeft = this._editor.layoutInfoContentLeft.read(reader); + const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); + if (start.x > end.x) { + return undefined; + } + const original = Rect.fromLeftTopWidthHeight(start.x + contentLeft - this._editor.scrollLeft.read(reader), start.y, end.x - start.x, lineHeight); + const w = this._editor.getOption(EditorOption.fontInfo).read(reader).typicalHalfwidthCharacterWidth; + const modified = Rect.fromLeftTopWidthHeight(original.left + 20, original.top + lineHeight + 5, this._edit.text.length * w + 5, original.height); + const background = Rect.hull([original, modified]).withMargin(4); + + return { + original, + modified, + background, + lowerBackground: background.intersectVertical(new OffsetRange(original.bottom, Number.MAX_SAFE_INTEGER)), + }; + }); + + + + private readonly _div = n.div({ + class: 'word-replacement', + }, [ + derived(reader => { + const layout = mapOutFalsy(this._layout).read(reader); + if (!layout) { + return []; + } + + return [ + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).lowerBackground), + borderRadius: '4px', + background: 'var(--vscode-editor-background)' + } + }, []), + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).modified), + borderRadius: '4px', + padding: '0px', + textAlign: 'center', + background: 'var(--vscode-inlineEdit-modifiedChangedTextBackground)', + fontFamily: this._editor.getOption(EditorOption.fontFamily), + fontSize: this._editor.getOption(EditorOption.fontSize), + fontWeight: this._editor.getOption(EditorOption.fontWeight), + } + }, [ + this._line, + ]), + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).original), + borderRadius: '4px', + boxSizing: 'border-box', + background: 'var(--vscode-inlineEdit-originalChangedTextBackground)', + } + }, []), + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).background), + borderRadius: '4px', + + border: '1px solid var(--vscode-editorHoverWidget-border)', + //background: 'rgba(122, 122, 122, 0.12)', looks better + background: 'var(--vscode-inlineEdit-wordReplacementView-background)', + } + }, []), + + n.svg({ + width: 11, + height: 13, + viewBox: '0 0 11 13', + fill: 'none', + style: { + position: 'absolute', + left: derived(reader => layout.read(reader).modified.left - 15), + top: derived(reader => layout.read(reader).modified.top), + } + }, [ + n.svgElem('path', { + d: 'M1 0C1 2.98966 1 4.92087 1 7.49952C1 8.60409 1.89543 9.5 3 9.5H10.5', + stroke: 'var(--vscode-editorHoverWidget-foreground)', + }), + n.svgElem('path', { + d: 'M6 6.5L9.99999 9.49998L6 12.5', + stroke: 'var(--vscode-editorHoverWidget-foreground)', + }) + ]), + + ]; + }) + ]).keepUpdated(this._store); + + constructor( + private readonly _editor: ObservableCodeEditor, + /** Must be single-line in both sides */ + private readonly _edit: SingleTextEdit, + @ILanguageService private readonly _languageService: ILanguageService, + ) { + super(); + + this._register(this._editor.createOverlayWidget({ + domNode: this._div.element, + minContentWidthInPx: constObservable(0), + position: constObservable({ preference: { top: 0, left: 0 } }), + allowEditorOverflow: false, + })); + } +} + +export class WordInsertView extends Disposable { + private readonly _start = this._editor.observePosition(constObservable(this._edit.range.getStartPosition()), this._store); + + private readonly _layout = derived(this, reader => { + const start = this._start.read(reader); + if (!start) { + return undefined; + } + const contentLeft = this._editor.layoutInfoContentLeft.read(reader); + const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); + + const w = this._editor.getOption(EditorOption.fontInfo).read(reader).typicalHalfwidthCharacterWidth; + const width = this._edit.text.length * w + 5; + + const center = new Point(contentLeft + start.x + w / 2 - this._editor.scrollLeft.read(reader), start.y); + + const modified = Rect.fromLeftTopWidthHeight(center.x - width / 2, center.y + lineHeight + 5, width, lineHeight); + const background = Rect.hull([Rect.fromPoint(center), modified]).withMargin(4); + + return { + modified, + center, + background, + lowerBackground: background.intersectVertical(new OffsetRange(modified.top - 2, Number.MAX_SAFE_INTEGER)), + }; + }); + + private readonly _div = n.div({ + class: 'word-insert', + }, [ + derived(reader => { + const layout = mapOutFalsy(this._layout).read(reader); + if (!layout) { + return []; + } + + return [ + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).lowerBackground), + borderRadius: '4px', + background: 'var(--vscode-editor-background)' + } + }, []), + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).modified), + borderRadius: '4px', + padding: '0px', + textAlign: 'center', + background: 'var(--vscode-inlineEdit-modifiedChangedTextBackground)', + fontFamily: this._editor.getOption(EditorOption.fontFamily), + fontSize: this._editor.getOption(EditorOption.fontSize), + fontWeight: this._editor.getOption(EditorOption.fontWeight), + } + }, [ + this._edit.text, + ]), + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).background), + borderRadius: '4px', + border: '1px solid var(--vscode-editorHoverWidget-border)', + //background: 'rgba(122, 122, 122, 0.12)', looks better + background: 'var(--vscode-inlineEdit-wordReplacementView-background)', + } + }, []), + n.svg({ + viewBox: '0 0 12 18', + width: 12, + height: 18, + fill: 'none', + style: { + position: 'absolute', + left: derived(reader => layout.read(reader).center.x - 9), + top: derived(reader => layout.read(reader).center.y + 4), + transform: 'scale(1.4, 1.4)', + } + }, [ + n.svgElem('path', { + d: 'M5.06445 0H7.35759C7.35759 0 7.35759 8.47059 7.35759 11.1176C7.35759 13.7647 9.4552 18 13.4674 18C17.4795 18 -2.58445 18 0.281373 18C3.14719 18 5.06477 14.2941 5.06477 11.1176C5.06477 7.94118 5.06445 0 5.06445 0Z', + fill: 'var(--vscode-inlineEdit-modifiedChangedTextBackground)', + }) + ]) + + ]; + }) + ]).keepUpdated(this._store); + + constructor( + private readonly _editor: ObservableCodeEditor, + /** Must be single-line in both sides */ + private readonly _edit: SingleTextEdit, + ) { + super(); + + this._register(this._editor.createOverlayWidget({ + domNode: this._div.element, + minContentWidthInPx: constObservable(0), + position: constObservable({ preference: { top: 0, left: 0 } }), + allowEditorOverflow: false, + })); + } +} diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 7b6d78e85528..c1df5f99ffe1 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -773,6 +773,7 @@ declare namespace monaco { * Moves the range by the given amount of lines. */ delta(lineCount: number): Range; + isSingleLine(): boolean; static fromPositions(start: IPosition, end?: IPosition): Range; /** * Create a `Range` from an `IRange`. @@ -4604,6 +4605,8 @@ declare namespace monaco.editor { enabled?: boolean; useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; + useWordInsertionView?: 'never' | 'whenPossible'; + useWordReplacementView?: 'never' | 'whenPossible'; onlyShowWhenCloseToCursor?: boolean; useGutterIndicator?: boolean; }; From a6210a08ac0156635424b396625d52256ac188fc Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 19 Dec 2024 12:18:50 -0600 Subject: [PATCH 0267/3587] user aria alert vs native when navigating in terminal accessible view (#236622) fix #236621 --- .../accessibility/browser/terminal.accessibility.contribution.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts index 5c69ee4bae96..b0f68a6eecfc 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts @@ -33,6 +33,7 @@ import { BufferContentTracker } from './bufferContentTracker.js'; import { TerminalAccessibilityHelpProvider } from './terminalAccessibilityHelp.js'; import { ICommandWithEditorLine, TerminalAccessibleBufferProvider } from './terminalAccessibleBufferProvider.js'; import { TextAreaSyncAddon } from './textAreaSyncAddon.js'; +import { alert } from '../../../../../base/browser/ui/aria/aria.js'; // #region Terminal Contributions From 8be4be068e75248770be81abb5e8fc9f5d2e2302 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 19 Dec 2024 19:21:00 +0100 Subject: [PATCH 0268/3587] Fixes bug (#236620) --- .../browser/view/inlineEdits/gutterIndicatorView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index b0f8b3dc54dc..739f12b4feb2 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -108,8 +108,8 @@ export class InlineEditsGutterIndicator extends Disposable { private readonly _tabAction = derived(this, reader => { const m = this._model.read(reader); - if (m && m.tabShouldAcceptInlineEdit.read(reader)) { return 'accept' as const; } if (m && m.tabShouldJumpToInlineEdit.read(reader)) { return 'jump' as const; } + if (m && m.tabShouldAcceptInlineEdit.read(reader)) { return 'accept' as const; } return 'inactive' as const; }); From d55cb9a7a04ab9b894b3d3f51ae5a0f4589bf924 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 19 Dec 2024 10:25:37 -0800 Subject: [PATCH 0269/3587] Use claims to force an idToken in Broker flow (#236623) Looks like the Broker doesn't support `forceRefresh`... This is an alternative way of forcing a refresh. Fixes https://github.com/microsoft/vscode/issues/229456 --- .../src/node/cachedPublicClientApplication.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts index 7396da179901..0f27c2c0e4d6 100644 --- a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts +++ b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts @@ -102,9 +102,19 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica ); if (fiveMinutesBefore < new Date()) { this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] id token is expired or about to expire. Forcing refresh...`); - result = await this._sequencer.queue(() => this._pca.acquireTokenSilent({ ...request, forceRefresh: true })); + const newRequest = this._isBrokerAvailable + // HACK: Broker doesn't support forceRefresh so we need to pass in claims which will force a refresh + ? { ...request, claims: '{ "id_token": {}}' } + : { ...request, forceRefresh: true }; + result = await this._sequencer.queue(() => this._pca.acquireTokenSilent(newRequest)); this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] got refreshed result`); } + const newIdTokenExpirationInSecs = (result.idTokenClaims as { exp?: number }).exp; + if (newIdTokenExpirationInSecs) { + if (new Date(newIdTokenExpirationInSecs * 1000) < new Date()) { + this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] id token is still expired.`); + } + } } // this._setupRefresh(result); From fe68aa5447827cbdc970fa14e572ab8c0b572f71 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 19 Dec 2024 12:52:06 -0600 Subject: [PATCH 0270/3587] fix go to symbol in accessible view (#236624) --- .../accessibility/browser/accessibleView.ts | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index c9acf7b6a28c..fde082ed5d56 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -78,7 +78,7 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi private _hasAssignedKeybindings: IContextKey; private _codeBlocks?: ICodeBlock[]; - private _inQuickPick: boolean = false; + private _isInQuickPick: boolean = false; get editorWidget() { return this._editorWidget; } private _container: HTMLElement; @@ -352,6 +352,7 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi if (!this._currentProvider) { return; } + this._isInQuickPick = true; this._instantiationService.createInstance(AccessibleViewSymbolQuickPick, this).show(this._currentProvider); } @@ -388,11 +389,11 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi } getSymbols(): IAccessibleViewSymbol[] | undefined { - const provider = this._currentProvider instanceof AccessibleContentProvider ? this._currentProvider : undefined; + const provider = this._currentProvider ? this._currentProvider : undefined; if (!this._currentContent || !provider) { return; } - const symbols: IAccessibleViewSymbol[] = provider.getSymbols?.() || []; + const symbols: IAccessibleViewSymbol[] = 'getSymbols' in provider ? provider.getSymbols?.() || [] : []; if (symbols?.length) { return symbols; } @@ -416,7 +417,7 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi } configureKeybindings(unassigned: boolean): void { - this._inQuickPick = true; + this._isInQuickPick = true; const provider = this._updateLastProvider(); const items = unassigned ? provider?.options?.configureKeybindingItems : provider?.options?.configuredKeybindingItems; if (!items) { @@ -440,7 +441,7 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi this.show(provider); } disposables.dispose(); - this._inQuickPick = false; + this._isInQuickPick = false; })); } @@ -495,6 +496,7 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi if (lineNumber === undefined) { return; } + this._isInQuickPick = false; this.show(provider, undefined, undefined, { lineNumber, column: 1 }); this._updateContextKeys(provider, true); } @@ -609,11 +611,14 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi this._updateToolbar(this._currentProvider.actions, provider.options.type); const hide = (e?: KeyboardEvent | IKeyboardEvent): void => { - if (!this._inQuickPick) { + if (!this._isInQuickPick) { provider.onClose(); } e?.stopPropagation(); this._contextViewService.hideContextView(); + if (this._isInQuickPick) { + return; + } this._updateContextKeys(provider, false); this._lastProvider = undefined; this._currentContent = undefined; @@ -938,11 +943,15 @@ class AccessibleViewSymbolQuickPick { for (const symbol of symbols) { picks.push({ label: symbol.label, - ariaLabel: symbol.ariaLabel + ariaLabel: symbol.ariaLabel, + firstListItem: symbol.firstListItem, + lineNumber: symbol.lineNumber, + endLineNumber: symbol.endLineNumber, + markdownToParse: symbol.markdownToParse }); } quickPick.canSelectMany = false; - quickPick.items = symbols; + quickPick.items = picks; quickPick.show(); disposables.add(quickPick.onDidAccept(() => { this._accessibleView.showSymbol(provider, quickPick.selectedItems[0]); From d34e04970c4ad3955693708dd2c481bf7c8ccd80 Mon Sep 17 00:00:00 2001 From: RedCMD <33529441+RedCMD@users.noreply.github.com> Date: Fri, 20 Dec 2024 07:54:50 +1300 Subject: [PATCH 0271/3587] Add `outdated` and `recentlyUpdated` suggestions to extension filter (#235884) --- src/vs/workbench/contrib/extensions/common/extensionQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/extensions/common/extensionQuery.ts b/src/vs/workbench/contrib/extensions/common/extensionQuery.ts index d6cd57bb5bc9..68320b1455a8 100644 --- a/src/vs/workbench/contrib/extensions/common/extensionQuery.ts +++ b/src/vs/workbench/contrib/extensions/common/extensionQuery.ts @@ -12,7 +12,7 @@ export class Query { } static suggestions(query: string): string[] { - const commands = ['installed', 'updates', 'enabled', 'disabled', 'builtin', 'featured', 'popular', 'recommended', 'recentlyPublished', 'workspaceUnsupported', 'deprecated', 'sort', 'category', 'tag', 'ext', 'id'] as const; + const commands = ['installed', 'updates', 'enabled', 'disabled', 'builtin', 'featured', 'popular', 'recommended', 'recentlyPublished', 'workspaceUnsupported', 'deprecated', 'sort', 'category', 'tag', 'ext', 'id', 'outdated', 'recentlyUpdated'] as const; const subcommands = { 'sort': ['installs', 'rating', 'name', 'publishedDate', 'updateDate'], 'category': EXTENSION_CATEGORIES.map(c => `"${c.toLowerCase()}"`), From 1cbc2eedcd75198db22137ab81701d09d6ee1290 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 19 Dec 2024 13:30:17 -0600 Subject: [PATCH 0272/3587] rm `auto` as a type for voice synthesizer setting (#236616) fix #229403 --- .../accessibility/browser/accessibilityConfiguration.ts | 5 ++--- .../chat/electron-sandbox/actions/voiceChatActions.ts | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 32eff1e15940..fc060c2832e6 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -806,14 +806,13 @@ export class DynamicSpeechAccessibilityConfiguration extends Disposable implemen }, [AccessibilityVoiceSettingId.AutoSynthesize]: { 'type': 'string', - 'enum': ['on', 'off', 'auto'], + 'enum': ['on', 'off'], 'enumDescriptions': [ localize('accessibility.voice.autoSynthesize.on', "Enable the feature. When a screen reader is enabled, note that this will disable aria updates."), localize('accessibility.voice.autoSynthesize.off', "Disable the feature."), - localize('accessibility.voice.autoSynthesize.auto', "When a screen reader is detected, disable the feature. Otherwise, enable the feature.") ], 'markdownDescription': localize('autoSynthesize', "Whether a textual response should automatically be read out aloud when speech was used as input. For example in a chat session, a response is automatically synthesized when voice was used as chat request."), - 'default': this.productService.quality !== 'stable' ? 'auto' : 'off', + 'default': this.productService.quality !== 'stable' ? 'on' : 'off', 'tags': ['accessibility'] } } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 57310027bc9e..f3fe9985e72c 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -369,8 +369,8 @@ class VoiceChatSessions { if (!response) { return; } - const autoSynthesize = this.configurationService.getValue<'on' | 'off' | 'auto'>(AccessibilityVoiceSettingId.AutoSynthesize); - if (autoSynthesize === 'on' || autoSynthesize === 'auto' && !this.accessibilityService.isScreenReaderOptimized()) { + const autoSynthesize = this.configurationService.getValue<'on' | 'off'>(AccessibilityVoiceSettingId.AutoSynthesize); + if (autoSynthesize === 'on' || (autoSynthesize !== 'off' && !this.accessibilityService.isScreenReaderOptimized())) { let context: IVoiceChatSessionController | 'focused'; if (controller.context === 'inline') { // This is ugly, but the lightweight inline chat turns into From 2a99d4331539247b4933b992ec768653d3634b61 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 19 Dec 2024 11:44:22 -0800 Subject: [PATCH 0273/3587] fix: don't watch untitled files for chat editing (#236634) --- .../chatEditing/chatEditingModifiedFileEntry.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 6becb0867930..aa105b2e7673 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -6,6 +6,7 @@ import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Disposable, IReference, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../../base/common/network.js'; import { IObservable, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; import { themeColorFromId } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -171,12 +172,15 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie this._register(this.doc.onDidChangeContent(e => this._mirrorEdits(e))); - this._register(this._fileService.watch(this.modifiedURI)); - this._register(this._fileService.onDidFilesChange(e => { - if (e.affects(this.modifiedURI) && kind === ChatEditKind.Created && e.gotDeleted()) { - this._onDidDelete.fire(); - } - })); + + if (this.modifiedURI.scheme !== Schemas.untitled) { + this._register(this._fileService.watch(this.modifiedURI)); + this._register(this._fileService.onDidFilesChange(e => { + if (e.affects(this.modifiedURI) && kind === ChatEditKind.Created && e.gotDeleted()) { + this._onDidDelete.fire(); + } + })); + } this._register(toDisposable(() => { this._clearCurrentEditLineDecoration(); From 2217f51cf8d7022cafe49301a523ea55cbddc5b4 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 19 Dec 2024 21:01:41 +0100 Subject: [PATCH 0274/3587] Fix title bar focus command when hidden (#236635) fix #236597 --- src/vs/workbench/browser/parts/titlebar/titlebarPart.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 3facaf51e236..32dad57a2183 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -54,6 +54,7 @@ import { IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionba import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; import { safeIntl } from '../../../../base/common/date.js'; +import { TitleBarVisibleContext } from '../../../common/contextkeys.js'; export interface ITitleVariable { readonly name: string; @@ -119,11 +120,12 @@ export class BrowserTitleService extends MultiWindowParts i title: localize2('focusTitleBar', 'Focus Title Bar'), category: Categories.View, f1: true, + precondition: TitleBarVisibleContext }); } run(): void { - that.getPartByDocument(getActiveDocument()).focus(); + that.getPartByDocument(getActiveDocument())?.focus(); } })); } From fca5600bbdb557edcf92f73839a55ef4cf6d6355 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 19 Dec 2024 12:03:31 -0800 Subject: [PATCH 0275/3587] Switch to use nodenext module resolution This better enforces that imports end with a file extension and also use relative paths --- package-lock.json | 9 +++++---- package.json | 2 +- src/bootstrap-node.ts | 2 +- src/tsconfig.base.json | 4 ++-- src/vs/platform/telemetry/common/1dsAppender.ts | 2 +- src/vs/platform/telemetry/node/1dsAppender.ts | 2 +- .../contrib/webview/browser/webviewMessages.d.ts | 4 ++-- 7 files changed, 13 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index fd395ebd4320..653966913947 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ "v8-inspect-profiler": "^0.1.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", - "vscode-textmate": "9.1.0", + "vscode-textmate": "9.2.0", "yauzl": "^3.0.0", "yazl": "^2.4.3" }, @@ -18248,9 +18248,10 @@ } }, "node_modules/vscode-textmate": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.1.0.tgz", - "integrity": "sha512-lxKSVp2DkFOx9RDAvpiYUrB9/KT1fAfi1aE8CBGstP8N7rLF+Seifj8kDA198X0mYj1CjQUC+81+nQf8CO0nVA==" + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.2.0.tgz", + "integrity": "sha512-rkvG4SraZQaPSN/5XjwKswdU0OP9MF28QjrYzUBbhb8QyG3ljB1Ky996m++jiI7KdiAP2CkBiQZd9pqEDTClqA==", + "license": "MIT" }, "node_modules/vscode-uri": { "version": "3.0.8", diff --git a/package.json b/package.json index eb0abcc3221b..8af17f6ef377 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "v8-inspect-profiler": "^0.1.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", - "vscode-textmate": "9.1.0", + "vscode-textmate": "9.2.0", "yauzl": "^3.0.0", "yazl": "^2.4.3" }, diff --git a/src/bootstrap-node.ts b/src/bootstrap-node.ts index c0d5cd3693cf..0fcd81f9a623 100644 --- a/src/bootstrap-node.ts +++ b/src/bootstrap-node.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import * as fs from 'fs'; import { fileURLToPath } from 'url'; import { createRequire } from 'node:module'; -import type { IProductConfiguration } from './vs/base/common/product'; +import type { IProductConfiguration } from './vs/base/common/product.ts'; const require = createRequire(import.meta.url); const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index 9c7aacd4f11b..a817569101c9 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "es2022", - "moduleResolution": "node", + "moduleResolution": "nodenext", "experimentalDecorators": true, "noImplicitReturns": true, "noImplicitOverride": true, @@ -27,4 +27,4 @@ ], "allowSyntheticDefaultImports": true } -} \ No newline at end of file +} diff --git a/src/vs/platform/telemetry/common/1dsAppender.ts b/src/vs/platform/telemetry/common/1dsAppender.ts index 7efb786c7630..59a0dadcb7d8 100644 --- a/src/vs/platform/telemetry/common/1dsAppender.ts +++ b/src/vs/platform/telemetry/common/1dsAppender.ts @@ -57,7 +57,7 @@ async function getClient(instrumentationKey: string, addInternalFlag?: boolean, appInsightsCore.initialize(coreConfig, []); - appInsightsCore.addTelemetryInitializer((envelope) => { + appInsightsCore.addTelemetryInitializer((envelope: any) => { // Opt the user out of 1DS data sharing envelope['ext'] = envelope['ext'] ?? {}; envelope['ext']['web'] = envelope['ext']['web'] ?? {}; diff --git a/src/vs/platform/telemetry/node/1dsAppender.ts b/src/vs/platform/telemetry/node/1dsAppender.ts index 08be57db219f..fe271ec428da 100644 --- a/src/vs/platform/telemetry/node/1dsAppender.ts +++ b/src/vs/platform/telemetry/node/1dsAppender.ts @@ -105,7 +105,7 @@ export class OneDataSystemAppender extends AbstractOneDataSystemAppender { ) { // Override the way events get sent since node doesn't have XHTMLRequest const customHttpXHROverride: IXHROverride = { - sendPOST: (payload: IPayloadData, oncomplete) => { + sendPOST: (payload: IPayloadData, oncomplete: OnCompleteFunc) => { // Fire off the async request without awaiting it sendPostAsync(requestService, payload, oncomplete); } diff --git a/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts b/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts index dde553ec0572..d04a15e1c672 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; -import type { WebviewStyles } from 'vs/workbench/contrib/webview/browser/webview'; +import type { IMouseWheelEvent } from '../../../../base/browser/mouseEvent.ts'; +import type { WebviewStyles } from './webview.ts'; type KeyEvent = { key: string; From e48d729d217084342cacf71bbfa6c1fa0d867feb Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 19 Dec 2024 12:09:55 -0800 Subject: [PATCH 0276/3587] Apply margin to action items (#236638) Fixes https://github.com/microsoft/vscode/issues/233699 --- .../platform/quickinput/browser/media/quickInput.css | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 9108ee9ae80e..53f42dfa5ae2 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -26,8 +26,14 @@ flex: 1; } -.quick-input-inline-action-bar { - margin: 2px 0 0 5px; +/* give some space between input and action bar */ +.quick-input-inline-action-bar > .actions-container > .action-item:first-child { + margin-left: 5px; +} + +/* center horizontally */ +.quick-input-inline-action-bar > .actions-container > .action-item { + margin-top: 2px; } .quick-input-title { From 83c8c2074bbdf3bdd39905a713ebb5ad446c3d11 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 19 Dec 2024 12:34:03 -0800 Subject: [PATCH 0277/3587] Also bump in remote --- remote/package-lock.json | 9 +++++---- remote/package.json | 2 +- remote/web/package-lock.json | 9 +++++---- remote/web/package.json | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/remote/package-lock.json b/remote/package-lock.json index 3cf229398615..71119e97cf66 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -40,7 +40,7 @@ "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", - "vscode-textmate": "9.1.0", + "vscode-textmate": "9.2.0", "yauzl": "^3.0.0", "yazl": "^2.4.3" } @@ -1447,9 +1447,10 @@ } }, "node_modules/vscode-textmate": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.1.0.tgz", - "integrity": "sha512-lxKSVp2DkFOx9RDAvpiYUrB9/KT1fAfi1aE8CBGstP8N7rLF+Seifj8kDA198X0mYj1CjQUC+81+nQf8CO0nVA==" + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.2.0.tgz", + "integrity": "sha512-rkvG4SraZQaPSN/5XjwKswdU0OP9MF28QjrYzUBbhb8QyG3ljB1Ky996m++jiI7KdiAP2CkBiQZd9pqEDTClqA==", + "license": "MIT" }, "node_modules/wrappy": { "version": "1.0.2", diff --git a/remote/package.json b/remote/package.json index 5a61a15c0513..26c002943e69 100644 --- a/remote/package.json +++ b/remote/package.json @@ -35,7 +35,7 @@ "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", - "vscode-textmate": "9.1.0", + "vscode-textmate": "9.2.0", "yauzl": "^3.0.0", "yazl": "^2.4.3" }, diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 3e45e62a14dc..4d27bc8a29cd 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -24,7 +24,7 @@ "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", - "vscode-textmate": "9.1.0" + "vscode-textmate": "9.2.0" } }, "node_modules/@microsoft/1ds-core-js": { @@ -266,9 +266,10 @@ "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==" }, "node_modules/vscode-textmate": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.1.0.tgz", - "integrity": "sha512-lxKSVp2DkFOx9RDAvpiYUrB9/KT1fAfi1aE8CBGstP8N7rLF+Seifj8kDA198X0mYj1CjQUC+81+nQf8CO0nVA==" + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.2.0.tgz", + "integrity": "sha512-rkvG4SraZQaPSN/5XjwKswdU0OP9MF28QjrYzUBbhb8QyG3ljB1Ky996m++jiI7KdiAP2CkBiQZd9pqEDTClqA==", + "license": "MIT" }, "node_modules/yallist": { "version": "4.0.0", diff --git a/remote/web/package.json b/remote/web/package.json index 8c1decd65bba..3bd5d4a937dd 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -19,6 +19,6 @@ "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", - "vscode-textmate": "9.1.0" + "vscode-textmate": "9.2.0" } } From 358e96ab1e9017d8dc90b7a24e617d9f39e017b4 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 19 Dec 2024 12:40:19 -0800 Subject: [PATCH 0278/3587] Cancel if the user dismisses the modal (#236642) Fixes https://github.com/microsoft/vscode/issues/235364 --- .../microsoft-authentication/src/node/authProvider.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/extensions/microsoft-authentication/src/node/authProvider.ts b/extensions/microsoft-authentication/src/node/authProvider.ts index af34273afa4d..cc8eb2bc5c7d 100644 --- a/extensions/microsoft-authentication/src/node/authProvider.ts +++ b/extensions/microsoft-authentication/src/node/authProvider.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AccountInfo, AuthenticationResult, ServerError } from '@azure/msal-node'; +import { AccountInfo, AuthenticationResult, ClientAuthError, ClientAuthErrorCodes, ServerError } from '@azure/msal-node'; import { AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationProviderSessionOptions, AuthenticationSession, AuthenticationSessionAccountInformation, CancellationError, env, EventEmitter, ExtensionContext, l10n, LogOutputChannel, Uri, window } from 'vscode'; import { Environment } from '@azure/ms-rest-azure-env'; import { CachedPublicClientApplicationManager } from './publicClientCache'; @@ -229,6 +229,12 @@ export class MsalAuthProvider implements AuthenticationProvider { throw e; } + // The user closed the modal window + if ((e as ClientAuthError).errorCode === ClientAuthErrorCodes.userCanceled) { + this._telemetryReporter.sendLoginFailedEvent(); + throw e; + } + // The user wants to try the loopback client or we got an error likely due to spinning up the server const loopbackClient = new UriHandlerLoopbackClient(this._uriHandler, redirectUri, this._logger); try { From ad359b1ca9090f340655e8839a76abb9b59f8761 Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Thu, 19 Dec 2024 12:58:38 -0800 Subject: [PATCH 0279/3587] Clear nb selection highlights on non-explicit cursor events (#236641) ugh I overthought this. clear on non-explicit cursor events. (ie typing) --- .../browser/contrib/multicursor/notebookSelectionHighlight.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookSelectionHighlight.ts b/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookSelectionHighlight.ts index a7e82c41a73b..305e4d8fde45 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookSelectionHighlight.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookSelectionHighlight.ts @@ -63,6 +63,7 @@ class NotebookSelectionHighlighter extends Disposable implements INotebookEditor this.anchorDisposables.clear(); this.anchorDisposables.add(this.anchorCell[1].onDidChangeCursorPosition((e) => { if (e.reason !== CursorChangeReason.Explicit) { + this.clearNotebookSelectionDecorations(); return; } From 6ec5e7b296e7d96e8fbd87d1329bf5642d738da2 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 19 Dec 2024 13:06:18 -0800 Subject: [PATCH 0280/3587] debug: fix tree 'measuring node not in DOM error' (#236645) --- .../workbench/contrib/debug/browser/repl.ts | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index edef7e72b706..04823d1f137c 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -174,7 +174,11 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this.onDidFocusSession(this.debugService.getViewModel().focusedSession); } - this._register(this.debugService.getViewModel().onDidFocusSession(async session => this.onDidFocusSession(session))); + this._register(this.debugService.getViewModel().onDidFocusSession(session => { + if (this.isVisible()) { + this.onDidFocusSession(session); + } + })); this._register(this.debugService.getViewModel().onDidEvaluateLazyExpression(async e => { if (e instanceof Variable && this.tree?.hasNode(e)) { await this.tree.updateChildren(e, false, true); @@ -201,19 +205,27 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { } })); this._register(this.onDidChangeBodyVisibility(visible => { - if (visible) { - if (!this.model) { - this.model = this.modelService.getModel(Repl.URI) || this.modelService.createModel('', null, Repl.URI, true); - } - this.setMode(); - this.replInput.setModel(this.model); - this.updateInputDecoration(); - this.refreshReplElements(true); - if (this.styleChangedWhenInvisible) { - this.styleChangedWhenInvisible = false; - this.tree?.updateChildren(undefined, true, false); - this.onDidStyleChange(); - } + if (!visible) { + return; + } + if (!this.model) { + this.model = this.modelService.getModel(Repl.URI) || this.modelService.createModel('', null, Repl.URI, true); + } + + const focusedSession = this.debugService.getViewModel().focusedSession; + if (this.tree && this.tree.getInput() !== focusedSession) { + this.onDidFocusSession(focusedSession); + } + + this.setMode(); + this.replInput.setModel(this.model); + this.updateInputDecoration(); + this.refreshReplElements(true); + + if (this.styleChangedWhenInvisible) { + this.styleChangedWhenInvisible = false; + this.tree?.updateChildren(undefined, true, false); + this.onDidStyleChange(); } })); this._register(this.configurationService.onDidChangeConfiguration(e => { From f442df17470c5b95936acc79bbca27509177cf31 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 19 Dec 2024 22:06:52 +0100 Subject: [PATCH 0281/3587] Git - better match git conflict decorations (#236646) --- extensions/git/src/commands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index dde1c99049a8..95ab9ada0713 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -309,7 +309,7 @@ async function categorizeResourceByResolution(resources: Resource[]): Promise<{ const isBothAddedOrModified = (s: Resource) => s.type === Status.BOTH_MODIFIED || s.type === Status.BOTH_ADDED; const isAnyDeleted = (s: Resource) => s.type === Status.DELETED_BY_THEM || s.type === Status.DELETED_BY_US; const possibleUnresolved = merge.filter(isBothAddedOrModified); - const promises = possibleUnresolved.map(s => grep(s.resourceUri.fsPath, /^<{7}|^={7}|^>{7}/)); + const promises = possibleUnresolved.map(s => grep(s.resourceUri.fsPath, /^<{7}\s|^={7}$|^>{7}\s/)); const unresolvedBothModified = await Promise.all(promises); const resolved = possibleUnresolved.filter((_s, i) => !unresolvedBothModified[i]); const deletionConflicts = merge.filter(s => isAnyDeleted(s)); From ee8c3e9e8acd54968405d96dea233b31c32c6830 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 19 Dec 2024 15:52:14 -0600 Subject: [PATCH 0282/3587] add `Terminal: resize suggest widget size` command (#236639) fix #235091 --- .../suggest/browser/terminal.suggest.contribution.ts | 6 ++++++ .../terminalContrib/suggest/browser/terminalSuggestAddon.ts | 4 ++++ .../terminalContrib/suggest/common/terminal.suggest.ts | 1 + .../services/suggest/browser/simpleSuggestWidget.ts | 4 ++++ 4 files changed, 15 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 96676a5a12c1..35a845c1f7b8 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -195,6 +195,12 @@ registerActiveInstanceAction({ run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.requestCompletions(true) }); +registerActiveInstanceAction({ + id: TerminalSuggestCommandId.ResetWidgetSize, + title: localize2('workbench.action.terminal.resetSuggestWidgetSize', 'Reset Suggest Widget Size'), + run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.resetWidgetSize() +}); + registerActiveInstanceAction({ id: TerminalSuggestCommandId.SelectPrevSuggestion, title: localize2('workbench.action.terminal.selectPrevSuggestion', 'Select the Previous Suggestion'), diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 9f173cfc15a6..94650c7525c0 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -231,6 +231,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._screen = screen; } + resetWidgetSize(): void { + this._suggestWidget?.resetWidgetSize(); + } + async requestCompletions(explicitlyInvoked?: boolean): Promise { if (!this._promptInputModel) { return; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts index b9fe0dcbe608..0b62f7901242 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts @@ -13,6 +13,7 @@ export const enum TerminalSuggestCommandId { HideSuggestWidget = 'workbench.action.terminal.hideSuggestWidget', ClearSuggestCache = 'workbench.action.terminal.clearSuggestCache', RequestCompletions = 'workbench.action.terminal.requestCompletions', + ResetWidgetSize = 'workbench.action.terminal.resetSuggestWidgetSize', } export const defaultTerminalSuggestCommandsToSkipShell = [ diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index bc9c9ea0cba9..af70d32b896d 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -290,6 +290,10 @@ export class SimpleSuggestWidget extends Disposable { return this._completionModel?.items.length !== 0; } + resetWidgetSize(): void { + this._persistedSize.reset(); + } + showSuggestions(selectionIndex: number, isFrozen: boolean, isAuto: boolean, cursorPosition: { top: number; left: number; height: number }): void { this._cursorPosition = cursorPosition; From 41793c2d43f18397608003a7af524ce660b0b4c3 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 19 Dec 2024 16:26:26 -0600 Subject: [PATCH 0283/3587] use keyboard vs key icon for configure keybinding action icons (#236649) fix #229827 --- .../contrib/accessibility/browser/accessibleViewActions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts index d1be576a1316..f587bd7cb1ee 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts @@ -233,7 +233,7 @@ class AccessibilityHelpConfigureKeybindingsAction extends Action2 { super({ id: AccessibilityCommandId.AccessibilityHelpConfigureKeybindings, precondition: ContextKeyExpr.and(accessibilityHelpIsShown, accessibleViewHasUnassignedKeybindings), - icon: Codicon.key, + icon: Codicon.recordKeys, keybinding: { primary: KeyMod.Alt | KeyCode.KeyK, weight: KeybindingWeight.WorkbenchContrib @@ -260,7 +260,7 @@ class AccessibilityHelpConfigureAssignedKeybindingsAction extends Action2 { super({ id: AccessibilityCommandId.AccessibilityHelpConfigureAssignedKeybindings, precondition: ContextKeyExpr.and(accessibilityHelpIsShown, accessibleViewHasAssignedKeybindings), - icon: Codicon.key, + icon: Codicon.recordKeys, keybinding: { primary: KeyMod.Alt | KeyCode.KeyA, weight: KeybindingWeight.WorkbenchContrib From 38c3ebef2e3e9069552b835855271dcf0ac3fe4e Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 19 Dec 2024 17:39:08 -0800 Subject: [PATCH 0284/3587] Remove repeated call, warn on invalid persisted data (#236658) --- src/vs/workbench/contrib/chat/common/chatModel.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 14fa03dffa0f..d1b092b114cd 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -999,12 +999,17 @@ export class ChatModel extends Disposable implements IChatModel { ) { super(); - this._isImported = (!!initialData && !isSerializableSessionData(initialData)) || (initialData?.isImported ?? false); - this._sessionId = (isSerializableSessionData(initialData) && initialData.sessionId) || generateUuid(); + const isValid = isSerializableSessionData(initialData); + if (initialData && !isValid) { + this.logService.warn(`ChatModel#constructor: Loaded malformed session data: ${JSON.stringify(initialData)}`); + } + + this._isImported = (!!initialData && !isValid) || (initialData?.isImported ?? false); + this._sessionId = (isValid && initialData.sessionId) || generateUuid(); this._requests = initialData ? this._deserialize(initialData) : []; - this._creationDate = (isSerializableSessionData(initialData) && initialData.creationDate) || Date.now(); - this._lastMessageDate = (isSerializableSessionData(initialData) && initialData.lastMessageDate) || this._creationDate; - this._customTitle = isSerializableSessionData(initialData) ? initialData.customTitle : undefined; + this._creationDate = (isValid && initialData.creationDate) || Date.now(); + this._lastMessageDate = (isValid && initialData.lastMessageDate) || this._creationDate; + this._customTitle = isValid ? initialData.customTitle : undefined; this._initialRequesterAvatarIconUri = initialData?.requesterAvatarIconUri && URI.revive(initialData.requesterAvatarIconUri); this._initialResponderAvatarIconUri = isUriComponents(initialData?.responderAvatarIconUri) ? URI.revive(initialData.responderAvatarIconUri) : initialData?.responderAvatarIconUri; From 89f808979a5151bd91324e65d4f7ab1b62896983 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 19 Dec 2024 19:39:33 -0800 Subject: [PATCH 0285/3587] Cancel request tool calls when cancelling chat request (#236663) * Cancel request tool calls when cancelling chat request Fix #232775 * Don't invoke tool if cancelled before * Fix tests --- .../chat/browser/languageModelToolsService.ts | 72 +++++++++++++++++-- .../contrib/chat/common/chatServiceImpl.ts | 11 ++- .../chat/common/languageModelToolsService.ts | 1 + .../browser/languageModelToolsService.test.ts | 71 +++++++++++++++++- .../chat/test/common/mockChatService.ts | 7 +- .../common/mockLanguageModelToolsService.ts | 3 + .../test/browser/inlineChatController.test.ts | 3 + 7 files changed, 156 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index 2b3f6b2dc497..d0bce841451c 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -5,11 +5,11 @@ import { renderStringAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'; import { Emitter } from '../../../../base/common/event.js'; import { Iterable } from '../../../../base/common/iterator.js'; -import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { localize } from '../../../../nls.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; @@ -37,6 +37,9 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private _tools = new Map(); private _toolContextKeys = new Set(); + + private _callsByRequestId = new Map(); + constructor( @IExtensionService private readonly _extensionService: IExtensionService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @@ -141,10 +144,34 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo // Shortcut to write to the model directly here, but could call all the way back to use the real stream. let toolInvocation: ChatToolInvocation | undefined; + let requestId: string | undefined; + let store: DisposableStore | undefined; try { if (dto.context) { - const model = this._chatService.getSession(dto.context?.sessionId) as ChatModel; + store = new DisposableStore(); + const model = this._chatService.getSession(dto.context?.sessionId) as ChatModel | undefined; + if (!model) { + throw new Error(`Tool called for unknown chat session`); + } + const request = model.getRequests().at(-1)!; + requestId = request.id; + + // Replace the token with a new token that we can cancel when cancelToolCallsForRequest is called + if (!this._callsByRequestId.has(requestId)) { + this._callsByRequestId.set(requestId, []); + } + this._callsByRequestId.get(requestId)!.push(store); + + const source = new CancellationTokenSource(); + store.add(toDisposable(() => { + toolInvocation!.confirmed.complete(false); + source.dispose(true); + })); + store.add(token.onCancellationRequested(() => { + source.cancel(); + })); + token = source.token; const prepared = tool.impl.prepareToolInvocation ? await tool.impl.prepareToolInvocation(dto.parameters, token) @@ -153,9 +180,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo const defaultMessage = localize('toolInvocationMessage', "Using {0}", `"${tool.data.displayName}"`); const invocationMessage = prepared?.invocationMessage ?? defaultMessage; toolInvocation = new ChatToolInvocation(invocationMessage, prepared?.confirmationMessages); - token.onCancellationRequested(() => { - toolInvocation!.confirmed.complete(false); - }); + model.acceptResponseProgress(request, toolInvocation); if (prepared?.confirmationMessages) { const userConfirmed = await toolInvocation.confirmed.p; @@ -176,6 +201,9 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } } + if (token.isCancellationRequested) { + throw new CancellationError(); + } const result = await tool.impl.invoke(dto, countTokens, token); this._telemetryService.publicLog2( @@ -200,7 +228,39 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo throw err; } finally { toolInvocation?.isCompleteDeferred.complete(); + + if (requestId && store) { + this.cleanupCallDisposables(requestId, store); + } + } + } + + private cleanupCallDisposables(requestId: string, store: DisposableStore): void { + const disposables = this._callsByRequestId.get(requestId); + if (disposables) { + const index = disposables.indexOf(store); + if (index > -1) { + disposables.splice(index, 1); + } + if (disposables.length === 0) { + this._callsByRequestId.delete(requestId); + } } + store.dispose(); + } + + cancelToolCallsForRequest(requestId: string): void { + const calls = this._callsByRequestId.get(requestId); + if (calls) { + calls.forEach(call => call.dispose()); + this._callsByRequestId.delete(requestId); + } + } + + public override dispose(): void { + super.dispose(); + + this._callsByRequestId.forEach(calls => dispose(calls)); } } diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index d5a8839dd11b..6cfb29ce0c95 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -33,6 +33,7 @@ import { ChatServiceTelemetry } from './chatServiceTelemetry.js'; import { IChatSlashCommandService } from './chatSlashCommands.js'; import { IChatVariablesService } from './chatVariables.js'; import { ChatMessageRole, IChatMessage } from './languageModels.js'; +import { ILanguageModelToolsService } from './languageModelToolsService.js'; const serializedChatKey = 'interactive.sessions'; @@ -86,7 +87,8 @@ const maxPersistedSessions = 25; class CancellableRequest implements IDisposable { constructor( public readonly cancellationTokenSource: CancellationTokenSource, - public requestId?: string | undefined + public requestId: string | undefined, + @ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService ) { } dispose() { @@ -94,6 +96,10 @@ class CancellableRequest implements IDisposable { } cancel() { + if (this.requestId) { + this.toolsService.cancelToolCallsForRequest(this.requestId); + } + this.cancellationTokenSource.cancel(); } } @@ -778,7 +784,8 @@ export class ChatService extends Disposable implements IChatService { } }; const rawResponsePromise = sendRequestInternal(); - this._pendingRequests.set(model.sessionId, new CancellableRequest(source)); + // Note- requestId is not known at this point, assigned later + this._pendingRequests.set(model.sessionId, this.instantiationService.createInstance(CancellableRequest, source, undefined)); rawResponsePromise.finally(() => { this._pendingRequests.deleteAndDispose(model.sessionId); }); diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index f2251ca8166f..f69be4fed5c6 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -86,4 +86,5 @@ export interface ILanguageModelToolsService { getTool(id: string): IToolData | undefined; getToolByName(name: string): IToolData | undefined; invokeTool(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise; + cancelToolCallsForRequest(requestId: string): void; } diff --git a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts index 9679c7a89089..fb866bbfade5 100644 --- a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts @@ -6,24 +6,32 @@ import * as assert from 'assert'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { ContextKeyService } from '../../../../../platform/contextkey/browser/contextKeyService.js'; import { ContextKeyEqualsExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; import { LanguageModelToolsService } from '../../browser/languageModelToolsService.js'; +import { IChatModel } from '../../common/chatModel.js'; +import { IChatService } from '../../common/chatService.js'; import { IToolData, IToolImpl, IToolInvocation } from '../../common/languageModelToolsService.js'; -import { ContextKeyService } from '../../../../../platform/contextkey/browser/contextKeyService.js'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { MockChatService } from '../common/mockChatService.js'; +import { CancellationError, isCancellationError } from '../../../../../base/common/errors.js'; +import { Barrier } from '../../../../../base/common/async.js'; suite('LanguageModelToolsService', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); let contextKeyService: IContextKeyService; let service: LanguageModelToolsService; + let chatService: MockChatService; setup(() => { const instaService = workbenchInstantiationService({ - contextKeyService: () => store.add(new ContextKeyService(new TestConfigurationService)) + contextKeyService: () => store.add(new ContextKeyService(new TestConfigurationService)), }, store); contextKeyService = instaService.get(IContextKeyService); + chatService = new MockChatService(); + instaService.stub(IChatService, chatService); service = store.add(instaService.createInstance(LanguageModelToolsService)); }); @@ -122,4 +130,61 @@ suite('LanguageModelToolsService', () => { const result = await service.invokeTool(dto, async () => 0, CancellationToken.None); assert.strictEqual(result.content[0].value, 'result'); }); + + test('cancel tool call', async () => { + const toolData: IToolData = { + id: 'testTool', + modelDescription: 'Test Tool', + displayName: 'Test Tool' + }; + + store.add(service.registerToolData(toolData)); + + const toolBarrier = new Barrier(); + const toolImpl: IToolImpl = { + invoke: async (invocation, countTokens, cancelToken) => { + assert.strictEqual(invocation.callId, '1'); + assert.strictEqual(invocation.toolId, 'testTool'); + assert.deepStrictEqual(invocation.parameters, { a: 1 }); + await toolBarrier.wait(); + if (cancelToken.isCancellationRequested) { + throw new CancellationError(); + } else { + throw new Error('Tool call should be cancelled'); + } + } + }; + + store.add(service.registerToolImplementation('testTool', toolImpl)); + + const sessionId = 'sessionId'; + const requestId = 'requestId'; + const dto: IToolInvocation = { + callId: '1', + toolId: 'testTool', + tokenBudget: 100, + parameters: { + a: 1 + }, + context: { + sessionId + }, + }; + chatService.addSession({ + sessionId: sessionId, + getRequests: () => { + return [{ + id: requestId + }]; + }, + acceptResponseProgress: () => { } + } as any as IChatModel); + + const toolPromise = service.invokeTool(dto, async () => 0, CancellationToken.None); + service.cancelToolCallsForRequest(requestId); + toolBarrier.open(); + await assert.rejects(toolPromise, err => { + return isCancellationError(err); + }, 'Expected tool call to be cancelled'); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index ad4262a7dbd5..31fbb654b635 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -15,6 +15,8 @@ export class MockChatService implements IChatService { _serviceBrand: undefined; transferredSessionData: IChatTransferredSessionData | undefined; + private sessions = new Map(); + isEnabled(location: ChatAgentLocation): boolean { throw new Error('Method not implemented.'); } @@ -27,9 +29,12 @@ export class MockChatService implements IChatService { startSession(location: ChatAgentLocation, token: CancellationToken): ChatModel | undefined { throw new Error('Method not implemented.'); } + addSession(session: IChatModel): void { + this.sessions.set(session.sessionId, session); + } getSession(sessionId: string): IChatModel | undefined { // eslint-disable-next-line local/code-no-dangerous-type-assertions - return {} as IChatModel; + return this.sessions.get(sessionId) ?? {} as IChatModel; } getOrRestoreSession(sessionId: string): IChatModel | undefined { throw new Error('Method not implemented.'); diff --git a/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts b/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts index a192334cb9dc..d854a0beb5e9 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts @@ -13,6 +13,9 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService constructor() { } + cancelToolCallsForRequest(requestId: string): void { + } + onDidChangeTools: Event = Event.None; registerToolData(toolData: IToolData): IDisposable { diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 7d07547770ee..75db4dbb7f1f 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -71,6 +71,8 @@ import { ITextModelService } from '../../../../../editor/common/services/resolve import { TextModelResolverService } from '../../../../services/textmodelResolver/common/textModelResolverService.js'; import { ChatInputBoxContentProvider } from '../../../chat/browser/chatEdinputInputContentProvider.js'; import { IObservable, observableValue } from '../../../../../base/common/observable.js'; +import { ILanguageModelToolsService } from '../../../chat/common/languageModelToolsService.js'; +import { MockLanguageModelToolsService } from '../../../chat/test/common/mockLanguageModelToolsService.js'; suite('InlineChatController', function () { @@ -198,6 +200,7 @@ suite('InlineChatController', function () { [IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()], [ILanguageModelsService, new SyncDescriptor(LanguageModelsService)], [ITextModelService, new SyncDescriptor(TextModelResolverService)], + [ILanguageModelToolsService, new SyncDescriptor(MockLanguageModelToolsService)], ); instaService = store.add((store.add(workbenchInstantiationService(undefined, store))).createChild(serviceCollection)); From 77cec55e495ea7b9dceee6f589c781799e325d20 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 20 Dec 2024 08:53:35 +0100 Subject: [PATCH 0286/3587] Git - git installation welcome view should use remoteName context key (#236672) --- extensions/git/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index d63027619d18..53aa0fad747e 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -3372,22 +3372,22 @@ { "view": "scm", "contents": "%view.workbench.scm.missing%", - "when": "config.git.enabled && git.missing" + "when": "config.git.enabled && git.missing && remoteName != ''" }, { "view": "scm", "contents": "%view.workbench.scm.missing.mac%", - "when": "config.git.enabled && git.missing && isMac" + "when": "config.git.enabled && git.missing && remoteName == '' && isMac" }, { "view": "scm", "contents": "%view.workbench.scm.missing.windows%", - "when": "config.git.enabled && git.missing && isWindows" + "when": "config.git.enabled && git.missing && remoteName == '' && isWindows" }, { "view": "scm", "contents": "%view.workbench.scm.missing.linux%", - "when": "config.git.enabled && git.missing && isLinux" + "when": "config.git.enabled && git.missing && remoteName == '' && isLinux" }, { "view": "scm", From da59ef74e2587e707944db56bcc0f84a9e6e3ff8 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 20 Dec 2024 09:11:12 +0100 Subject: [PATCH 0287/3587] workbench.action.focus*GroupWithoutWrap commands fail to focus *-most group when sibebar/panel is focused (fix #236648) (#236673) --- src/vs/workbench/browser/parts/editor/editorCommands.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 1bbe6258eb05..20c084c96032 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -312,6 +312,7 @@ function registerActiveEditorMoveCopyCommand(): void { } else if (sourceGroup.id !== targetGroup.id) { sourceGroup.copyEditors(editors.map(editor => ({ editor })), targetGroup); } + targetGroup.focus(); } } @@ -972,8 +973,8 @@ function registerFocusEditorGroupWihoutWrapCommands(): void { CommandsRegistry.registerCommand(command.id, async (accessor: ServicesAccessor) => { const editorGroupsService = accessor.get(IEditorGroupsService); - const group = editorGroupsService.findGroup({ direction: command.direction }, editorGroupsService.activeGroup, false); - group?.focus(); + const group = editorGroupsService.findGroup({ direction: command.direction }, editorGroupsService.activeGroup, false) ?? editorGroupsService.activeGroup; + group.focus(); }); } } From b9084edd1cf2414e21de4f9e5b5acae240a22218 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:08:22 +0100 Subject: [PATCH 0288/3587] Git - file system provider should throw `FileNotFound` if the resource does not exist in git instead of returning an empty file (#236676) --- extensions/git/src/fileSystemProvider.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/git/src/fileSystemProvider.ts b/extensions/git/src/fileSystemProvider.ts index af80924ae138..24ae4e6df9a7 100644 --- a/extensions/git/src/fileSystemProvider.ts +++ b/extensions/git/src/fileSystemProvider.ts @@ -192,7 +192,8 @@ export class GitFileSystemProvider implements FileSystemProvider { try { return await repository.buffer(sanitizeRef(ref, path, repository), path); } catch (err) { - return new Uint8Array(0); + // File does not exist in git (ex: git ignored) + throw FileSystemError.FileNotFound(); } } From 91ced52bc8547ce1c21138ee4a6a5061cf995c92 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Dec 2024 11:01:48 +0100 Subject: [PATCH 0289/3587] Add the possibility to define context keys on CodeEditorWidgets --- .../browser/widget/codeEditor/codeEditorWidget.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index ed20f95580a8..3bd2f0a903e7 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -290,6 +290,11 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE })); this._contextKeyService = this._register(contextKeyService.createScoped(this._domElement)); + if (codeEditorWidgetOptions.contextKeyValues) { + for (const [key, value] of Object.entries(codeEditorWidgetOptions.contextKeyValues)) { + this._contextKeyService.createKey(key, value); + } + } this._notificationService = notificationService; this._codeEditorService = codeEditorService; this._commandService = commandService; @@ -1988,6 +1993,12 @@ export interface ICodeEditorWidgetOptions { * Defaults to MenuId.SimpleEditorContext or MenuId.EditorContext depending on whether the widget is simple. */ contextMenuId?: MenuId; + + /** + * Define extra context keys that will be defined in the context service + * for the editor. + */ + contextKeyValues?: Record; } class ModelData { From 7d4b23f21adb12d47f582388ce96f9e447908f77 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Dec 2024 11:02:35 +0100 Subject: [PATCH 0290/3587] Move `getOuterEditor` near the EmbeddedCodeEditorWidget --- .../widget/codeEditor/embeddedCodeEditorWidget.ts | 10 +++++++++- .../gotoSymbol/browser/peek/referencesController.ts | 3 ++- src/vs/editor/contrib/peekView/browser/peekView.ts | 11 +---------- .../workbench/contrib/scm/browser/quickDiffWidget.ts | 3 ++- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget.ts index b716aa611097..3852374d3949 100644 --- a/src/vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget.ts @@ -13,7 +13,7 @@ import { ILanguageFeaturesService } from '../../../common/services/languageFeatu import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; @@ -61,3 +61,11 @@ export class EmbeddedCodeEditorWidget extends CodeEditorWidget { super.updateOptions(this._overwriteOptions); } } + +export function getOuterEditor(accessor: ServicesAccessor): ICodeEditor | null { + const editor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); + if (editor instanceof EmbeddedCodeEditorWidget) { + return editor.getParentEditor(); + } + return editor; +} diff --git a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesController.ts b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesController.ts index 175e4c8b49cc..48ba268a6990 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesController.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesController.ts @@ -14,7 +14,8 @@ import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; import { IEditorContribution } from '../../../../common/editorCommon.js'; import { Location } from '../../../../common/languages.js'; -import { getOuterEditor, PeekContext } from '../../../peekView/browser/peekView.js'; +import { PeekContext } from '../../../peekView/browser/peekView.js'; +import { getOuterEditor } from '../../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import * as nls from '../../../../../nls.js'; import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; diff --git a/src/vs/editor/contrib/peekView/browser/peekView.ts b/src/vs/editor/contrib/peekView/browser/peekView.ts index 17084e7ae641..c774617241fa 100644 --- a/src/vs/editor/contrib/peekView/browser/peekView.ts +++ b/src/vs/editor/contrib/peekView/browser/peekView.ts @@ -16,7 +16,6 @@ import * as objects from '../../../../base/common/objects.js'; import './media/peekViewWidget.css'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../browser/editorExtensions.js'; -import { ICodeEditorService } from '../../../browser/services/codeEditorService.js'; import { EmbeddedCodeEditorWidget } from '../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; import { IEditorContribution } from '../../../common/editorCommon.js'; @@ -25,7 +24,7 @@ import * as nls from '../../../../nls.js'; import { createActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { createDecorator, IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { activeContrastBorder, contrastBorder, editorForeground, editorInfoForeground, registerColor } from '../../../../platform/theme/common/colorRegistry.js'; export const IPeekViewService = createDecorator('IPeekViewService'); @@ -79,14 +78,6 @@ class PeekContextController implements IEditorContribution { registerEditorContribution(PeekContextController.ID, PeekContextController, EditorContributionInstantiation.Eager); // eager because it needs to define a context key -export function getOuterEditor(accessor: ServicesAccessor): ICodeEditor | null { - const editor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); - if (editor instanceof EmbeddedCodeEditorWidget) { - return editor.getParentEditor(); - } - return editor; -} - export interface IPeekViewStyles extends IStyles { headerBackgroundColor?: Color; primaryHeadingColor?: Color; diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts index 8ac67f6cd539..37ed63fb396f 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts @@ -13,7 +13,7 @@ import { ISelectOptionItem } from '../../../../base/browser/ui/selectBox/selectB import { SelectActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { defaultSelectBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { IColorTheme, IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { getOuterEditor, peekViewBorder, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground, PeekViewWidget } from '../../../../editor/contrib/peekView/browser/peekView.js'; +import { peekViewBorder, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground, PeekViewWidget } from '../../../../editor/contrib/peekView/browser/peekView.js'; import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; import { IMenu, IMenuService, MenuId, MenuItemAction, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; @@ -48,6 +48,7 @@ import { gotoNextLocation, gotoPreviousLocation } from '../../../../platform/the import { Codicon } from '../../../../base/common/codicons.js'; import { Color } from '../../../../base/common/color.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; +import { getOuterEditor } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; export const isQuickDiffVisible = new RawContextKey('dirtyDiffVisible', false); From 9cb7a27bd0bdecd16003711015d0a5172d5a8804 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Dec 2024 11:14:30 +0100 Subject: [PATCH 0291/3587] Make Tab and Escape work when focus is in the preview --- .../browser/controller/commands.ts | 28 +++++++++++++++---- .../controller/inlineCompletionContextKeys.ts | 3 ++ .../controller/inlineCompletionsController.ts | 14 +++++++++- .../view/inlineEdits/sideBySideDiff.ts | 9 ++++-- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts index f183f9f896ff..2fe48fc4eb9b 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts @@ -11,7 +11,8 @@ import { Action2, MenuId } from '../../../../../platform/actions/common/actions. import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { KeybindingsRegistry, KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { INotificationService, Severity } from '../../../../../platform/notification/common/notification.js'; import { ICodeEditor } from '../../../../browser/editorBrowser.js'; import { EditorAction, EditorCommand, ServicesAccessor } from '../../../../browser/editorExtensions.js'; import { EditorContextKeys } from '../../../../common/editorContextKeys.js'; @@ -19,7 +20,6 @@ import { Context as SuggestContext } from '../../../suggest/browser/suggest.js'; import { inlineSuggestCommitId, showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId } from './commandIds.js'; import { InlineCompletionContextKeys } from './inlineCompletionContextKeys.js'; import { InlineCompletionsController } from './inlineCompletionsController.js'; -import { INotificationService, Severity } from '../../../../../platform/notification/common/notification.js'; export class ShowNextInlineSuggestionAction extends EditorAction { public static ID = showNextInlineSuggestionActionId; @@ -211,14 +211,21 @@ export class AcceptInlineCompletion extends EditorAction { }); } - public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { - const controller = InlineCompletionsController.get(editor); + public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + const controller = InlineCompletionsController.getInFocusedEditorOrParent(accessor); if (controller) { controller.model.get()?.accept(controller.editor); controller.editor.focus(); } } } +KeybindingsRegistry.registerKeybindingRule({ + id: inlineSuggestCommitId, + weight: 202, // greater than jump + primary: KeyCode.Tab, + when: ContextKeyExpr.and(InlineCompletionContextKeys.inInlineEditsPreviewEditor) +}); + export class JumpToNextInlineEdit extends EditorAction { constructor() { @@ -276,14 +283,23 @@ export class HideInlineCompletion extends EditorAction { }); } - public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { - const controller = InlineCompletionsController.get(editor); + public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + const controller = InlineCompletionsController.getInFocusedEditorOrParent(accessor); transaction(tx => { controller?.model.get()?.stop('explicitCancel', tx); }); + controller?.editor.focus(); } } +KeybindingsRegistry.registerKeybindingRule({ + id: HideInlineCompletion.ID, + weight: -1, // very weak + primary: KeyCode.Escape, + secondary: [KeyMod.Shift | KeyCode.Escape], + when: ContextKeyExpr.and(InlineCompletionContextKeys.inInlineEditsPreviewEditor) +}); + export class ToggleAlwaysShowInlineSuggestionToolbar extends Action2 { public static ID = 'editor.action.inlineSuggest.toggleAlwaysShowToolbar'; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionContextKeys.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionContextKeys.ts index 7fbd4dc19fc3..834adac93caa 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionContextKeys.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionContextKeys.ts @@ -5,6 +5,7 @@ import { RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; import { localize } from '../../../../../nls.js'; +import * as nls from '../../../../../nls.js'; export abstract class InlineCompletionContextKeys { @@ -19,4 +20,6 @@ export abstract class InlineCompletionContextKeys { public static readonly inlineEditVisible = new RawContextKey('inlineEditIsVisible', false, localize('inlineEditVisible', "Whether an inline edit is visible")); public static readonly tabShouldJumpToInlineEdit = new RawContextKey('tabShouldJumpToInlineEdit', false, localize('tabShouldJumpToInlineEdit', "Whether tab should jump to an inline edit.")); public static readonly tabShouldAcceptInlineEdit = new RawContextKey('tabShouldAcceptInlineEdit', false, localize('tabShouldAcceptInlineEdit', "Whether tab should accept the inline edit.")); + + public static readonly inInlineEditsPreviewEditor = new RawContextKey('inInlineEditsPreviewEditor', true, nls.localize('inInlineEditsPreviewEditor', "Whether the current code editor is showing an inline edits preview")); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index 79fdfb85afde..b2b4b8ca6c69 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -16,7 +16,7 @@ import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../.. import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { hotClassGetOriginalInstance } from '../../../../../platform/observable/common/wrapInHotClass.js'; import { CoreEditingCommands } from '../../../../browser/coreCommands.js'; @@ -36,6 +36,7 @@ import { ObservableContextKeyService } from '../utils.js'; import { inlineSuggestCommitId } from './commandIds.js'; import { InlineCompletionContextKeys } from './inlineCompletionContextKeys.js'; import { InlineCompletionsView } from '../view/inlineCompletionsView.js'; +import { getOuterEditor } from '../../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js'; export class InlineCompletionsController extends Disposable { private static readonly _instances = new Set(); @@ -43,6 +44,17 @@ export class InlineCompletionsController extends Disposable { public static hot = createHotClass(InlineCompletionsController); public static ID = 'editor.contrib.inlineCompletionsController'; + /** + * Find the controller in the focused editor or in the outer editor (if applicable) + */ + public static getInFocusedEditorOrParent(accessor: ServicesAccessor): InlineCompletionsController | null { + const outerEditor = getOuterEditor(accessor); + if (!outerEditor) { + return null; + } + return InlineCompletionsController.get(outerEditor); + } + public static get(editor: ICodeEditor): InlineCompletionsController | null { return hotClassGetOriginalInstance(editor.getContribution(InlineCompletionsController.ID)); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index b34dc44ac1df..042dc653cf90 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -28,11 +28,11 @@ import { Range } from '../../../../../common/core/range.js'; import { Command } from '../../../../../common/languages.js'; import { ITextModel } from '../../../../../common/model.js'; import { StickyScrollController } from '../../../../stickyScroll/browser/stickyScrollController.js'; +import { InlineCompletionContextKeys } from '../../controller/inlineCompletionContextKeys.js'; import { CustomizedMenuWorkbenchToolBar } from '../../hintsWidget/inlineCompletionsHintsWidget.js'; import { PathBuilder, StatusBarViewItem, getOffsetForPos, mapOutFalsy, maxContentWidthInRange, n } from './utils.js'; import { InlineEditWithChanges } from './viewAndDiffProducer.js'; - export const originalBackgroundColor = registerColor( 'inlineEdit.originalBackground', Color.transparent, @@ -271,7 +271,12 @@ export class InlineEditsSideBySideDiff extends Disposable { wordWrapOverride1: 'off', wordWrapOverride2: 'off', }, - { contributions: [], }, + { + contextKeyValues: { + [InlineCompletionContextKeys.inInlineEditsPreviewEditor.key]: true, + }, + contributions: [], + }, this._editor )); From 1730c76f6b3f2d6ab5819031cded0a6e24d54b6e Mon Sep 17 00:00:00 2001 From: zWing <371657110@qq.com> Date: Fri, 20 Dec 2024 18:29:31 +0800 Subject: [PATCH 0292/3587] fix(git-ext): fix limitWarning block the git status progress (#226577) --- extensions/git/src/repository.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 0c4b50cd5e5c..9e6e0196abeb 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -2359,24 +2359,26 @@ export class Repository implements Disposable { const yes = { title: l10n.t('Yes') }; const no = { title: l10n.t('No') }; - const result = await window.showWarningMessage(`${gitWarn} ${addKnown}`, yes, no, neverAgain); - if (result === yes) { - this.ignore([Uri.file(folderPath)]); - } else { + window.showWarningMessage(`${gitWarn} ${addKnown}`, yes, no, neverAgain).then(result => { + if (result === yes) { + this.ignore([Uri.file(folderPath)]); + } else { + if (result === neverAgain) { + config.update('ignoreLimitWarning', true, false); + } + + this.didWarnAboutLimit = true; + } + }); + } else { + const ok = { title: l10n.t('OK') }; + window.showWarningMessage(gitWarn, ok, neverAgain).then(result => { if (result === neverAgain) { config.update('ignoreLimitWarning', true, false); } this.didWarnAboutLimit = true; - } - } else { - const ok = { title: l10n.t('OK') }; - const result = await window.showWarningMessage(gitWarn, ok, neverAgain); - if (result === neverAgain) { - config.update('ignoreLimitWarning', true, false); - } - - this.didWarnAboutLimit = true; + }); } } From c1cff695d70848d892cc09908996e7f7a790e374 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:40:15 +0100 Subject: [PATCH 0293/3587] Consistent layout actions for views/containers/activitybar (#236686) consistent layout actions --- .../browser/actions/layoutActions.ts | 44 +++---------------- .../parts/activitybar/activitybarPart.ts | 25 ++++++----- .../parts/auxiliarybar/auxiliaryBarActions.ts | 4 +- .../browser/parts/paneCompositeBar.ts | 2 +- .../browser/parts/panel/panelActions.ts | 13 +----- .../views/browser/viewDescriptorService.ts | 8 +--- 6 files changed, 25 insertions(+), 71 deletions(-) diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 75253e235de8..aed2fe31f746 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -181,17 +181,6 @@ MenuRegistry.appendMenuItems([{ when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), order: 1 } -}, { - id: MenuId.ViewTitleContext, - item: { - group: '3_workbench_layout_move', - command: { - id: ToggleSidebarPositionAction.ID, - title: localize('move sidebar right', "Move Primary Side Bar Right") - }, - when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), - order: 1 - } }, { id: MenuId.ViewContainerTitleContext, item: { @@ -204,36 +193,25 @@ MenuRegistry.appendMenuItems([{ order: 1 } }, { - id: MenuId.ViewTitleContext, - item: { - group: '3_workbench_layout_move', - command: { - id: ToggleSidebarPositionAction.ID, - title: localize('move sidebar left', "Move Primary Side Bar Left") - }, - when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), - order: 1 - } -}, { - id: MenuId.ViewTitleContext, + id: MenuId.ViewContainerTitleContext, item: { group: '3_workbench_layout_move', command: { id: ToggleSidebarPositionAction.ID, title: localize('move second sidebar left', "Move Secondary Side Bar Left") }, - when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))), + when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))), order: 1 } }, { - id: MenuId.ViewTitleContext, + id: MenuId.ViewContainerTitleContext, item: { group: '3_workbench_layout_move', command: { id: ToggleSidebarPositionAction.ID, title: localize('move second sidebar right', "Move Secondary Side Bar Right") }, - when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))), + when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))), order: 1 } }]); @@ -291,9 +269,10 @@ MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { // Toggle Sidebar Visibility -class ToggleSidebarVisibilityAction extends Action2 { +export class ToggleSidebarVisibilityAction extends Action2 { static readonly ID = 'workbench.action.toggleSidebarVisibility'; + static readonly LABEL = localize('compositePart.hideSideBarLabel', "Hide Primary Side Bar"); constructor() { super({ @@ -349,17 +328,6 @@ MenuRegistry.appendMenuItems([ when: ContextKeyExpr.and(SideBarVisibleContext, ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), order: 2 } - }, { - id: MenuId.ViewTitleContext, - item: { - group: '3_workbench_layout_move', - command: { - id: ToggleSidebarVisibilityAction.ID, - title: localize('compositePart.hideSideBarLabel', "Hide Primary Side Bar"), - }, - when: ContextKeyExpr.and(SideBarVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), - order: 2 - } }, { id: MenuId.LayoutControlMenu, item: { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 0935255446a3..f2af5fb3a2f2 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -11,7 +11,7 @@ import { Part } from '../../part.js'; import { ActivityBarPosition, IWorkbenchLayoutService, LayoutSettings, Parts, Position } from '../../../services/layout/browser/layoutService.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { ToggleSidebarPositionAction } from '../../actions/layoutActions.js'; +import { ToggleSidebarPositionAction, ToggleSidebarVisibilityAction } from '../../actions/layoutActions.js'; import { IThemeService, IColorTheme, registerThemingParticipant } from '../../../../platform/theme/common/themeService.js'; import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND, ACTIVITY_BAR_ACTIVE_BACKGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BORDER, ACTIVITY_BAR_ACTIVE_FOCUS_BORDER } from '../../../common/theme.js'; import { activeContrastBorder, contrastBorder, focusBorder } from '../../../../platform/theme/common/colorRegistry.js'; @@ -371,10 +371,16 @@ export class ActivityBarCompositeBar extends PaneCompositeBar { getActivityBarContextMenuActions(): IAction[] { const activityBarPositionMenu = this.menuService.getMenuActions(MenuId.ActivityBarPositionMenu, this.contextKeyService, { shouldForwardArgs: true, renderShortTitle: true }); const positionActions = getContextMenuActions(activityBarPositionMenu).secondary; - return [ + const actions = [ new SubmenuAction('workbench.action.panel.position', localize('activity bar position', "Activity Bar Position"), positionActions), - toAction({ id: ToggleSidebarPositionAction.ID, label: ToggleSidebarPositionAction.getLabel(this.layoutService), run: () => this.instantiationService.invokeFunction(accessor => new ToggleSidebarPositionAction().run(accessor)) }) + toAction({ id: ToggleSidebarPositionAction.ID, label: ToggleSidebarPositionAction.getLabel(this.layoutService), run: () => this.instantiationService.invokeFunction(accessor => new ToggleSidebarPositionAction().run(accessor)) }), ]; + + if (this.part === Parts.SIDEBAR_PART) { + actions.push(toAction({ id: ToggleSidebarVisibilityAction.ID, label: ToggleSidebarVisibilityAction.LABEL, run: () => this.instantiationService.invokeFunction(accessor => new ToggleSidebarVisibilityAction().run(accessor)) })); + } + + return actions; } } @@ -493,15 +499,10 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { MenuRegistry.appendMenuItem(MenuId.ViewContainerTitleContext, { submenu: MenuId.ActivityBarPositionMenu, title: localize('positionActivituBar', "Activity Bar Position"), - when: ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar)), - group: '3_workbench_layout_move', - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.ViewTitleContext, { - submenu: MenuId.ActivityBarPositionMenu, - title: localize('positionActivituBar', "Activity Bar Position"), - when: ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar)), + when: ContextKeyExpr.or( + ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar)), + ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar)) + ), group: '3_workbench_layout_move', order: 1 }); diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts index e564a8d49d77..e5cc8c365503 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts @@ -149,14 +149,14 @@ MenuRegistry.appendMenuItems([ order: 2 } }, { - id: MenuId.ViewTitleContext, + id: MenuId.ViewContainerTitleContext, item: { group: '3_workbench_layout_move', command: { id: ToggleAuxiliaryBarAction.ID, title: localize2('hideAuxiliaryBar', 'Hide Secondary Side Bar'), }, - when: ContextKeyExpr.and(AuxiliaryBarVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))), + when: ContextKeyExpr.and(AuxiliaryBarVisibleContext, ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))), order: 2 } } diff --git a/src/vs/workbench/browser/parts/paneCompositeBar.ts b/src/vs/workbench/browser/parts/paneCompositeBar.ts index 05b43a296e17..89d07a87bc03 100644 --- a/src/vs/workbench/browser/parts/paneCompositeBar.ts +++ b/src/vs/workbench/browser/parts/paneCompositeBar.ts @@ -99,7 +99,7 @@ export class PaneCompositeBar extends Disposable { constructor( protected readonly options: IPaneCompositeBarOptions, - private readonly part: Parts, + protected readonly part: Parts, private readonly paneCompositePart: IPaneCompositePart, @IInstantiationService protected readonly instantiationService: IInstantiationService, @IStorageService private readonly storageService: IStorageService, diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index b3be54f49d78..6e43176a62e1 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -14,7 +14,7 @@ import { ContextKeyExpr, ContextKeyExpression } from '../../../../platform/conte import { Codicon } from '../../../../base/common/codicons.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; -import { ViewContainerLocationToString, ViewContainerLocation, IViewDescriptorService } from '../../../common/views.js'; +import { ViewContainerLocation, IViewDescriptorService } from '../../../common/views.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; @@ -326,17 +326,6 @@ MenuRegistry.appendMenuItems([ when: ContextKeyExpr.or(ContextKeyExpr.equals('config.workbench.layoutControl.type', 'toggles'), ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both')), order: 1 } - }, { - id: MenuId.ViewTitleContext, - item: { - group: '3_workbench_layout_move', - command: { - id: TogglePanelAction.ID, - title: localize2('hidePanel', 'Hide Panel'), - }, - when: ContextKeyExpr.and(PanelVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Panel))), - order: 2 - } } ]); diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index ad59c3e2a2a4..c667e719ae4d 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -792,16 +792,12 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor order: index, }, { id: MenuId.ViewContainerTitleContext, - when: ContextKeyExpr.and( - ContextKeyExpr.equals('viewContainer', viewContainerModel.viewContainer.id), - ), + when: ContextKeyExpr.equals('viewContainer', viewContainerModel.viewContainer.id), order: index, group: '1_toggleVisibility' }, { id: MenuId.ViewTitleContext, - when: ContextKeyExpr.and( - ContextKeyExpr.or(...viewContainerModel.visibleViewDescriptors.map(v => ContextKeyExpr.equals('view', v.id))) - ), + when: ContextKeyExpr.or(...viewContainerModel.visibleViewDescriptors.map(v => ContextKeyExpr.equals('view', v.id))), order: index, group: '2_toggleVisibility' }] From d239347a1c75707cfa7d9b4d9bfe5b7f28277c37 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Fri, 20 Dec 2024 12:05:21 +0100 Subject: [PATCH 0294/3587] Add a context key for whether commenting is enabled (#236679) --- .../workbench/contrib/comments/browser/commentService.ts | 3 +++ .../contrib/comments/common/commentContextKeys.ts | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index ca5a8aa59d91..b7d1cde9aead 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -167,6 +167,7 @@ export class CommentService extends Disposable implements ICommentService { private _commentMenus = new Map(); private _isCommentingEnabled: boolean = true; private _workspaceHasCommenting: IContextKey; + private _commentingEnabled: IContextKey; private _continueOnComments = new Map(); // uniqueOwner -> PendingCommentThread[] private _continueOnCommentProviders = new Set(); @@ -190,6 +191,7 @@ export class CommentService extends Disposable implements ICommentService { this._handleConfiguration(); this._handleZenMode(); this._workspaceHasCommenting = CommentContextKeys.WorkspaceHasCommenting.bindTo(contextKeyService); + this._commentingEnabled = CommentContextKeys.commentingEnabled.bindTo(contextKeyService); const storageListener = this._register(new DisposableStore()); const storageEvent = Event.debounce(this.storageService.onDidChangeValue(StorageScope.WORKSPACE, CONTINUE_ON_COMMENTS, storageListener), (last, event) => last?.external ? last : event, 500); @@ -277,6 +279,7 @@ export class CommentService extends Disposable implements ICommentService { enableCommenting(enable: boolean): void { if (enable !== this._isCommentingEnabled) { this._isCommentingEnabled = enable; + this._commentingEnabled.set(enable); this._onDidChangeCommentingEnabled.fire(enable); } } diff --git a/src/vs/workbench/contrib/comments/common/commentContextKeys.ts b/src/vs/workbench/contrib/comments/common/commentContextKeys.ts index 210465efe6f7..2a5d0776c450 100644 --- a/src/vs/workbench/contrib/comments/common/commentContextKeys.ts +++ b/src/vs/workbench/contrib/comments/common/commentContextKeys.ts @@ -66,4 +66,12 @@ export namespace CommentContextKeys { * The comment widget is focused. */ export const commentFocused = new RawContextKey('commentFocused', false, { type: 'boolean', description: nls.localize('commentFocused', "Set when the comment is focused") }); + + /** + * A context key that is set when commenting is enabled. + */ + export const commentingEnabled = new RawContextKey('commentingEnabled', true, { + description: nls.localize('commentingEnabled', "Whether commenting functionality is enabled"), + type: 'boolean' + }); } From 692fbdbd4d72edbd6bb2507f6a71d320f3a12647 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 04:52:33 -0800 Subject: [PATCH 0295/3587] Set multi-line setting to a valid value when disabling Fixes #231562 --- .../terminalContrib/clipboard/browser/terminalClipboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/clipboard/browser/terminalClipboard.ts b/src/vs/workbench/contrib/terminalContrib/clipboard/browser/terminalClipboard.ts index ae9dc0b6699d..e8ba759f61fd 100644 --- a/src/vs/workbench/contrib/terminalContrib/clipboard/browser/terminalClipboard.ts +++ b/src/vs/workbench/contrib/terminalContrib/clipboard/browser/terminalClipboard.ts @@ -95,7 +95,7 @@ export async function shouldPasteTerminalText(accessor: ServicesAccessor, text: } if (result.confirmed && checkboxChecked) { - await configurationService.updateValue(TerminalSettingId.EnableMultiLinePasteWarning, false); + await configurationService.updateValue(TerminalSettingId.EnableMultiLinePasteWarning, 'never'); } if (result.singleLine) { From 78cd9519c5c7b2b8ddb0a092e658d240db6bcbe0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 05:10:57 -0800 Subject: [PATCH 0296/3587] Close the terminal view when there's only one visible view Part of #236517 --- .../workbench/contrib/terminal/browser/terminalGroupService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts index ff4c4103eba7..6f5040cd4d77 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts @@ -83,7 +83,7 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe hidePanel(): void { // Hide the panel if the terminal is in the panel and it has no sibling views const panel = this._viewDescriptorService.getViewContainerByViewId(TERMINAL_VIEW_ID); - if (panel && this._viewDescriptorService.getViewContainerModel(panel).activeViewDescriptors.length === 1) { + if (panel && this._viewDescriptorService.getViewContainerModel(panel).visibleViewDescriptors.length === 1) { this._viewsService.closeView(TERMINAL_VIEW_ID); TerminalContextKeys.tabsMouse.bindTo(this._contextKeyService).set(false); } From 2dc420d6387896b49298a05288704ccf75220271 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 05:31:07 -0800 Subject: [PATCH 0297/3587] Create terminal when toggled without contents Fixes #236517 --- .../contrib/terminal/browser/terminalView.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index b71064b87f0c..38e6d3429421 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -57,6 +57,11 @@ export class TerminalViewPane extends ViewPane { private _terminalTabbedView?: TerminalTabbedView; get terminalTabbedView(): TerminalTabbedView | undefined { return this._terminalTabbedView; } private _isInitialized: boolean = false; + /** + * Tracks an active promise of terminal creation requested by this component. This helps prevent + * double creation for example when toggling a terminal's visibility and focusing it. + */ + private _isTerminalBeingCreated: boolean = false; private readonly _newDropdown: MutableDisposable = this._register(new MutableDisposable()); private readonly _dropdownMenu: IMenu; private readonly _singleTabMenu: IMenu; @@ -164,7 +169,8 @@ export class TerminalViewPane extends ViewPane { if (!wasInitialized) { switch (hideOnStartup) { case 'never': - this._terminalService.createTerminal({ location: TerminalLocation.Panel }); + this._isTerminalBeingCreated = true; + this._terminalService.createTerminal({ location: TerminalLocation.Panel }).finally(() => this._isTerminalBeingCreated = false); break; case 'whenEmpty': if (this._terminalService.restoredGroupCount === 0) { @@ -175,7 +181,10 @@ export class TerminalViewPane extends ViewPane { return; } - this._terminalService.createTerminal({ location: TerminalLocation.Panel }); + if (!this._isTerminalBeingCreated) { + this._isTerminalBeingCreated = true; + this._terminalService.createTerminal({ location: TerminalLocation.Panel }).finally(() => this._isTerminalBeingCreated = false); + } } } @@ -320,6 +329,10 @@ export class TerminalViewPane extends ViewPane { override focus() { super.focus(); if (this._terminalService.connectionState === TerminalConnectionState.Connected) { + if (this._terminalGroupService.instances.length === 0 && !this._isTerminalBeingCreated) { + this._isTerminalBeingCreated = true; + this._terminalService.createTerminal({ location: TerminalLocation.Panel }).finally(() => this._isTerminalBeingCreated = false); + } this._terminalGroupService.showPanel(true); return; } From 8d0d731b7c53b7656e9a7014b14b797dd1796a1a Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:31:20 +0100 Subject: [PATCH 0298/3587] SCM - add scm.historyProviderCount context key (#236694) --- .../contrib/scm/browser/scm.contribution.ts | 12 ++----- .../contrib/scm/common/scmService.ts | 34 +++++++++++++++---- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 0c2d7c1af5f1..09ecabe67eef 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -38,8 +38,6 @@ import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IListService, WorkbenchList } from '../../../../platform/list/browser/listService.js'; import { isSCMRepository } from './util.js'; import { SCMHistoryViewPane } from './scmHistoryViewPane.js'; -import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js'; -import { RemoteNameContext } from '../../../common/contextkeys.js'; import { QuickDiffModelService, IQuickDiffModelService } from './quickDiffModel.js'; import { QuickDiffEditorController } from './quickDiffWidget.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; @@ -137,14 +135,8 @@ viewsRegistry.registerViews([{ weight: 40, order: 2, when: ContextKeyExpr.and( - // Repository Count - ContextKeyExpr.and( - ContextKeyExpr.has('scm.providerCount'), - ContextKeyExpr.notEquals('scm.providerCount', 0)), - // Not Serverless - ContextKeyExpr.and( - IsWebContext, - RemoteNameContext.isEqualTo(''))?.negate() + ContextKeyExpr.has('scm.historyProviderCount'), + ContextKeyExpr.notEquals('scm.historyProviderCount', 0), ), containerIcon: sourceControlViewIcon }], viewContainer); diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index c9af734400a5..14c69f73b0d7 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository, IInputValidator, ISCMInputChangeEvent, SCMInputChangeReason, InputValidationType, IInputValidation } from './scm.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -17,6 +17,7 @@ import { Iterable } from '../../../../base/common/iterator.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { Schemas } from '../../../../base/common/network.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { runOnChange } from '../../../../base/common/observable.js'; class SCMInput extends Disposable implements ISCMInput { @@ -188,7 +189,7 @@ class SCMRepository implements ISCMRepository { constructor( public readonly id: string, public readonly provider: ISCMProvider, - private disposable: IDisposable, + private readonly disposables: DisposableStore, inputHistory: SCMInputHistory ) { this.input = new SCMInput(this, inputHistory); @@ -204,7 +205,7 @@ class SCMRepository implements ISCMRepository { } dispose(): void { - this.disposable.dispose(); + this.disposables.dispose(); this.provider.dispose(); } } @@ -355,6 +356,7 @@ export class SCMService implements ISCMService { private inputHistory: SCMInputHistory; private providerCount: IContextKey; + private historyProviderCount: IContextKey; private readonly _onDidAddProvider = new Emitter(); readonly onDidAddRepository: Event = this._onDidAddProvider.event; @@ -370,7 +372,9 @@ export class SCMService implements ISCMService { @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { this.inputHistory = new SCMInputHistory(storageService, workspaceContextService); + this.providerCount = contextKeyService.createKey('scm.providerCount', 0); + this.historyProviderCount = contextKeyService.createKey('scm.historyProviderCount', 0); } registerSCMProvider(provider: ISCMProvider): ISCMRepository { @@ -380,17 +384,33 @@ export class SCMService implements ISCMService { throw new Error(`SCM Provider ${provider.id} already exists.`); } - const disposable = toDisposable(() => { + const disposables = new DisposableStore(); + + const historyProviderCount = () => { + return Array.from(this._repositories.values()) + .filter(r => !!r.provider.historyProvider).length; + }; + + disposables.add(toDisposable(() => { this._repositories.delete(provider.id); this._onDidRemoveProvider.fire(repository); + this.providerCount.set(this._repositories.size); - }); + this.historyProviderCount.set(historyProviderCount()); + })); - const repository = new SCMRepository(provider.id, provider, disposable, this.inputHistory); + const repository = new SCMRepository(provider.id, provider, disposables, this.inputHistory); this._repositories.set(provider.id, repository); - this._onDidAddProvider.fire(repository); + + disposables.add(runOnChange(provider.historyProvider, () => { + this.historyProviderCount.set(historyProviderCount()); + })); this.providerCount.set(this._repositories.size); + this.historyProviderCount.set(historyProviderCount()); + + this._onDidAddProvider.fire(repository); + return repository; } From f7fd6660f9f11e5a270fee834ca0382416ba9dfe Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 05:35:12 -0800 Subject: [PATCH 0299/3587] Add hideOnLastClosed setting Fixes #236517 --- src/vs/platform/terminal/common/terminal.ts | 1 + src/vs/workbench/contrib/terminal/browser/terminalService.ts | 2 +- src/vs/workbench/contrib/terminal/common/terminal.ts | 1 + .../contrib/terminal/common/terminalConfiguration.ts | 5 +++++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 8235e8a0ab40..246ccaf6a765 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -103,6 +103,7 @@ export const enum TerminalSettingId { EnablePersistentSessions = 'terminal.integrated.enablePersistentSessions', PersistentSessionReviveProcess = 'terminal.integrated.persistentSessionReviveProcess', HideOnStartup = 'terminal.integrated.hideOnStartup', + HideOnLastClosed = 'terminal.integrated.hideOnLastClosed', CustomGlyphs = 'terminal.integrated.customGlyphs', RescaleOverlappingGlyphs = 'terminal.integrated.rescaleOverlappingGlyphs', PersistentSessionScrollback = 'terminal.integrated.persistentSessionScrollback', diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 698559d3b2ce..7fea79a51a70 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -208,7 +208,7 @@ export class TerminalService extends Disposable implements ITerminalService { // down. When shutting down the panel is locked in place so that it is restored upon next // launch. this._register(this._terminalGroupService.onDidChangeActiveInstance(instance => { - if (!instance && !this._isShuttingDown) { + if (!instance && !this._isShuttingDown && this._terminalConfigService.config.hideOnLastClosed) { this._terminalGroupService.hidePanel(); } if (instance?.shellType) { diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index cf5c306bb709..6fbd41d8637f 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -211,6 +211,7 @@ export interface ITerminalConfiguration { experimental?: { windowsUseConptyDll?: boolean; }; + hideOnLastClosed: boolean; } export interface ITerminalFont { diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index b90a67cbbc63..6743ad25d3c9 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -549,6 +549,11 @@ const terminalConfiguration: IConfigurationNode = { ], default: 'never' }, + [TerminalSettingId.HideOnLastClosed]: { + description: localize('terminal.integrated.hideOnLastClosed', "Whether to hide the terminal view when the last terminal is closed. This will only happen when the terminal is the only visible view in the view container."), + type: 'boolean', + default: true + }, [TerminalSettingId.CustomGlyphs]: { markdownDescription: localize('terminal.integrated.customGlyphs', "Whether to draw custom glyphs for block element and box drawing characters instead of using the font, which typically yields better rendering with continuous lines. Note that this doesn't work when {0} is disabled.", `\`#${TerminalSettingId.GpuAcceleration}#\``), type: 'boolean', From 502a7e5d43b277bd4b8783f837a7cba65234e4f4 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:16:06 +0100 Subject: [PATCH 0300/3587] SCM - do not show "Open in External Terminal" action when connected to a remote (#236697) --- .../contrib/scm/browser/scm.contribution.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 09ecabe67eef..7f8e9c71a099 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -41,6 +41,7 @@ import { SCMHistoryViewPane } from './scmHistoryViewPane.js'; import { QuickDiffModelService, IQuickDiffModelService } from './quickDiffModel.js'; import { QuickDiffEditorController } from './quickDiffWidget.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; +import { RemoteNameContext } from '../../../common/contextkeys.js'; ModesRegistry.registerLanguage({ id: 'scminput', @@ -529,7 +530,12 @@ MenuRegistry.appendMenuItem(MenuId.SCMSourceControl, { id: 'scm.openInTerminal', title: localize('open in external terminal', "Open in External Terminal") }, - when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProviderHasRootUri', true), ContextKeyExpr.or(ContextKeyExpr.equals('config.terminal.sourceControlRepositoriesKind', 'external'), ContextKeyExpr.equals('config.terminal.sourceControlRepositoriesKind', 'both'))) + when: ContextKeyExpr.and( + RemoteNameContext.isEqualTo(''), + ContextKeyExpr.equals('scmProviderHasRootUri', true), + ContextKeyExpr.or( + ContextKeyExpr.equals('config.terminal.sourceControlRepositoriesKind', 'external'), + ContextKeyExpr.equals('config.terminal.sourceControlRepositoriesKind', 'both'))) }); MenuRegistry.appendMenuItem(MenuId.SCMSourceControl, { @@ -538,7 +544,11 @@ MenuRegistry.appendMenuItem(MenuId.SCMSourceControl, { id: 'scm.openInIntegratedTerminal', title: localize('open in integrated terminal', "Open in Integrated Terminal") }, - when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProviderHasRootUri', true), ContextKeyExpr.or(ContextKeyExpr.equals('config.terminal.sourceControlRepositoriesKind', 'integrated'), ContextKeyExpr.equals('config.terminal.sourceControlRepositoriesKind', 'both'))) + when: ContextKeyExpr.and( + ContextKeyExpr.equals('scmProviderHasRootUri', true), + ContextKeyExpr.or( + ContextKeyExpr.equals('config.terminal.sourceControlRepositoriesKind', 'integrated'), + ContextKeyExpr.equals('config.terminal.sourceControlRepositoriesKind', 'both'))) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ From d8c2678e9a4153f56a4160481b1c983558637b6a Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:25:40 +0100 Subject: [PATCH 0301/3587] Add share context menu action (#236699) share context menu action --- src/vs/workbench/browser/parts/titlebar/titlebarActions.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts b/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts index f84bf3f06b73..e4b8a3e6b9d6 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts @@ -64,6 +64,12 @@ registerAction2(class ToggleNavigationControl extends ToggleTitleBarConfigAction } }); +registerAction2(class ToggleShareControl extends ToggleTitleBarConfigAction { + constructor() { + super('workbench.experimental.share.enabled', localize('toggle.share', 'Share'), localize('toggle.shareDescription', "Toggle visibility of the Share action in title bar"), 2, false, ContextKeyExpr.has('config.window.commandCenter')); + } +}); + registerAction2(class ToggleLayoutControl extends ToggleTitleBarConfigAction { constructor() { super(LayoutSettings.LAYOUT_ACTIONS, localize('toggle.layout', 'Layout Controls'), localize('toggle.layoutDescription', "Toggle visibility of the Layout Controls in title bar"), 3, true); From b8b4db329e93816f34611d1a653057c323485463 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:51:58 +0100 Subject: [PATCH 0302/3587] Git - cleanup `vscode-merge-base` git config key when deleteing a branch (#236716) --- extensions/git/src/git.ts | 6 +++--- extensions/git/src/repository.ts | 11 +++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 87ea23e3a853..62bae1422df0 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1166,11 +1166,11 @@ export class Repository { return this.git.spawn(args, options); } - async config(scope: string, key: string, value: any = null, options: SpawnOptions = {}): Promise { - const args = ['config']; + async config(command: string, scope: string, key: string, value: any = null, options: SpawnOptions = {}): Promise { + const args = ['config', command]; if (scope) { - args.push('--' + scope); + args.push(`--${scope}`); } args.push(key); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 9e6e0196abeb..ec66d510c720 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1082,15 +1082,15 @@ export class Repository implements Disposable { } getConfig(key: string): Promise { - return this.run(Operation.Config(true), () => this.repository.config('local', key)); + return this.run(Operation.Config(true), () => this.repository.config('get', 'local', key)); } getGlobalConfig(key: string): Promise { - return this.run(Operation.Config(true), () => this.repository.config('global', key)); + return this.run(Operation.Config(true), () => this.repository.config('get', 'global', key)); } setConfig(key: string, value: string): Promise { - return this.run(Operation.Config(false), () => this.repository.config('local', key, value)); + return this.run(Operation.Config(false), () => this.repository.config('set', 'local', key, value)); } log(options?: LogOptions & { silent?: boolean }): Promise { @@ -1465,7 +1465,10 @@ export class Repository implements Disposable { } async deleteBranch(name: string, force?: boolean): Promise { - await this.run(Operation.DeleteBranch, () => this.repository.deleteBranch(name, force)); + return this.run(Operation.DeleteBranch, async () => { + await this.repository.deleteBranch(name, force); + await this.repository.config('unset', 'local', `branch.${name}.vscode-merge-base`); + }); } async renameBranch(name: string): Promise { From 5cdf4dd74bae97e2701de972294aa3e501c01920 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 08:14:37 -0800 Subject: [PATCH 0303/3587] Register editor gpu acceleration with included:false Fixes #235730 --- src/vs/editor/common/config/editorOptions.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index e355a960db99..5fe2028e3666 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -5833,15 +5833,14 @@ export const EditorOptions = { EditorOption.experimentalGpuAcceleration, 'experimentalGpuAcceleration', 'off' as 'off' | 'on', ['off', 'on'] as const, - undefined - // TODO: Uncomment when we want to expose the setting to VS Code users - // { - // enumDescriptions: [ - // nls.localize('experimentalGpuAcceleration.off', "Use regular DOM-based rendering."), - // nls.localize('experimentalGpuAcceleration.on', "Use GPU acceleration."), - // ], - // description: nls.localize('experimentalGpuAcceleration', "Controls whether to use the (very) experimental GPU acceleration to render the editor.") - // } + { + included: false, // Hide the setting from users while it's unstable + enumDescriptions: [ + nls.localize('experimentalGpuAcceleration.off', "Use regular DOM-based rendering."), + nls.localize('experimentalGpuAcceleration.on', "Use GPU acceleration."), + ], + description: nls.localize('experimentalGpuAcceleration', "Controls whether to use the (very) experimental GPU acceleration to render the editor.") + } )), experimentalWhitespaceRendering: register(new EditorStringEnumOption( EditorOption.experimentalWhitespaceRendering, 'experimentalWhitespaceRendering', From decaa08e9945fad06912b5aafbd8f0f0e88e11c2 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 20 Dec 2024 17:15:05 +0100 Subject: [PATCH 0304/3587] Use correct context keys for share provider title context action (#236718) use correct context keys for share provider titile context action --- .../browser/parts/titlebar/titlebarActions.ts | 8 +------- .../contrib/chat/browser/actions/chatActions.ts | 2 +- .../workbench/contrib/share/browser/shareService.ts | 12 ++++++++++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts b/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts index e4b8a3e6b9d6..e09172e4a7f2 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts @@ -64,15 +64,9 @@ registerAction2(class ToggleNavigationControl extends ToggleTitleBarConfigAction } }); -registerAction2(class ToggleShareControl extends ToggleTitleBarConfigAction { - constructor() { - super('workbench.experimental.share.enabled', localize('toggle.share', 'Share'), localize('toggle.shareDescription', "Toggle visibility of the Share action in title bar"), 2, false, ContextKeyExpr.has('config.window.commandCenter')); - } -}); - registerAction2(class ToggleLayoutControl extends ToggleTitleBarConfigAction { constructor() { - super(LayoutSettings.LAYOUT_ACTIONS, localize('toggle.layout', 'Layout Controls'), localize('toggle.layoutDescription', "Toggle visibility of the Layout Controls in title bar"), 3, true); + super(LayoutSettings.LAYOUT_ACTIONS, localize('toggle.layout', 'Layout Controls'), localize('toggle.layoutDescription', "Toggle visibility of the Layout Controls in title bar"), 4, true); } }); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index d1d7d951559f..f0ece5fa2b95 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -543,7 +543,7 @@ registerAction2(class ToggleCopilotControl extends ToggleTitleBarConfigAction { super( 'chat.commandCenter.enabled', localize('toggle.chatControl', 'Copilot Controls'), - localize('toggle.chatControlsDescription', "Toggle visibility of the Copilot Controls in title bar"), 4, false, + localize('toggle.chatControlsDescription', "Toggle visibility of the Copilot Controls in title bar"), 5, false, ContextKeyExpr.and( ChatContextKeys.supported, ContextKeyExpr.has('config.window.commandCenter') diff --git a/src/vs/workbench/contrib/share/browser/shareService.ts b/src/vs/workbench/contrib/share/browser/shareService.ts index 0d684acf736c..9a94bd370beb 100644 --- a/src/vs/workbench/contrib/share/browser/shareService.ts +++ b/src/vs/workbench/contrib/share/browser/shareService.ts @@ -9,11 +9,13 @@ import { URI } from '../../../../base/common/uri.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { score } from '../../../../editor/common/languageSelector.js'; import { localize } from '../../../../nls.js'; -import { ISubmenuItem } from '../../../../platform/actions/common/actions.js'; -import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { ISubmenuItem, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { ToggleTitleBarConfigAction } from '../../../browser/parts/titlebar/titlebarActions.js'; +import { WorkspaceFolderCountContext } from '../../../common/contextkeys.js'; import { IShareProvider, IShareService, IShareableItem } from '../common/share.js'; export const ShareProviderCountContext = new RawContextKey('shareProviderCount', 0, localize('shareProviderCount', "The number of available share providers")); @@ -84,3 +86,9 @@ export class ShareService implements IShareService { return; } } + +registerAction2(class ToggleShareControl extends ToggleTitleBarConfigAction { + constructor() { + super('workbench.experimental.share.enabled', localize('toggle.share', 'Share'), localize('toggle.shareDescription', "Toggle visibility of the Share action in title bar"), 3, false, ContextKeyExpr.and(ContextKeyExpr.has('config.window.commandCenter'), ContextKeyExpr.and(ShareProviderCountContext.notEqualsTo(0), WorkspaceFolderCountContext.notEqualsTo(0)))); + } +}); From 62948ea6d834ffed42b20fc0cba28ffaa3cf0cbe Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 08:23:25 -0800 Subject: [PATCH 0305/3587] Indicate when terminal sticky scroll is truncated Fixes #199974 --- .../stickyScroll/browser/terminalStickyScrollOverlay.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts index 7b3d3de33714..0da38ee59940 100644 --- a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts +++ b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts @@ -265,6 +265,7 @@ export class TerminalStickyScrollOverlay extends Disposable { const rowOffset = !isPartialCommand && command.endMarker ? Math.max(buffer.viewportY - command.endMarker.line + 1, 0) : 0; const maxLineCount = Math.min(this._rawMaxLineCount, Math.floor(xterm.rows * Constants.StickyScrollPercentageCap)); const stickyScrollLineCount = Math.min(promptRowCount + commandRowCount - 1, maxLineCount) - rowOffset; + const isTruncated = stickyScrollLineCount < promptRowCount + commandRowCount - 1; // Hide sticky scroll if it's currently on a line that contains it if (buffer.viewportY <= stickyScrollLineStart) { @@ -293,7 +294,7 @@ export class TerminalStickyScrollOverlay extends Disposable { start: stickyScrollLineStart + rowOffset, end: stickyScrollLineStart + rowOffset + Math.max(stickyScrollLineCount - 1, 0) } - }); + }) + (isTruncated ? '\x1b[0m …' : ''); // If a partial command's sticky scroll would show nothing, just hide it. This is another // edge case when using a pager or interactive editor. From 12708746496225474c020218d3eae298977266f5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:43:35 -0800 Subject: [PATCH 0306/3587] Suppress exit warning after ctrl+d Fixes #160325 --- src/vs/workbench/contrib/terminal/browser/terminal.ts | 5 +++++ .../workbench/contrib/terminal/browser/terminalInstance.ts | 2 +- .../contrib/terminal/browser/xterm/xtermTerminal.ts | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 2a56aeca9795..ead75598c0ec 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -1095,6 +1095,11 @@ export interface IXtermTerminal extends IDisposable { */ readonly isGpuAccelerated: boolean; + /** + * The last `onData` input event fired by {@link RawXtermTerminal.onData}. + */ + readonly lastInputEvent: string | undefined; + /** * Attached the terminal to the given element * @param container Container the terminal will be rendered in diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 0a1646792a03..df286dd974ad 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -1598,7 +1598,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } else { if (exitMessage) { const failedDuringLaunch = this._processManager.processState === ProcessState.KilledDuringLaunch; - if (failedDuringLaunch || this._terminalConfigurationService.config.showExitAlert) { + if (failedDuringLaunch || (this._terminalConfigurationService.config.showExitAlert && this.xterm?.lastInputEvent !== /*Ctrl+D*/'\x04')) { // Always show launch failures this._notificationService.notify({ message: exitMessage, diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 5ef6d497196e..4e860821f7e9 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -96,6 +96,8 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach private static _suggestedRendererType: 'dom' | undefined = undefined; private _attached?: { container: HTMLElement; options: IXtermAttachToElementOptions }; private _isPhysicalMouseWheel = MouseWheelClassifier.INSTANCE.isPhysicalMouseWheel(); + private _lastInputEvent: string | undefined; + get lastInputEvent(): string | undefined { return this._lastInputEvent; } // Always on addons private _markNavigationAddon: MarkNavigationAddon; @@ -256,6 +258,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach this._anyFocusedTerminalHasSelection.set(this.raw.hasSelection()); } })); + this._register(this.raw.onData(e => this._lastInputEvent = e)); // Load addons this._updateUnicodeVersion(); From 1be76b3c97bbe6ce32822c2e7baac4d115376478 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 20 Dec 2024 09:47:26 -0800 Subject: [PATCH 0307/3587] Fix response toString for inline refs Fixes #236655 --- .../contrib/chat/common/chatModel.ts | 98 +++++++++++++------ .../Response_inline_reference.0.snap | 4 +- .../chat/test/common/chatModel.test.ts | 9 +- 3 files changed, 78 insertions(+), 33 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 14fa03dffa0f..3cf9a8585c78 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -9,6 +9,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { revive } from '../../../../base/common/marshalling.js'; +import { Schemas } from '../../../../base/common/network.js'; import { equals } from '../../../../base/common/objects.js'; import { basename, isEqual } from '../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; @@ -397,38 +398,12 @@ export class Response extends Disposable implements IResponse { } private _updateRepr(quiet?: boolean) { - const inlineRefToRepr = (part: IChatContentInlineReference) => - 'uri' in part.inlineReference - ? basename(part.inlineReference.uri) - : 'name' in part.inlineReference - ? part.inlineReference.name - : basename(part.inlineReference); - - this._responseRepr = this._responseParts.map(part => { - if (part.kind === 'treeData') { - return ''; - } else if (part.kind === 'inlineReference') { - return inlineRefToRepr(part); - } else if (part.kind === 'command') { - return part.command.title; - } else if (part.kind === 'textEditGroup') { - return localize('editsSummary', "Made changes."); - } else if (part.kind === 'progressMessage' || part.kind === 'codeblockUri' || part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') { - return ''; - } else if (part.kind === 'confirmation') { - return `${part.title}\n${part.message}`; - } else { - return part.content.value; - } - }) - .filter(s => s.length > 0) - .join('\n\n'); - + this._responseRepr = this.partsToRepr(this._responseParts); this._responseRepr += this._citations.length ? '\n\n' + getCodeCitationsMessage(this._citations) : ''; this._markdownContent = this._responseParts.map(part => { if (part.kind === 'inlineReference') { - return inlineRefToRepr(part); + return this.inlineRefToRepr(part); } else if (part.kind === 'markdownContent' || part.kind === 'markdownVuln') { return part.content.value; } else { @@ -442,6 +417,73 @@ export class Response extends Disposable implements IResponse { this._onDidChangeValue.fire(); } } + + private partsToRepr(parts: readonly IChatProgressResponseContent[]): string { + const blocks: string[] = []; + let currentBlockSegments: string[] = []; + + for (const part of parts) { + let segment: { text: string; isBlock?: boolean } | undefined; + switch (part.kind) { + case 'treeData': + case 'progressMessage': + case 'codeblockUri': + case 'toolInvocation': + case 'toolInvocationSerialized': + // Ignore + continue; + case 'inlineReference': + segment = { text: this.inlineRefToRepr(part) }; + break; + case 'command': + segment = { text: part.command.title, isBlock: true }; + break; + case 'textEditGroup': + segment = { text: localize('editsSummary', "Made changes."), isBlock: true }; + break; + case 'confirmation': + segment = { text: `${part.title}\n${part.message}`, isBlock: true }; + break; + default: + segment = { text: part.content.value }; + break; + } + + if (segment.isBlock) { + if (currentBlockSegments.length) { + blocks.push(currentBlockSegments.join('')); + currentBlockSegments = []; + } + blocks.push(segment.text); + } else { + currentBlockSegments.push(segment.text); + } + } + + if (currentBlockSegments.length) { + blocks.push(currentBlockSegments.join('')); + } + + return blocks.join('\n\n'); + } + + private inlineRefToRepr(part: IChatContentInlineReference) { + if ('uri' in part.inlineReference) { + return this.uriToRepr(part.inlineReference.uri); + } + + return 'name' in part.inlineReference + ? '`' + part.inlineReference.name + '`' + : this.uriToRepr(part.inlineReference); + } + + private uriToRepr(uri: URI): string { + if (uri.scheme === Schemas.http || uri.scheme === Schemas.https) { + return uri.toString(false); + } + + return basename(uri); + } } export class ChatResponseModel extends Disposable implements IChatResponseModel { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap index b26d84334a08..3a54719571df 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap @@ -1,7 +1,7 @@ [ { content: { - value: "text before", + value: "text before ", isTrusted: false, supportThemeIcons: false, supportHtml: false @@ -14,7 +14,7 @@ }, { content: { - value: "text after", + value: " text after", isTrusted: false, supportThemeIcons: false, supportHtml: false diff --git a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index be40ba599bd0..c9c2059b3071 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -191,10 +191,13 @@ suite('Response', () => { test('inline reference', async () => { const response = store.add(new Response([])); - response.updateContent({ content: new MarkdownString('text before'), kind: 'markdownContent' }); - response.updateContent({ inlineReference: URI.parse('https://microsoft.com'), kind: 'inlineReference' }); - response.updateContent({ content: new MarkdownString('text after'), kind: 'markdownContent' }); + response.updateContent({ content: new MarkdownString('text before '), kind: 'markdownContent' }); + response.updateContent({ inlineReference: URI.parse('https://microsoft.com/'), kind: 'inlineReference' }); + response.updateContent({ content: new MarkdownString(' text after'), kind: 'markdownContent' }); await assertSnapshot(response.value); + + assert.strictEqual(response.toString(), 'text before https://microsoft.com/ text after'); + }); }); From 437450a35c25538b5a8719aeee03d060be883f0c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:08:18 -0800 Subject: [PATCH 0308/3587] Explain exitCode undefined in more detail Fixes #236670 --- src/vscode-dts/vscode.d.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index c9ecff2b0b98..7edf86d0780c 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -7724,10 +7724,17 @@ declare module 'vscode' { /** * The exit code reported by the shell. * - * Note that `undefined` means the shell either did not report an exit code (ie. the shell - * integration script is misbehaving) or the shell reported a command started before the command - * finished (eg. a sub-shell was opened). Generally this should not happen, depending on the use - * case, it may be best to treat this as a failure. + * When this is `undefined` it can mean several things: + * + * - The shell either did not report an exit code (ie. the shell integration script is + * misbehaving) + * - The shell reported a command started before the command finished (eg. a sub-shell was + * opened). + * - The user canceled the command via ctrl+c. + * - The user pressed enter when there was no input. + * + * Generally this should not happen. Depending on the use case, it may be best to treat this + * as a failure. * * @example * const execution = shellIntegration.executeCommand({ From dfdece69e67e45a2a563929608c69dad8de6fe6e Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 20 Dec 2024 12:23:36 -0600 Subject: [PATCH 0309/3587] fix types for task API props (#236735) fix #231858 --- src/vs/workbench/api/common/extHostTypes.ts | 6 ++++-- src/vscode-dts/vscode.d.ts | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 90f82a69ce06..d388236e8379 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2343,7 +2343,9 @@ export class ShellExecution implements vscode.ShellExecution { throw illegalArgument('command'); } this._command = arg0; - this._args = arg1 as (string | vscode.ShellQuotedString)[]; + if (arg1) { + this._args = arg1; + } this._options = arg2; } else { if (typeof arg0 !== 'string') { @@ -2380,7 +2382,7 @@ export class ShellExecution implements vscode.ShellExecution { return this._args; } - set args(value: (string | vscode.ShellQuotedString)[]) { + set args(value: (string | vscode.ShellQuotedString)[] | undefined) { this._args = value || []; } diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index c9ecff2b0b98..3e389318075b 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -8668,12 +8668,12 @@ declare module 'vscode' { /** * The shell command. Is `undefined` if created with a full command line. */ - command: string | ShellQuotedString; + command: string | ShellQuotedString | undefined; /** * The shell args. Is `undefined` if created with a full command line. */ - args: Array; + args: Array | undefined; /** * The shell options used when the command line is executed in a shell. From 23aee3d360c197e156556dd9a4984c813bd818dc Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 20 Dec 2024 12:28:50 -0600 Subject: [PATCH 0310/3587] Revert "Reland fix custom task shell doesn't work without manually passing in "run command" arg/flag" (#236726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "Reland fix custom task shell doesn't work without manually passing in…" This reverts commit 330ab6c2928963dd83a7d90a271d9f2eb8db90d2. --- src/vs/platform/terminal/common/terminal.ts | 1 - .../tasks/browser/terminalTaskSystem.ts | 33 ++++++++----------- .../browser/terminalProfileResolverService.ts | 1 - 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 246ccaf6a765..97fc8e80462c 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -872,7 +872,6 @@ export interface ITerminalProfile { overrideName?: boolean; color?: string; icon?: ThemeIcon | URI | { light: URI; dark: URI }; - isAutomationShell?: boolean; } export interface ITerminalDimensionsOverride extends Readonly { diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 3b2c1e51c7f0..e44eaa47cf96 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -1112,6 +1112,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { color: task.configurationProperties.icon?.color || undefined, waitOnExit }; + let shellSpecified: boolean = false; const shellOptions: IShellConfiguration | undefined = task.command.options && task.command.options.shell; if (shellOptions) { if (shellOptions.executable) { @@ -1120,12 +1121,12 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { shellLaunchConfig.args = undefined; } shellLaunchConfig.executable = await this._resolveVariable(variableResolver, shellOptions.executable); + shellSpecified = true; } if (shellOptions.args) { shellLaunchConfig.args = await this._resolveVariables(variableResolver, shellOptions.args.slice()); } } - const taskShellArgsSpecified = (defaultProfile.isAutomationShell ? shellLaunchConfig.args : shellOptions?.args) !== undefined; if (shellLaunchConfig.args === undefined) { shellLaunchConfig.args = []; } @@ -1138,34 +1139,29 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { windowsShellArgs = true; // If we don't have a cwd, then the terminal uses the home dir. const userHome = await this._pathService.userHome(); - if (basename === 'cmd.exe') { - if ((options.cwd && isUNC(options.cwd)) || (!options.cwd && isUNC(userHome.fsPath))) { - return undefined; - } - if (!taskShellArgsSpecified) { - toAdd.push('/d', '/c'); - } - } else if ((basename === 'powershell.exe') || (basename === 'pwsh.exe')) { - if (!taskShellArgsSpecified) { + if (basename === 'cmd.exe' && ((options.cwd && isUNC(options.cwd)) || (!options.cwd && isUNC(userHome.fsPath)))) { + return undefined; + } + if ((basename === 'powershell.exe') || (basename === 'pwsh.exe')) { + if (!shellSpecified) { toAdd.push('-Command'); } } else if ((basename === 'bash.exe') || (basename === 'zsh.exe')) { windowsShellArgs = false; - if (!taskShellArgsSpecified) { + if (!shellSpecified) { toAdd.push('-c'); } } else if (basename === 'wsl.exe') { - if (!taskShellArgsSpecified) { + if (!shellSpecified) { toAdd.push('-e'); } } else { - if (!taskShellArgsSpecified) { - // Push `-c` for unknown shells if the user didn't specify the args - toAdd.push('-c'); + if (!shellSpecified) { + toAdd.push('/d', '/c'); } } } else { - if (!taskShellArgsSpecified) { + if (!shellSpecified) { // Under Mac remove -l to not start it as a login shell. if (platform === Platform.Platform.Mac) { // Background on -l on osx https://github.com/microsoft/vscode/issues/107563 @@ -1272,12 +1268,11 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { const combinedShellArgs: string[] = Objects.deepClone(configuredShellArgs); shellCommandArgs.forEach(element => { const shouldAddShellCommandArg = configuredShellArgs.every((arg, index) => { - const isDuplicated = arg.toLowerCase() === element.toLowerCase(); - if (isDuplicated && (configuredShellArgs.length > index + 1)) { + if ((arg.toLowerCase() === element) && (configuredShellArgs.length > index + 1)) { // We can still add the argument, but only if not all of the following arguments begin with "-". return !configuredShellArgs.slice(index + 1).every(testArg => testArg.startsWith('-')); } else { - return !isDuplicated; + return arg.toLowerCase() !== element; } }); if (shouldAddShellCommandArg) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts index e54a12a1b44d..6208cfc0413c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts @@ -268,7 +268,6 @@ export abstract class BaseTerminalProfileResolverService extends Disposable impl const automationProfile = this._configurationService.getValue(`terminal.integrated.automationProfile.${this._getOsKey(options.os)}`); if (this._isValidAutomationProfile(automationProfile, options.os)) { automationProfile.icon = this._getCustomIcon(automationProfile.icon) || Codicon.tools; - automationProfile.isAutomationShell = true; return automationProfile; } From 0f2ee1f669f1e3219be9f8764aafb34208f1f586 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 20 Dec 2024 10:29:11 -0800 Subject: [PATCH 0311/3587] fix: promote suggested file to working set when opened (#236737) --- .../contrib/chat/browser/chatEditing/chatEditingSession.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 31e9a672a133..8552946891ff 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -265,8 +265,8 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } if (existingTransientEntries.has(uri)) { existingTransientEntries.delete(uri); - } else if (!this._workingSet.has(uri) && !this._removedTransientEntries.has(uri)) { - // Don't add as a transient entry if it's already part of the working set + } 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); } From 720422ca070ad31eaafb8ab0e1b89a7d781f5e07 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 20 Dec 2024 10:29:24 -0800 Subject: [PATCH 0312/3587] chore: bump milestone in work notebook (#236730) --- .vscode/notebooks/my-work.github-issues | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index b47a08b5a186..e8b184f8e573 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"November 2024\"\n" + "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"January 2025\"\n" }, { "kind": 1, From eee5e7643a2481ee1dac9a7e75f922ccc4e40f40 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 20 Dec 2024 10:29:40 -0800 Subject: [PATCH 0313/3587] cli: propagate server-data-dir and extensions-dir when installing service (#236734) Fixes #236195 --- cli/src/bin/code/main.rs | 4 +-- cli/src/commands/tunnels.rs | 55 ++++++++++++++++++++++++------------- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/cli/src/bin/code/main.rs b/cli/src/bin/code/main.rs index 0e4809b78c74..b73d0aa885b0 100644 --- a/cli/src/bin/code/main.rs +++ b/cli/src/bin/code/main.rs @@ -103,7 +103,7 @@ async fn main() -> Result<(), std::convert::Infallible> { serve_web::serve_web(context!(), sw_args).await } - Some(args::Commands::Tunnel(tunnel_args)) => match tunnel_args.subcommand { + Some(args::Commands::Tunnel(mut tunnel_args)) => match tunnel_args.subcommand.take() { Some(args::TunnelSubcommand::Prune) => tunnels::prune(context!()).await, Some(args::TunnelSubcommand::Unregister) => tunnels::unregister(context!()).await, Some(args::TunnelSubcommand::Kill) => tunnels::kill(context!()).await, @@ -116,7 +116,7 @@ async fn main() -> Result<(), std::convert::Infallible> { tunnels::user(context!(), user_command).await } Some(args::TunnelSubcommand::Service(service_args)) => { - tunnels::service(context_no_logger(), service_args).await + tunnels::service(context_no_logger(), tunnel_args, service_args).await } Some(args::TunnelSubcommand::ForwardInternal(forward_args)) => { tunnels::forward(context_no_logger(), forward_args).await diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index 2a0b4c7ddcef..f52fa714793b 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -21,7 +21,7 @@ use tokio::{ use super::{ args::{ - AuthProvider, CliCore, CommandShellArgs, ExistingTunnelArgs, TunnelForwardArgs, + AuthProvider, CliCore, CommandShellArgs, ExistingTunnelArgs, TunnelArgs, TunnelForwardArgs, TunnelRenameArgs, TunnelServeArgs, TunnelServiceSubCommands, TunnelUserSubCommands, }, CommandContext, @@ -104,12 +104,16 @@ fn fulfill_existing_tunnel_args( } struct TunnelServiceContainer { - args: CliCore, + core_args: CliCore, + tunnel_args: TunnelArgs, } impl TunnelServiceContainer { - fn new(args: CliCore) -> Self { - Self { args } + fn new(core_args: CliCore, tunnel_args: TunnelArgs) -> Self { + Self { + core_args, + tunnel_args, + } } } @@ -120,7 +124,8 @@ impl ServiceContainer for TunnelServiceContainer { log: log::Logger, launcher_paths: LauncherPaths, ) -> Result<(), AnyError> { - let csa = (&self.args).into(); + let mut csa = (&self.core_args).into(); + self.tunnel_args.serve_args.server_args.apply_to(&mut csa); serve_with_csa( launcher_paths, log, @@ -242,8 +247,27 @@ async fn is_port_available(host: IpAddr, port: u16) -> bool { .is_ok() } +fn make_service_args<'a: 'c, 'b: 'c, 'c>( + root_path: &'a str, + tunnel_args: &'b TunnelArgs, +) -> Vec<&'c str> { + let mut args = ["--verbose", "--cli-data-dir", root_path, "tunnel"].to_vec(); + + if let Some(d) = tunnel_args.serve_args.server_args.extensions_dir.as_ref() { + args.extend_from_slice(&["--extensions-dir", d]); + } + if let Some(d) = tunnel_args.serve_args.server_args.server_data_dir.as_ref() { + args.extend_from_slice(&["--server-data-dir", d]); + } + + args.extend_from_slice(&["service", "internal-run"]); + + args +} + pub async fn service( ctx: CommandContext, + tunnel_args: TunnelArgs, service_args: TunnelServiceSubCommands, ) -> Result { let manager = create_service_manager(ctx.log.clone(), &ctx.paths); @@ -265,20 +289,10 @@ pub async fn service( legal::require_consent(&ctx.paths, args.accept_server_license_terms)?; let current_exe = canonical_exe().map_err(|e| wrap(e, "could not get current exe"))?; + let root_path = ctx.paths.root().as_os_str().to_string_lossy(); + let args = make_service_args(&root_path, &tunnel_args); - manager - .register( - current_exe, - &[ - "--verbose", - "--cli-data-dir", - ctx.paths.root().as_os_str().to_string_lossy().as_ref(), - "tunnel", - "service", - "internal-run", - ], - ) - .await?; + manager.register(current_exe, &args).await?; ctx.log.result(format!("Service successfully installed! You can use `{APPLICATION_NAME} tunnel service log` to monitor it, and `{APPLICATION_NAME} tunnel service uninstall` to remove it.")); } TunnelServiceSubCommands::Uninstall => { @@ -289,7 +303,10 @@ pub async fn service( } TunnelServiceSubCommands::InternalRun => { manager - .run(ctx.paths.clone(), TunnelServiceContainer::new(ctx.args)) + .run( + ctx.paths.clone(), + TunnelServiceContainer::new(ctx.args, tunnel_args), + ) .await?; } } From d4cda5c1a5721fc70aaf624cee4923f8f4ccbea4 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 20 Dec 2024 10:34:05 -0800 Subject: [PATCH 0314/3587] Add more logging around filePatternsToUse --- src/vs/workbench/api/common/extHostWorkspace.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index b490bf7a242d..52e1743b9901 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -479,7 +479,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac } - findFiles2(filePatterns: vscode.GlobPattern[], + findFiles2(filePatterns: readonly vscode.GlobPattern[], options: vscode.FindFiles2Options = {}, extensionId: ExtensionIdentifier, token: vscode.CancellationToken = CancellationToken.None): Promise { @@ -490,7 +490,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac private async _findFilesImpl( // the old `findFiles` used `include` to query, but the new `findFiles2` uses `filePattern` to query. // `filePattern` is the proper way to handle this, since it takes less precedence than the ignore files. - query: { type: 'include'; value: vscode.GlobPattern | undefined } | { type: 'filePatterns'; value: vscode.GlobPattern[] }, + query: { readonly type: 'include'; readonly value: vscode.GlobPattern | undefined } | { readonly type: 'filePatterns'; readonly value: readonly vscode.GlobPattern[] }, options: vscode.FindFiles2Options, token: vscode.CancellationToken ): Promise { @@ -500,7 +500,8 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac const filePatternsToUse = query.type === 'include' ? [query.value] : query.value ?? []; if (!Array.isArray(filePatternsToUse)) { - throw new Error(`Invalid file pattern provided ${filePatternsToUse}`); + console.error('Invalid file pattern provided', filePatternsToUse); + throw new Error(`Invalid file pattern provided ${JSON.stringify(filePatternsToUse)}`); } const queryOptions: QueryOptions[] = filePatternsToUse.map(filePattern => { From 15da1932a92e1ba185931b57be5a07a9f2e1cd08 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 20 Dec 2024 12:36:07 -0600 Subject: [PATCH 0315/3587] Allow completions to be requested earlier (#236630) --- .../terminal/browser/terminalInstance.ts | 52 ++++++++++- .../browser/terminal.suggest.contribution.ts | 91 +++++++++---------- .../suggest/browser/terminalSuggestAddon.ts | 16 +++- 3 files changed, 107 insertions(+), 52 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index df286dd974ad..589b1deaf9ca 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -504,8 +504,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Resolve the executable ahead of time if shell integration is enabled, this should not // be done for custom PTYs as that would cause extension Pseudoterminal-based terminals // to hang in resolver extensions + let os: OperatingSystem | undefined; if (!this.shellLaunchConfig.customPtyImplementation && this._terminalConfigurationService.config.shellIntegration?.enabled && !this.shellLaunchConfig.executable) { - const os = await this._processManager.getBackendOS(); + os = await this._processManager.getBackendOS(); const defaultProfile = (await this._terminalProfileResolverService.getDefaultProfile({ remoteAuthority: this.remoteAuthority, os })); this.shellLaunchConfig.executable = defaultProfile.path; this.shellLaunchConfig.args = defaultProfile.args; @@ -521,6 +522,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } + // Resolve the shell type ahead of time to allow features that depend upon it to work + // before the process is actually created (like terminal suggest manual request) + if (os && this.shellLaunchConfig.executable) { + this.setShellType(guessShellTypeFromExecutable(os, this.shellLaunchConfig.executable)); + } + await this._createProcess(); // Re-establish the title after reconnect @@ -2656,3 +2663,46 @@ export class TerminalInstanceColorProvider implements IXtermColorProvider { return theme.getColor(SIDE_BAR_BACKGROUND); } } + +function guessShellTypeFromExecutable(os: OperatingSystem, executable: string): TerminalShellType | undefined { + const exeBasename = path.basename(executable); + const generalShellTypeMap: Map = new Map([ + [GeneralShellType.Julia, /^julia$/], + [GeneralShellType.NuShell, /^nu$/], + [GeneralShellType.PowerShell, /^pwsh(-preview)?|powershell$/], + [GeneralShellType.Python, /^py(?:thon)?$/] + ]); + for (const [shellType, pattern] of generalShellTypeMap) { + if (exeBasename.match(pattern)) { + return shellType; + } + } + + if (os === OperatingSystem.Windows) { + const windowsShellTypeMap: Map = new Map([ + [WindowsShellType.CommandPrompt, /^cmd$/], + [WindowsShellType.GitBash, /^bash$/], + [WindowsShellType.Wsl, /^wsl$/] + ]); + for (const [shellType, pattern] of windowsShellTypeMap) { + if (exeBasename.match(pattern)) { + return shellType; + } + } + } else { + const posixShellTypes: PosixShellType[] = [ + PosixShellType.Bash, + PosixShellType.Csh, + PosixShellType.Fish, + PosixShellType.Ksh, + PosixShellType.Sh, + PosixShellType.Zsh, + ]; + for (const type of posixShellTypes) { + if (exeBasename === type) { + return type; + } + } + } + return undefined; +} diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 35a845c1f7b8..473de7f5f08a 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -12,14 +12,13 @@ import { DisposableStore, MutableDisposable, toDisposable } from '../../../../.. import { isWindows } from '../../../../../base/common/platform.js'; import { localize2 } from '../../../../../nls.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { ContextKeyExpr, IContextKey, IContextKeyService, IReadableSet } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { GeneralShellType, TerminalLocation, TerminalSettingId } from '../../../../../platform/terminal/common/terminal.js'; +import { GeneralShellType, TerminalLocation } from '../../../../../platform/terminal/common/terminal.js'; import { ITerminalContribution, ITerminalInstance, IXtermTerminal } from '../../../terminal/browser/terminal.js'; import { registerActiveInstanceAction } from '../../../terminal/browser/terminalActions.js'; import { registerTerminalContribution, type ITerminalContributionContext } from '../../../terminal/browser/terminalExtensions.js'; -import { TERMINAL_CONFIG_SECTION, type ITerminalConfiguration } from '../../../terminal/common/terminal.js'; import { TerminalContextKeys } from '../../../terminal/common/terminalContextKey.js'; import { TerminalSuggestCommandId } from '../common/terminal.suggest.js'; import { terminalSuggestConfigSection, TerminalSuggestSettingId, type ITerminalSuggestConfiguration } from '../common/terminalSuggestConfiguration.js'; @@ -42,8 +41,7 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo private readonly _addon: MutableDisposable = new MutableDisposable(); private readonly _pwshAddon: MutableDisposable = new MutableDisposable(); - private _terminalSuggestWidgetContextKeys: IReadableSet = new Set(TerminalContextKeys.suggestWidgetVisible.key); - private _terminalSuggestWidgetVisibleContextKey: IContextKey; + private readonly _terminalSuggestWidgetVisibleContextKey: IContextKey; get addon(): SuggestAddon | undefined { return this._addon.value; } get pwshAddon(): PwshCompletionProviderAddon | undefined { return this._pwshAddon.value; } @@ -87,23 +85,15 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo if (!enabled) { return; } + this._loadAddons(xterm.raw); this.add(Event.runAndSubscribe(this._ctx.instance.onDidChangeShellType, async () => { - this._loadAddons(xterm.raw); - })); - this.add(this._contextKeyService.onDidChangeContext(e => { - if (e.affectsSome(this._terminalSuggestWidgetContextKeys)) { - this._loadAddons(xterm.raw); - } - })); - this.add(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TerminalSettingId.SendKeybindingsToShell)) { - this._loadAddons(xterm.raw); - } + this._refreshAddons(); })); } private _loadPwshCompletionAddon(xterm: RawXtermTerminal): void { if (this._ctx.instance.shellType !== GeneralShellType.PowerShell) { + this._pwshAddon.clear(); return; } const pwshCompletionProviderAddon = this._pwshAddon.value = this._instantiationService.createInstance(PwshCompletionProviderAddon, undefined, this._ctx.instance.capabilities); @@ -139,42 +129,47 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo } private _loadAddons(xterm: RawXtermTerminal): void { - const sendingKeybindingsToShell = this._configurationService.getValue(TERMINAL_CONFIG_SECTION).sendKeybindingsToShell; - if (sendingKeybindingsToShell || !this._ctx.instance.shellType) { - this._addon.clear(); - this._pwshAddon.clear(); + // Don't re-create the addon + if (this._addon.value) { return; } - if (this._terminalSuggestWidgetVisibleContextKey) { - const addon = this._addon.value = this._instantiationService.createInstance(SuggestAddon, this._ctx.instance.shellType, this._ctx.instance.capabilities, this._terminalSuggestWidgetVisibleContextKey); - xterm.loadAddon(addon); - this._loadPwshCompletionAddon(xterm); - if (this._ctx.instance.target === TerminalLocation.Editor) { - addon.setContainerWithOverflow(xterm.element!); - } else { - addon.setContainerWithOverflow(dom.findParentWithClass(xterm.element!, 'panel')!); - } - addon.setScreen(xterm.element!.querySelector('.xterm-screen')!); - this.add(this._ctx.instance.onDidBlur(() => addon.hideSuggestWidget())); - this.add(addon.onAcceptedCompletion(async text => { - this._ctx.instance.focus(); - this._ctx.instance.sendText(text, false); - })); - const clipboardContrib = TerminalClipboardContribution.get(this._ctx.instance)!; - this.add(clipboardContrib.onWillPaste(() => addon.isPasting = true)); - this.add(clipboardContrib.onDidPaste(() => { - // Delay this slightly as synchronizing the prompt input is debounced - setTimeout(() => addon.isPasting = false, 100); + + const addon = this._addon.value = this._instantiationService.createInstance(SuggestAddon, this._ctx.instance.shellType, this._ctx.instance.capabilities, this._terminalSuggestWidgetVisibleContextKey); + xterm.loadAddon(addon); + this._loadPwshCompletionAddon(xterm); + if (this._ctx.instance.target === TerminalLocation.Editor) { + addon.setContainerWithOverflow(xterm.element!); + } else { + addon.setContainerWithOverflow(dom.findParentWithClass(xterm.element!, 'panel')!); + } + addon.setScreen(xterm.element!.querySelector('.xterm-screen')!); + this.add(this._ctx.instance.onDidBlur(() => addon.hideSuggestWidget())); + this.add(addon.onAcceptedCompletion(async text => { + this._ctx.instance.focus(); + this._ctx.instance.sendText(text, false); + })); + const clipboardContrib = TerminalClipboardContribution.get(this._ctx.instance)!; + this.add(clipboardContrib.onWillPaste(() => addon.isPasting = true)); + this.add(clipboardContrib.onDidPaste(() => { + // Delay this slightly as synchronizing the prompt input is debounced + setTimeout(() => addon.isPasting = false, 100); + })); + if (!isWindows) { + let barrier: AutoOpenBarrier | undefined; + this.add(addon.onDidReceiveCompletions(() => { + barrier?.open(); + barrier = undefined; })); - if (!isWindows) { - let barrier: AutoOpenBarrier | undefined; - this.add(addon.onDidReceiveCompletions(() => { - barrier?.open(); - barrier = undefined; - })); - } } } + + private _refreshAddons(): void { + const addon = this._addon.value; + if (!addon) { + return; + } + addon.shellType = this._ctx.instance.shellType; + } } registerTerminalContribution(TerminalSuggestContribution.ID, TerminalSuggestContribution); @@ -190,7 +185,7 @@ registerActiveInstanceAction({ primary: KeyMod.CtrlCmd | KeyCode.Space, mac: { primary: KeyMod.WinCtrl | KeyCode.Space }, weight: KeybindingWeight.WorkbenchContrib + 1, - when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.terminalShellIntegrationEnabled, ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.Enabled}`, true)) + when: ContextKeyExpr.and(TerminalContextKeys.focus, ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.Enabled}`, true)) }, run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.requestCompletions(true) }); diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 94650c7525c0..f750d76f089a 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -73,6 +73,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest private _cancellationTokenSource: CancellationTokenSource | undefined; isPasting: boolean = false; + shellType: TerminalShellType | undefined; private readonly _onBell = this._register(new Emitter()); readonly onBell = this._onBell.event; @@ -89,8 +90,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest [TerminalCompletionItemKind.Argument, Codicon.symbolVariable] ]); + private _shouldSyncWhenReady: boolean = false; + constructor( - private readonly _shellType: TerminalShellType | undefined, + shellType: TerminalShellType | undefined, private readonly _capabilities: ITerminalCapabilityStore, private readonly _terminalSuggestWidgetVisibleContextKey: IContextKey, @ITerminalCompletionService private readonly _terminalCompletionService: ITerminalCompletionService, @@ -101,6 +104,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest ) { super(); + this.shellType = shellType; + this._register(Event.runAndSubscribe(Event.any( this._capabilities.onDidAddCapabilityType, this._capabilities.onDidRemoveCapabilityType @@ -113,6 +118,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._promptInputModel.onDidChangeInput(e => this._sync(e)), this._promptInputModel.onDidFinishInput(() => this.hideSuggestWidget()), ); + if (this._shouldSyncWhenReady) { + this._sync(this._promptInputModel); + this._shouldSyncWhenReady = false; + } } this._register(commandDetection.onCommandExecuted(() => this.hideSuggestWidget())); } else { @@ -140,7 +149,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest return; } - if (!this._shellType) { + if (!this.shellType) { return; } @@ -156,7 +165,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest await this._extensionService.activateByEvent('onTerminalCompletionsRequested'); } - const providedCompletions = await this._terminalCompletionService.provideCompletions(this._promptInputModel.prefix, this._promptInputModel.cursorIndex, this._shellType, token, doNotRequestExtensionCompletions); + const providedCompletions = await this._terminalCompletionService.provideCompletions(this._promptInputModel.prefix, this._promptInputModel.cursorIndex, this.shellType, token, doNotRequestExtensionCompletions); if (!providedCompletions?.length || token.isCancellationRequested) { return; } @@ -237,6 +246,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest async requestCompletions(explicitlyInvoked?: boolean): Promise { if (!this._promptInputModel) { + this._shouldSyncWhenReady = true; return; } From 7cf9dbecf611fbc38c006439e704ccc807fa5c0f Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 20 Dec 2024 10:52:07 -0800 Subject: [PATCH 0316/3587] Fix escaping of raw values that contain `&` in md preview Fixes #236660 --- extensions/markdown-language-features/src/util/dom.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/markdown-language-features/src/util/dom.ts b/extensions/markdown-language-features/src/util/dom.ts index 0f6c00da9daf..8bbce79c3037 100644 --- a/extensions/markdown-language-features/src/util/dom.ts +++ b/extensions/markdown-language-features/src/util/dom.ts @@ -5,7 +5,10 @@ import * as vscode from 'vscode'; export function escapeAttribute(value: string | vscode.Uri): string { - return value.toString().replace(/"/g, '"'); + return value.toString() + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, '''); } export function getNonce() { From 094e96a2eacd2b0a14af7884c2988b00261f8505 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 20 Dec 2024 19:55:45 +0100 Subject: [PATCH 0317/3587] Introduces IObservableWithChange so that typescript does not show the default type for TChange in hovers. (#236629) * Introduces IObservableWithChange so that typescript does not show the default type for TChange in hovers. This should make it easier to understand types when observables (potentially nested) are involved. * Fixes monaco editor --- build/monaco/monaco.usage.recipe | 2 +- src/vs/base/common/event.ts | 16 +++++----- .../base/common/observableInternal/autorun.ts | 4 +-- src/vs/base/common/observableInternal/base.ts | 29 ++++++++++++------- .../base/common/observableInternal/derived.ts | 8 ++--- .../base/common/observableInternal/index.ts | 2 +- .../base/common/observableInternal/logging.ts | 8 ++--- .../base/common/observableInternal/utils.ts | 16 +++++----- src/vs/base/test/common/observable.test.ts | 10 +++---- src/vs/editor/browser/observableCodeEditor.ts | 6 ++-- .../diffEditor/components/diffEditorSash.ts | 2 +- .../widget/diffEditor/diffEditorOptions.ts | 4 +-- .../diffEditor/features/gutterFeature.ts | 2 +- .../editor/browser/widget/diffEditor/utils.ts | 4 +-- .../browser/model/inlineCompletionsModel.ts | 4 +-- .../widget/observableCodeEditor.test.ts | 4 +-- .../browser/model/textModelDiffs.ts | 6 ++-- .../chatEdit/notebookChatActionsOverlay.ts | 6 ++-- .../contrib/testing/common/observableUtils.ts | 6 ++-- 19 files changed, 73 insertions(+), 66 deletions(-) diff --git a/build/monaco/monaco.usage.recipe b/build/monaco/monaco.usage.recipe index e3c8cdd09163..a3369eb25a7d 100644 --- a/build/monaco/monaco.usage.recipe +++ b/build/monaco/monaco.usage.recipe @@ -35,6 +35,6 @@ import * as editorAPI from './vs/editor/editor.api'; a = editorAPI.editor; a = editorAPI.languages; - const o: IObservable = null!; + const o: IObservable = null!; o.TChange; })(); diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 82921d00dac6..d49f2ef276f4 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -9,7 +9,7 @@ import { onUnexpectedError } from './errors.js'; import { createSingleCallFunction } from './functional.js'; import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from './lifecycle.js'; import { LinkedList } from './linkedList.js'; -import { IObservable, IObserver } from './observable.js'; +import { IObservable, IObservableWithChange, IObserver } from './observable.js'; import { StopWatch } from './stopwatch.js'; import { MicrotaskDelay } from './symbols.js'; @@ -666,7 +666,7 @@ export namespace Event { private _counter = 0; private _hasChanged = false; - constructor(readonly _observable: IObservable, store: DisposableStore | undefined) { + constructor(readonly _observable: IObservable, store: DisposableStore | undefined) { const options: EmitterOptions = { onWillAddFirstListener: () => { _observable.addObserver(this); @@ -687,21 +687,21 @@ export namespace Event { } } - beginUpdate(_observable: IObservable): void { + beginUpdate(_observable: IObservable): void { // assert(_observable === this.obs); this._counter++; } - handlePossibleChange(_observable: IObservable): void { + handlePossibleChange(_observable: IObservable): void { // assert(_observable === this.obs); } - handleChange(_observable: IObservable, _change: TChange): void { + handleChange(_observable: IObservableWithChange, _change: TChange): void { // assert(_observable === this.obs); this._hasChanged = true; } - endUpdate(_observable: IObservable): void { + endUpdate(_observable: IObservable): void { // assert(_observable === this.obs); this._counter--; if (this._counter === 0) { @@ -718,7 +718,7 @@ export namespace Event { * Creates an event emitter that is fired when the observable changes. * Each listeners subscribes to the emitter. */ - export function fromObservable(obs: IObservable, store?: DisposableStore): Event { + export function fromObservable(obs: IObservable, store?: DisposableStore): Event { const observer = new EmitterObserver(obs, store); return observer.emitter.event; } @@ -726,7 +726,7 @@ export namespace Event { /** * Each listener is attached to the observable directly. */ - export function fromObservableLight(observable: IObservable): Event { + export function fromObservableLight(observable: IObservable): Event { return (listener, thisArgs, disposables) => { let count = 0; let didChange = false; diff --git a/src/vs/base/common/observableInternal/autorun.ts b/src/vs/base/common/observableInternal/autorun.ts index b8a626299379..d2425e110121 100644 --- a/src/vs/base/common/observableInternal/autorun.ts +++ b/src/vs/base/common/observableInternal/autorun.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IChangeContext, IObservable, IObserver, IReader } from './base.js'; +import { IChangeContext, IObservable, IObservableWithChange, IObserver, IReader } from './base.js'; import { DebugNameData, IDebugNameData } from './debugName.js'; import { assertFn, BugIndicatingError, DisposableStore, IDisposable, markAsDisposed, onBugIndicatingError, toDisposable, trackDisposable } from './commonFacade/deps.js'; import { getLogger } from './logging.js'; @@ -286,7 +286,7 @@ export class AutorunObserver implements IObserver, IReader } } - public handleChange(observable: IObservable, change: TChange): void { + public handleChange(observable: IObservableWithChange, change: TChange): void { if (this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) { try { const shouldReact = this._handleChange ? this._handleChange({ diff --git a/src/vs/base/common/observableInternal/base.ts b/src/vs/base/common/observableInternal/base.ts index c28e773b2688..f2aa0466f783 100644 --- a/src/vs/base/common/observableInternal/base.ts +++ b/src/vs/base/common/observableInternal/base.ts @@ -9,6 +9,13 @@ import type { derivedOpts } from './derived.js'; import { getLogger, logObservable } from './logging.js'; import { keepObserved, recomputeInitiallyAndOnChange } from './utils.js'; +/** + * Represents an observable value. + * + * @template T The type of the values the observable can hold. + */ +export interface IObservable extends IObservableWithChange { } + /** * Represents an observable value. * @@ -18,7 +25,7 @@ import { keepObserved, recomputeInitiallyAndOnChange } from './utils.js'; * While observers can miss temporary values of an observable, * they will receive all change values (as long as they are subscribed)! */ -export interface IObservable { +export interface IObservableWithChange { /** * Returns the current value. * @@ -71,7 +78,7 @@ export interface IObservable { * ONLY FOR DEBUGGING! * Logs computations of this derived. */ - log(): IObservable; + log(): IObservableWithChange; /** * Makes sure this value is computed eagerly. @@ -98,7 +105,7 @@ export interface IReader { /** * Reads the value of an observable and subscribes to it. */ - readObservable(observable: IObservable): T; + readObservable(observable: IObservableWithChange): T; } /** @@ -143,7 +150,7 @@ export interface IObserver { * * @param change Indicates how or why the value changed. */ - handleChange(observable: IObservable, change: TChange): void; + handleChange(observable: IObservableWithChange, change: TChange): void; } export interface ISettable { @@ -162,7 +169,7 @@ export interface ITransaction { * Calls {@link Observer.beginUpdate} immediately * and {@link Observer.endUpdate} when the transaction ends. */ - updateObserver(observer: IObserver, observable: IObservable): void; + updateObserver(observer: IObserver, observable: IObservableWithChange): void; } let _recomputeInitiallyAndOnChange: typeof recomputeInitiallyAndOnChange; @@ -185,7 +192,7 @@ export function _setDerivedOpts(derived: typeof _derived) { _derived = derived; } -export abstract class ConvenientObservable implements IObservable { +export abstract class ConvenientObservable implements IObservableWithChange { get TChange(): TChange { return null!; } public abstract get(): T; @@ -239,7 +246,7 @@ export abstract class ConvenientObservable implements IObservable { + public log(): IObservableWithChange { logObservable(this); return this; } @@ -248,7 +255,7 @@ export abstract class ConvenientObservable implements IObservable(this: IObservable>): IObservable { + public flatten(this: IObservable>): IObservable { return _derived( { owner: undefined, @@ -390,7 +397,7 @@ export class TransactionImpl implements ITransaction { /** * A settable observable. */ -export interface ISettableObservable extends IObservable, ISettable { +export interface ISettableObservable extends IObservableWithChange, ISettable { } /** @@ -505,11 +512,11 @@ export interface IChangeTracker { } export interface IChangeContext { - readonly changedObservable: IObservable; + readonly changedObservable: IObservableWithChange; readonly change: unknown; /** * Returns if the given observable caused the change. */ - didChange(observable: IObservable): this is { change: TChange }; + didChange(observable: IObservableWithChange): this is { change: TChange }; } diff --git a/src/vs/base/common/observableInternal/derived.ts b/src/vs/base/common/observableInternal/derived.ts index ba018041799d..5421b023df9f 100644 --- a/src/vs/base/common/observableInternal/derived.ts +++ b/src/vs/base/common/observableInternal/derived.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BaseObservable, IChangeContext, IObservable, IObserver, IReader, ISettableObservable, ITransaction, _setDerivedOpts, } from './base.js'; +import { BaseObservable, IChangeContext, IObservable, IObservableWithChange, IObserver, IReader, ISettableObservable, ITransaction, _setDerivedOpts, } from './base.js'; import { DebugNameData, DebugOwner, IDebugNameData } from './debugName.js'; import { BugIndicatingError, DisposableStore, EqualityComparer, IDisposable, assertFn, onBugIndicatingError, strictEquals } from './commonFacade/deps.js'; import { getLogger } from './logging.js'; @@ -386,7 +386,7 @@ export class Derived extends BaseObservable im assertFn(() => this.updateCount >= 0); } - public handlePossibleChange(observable: IObservable): void { + public handlePossibleChange(observable: IObservable): void { // In all other states, observers already know that we might have changed. if (this.state === DerivedState.upToDate && this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) { this.state = DerivedState.dependenciesMightHaveChanged; @@ -396,7 +396,7 @@ export class Derived extends BaseObservable im } } - public handleChange(observable: IObservable, change: TChange): void { + public handleChange(observable: IObservableWithChange, change: TChange): void { if (this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) { let shouldReact = false; try { @@ -460,7 +460,7 @@ export class Derived extends BaseObservable im super.removeObserver(observer); } - public override log(): IObservable { + public override log(): IObservableWithChange { if (!getLogger()) { super.log(); getLogger()?.handleDerivedCreated(this); diff --git a/src/vs/base/common/observableInternal/index.ts b/src/vs/base/common/observableInternal/index.ts index 9295f2697d50..873cf946171c 100644 --- a/src/vs/base/common/observableInternal/index.ts +++ b/src/vs/base/common/observableInternal/index.ts @@ -7,7 +7,7 @@ export { observableValueOpts } from './api.js'; export { autorun, autorunDelta, autorunHandleChanges, autorunOpts, autorunWithStore, autorunWithStoreHandleChanges } from './autorun.js'; -export { asyncTransaction, disposableObservableValue, globalTransaction, observableValue, subtransaction, transaction, TransactionImpl, type IChangeContext, type IChangeTracker, type IObservable, type IObserver, type IReader, type ISettable, type ISettableObservable, type ITransaction, } from './base.js'; +export { asyncTransaction, disposableObservableValue, globalTransaction, observableValue, subtransaction, transaction, TransactionImpl, type IChangeContext, type IChangeTracker, type IObservable, type IObservableWithChange, type IObserver, type IReader, type ISettable, type ISettableObservable, type ITransaction, } from './base.js'; export { derived, derivedDisposable, derivedHandleChanges, derivedOpts, derivedWithSetter, derivedWithStore } from './derived.js'; export { ObservableLazy, ObservableLazyPromise, ObservablePromise, PromiseResult, } from './promise.js'; export { derivedWithCancellationToken, waitForState } from './utilsCancellation.js'; diff --git a/src/vs/base/common/observableInternal/logging.ts b/src/vs/base/common/observableInternal/logging.ts index a6a5bb78a069..8dc526a45be0 100644 --- a/src/vs/base/common/observableInternal/logging.ts +++ b/src/vs/base/common/observableInternal/logging.ts @@ -39,7 +39,7 @@ interface IChangeInformation { } export interface IObservableLogger { - handleObservableChanged(observable: IObservable, info: IChangeInformation): void; + handleObservableChanged(observable: IObservable, info: IChangeInformation): void; handleFromEventObservableTriggered(observable: FromEventObservable, info: IChangeInformation): void; handleAutorunCreated(autorun: AutorunObserver): void; @@ -101,7 +101,7 @@ export class ConsoleObservableLogger implements IObservableLogger { : [normalText(` (unchanged)`)]; } - handleObservableChanged(observable: IObservable, info: IChangeInformation): void { + handleObservableChanged(observable: IObservable, info: IChangeInformation): void { if (!this._isIncluded(observable)) { return; } console.log(...this.textToConsoleArgs([ formatKind('observable value changed'), @@ -110,9 +110,9 @@ export class ConsoleObservableLogger implements IObservableLogger { ])); } - private readonly changedObservablesSets = new WeakMap>>(); + private readonly changedObservablesSets = new WeakMap>>(); - formatChanges(changes: Set>): ConsoleText | undefined { + formatChanges(changes: Set>): ConsoleText | undefined { if (changes.size === 0) { return undefined; } diff --git a/src/vs/base/common/observableInternal/utils.ts b/src/vs/base/common/observableInternal/utils.ts index 2fb57d1a42ae..c42f12f7b8ef 100644 --- a/src/vs/base/common/observableInternal/utils.ts +++ b/src/vs/base/common/observableInternal/utils.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { autorun, autorunOpts, autorunWithStoreHandleChanges } from './autorun.js'; -import { BaseObservable, ConvenientObservable, IObservable, IObserver, IReader, ITransaction, _setKeepObserved, _setRecomputeInitiallyAndOnChange, observableValue, subtransaction, transaction } from './base.js'; +import { BaseObservable, ConvenientObservable, IObservable, IObservableWithChange, IObserver, IReader, ITransaction, _setKeepObserved, _setRecomputeInitiallyAndOnChange, observableValue, subtransaction, transaction } from './base.js'; import { DebugNameData, DebugOwner, IDebugNameData, getDebugName, } from './debugName.js'; import { BugIndicatingError, DisposableStore, EqualityComparer, Event, IDisposable, IValueWithChangeEvent, strictEquals, toDisposable } from './commonFacade/deps.js'; import { derived, derivedOpts } from './derived.js'; @@ -259,7 +259,7 @@ export function observableSignal(debugNameOrOwner: string | objec } } -export interface IObservableSignal extends IObservable { +export interface IObservableSignal extends IObservableWithChange { trigger(tx: ITransaction | undefined, change: TChange): void; } @@ -434,11 +434,11 @@ export class KeepAliveObserver implements IObserver { private readonly _handleValue: ((value: any) => void) | undefined, ) { } - beginUpdate(observable: IObservable): void { + beginUpdate(observable: IObservable): void { this._counter++; } - endUpdate(observable: IObservable): void { + endUpdate(observable: IObservable): void { this._counter--; if (this._counter === 0 && this._forceRecompute) { if (this._handleValue) { @@ -449,11 +449,11 @@ export class KeepAliveObserver implements IObserver { } } - handlePossibleChange(observable: IObservable): void { + handlePossibleChange(observable: IObservable): void { // NO OP } - handleChange(observable: IObservable, change: TChange): void { + handleChange(observable: IObservableWithChange, change: TChange): void { // NO OP } } @@ -625,7 +625,7 @@ export function derivedConstOnceDefined(owner: DebugOwner, fn: (reader: IRead type RemoveUndefined = T extends undefined ? never : T; -export function runOnChange(observable: IObservable, cb: (value: T, previousValue: undefined | T, deltas: RemoveUndefined[]) => void): IDisposable { +export function runOnChange(observable: IObservableWithChange, cb: (value: T, previousValue: undefined | T, deltas: RemoveUndefined[]) => void): IDisposable { let _previousValue: T | undefined; return autorunWithStoreHandleChanges({ createEmptyChangeSummary: () => ({ deltas: [] as RemoveUndefined[], didChange: false }), @@ -649,7 +649,7 @@ export function runOnChange(observable: IObservable, cb: }); } -export function runOnChangeWithStore(observable: IObservable, cb: (value: T, previousValue: undefined | T, deltas: RemoveUndefined[], store: DisposableStore) => void): IDisposable { +export function runOnChangeWithStore(observable: IObservableWithChange, cb: (value: T, previousValue: undefined | T, deltas: RemoveUndefined[], store: DisposableStore) => void): IDisposable { const store = new DisposableStore(); const disposable = runOnChange(observable, (value, previousValue: undefined | T, deltas) => { store.clear(); diff --git a/src/vs/base/test/common/observable.test.ts b/src/vs/base/test/common/observable.test.ts index 3d7ed472fcdb..5d33ac30a9c1 100644 --- a/src/vs/base/test/common/observable.test.ts +++ b/src/vs/base/test/common/observable.test.ts @@ -8,7 +8,7 @@ import { setUnexpectedErrorHandler } from '../../common/errors.js'; import { Emitter, Event } from '../../common/event.js'; import { DisposableStore } from '../../common/lifecycle.js'; import { autorun, autorunHandleChanges, derived, derivedDisposable, IObservable, IObserver, ISettableObservable, ITransaction, keepObserved, observableFromEvent, observableSignal, observableValue, transaction, waitForState } from '../../common/observable.js'; -import { BaseObservable } from '../../common/observableInternal/base.js'; +import { BaseObservable, IObservableWithChange } from '../../common/observableInternal/base.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; suite('observables', () => { @@ -1486,18 +1486,18 @@ export class LoggingObserver implements IObserver { constructor(public readonly debugName: string, private readonly log: Log) { } - beginUpdate(observable: IObservable): void { + beginUpdate(observable: IObservable): void { this.count++; this.log.log(`${this.debugName}.beginUpdate (count ${this.count})`); } - endUpdate(observable: IObservable): void { + endUpdate(observable: IObservable): void { this.log.log(`${this.debugName}.endUpdate (count ${this.count})`); this.count--; } - handleChange(observable: IObservable, change: TChange): void { + handleChange(observable: IObservableWithChange, change: TChange): void { this.log.log(`${this.debugName}.handleChange (count ${this.count})`); } - handlePossibleChange(observable: IObservable): void { + handlePossibleChange(observable: IObservable): void { this.log.log(`${this.debugName}.handlePossibleChange`); } } diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts index 3633d4cac45c..ac1ab182fa44 100644 --- a/src/vs/editor/browser/observableCodeEditor.ts +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -5,7 +5,7 @@ import { equalsIfDefined, itemsEquals } from '../../base/common/equals.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../base/common/lifecycle.js'; -import { IObservable, ITransaction, TransactionImpl, autorun, autorunOpts, derived, derivedOpts, derivedWithSetter, observableFromEvent, observableSignal, observableValue, observableValueOpts } from '../../base/common/observable.js'; +import { IObservable, IObservableWithChange, ITransaction, TransactionImpl, autorun, autorunOpts, derived, derivedOpts, derivedWithSetter, observableFromEvent, observableSignal, observableValue, observableValueOpts } from '../../base/common/observable.js'; import { EditorOption, FindComputedEditorOptionValueById } from '../common/config/editorOptions.js'; import { LineRange } from '../common/core/lineRange.js'; import { OffsetRange } from '../common/core/offsetRange.js'; @@ -155,13 +155,13 @@ export class ObservableCodeEditor extends Disposable { public readonly isReadonly = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.readOnly)); private readonly _versionId = observableValueOpts({ owner: this, lazy: true }, this.editor.getModel()?.getVersionId() ?? null); - public readonly versionId: IObservable = this._versionId; + public readonly versionId: IObservableWithChange = this._versionId; private readonly _selections = observableValueOpts( { owner: this, equalsFn: equalsIfDefined(itemsEquals(Selection.selectionsEqual)), lazy: true }, this.editor.getSelections() ?? null ); - public readonly selections: IObservable = this._selections; + public readonly selections: IObservableWithChange = this._selections; public readonly positions = derivedOpts( diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts index 8abaddcceb95..468e6323c7f6 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts @@ -62,7 +62,7 @@ export class DiffEditorSash extends Disposable { private readonly _domNode: HTMLElement, private readonly _dimensions: { height: IObservable; width: IObservable }, private readonly _enabled: IObservable, - private readonly _boundarySashes: IObservable, + private readonly _boundarySashes: IObservable, public readonly sashLeft: ISettableObservable, private readonly _resetSash: () => void, ) { diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts index 0c1a533681f3..3d2e483ca154 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IObservable, ISettableObservable, derived, derivedConstOnceDefined, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; +import { IObservable, IObservableWithChange, ISettableObservable, derived, derivedConstOnceDefined, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { Constants } from '../../../../base/common/uint.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { diffEditorDefaultOptions } from '../../../common/config/diffEditor.js'; @@ -15,7 +15,7 @@ import { DiffEditorViewModel, DiffState } from './diffEditorViewModel.js'; export class DiffEditorOptions { private readonly _options: ISettableObservable, { changedOptions: IDiffEditorOptions }>; - public get editorOptions(): IObservable { return this._options; } + public get editorOptions(): IObservableWithChange { return this._options; } private readonly _diffEditorWidth = observableValue(this, 0); diff --git a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts index 556378aebf3b..17ff41896b41 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts @@ -50,7 +50,7 @@ export class DiffEditorGutter extends Disposable { private readonly _editors: DiffEditorEditors, private readonly _options: DiffEditorOptions, private readonly _sashLayout: SashLayout, - private readonly _boundarySashes: IObservable, + private readonly _boundarySashes: IObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IMenuService private readonly _menuService: IMenuService, diff --git a/src/vs/editor/browser/widget/diffEditor/utils.ts b/src/vs/editor/browser/widget/diffEditor/utils.ts index d34d60fd4574..47faca2f8cf2 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils.ts @@ -7,7 +7,7 @@ import { IDimension } from '../../../../base/browser/dom.js'; import { findLast } from '../../../../base/common/arraysFind.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableValue, transaction } from '../../../../base/common/observable.js'; +import { IObservable, IObservableWithChange, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableValue, transaction } from '../../../../base/common/observable.js'; import { ElementSizeObserver } from '../../config/elementSizeObserver.js'; import { ICodeEditor, IOverlayWidget, IViewZone } from '../../editorBrowser.js'; import { Position } from '../../../common/core/position.js'; @@ -126,7 +126,7 @@ export class ObservableElementSizeObserver extends Disposable { } } -export function animatedObservable(targetWindow: Window, base: IObservable, store: DisposableStore): IObservable { +export function animatedObservable(targetWindow: Window, base: IObservableWithChange, store: DisposableStore): IObservable { let targetVal = base.get(); let startVal = targetVal; let curVal = targetVal; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index a15005e75fa8..b77a2c0df81d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -7,7 +7,7 @@ import { mapFindFirst } from '../../../../../base/common/arraysFind.js'; import { itemsEquals } from '../../../../../base/common/equals.js'; import { BugIndicatingError, onUnexpectedError, onUnexpectedExternalError } from '../../../../../base/common/errors.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, IReader, ITransaction, autorun, derived, derivedHandleChanges, derivedOpts, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from '../../../../../base/common/observable.js'; +import { IObservable, IObservableWithChange, IReader, ITransaction, autorun, derived, derivedHandleChanges, derivedOpts, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from '../../../../../base/common/observable.js'; import { commonPrefixLength, firstNonWhitespaceIndex } from '../../../../../base/common/strings.js'; import { isDefined } from '../../../../../base/common/types.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; @@ -63,7 +63,7 @@ export class InlineCompletionsModel extends Disposable { constructor( public readonly textModel: ITextModel, private readonly _selectedSuggestItem: IObservable, - public readonly _textModelVersionId: IObservable, + public readonly _textModelVersionId: IObservableWithChange, private readonly _positions: IObservable, private readonly _debounceValue: IFeatureDebounceInformation, private readonly _enabled: IObservable, diff --git a/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts b/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts index d9144896afee..e8406d868f87 100644 --- a/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts +++ b/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts @@ -112,7 +112,7 @@ suite("CodeEditorWidget", () => { })); test("listener interaction (unforced)", () => { - let derived: IObservable; + let derived: IObservable; let log: Log; withEditorSetupTestFixture( (editor, disposables) => { @@ -143,7 +143,7 @@ suite("CodeEditorWidget", () => { }); test("listener interaction ()", () => { - let derived: IObservable; + let derived: IObservable; let log: Log; withEditorSetupTestFixture( (editor, disposables) => { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts index 63512ae0e383..2741b6681d49 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts @@ -12,7 +12,7 @@ import { LineRangeEdit } from './editing.js'; import { LineRange } from './lineRange.js'; import { ReentrancyBarrier } from '../../../../../base/common/controlFlow.js'; import { IMergeDiffComputer } from './diffComputer.js'; -import { autorun, IObservable, IReader, ITransaction, observableSignal, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { autorun, IObservableWithChange, IReader, ITransaction, observableSignal, observableValue, transaction } from '../../../../../base/common/observable.js'; import { UndoRedoGroup } from '../../../../../platform/undoRedo/common/undoRedo.js'; export class TextModelDiffs extends Disposable { @@ -61,14 +61,14 @@ export class TextModelDiffs extends Disposable { })); } - public get state(): IObservable { + public get state(): IObservableWithChange { return this._state; } /** * Diffs from base to input. */ - public get diffs(): IObservable { + public get diffs(): IObservableWithChange { return this._diffs; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts index 552a657df4d3..7ac61fbcb303 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts @@ -24,7 +24,7 @@ import { navigationBearingFakeActionId } from '../../../../chat/browser/chatEdit export class NotebookChatActionsOverlayController extends Disposable { constructor( private readonly notebookEditor: INotebookEditor, - cellDiffInfo: IObservable, + cellDiffInfo: IObservable, deletedCellDecorator: INotebookDeletedCellDecorator, @IChatEditingService private readonly _chatEditingService: IChatEditingService, @IInstantiationService instantiationService: IInstantiationService, @@ -60,7 +60,7 @@ export class NotebookChatActionsOverlay extends Disposable { constructor( private readonly notebookEditor: INotebookEditor, entry: IModifiedFileEntry, - cellDiffInfo: IObservable, + cellDiffInfo: IObservable, nextEntry: IModifiedFileEntry, previousEntry: IModifiedFileEntry, deletedCellDecorator: INotebookDeletedCellDecorator, @@ -196,7 +196,7 @@ export class NotebookChatActionsOverlay extends Disposable { class NextPreviousChangeActionRunner extends ActionRunner { constructor( private readonly notebookEditor: INotebookEditor, - private readonly cellDiffInfo: IObservable, + private readonly cellDiffInfo: IObservable, private readonly entry: IModifiedFileEntry, private readonly next: IModifiedFileEntry, private readonly direction: 'next' | 'previous', diff --git a/src/vs/workbench/contrib/testing/common/observableUtils.ts b/src/vs/workbench/contrib/testing/common/observableUtils.ts index 3153f2e5bd8a..9294c905002e 100644 --- a/src/vs/workbench/contrib/testing/common/observableUtils.ts +++ b/src/vs/workbench/contrib/testing/common/observableUtils.ts @@ -4,16 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, IObserver } from '../../../../base/common/observable.js'; +import { IObservableWithChange, IObserver } from '../../../../base/common/observable.js'; -export function onObservableChange(observable: IObservable, callback: (value: T) => void): IDisposable { +export function onObservableChange(observable: IObservableWithChange, callback: (value: T) => void): IDisposable { const o: IObserver = { beginUpdate() { }, endUpdate() { }, handlePossibleChange(observable) { observable.reportChanges(); }, - handleChange(_observable: IObservable, change: TChange) { + handleChange(_observable: IObservableWithChange, change: TChange) { callback(change as any as T); } }; From 9207b53cf370be498f1dd033d8c442034e707f41 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:58:46 -0800 Subject: [PATCH 0318/3587] @xterm/xterm@5.6.0-beta.74 Fixes #117741 --- package-lock.json | 86 ++++++++++++++++++------------------ package.json | 18 ++++---- remote/package-lock.json | 86 ++++++++++++++++++------------------ remote/package.json | 18 ++++---- remote/web/package-lock.json | 78 ++++++++++++++++---------------- remote/web/package.json | 16 +++---- 6 files changed, 151 insertions(+), 151 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5f2b14c3e397..9d8cbea7bb26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,15 +27,15 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.53", - "@xterm/addon-image": "^0.9.0-beta.70", - "@xterm/addon-ligatures": "^0.10.0-beta.70", - "@xterm/addon-search": "^0.16.0-beta.70", - "@xterm/addon-serialize": "^0.14.0-beta.70", - "@xterm/addon-unicode11": "^0.9.0-beta.70", - "@xterm/addon-webgl": "^0.19.0-beta.70", - "@xterm/headless": "^5.6.0-beta.70", - "@xterm/xterm": "^5.6.0-beta.70", + "@xterm/addon-clipboard": "^0.2.0-beta.57", + "@xterm/addon-image": "^0.9.0-beta.74", + "@xterm/addon-ligatures": "^0.10.0-beta.74", + "@xterm/addon-search": "^0.16.0-beta.74", + "@xterm/addon-serialize": "^0.14.0-beta.74", + "@xterm/addon-unicode11": "^0.9.0-beta.74", + "@xterm/addon-webgl": "^0.19.0-beta.74", + "@xterm/headless": "^5.6.0-beta.74", + "@xterm/xterm": "^5.6.0-beta.74", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -3457,30 +3457,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.53", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.53.tgz", - "integrity": "sha512-kCcBuGvF8mwzExU+Tm9eylPvp1kXTkvm+kO0V4qP7HI3ZCw5vfKmnlRn41FvNIylsK2hnmrFtxauPHEGBy/dfA==", + "version": "0.2.0-beta.57", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.57.tgz", + "integrity": "sha512-/GSI8Fkmb8s/V1t2EGc2U2PUfSqge6f9gAeob65EwarsfBf66cmCxMG0ZSPE8+nti1pGIsrJA8XfeEaJt4clcA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.70.tgz", - "integrity": "sha512-QLhy77i0sjnffkLuxj1yB/mBUJI64bbL86eMW+1g5XEsZnSevY8YwU/cEJg02PAGK0ggwQNNfiRwoO6VBCdYFg==", + "version": "0.9.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.74.tgz", + "integrity": "sha512-mJPWNPov2mqrUkYZCs6UCn5p6DBLeN6xjpLu5mLh8cmXr544VWfqEVNAYPQ9+8uNgXdzSsKInBv9ZGtbXV0SfA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.70.tgz", - "integrity": "sha512-BPDHHOybUWO6mjHf/RMDBjSKDl9QdyyGTyHvmlyhuI/2sma3lu98bA4U03F8nBnvj/6otzEFARuOeoN7rkfRng==", + "version": "0.10.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.74.tgz", + "integrity": "sha512-/X3OemVqPSgdely8OgdQb0cJqv9HqiMaBLeLe2QHfTWdXDBOLG/O4g8n/lChqR9rulEMwPCt2LqvtyIMA3iZsw==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -3490,55 +3490,55 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.70.tgz", - "integrity": "sha512-uaNBf77cr5Jikj69TDkTfe3V3wHA+4tDTFcv8DwJ/KGORPLUfBk8cn9HpbIJ+0jc1kOZr5xDrEjaYNwEVDkGeQ==", + "version": "0.16.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.74.tgz", + "integrity": "sha512-KMOeOu3EvtDWcH/HCs6fe4KyaMMdjoUjP1C7R3AtZwJVdm5GaFxASxdol+9evfGhUUei4qt+zVsaFraNKyFvsA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.70.tgz", - "integrity": "sha512-4ijqHU7xDRcZ4Gm6yN/tKsZzovUwzttE9/pPfhxpWIgSdm9c8i5R4rGyOAlAZCnuU3DT5vPplQO7W/0zwAmJsQ==", + "version": "0.14.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.74.tgz", + "integrity": "sha512-zBhQAoXagBMhKFGYQ6n52sKapY61Jt3hKT8awhG/49f04JkaX1Jhc6xohD+aZs6J2ABHI7EWW/y0xTN9BDLLBw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.70.tgz", - "integrity": "sha512-9UE4v2SpWtqd85Hqvk8LW4QA9Phe93RMVltrNtt9jCUmkAok/QLFOEbLqoq2JUOvlmfsYfXmIl11Wg4CYIsvwA==", + "version": "0.9.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.74.tgz", + "integrity": "sha512-1AyuDST77Xg33ed+3neNrQfsfZVwa+C16uWP9eTdJ1rO48ylqPcFTDnahCfIZGPHGjPqLl90ZKZ8tt1zWSefmA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.70.tgz", - "integrity": "sha512-3BkmF24i66SHCfAkcHy1VC6H715qyelfOIjd6n7sTqHon56J1bUO782Q1al/MK0vSvplElBRKVdR31SDvITzpg==", + "version": "0.19.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.74.tgz", + "integrity": "sha512-1wKiuv6WHMqOcSlLeIRU7UF8zkU4KU35rnPvLw2G45aAJSr9B3f7EVIaJ4IjXGeY/C+WCM3wWuybPN5VdB8qAQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.70.tgz", - "integrity": "sha512-npSJzmtJq8LLAzV3nwD9KkBQLNWSusi+cOBPyc3zlYYE63Vkqbtbp/iQS27zH2GxJ95rzCzDwnX6VOn0OoUPYQ==", + "version": "5.6.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.74.tgz", + "integrity": "sha512-6PSNk1/CaLGNZLhXULswjfbf/rJrG1EomT9hR2nNbX6Osm9LVbhgIzg/mYHPu9wX5Pz7Gpd1VWp4/1NcKpN53w==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.70.tgz", - "integrity": "sha512-qviQMVWhRtgPn4z8PHNH6D/ffSKkNBHmUX1HyJxl325QM2xF8M8met83uFv7JZm7a5OQYScnLGsFAoTreSgdew==", + "version": "5.6.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.74.tgz", + "integrity": "sha512-gVu7+4Cfd7O/6cQ/UK0sZM+TJBaI/VgQ/JFAhuAnNFj29wts2MzxSH3fIp3KUG1kqlJBWEUShCTwB8nKwtbCjQ==", "license": "MIT" }, "node_modules/@xtuc/ieee754": { diff --git a/package.json b/package.json index 3316ae225ea8..4cbd98ecdd54 100644 --- a/package.json +++ b/package.json @@ -85,15 +85,15 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.53", - "@xterm/addon-image": "^0.9.0-beta.70", - "@xterm/addon-ligatures": "^0.10.0-beta.70", - "@xterm/addon-search": "^0.16.0-beta.70", - "@xterm/addon-serialize": "^0.14.0-beta.70", - "@xterm/addon-unicode11": "^0.9.0-beta.70", - "@xterm/addon-webgl": "^0.19.0-beta.70", - "@xterm/headless": "^5.6.0-beta.70", - "@xterm/xterm": "^5.6.0-beta.70", + "@xterm/addon-clipboard": "^0.2.0-beta.57", + "@xterm/addon-image": "^0.9.0-beta.74", + "@xterm/addon-ligatures": "^0.10.0-beta.74", + "@xterm/addon-search": "^0.16.0-beta.74", + "@xterm/addon-serialize": "^0.14.0-beta.74", + "@xterm/addon-unicode11": "^0.9.0-beta.74", + "@xterm/addon-webgl": "^0.19.0-beta.74", + "@xterm/headless": "^5.6.0-beta.74", + "@xterm/xterm": "^5.6.0-beta.74", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", diff --git a/remote/package-lock.json b/remote/package-lock.json index 3cf229398615..53a1dddc2924 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -20,15 +20,15 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.53", - "@xterm/addon-image": "^0.9.0-beta.70", - "@xterm/addon-ligatures": "^0.10.0-beta.70", - "@xterm/addon-search": "^0.16.0-beta.70", - "@xterm/addon-serialize": "^0.14.0-beta.70", - "@xterm/addon-unicode11": "^0.9.0-beta.70", - "@xterm/addon-webgl": "^0.19.0-beta.70", - "@xterm/headless": "^5.6.0-beta.70", - "@xterm/xterm": "^5.6.0-beta.70", + "@xterm/addon-clipboard": "^0.2.0-beta.57", + "@xterm/addon-image": "^0.9.0-beta.74", + "@xterm/addon-ligatures": "^0.10.0-beta.74", + "@xterm/addon-search": "^0.16.0-beta.74", + "@xterm/addon-serialize": "^0.14.0-beta.74", + "@xterm/addon-unicode11": "^0.9.0-beta.74", + "@xterm/addon-webgl": "^0.19.0-beta.74", + "@xterm/headless": "^5.6.0-beta.74", + "@xterm/xterm": "^5.6.0-beta.74", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -520,30 +520,30 @@ "hasInstallScript": true }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.53", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.53.tgz", - "integrity": "sha512-kCcBuGvF8mwzExU+Tm9eylPvp1kXTkvm+kO0V4qP7HI3ZCw5vfKmnlRn41FvNIylsK2hnmrFtxauPHEGBy/dfA==", + "version": "0.2.0-beta.57", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.57.tgz", + "integrity": "sha512-/GSI8Fkmb8s/V1t2EGc2U2PUfSqge6f9gAeob65EwarsfBf66cmCxMG0ZSPE8+nti1pGIsrJA8XfeEaJt4clcA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.70.tgz", - "integrity": "sha512-QLhy77i0sjnffkLuxj1yB/mBUJI64bbL86eMW+1g5XEsZnSevY8YwU/cEJg02PAGK0ggwQNNfiRwoO6VBCdYFg==", + "version": "0.9.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.74.tgz", + "integrity": "sha512-mJPWNPov2mqrUkYZCs6UCn5p6DBLeN6xjpLu5mLh8cmXr544VWfqEVNAYPQ9+8uNgXdzSsKInBv9ZGtbXV0SfA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.70.tgz", - "integrity": "sha512-BPDHHOybUWO6mjHf/RMDBjSKDl9QdyyGTyHvmlyhuI/2sma3lu98bA4U03F8nBnvj/6otzEFARuOeoN7rkfRng==", + "version": "0.10.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.74.tgz", + "integrity": "sha512-/X3OemVqPSgdely8OgdQb0cJqv9HqiMaBLeLe2QHfTWdXDBOLG/O4g8n/lChqR9rulEMwPCt2LqvtyIMA3iZsw==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -553,55 +553,55 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.70.tgz", - "integrity": "sha512-uaNBf77cr5Jikj69TDkTfe3V3wHA+4tDTFcv8DwJ/KGORPLUfBk8cn9HpbIJ+0jc1kOZr5xDrEjaYNwEVDkGeQ==", + "version": "0.16.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.74.tgz", + "integrity": "sha512-KMOeOu3EvtDWcH/HCs6fe4KyaMMdjoUjP1C7R3AtZwJVdm5GaFxASxdol+9evfGhUUei4qt+zVsaFraNKyFvsA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.70.tgz", - "integrity": "sha512-4ijqHU7xDRcZ4Gm6yN/tKsZzovUwzttE9/pPfhxpWIgSdm9c8i5R4rGyOAlAZCnuU3DT5vPplQO7W/0zwAmJsQ==", + "version": "0.14.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.74.tgz", + "integrity": "sha512-zBhQAoXagBMhKFGYQ6n52sKapY61Jt3hKT8awhG/49f04JkaX1Jhc6xohD+aZs6J2ABHI7EWW/y0xTN9BDLLBw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.70.tgz", - "integrity": "sha512-9UE4v2SpWtqd85Hqvk8LW4QA9Phe93RMVltrNtt9jCUmkAok/QLFOEbLqoq2JUOvlmfsYfXmIl11Wg4CYIsvwA==", + "version": "0.9.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.74.tgz", + "integrity": "sha512-1AyuDST77Xg33ed+3neNrQfsfZVwa+C16uWP9eTdJ1rO48ylqPcFTDnahCfIZGPHGjPqLl90ZKZ8tt1zWSefmA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.70.tgz", - "integrity": "sha512-3BkmF24i66SHCfAkcHy1VC6H715qyelfOIjd6n7sTqHon56J1bUO782Q1al/MK0vSvplElBRKVdR31SDvITzpg==", + "version": "0.19.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.74.tgz", + "integrity": "sha512-1wKiuv6WHMqOcSlLeIRU7UF8zkU4KU35rnPvLw2G45aAJSr9B3f7EVIaJ4IjXGeY/C+WCM3wWuybPN5VdB8qAQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.70.tgz", - "integrity": "sha512-npSJzmtJq8LLAzV3nwD9KkBQLNWSusi+cOBPyc3zlYYE63Vkqbtbp/iQS27zH2GxJ95rzCzDwnX6VOn0OoUPYQ==", + "version": "5.6.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.74.tgz", + "integrity": "sha512-6PSNk1/CaLGNZLhXULswjfbf/rJrG1EomT9hR2nNbX6Osm9LVbhgIzg/mYHPu9wX5Pz7Gpd1VWp4/1NcKpN53w==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.70.tgz", - "integrity": "sha512-qviQMVWhRtgPn4z8PHNH6D/ffSKkNBHmUX1HyJxl325QM2xF8M8met83uFv7JZm7a5OQYScnLGsFAoTreSgdew==", + "version": "5.6.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.74.tgz", + "integrity": "sha512-gVu7+4Cfd7O/6cQ/UK0sZM+TJBaI/VgQ/JFAhuAnNFj29wts2MzxSH3fIp3KUG1kqlJBWEUShCTwB8nKwtbCjQ==", "license": "MIT" }, "node_modules/agent-base": { diff --git a/remote/package.json b/remote/package.json index 5a61a15c0513..09dc79f4b0ae 100644 --- a/remote/package.json +++ b/remote/package.json @@ -15,15 +15,15 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.53", - "@xterm/addon-image": "^0.9.0-beta.70", - "@xterm/addon-ligatures": "^0.10.0-beta.70", - "@xterm/addon-search": "^0.16.0-beta.70", - "@xterm/addon-serialize": "^0.14.0-beta.70", - "@xterm/addon-unicode11": "^0.9.0-beta.70", - "@xterm/addon-webgl": "^0.19.0-beta.70", - "@xterm/headless": "^5.6.0-beta.70", - "@xterm/xterm": "^5.6.0-beta.70", + "@xterm/addon-clipboard": "^0.2.0-beta.57", + "@xterm/addon-image": "^0.9.0-beta.74", + "@xterm/addon-ligatures": "^0.10.0-beta.74", + "@xterm/addon-search": "^0.16.0-beta.74", + "@xterm/addon-serialize": "^0.14.0-beta.74", + "@xterm/addon-unicode11": "^0.9.0-beta.74", + "@xterm/addon-webgl": "^0.19.0-beta.74", + "@xterm/headless": "^5.6.0-beta.74", + "@xterm/xterm": "^5.6.0-beta.74", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 3e45e62a14dc..6712d40c6282 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -13,14 +13,14 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.0.4", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.53", - "@xterm/addon-image": "^0.9.0-beta.70", - "@xterm/addon-ligatures": "^0.10.0-beta.70", - "@xterm/addon-search": "^0.16.0-beta.70", - "@xterm/addon-serialize": "^0.14.0-beta.70", - "@xterm/addon-unicode11": "^0.9.0-beta.70", - "@xterm/addon-webgl": "^0.19.0-beta.70", - "@xterm/xterm": "^5.6.0-beta.70", + "@xterm/addon-clipboard": "^0.2.0-beta.57", + "@xterm/addon-image": "^0.9.0-beta.74", + "@xterm/addon-ligatures": "^0.10.0-beta.74", + "@xterm/addon-search": "^0.16.0-beta.74", + "@xterm/addon-serialize": "^0.14.0-beta.74", + "@xterm/addon-unicode11": "^0.9.0-beta.74", + "@xterm/addon-webgl": "^0.19.0-beta.74", + "@xterm/xterm": "^5.6.0-beta.74", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", @@ -88,30 +88,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.53", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.53.tgz", - "integrity": "sha512-kCcBuGvF8mwzExU+Tm9eylPvp1kXTkvm+kO0V4qP7HI3ZCw5vfKmnlRn41FvNIylsK2hnmrFtxauPHEGBy/dfA==", + "version": "0.2.0-beta.57", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.57.tgz", + "integrity": "sha512-/GSI8Fkmb8s/V1t2EGc2U2PUfSqge6f9gAeob65EwarsfBf66cmCxMG0ZSPE8+nti1pGIsrJA8XfeEaJt4clcA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.70.tgz", - "integrity": "sha512-QLhy77i0sjnffkLuxj1yB/mBUJI64bbL86eMW+1g5XEsZnSevY8YwU/cEJg02PAGK0ggwQNNfiRwoO6VBCdYFg==", + "version": "0.9.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.74.tgz", + "integrity": "sha512-mJPWNPov2mqrUkYZCs6UCn5p6DBLeN6xjpLu5mLh8cmXr544VWfqEVNAYPQ9+8uNgXdzSsKInBv9ZGtbXV0SfA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.70.tgz", - "integrity": "sha512-BPDHHOybUWO6mjHf/RMDBjSKDl9QdyyGTyHvmlyhuI/2sma3lu98bA4U03F8nBnvj/6otzEFARuOeoN7rkfRng==", + "version": "0.10.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.74.tgz", + "integrity": "sha512-/X3OemVqPSgdely8OgdQb0cJqv9HqiMaBLeLe2QHfTWdXDBOLG/O4g8n/lChqR9rulEMwPCt2LqvtyIMA3iZsw==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -121,49 +121,49 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.70.tgz", - "integrity": "sha512-uaNBf77cr5Jikj69TDkTfe3V3wHA+4tDTFcv8DwJ/KGORPLUfBk8cn9HpbIJ+0jc1kOZr5xDrEjaYNwEVDkGeQ==", + "version": "0.16.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.74.tgz", + "integrity": "sha512-KMOeOu3EvtDWcH/HCs6fe4KyaMMdjoUjP1C7R3AtZwJVdm5GaFxASxdol+9evfGhUUei4qt+zVsaFraNKyFvsA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.70.tgz", - "integrity": "sha512-4ijqHU7xDRcZ4Gm6yN/tKsZzovUwzttE9/pPfhxpWIgSdm9c8i5R4rGyOAlAZCnuU3DT5vPplQO7W/0zwAmJsQ==", + "version": "0.14.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.74.tgz", + "integrity": "sha512-zBhQAoXagBMhKFGYQ6n52sKapY61Jt3hKT8awhG/49f04JkaX1Jhc6xohD+aZs6J2ABHI7EWW/y0xTN9BDLLBw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.70.tgz", - "integrity": "sha512-9UE4v2SpWtqd85Hqvk8LW4QA9Phe93RMVltrNtt9jCUmkAok/QLFOEbLqoq2JUOvlmfsYfXmIl11Wg4CYIsvwA==", + "version": "0.9.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.74.tgz", + "integrity": "sha512-1AyuDST77Xg33ed+3neNrQfsfZVwa+C16uWP9eTdJ1rO48ylqPcFTDnahCfIZGPHGjPqLl90ZKZ8tt1zWSefmA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.70.tgz", - "integrity": "sha512-3BkmF24i66SHCfAkcHy1VC6H715qyelfOIjd6n7sTqHon56J1bUO782Q1al/MK0vSvplElBRKVdR31SDvITzpg==", + "version": "0.19.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.74.tgz", + "integrity": "sha512-1wKiuv6WHMqOcSlLeIRU7UF8zkU4KU35rnPvLw2G45aAJSr9B3f7EVIaJ4IjXGeY/C+WCM3wWuybPN5VdB8qAQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.70.tgz", - "integrity": "sha512-qviQMVWhRtgPn4z8PHNH6D/ffSKkNBHmUX1HyJxl325QM2xF8M8met83uFv7JZm7a5OQYScnLGsFAoTreSgdew==", + "version": "5.6.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.74.tgz", + "integrity": "sha512-gVu7+4Cfd7O/6cQ/UK0sZM+TJBaI/VgQ/JFAhuAnNFj29wts2MzxSH3fIp3KUG1kqlJBWEUShCTwB8nKwtbCjQ==", "license": "MIT" }, "node_modules/font-finder": { diff --git a/remote/web/package.json b/remote/web/package.json index 8c1decd65bba..31f552e7f5c9 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -8,14 +8,14 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.0.4", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.53", - "@xterm/addon-image": "^0.9.0-beta.70", - "@xterm/addon-ligatures": "^0.10.0-beta.70", - "@xterm/addon-search": "^0.16.0-beta.70", - "@xterm/addon-serialize": "^0.14.0-beta.70", - "@xterm/addon-unicode11": "^0.9.0-beta.70", - "@xterm/addon-webgl": "^0.19.0-beta.70", - "@xterm/xterm": "^5.6.0-beta.70", + "@xterm/addon-clipboard": "^0.2.0-beta.57", + "@xterm/addon-image": "^0.9.0-beta.74", + "@xterm/addon-ligatures": "^0.10.0-beta.74", + "@xterm/addon-search": "^0.16.0-beta.74", + "@xterm/addon-serialize": "^0.14.0-beta.74", + "@xterm/addon-unicode11": "^0.9.0-beta.74", + "@xterm/addon-webgl": "^0.19.0-beta.74", + "@xterm/xterm": "^5.6.0-beta.74", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", From 83c336d7d606a8d2c6f50c403a7fecc770e20cbe Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 20 Dec 2024 20:01:59 +0100 Subject: [PATCH 0319/3587] Log when a derived gets cleared (#236739) --- src/vs/base/common/observableInternal/derived.ts | 1 + src/vs/base/common/observableInternal/logging.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/src/vs/base/common/observableInternal/derived.ts b/src/vs/base/common/observableInternal/derived.ts index 5421b023df9f..54a1e99296db 100644 --- a/src/vs/base/common/observableInternal/derived.ts +++ b/src/vs/base/common/observableInternal/derived.ts @@ -220,6 +220,7 @@ export class Derived extends BaseObservable im */ this.state = DerivedState.initial; this.value = undefined; + getLogger()?.handleDerivedCleared(this); for (const d of this.dependencies) { d.removeObserver(this); } diff --git a/src/vs/base/common/observableInternal/logging.ts b/src/vs/base/common/observableInternal/logging.ts index 8dc526a45be0..f0bc82170d8e 100644 --- a/src/vs/base/common/observableInternal/logging.ts +++ b/src/vs/base/common/observableInternal/logging.ts @@ -48,6 +48,7 @@ export interface IObservableLogger { handleDerivedCreated(observable: Derived): void; handleDerivedRecomputed(observable: Derived, info: IChangeInformation): void; + handleDerivedCleared(observable: Derived): void; handleBeginTransaction(transaction: TransactionImpl): void; handleEndTransaction(): void; @@ -170,6 +171,15 @@ export class ConsoleObservableLogger implements IObservableLogger { changedObservables.clear(); } + handleDerivedCleared(derived: Derived): void { + if (!this._isIncluded(derived)) { return; } + + console.log(...this.textToConsoleArgs([ + formatKind('derived cleared'), + styled(derived.debugName, { color: 'BlueViolet' }), + ])); + } + handleFromEventObservableTriggered(observable: FromEventObservable, info: IChangeInformation): void { if (!this._isIncluded(observable)) { return; } From b9f07f1e9478a470353503582e8f5192a84a65e9 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 20 Dec 2024 20:02:18 +0100 Subject: [PATCH 0320/3587] Adds color descriptions. (#236741) --- .../view/inlineEdits/gutterIndicatorView.ts | 40 +++++++++++++++---- .../browser/view/inlineEdits/indicatorView.ts | 9 ++--- .../view/inlineEdits/sideBySideDiff.ts | 18 ++++----- .../view/inlineEdits/wordReplacementView.ts | 4 +- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index 739f12b4feb2..a3d7dcddbc10 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -17,15 +17,39 @@ import { OffsetRange } from '../../../../../common/core/offsetRange.js'; import { StickyScrollController } from '../../../../stickyScroll/browser/stickyScrollController.js'; import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; import { mapOutFalsy, n, rectToProps } from './utils.js'; +import { localize } from '../../../../../../nls.js'; +export const inlineEditIndicatorPrimaryForeground = registerColor( + 'inlineEdit.gutterIndicator.primaryForeground', + buttonForeground, + localize('inlineEdit.gutterIndicator.primaryForeground', 'Foreground color for the primary inline edit gutter indicator.') +); +export const inlineEditIndicatorPrimaryBackground = registerColor( + 'inlineEdit.gutterIndicator.primaryBackground', + buttonBackground, + localize('inlineEdit.gutterIndicator.primaryBackground', 'Background color for the primary inline edit gutter indicator.') +); -export const inlineEditIndicatorPrimaryForeground = registerColor('inlineEdit.gutterIndicator.primaryForeground', buttonForeground, 'Foreground color for the primary inline edit gutter indicator.'); -export const inlineEditIndicatorPrimaryBackground = registerColor('inlineEdit.gutterIndicator.primaryBackground', buttonBackground, 'Background color for the primary inline edit gutter indicator.'); - -export const inlineEditIndicatorSecondaryForeground = registerColor('inlineEdit.gutterIndicator.secondaryForeground', buttonSecondaryForeground, 'Foreground color for the secondary inline edit gutter indicator.'); -export const inlineEditIndicatorSecondaryBackground = registerColor('inlineEdit.gutterIndicator.secondaryBackground', buttonSecondaryBackground, 'Background color for the secondary inline edit gutter indicator.'); +export const inlineEditIndicatorSecondaryForeground = registerColor( + 'inlineEdit.gutterIndicator.secondaryForeground', + buttonSecondaryForeground, + localize('inlineEdit.gutterIndicator.secondaryForeground', 'Foreground color for the secondary inline edit gutter indicator.') +); +export const inlineEditIndicatorSecondaryBackground = registerColor( + 'inlineEdit.gutterIndicator.secondaryBackground', + buttonSecondaryBackground, + localize('inlineEdit.gutterIndicator.secondaryBackground', 'Background color for the secondary inline edit gutter indicator.') +); -export const inlineEditIndicatorsuccessfulForeground = registerColor('inlineEdit.gutterIndicator.successfulForeground', buttonForeground, 'Foreground color for the successful inline edit gutter indicator.'); -export const inlineEditIndicatorsuccessfulBackground = registerColor('inlineEdit.gutterIndicator.successfulBackground', { light: '#2e825c', dark: '#2e825c', hcLight: '#2e825c', hcDark: '#2e825c' }, 'Background color for the successful inline edit gutter indicator.'); +export const inlineEditIndicatorsuccessfulForeground = registerColor( + 'inlineEdit.gutterIndicator.successfulForeground', + buttonForeground, + localize('inlineEdit.gutterIndicator.successfulForeground', 'Foreground color for the successful inline edit gutter indicator.') +); +export const inlineEditIndicatorsuccessfulBackground = registerColor( + 'inlineEdit.gutterIndicator.successfulBackground', + { light: '#2e825c', dark: '#2e825c', hcLight: '#2e825c', hcDark: '#2e825c' }, + localize('inlineEdit.gutterIndicator.successfulBackground', 'Background color for the successful inline edit gutter indicator.') +); export const inlineEditIndicatorBackground = registerColor( 'inlineEdit.gutterIndicator.background', @@ -35,7 +59,7 @@ export const inlineEditIndicatorBackground = registerColor( dark: transparent('tab.inactiveBackground', 0.5), light: '#5f5f5f18', }, - 'Background color for the inline edit gutter indicator.' + localize('inlineEdit.gutterIndicator.background', 'Background color for the inline edit gutter indicator.') ); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/indicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/indicatorView.ts index f58456636583..00b129bbbfb4 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/indicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/indicatorView.ts @@ -13,16 +13,15 @@ import { registerColor } from '../../../../../../platform/theme/common/colorUtil import { ObservableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { OffsetRange } from '../../../../../common/core/offsetRange.js'; import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; +import { localize } from '../../../../../../nls.js'; export interface IInlineEditsIndicatorState { editTop: number; showAlways: boolean; } - - -export const inlineEditIndicatorForeground = registerColor('inlineEdit.indicator.foreground', buttonForeground, ''); -export const inlineEditIndicatorBackground = registerColor('inlineEdit.indicator.background', buttonBackground, ''); -export const inlineEditIndicatorBorder = registerColor('inlineEdit.indicator.border', buttonSeparator, ''); +export const inlineEditIndicatorForeground = registerColor('inlineEdit.indicator.foreground', buttonForeground, localize('inlineEdit.indicator.foreground', 'Foreground color for the inline edit indicator.')); +export const inlineEditIndicatorBackground = registerColor('inlineEdit.indicator.background', buttonBackground, localize('inlineEdit.indicator.background', 'Background color for the inline edit indicator.')); +export const inlineEditIndicatorBorder = registerColor('inlineEdit.indicator.border', buttonSeparator, localize('inlineEdit.indicator.border', 'Border color for the inline edit indicator.')); export class InlineEditsIndicator extends Disposable { private readonly _indicator = h('div.inline-edits-view-indicator', { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index 042dc653cf90..7198e4870e92 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -32,48 +32,48 @@ import { InlineCompletionContextKeys } from '../../controller/inlineCompletionCo import { CustomizedMenuWorkbenchToolBar } from '../../hintsWidget/inlineCompletionsHintsWidget.js'; import { PathBuilder, StatusBarViewItem, getOffsetForPos, mapOutFalsy, maxContentWidthInRange, n } from './utils.js'; import { InlineEditWithChanges } from './viewAndDiffProducer.js'; +import { localize } from '../../../../../../nls.js'; export const originalBackgroundColor = registerColor( 'inlineEdit.originalBackground', Color.transparent, - '', + localize('inlineEdit.originalBackground', 'Background color for the original text in inline edits.'), true ); export const modifiedBackgroundColor = registerColor( 'inlineEdit.modifiedBackground', Color.transparent, - '', + localize('inlineEdit.modifiedBackground', 'Background color for the modified text in inline edits.'), true ); export const originalChangedLineBackgroundColor = registerColor( 'inlineEdit.originalChangedLineBackground', Color.transparent, - '', + localize('inlineEdit.originalChangedLineBackground', 'Background color for the changed lines in the original text of inline edits.'), true ); export const originalChangedTextOverlayColor = registerColor( 'inlineEdit.originalChangedTextBackground', diffRemoved, - '', + localize('inlineEdit.originalChangedTextBackground', 'Overlay color for the changed text in the original text of inline edits.'), true ); export const modifiedChangedLineBackgroundColor = registerColor( 'inlineEdit.modifiedChangedLineBackground', Color.transparent, - '', + localize('inlineEdit.modifiedChangedLineBackground', 'Background color for the changed lines in the modified text of inline edits.'), true ); export const modifiedChangedTextOverlayColor = registerColor( 'inlineEdit.modifiedChangedTextBackground', diffInserted, - '', + localize('inlineEdit.modifiedChangedTextBackground', 'Overlay color for the changed text in the modified text of inline edits.'), true ); - export const originalBorder = registerColor( 'inlineEdit.originalBorder', { @@ -82,7 +82,7 @@ export const originalBorder = registerColor( hcDark: editorLineHighlightBorder, hcLight: editorLineHighlightBorder }, - '' + localize('inlineEdit.originalBorder', 'Border color for the original text in inline edits.') ); export const modifiedBorder = registerColor( @@ -93,7 +93,7 @@ export const modifiedBorder = registerColor( hcDark: editorLineHighlightBorder, hcLight: editorLineHighlightBorder }, - '' + localize('inlineEdit.modifiedBorder', 'Border color for the modified text in inline edits.') ); export class InlineEditsSideBySideDiff extends Disposable { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index 6e6f213d38ae..19f28075c336 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -19,7 +19,7 @@ import { ILanguageService } from '../../../../../common/languages/language.js'; import { LineTokens } from '../../../../../common/tokens/lineTokens.js'; import { TokenArray } from '../../../../../common/tokens/tokenArray.js'; import { mapOutFalsy, n, rectToProps } from './utils.js'; - +import { localize } from '../../../../../../nls.js'; export const transparentHoverBackground = registerColor( 'inlineEdit.wordReplacementView.background', { @@ -28,7 +28,7 @@ export const transparentHoverBackground = registerColor( hcLight: transparent(editorHoverStatusBarBackground, 0.1), hcDark: transparent(editorHoverStatusBarBackground, 0.1), }, - 'Background color for the inline edit word replacement view.' + localize('inlineEdit.wordReplacementView.background', 'Background color for the inline edit word replacement view.') ); export class WordReplacementView extends Disposable { From 5d5976d10cde46dabc31bf264f637b8f6da413a9 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 20 Dec 2024 11:22:59 -0800 Subject: [PATCH 0321/3587] fix: macos external terminal not opening in darkwin remotes (#236426) Closes #236425 --- build/gulpfile.reh.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index 4f00317173dd..e0df76f1c8f2 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -63,6 +63,9 @@ const serverResourceIncludes = [ 'out-build/vs/base/node/cpuUsage.sh', 'out-build/vs/base/node/ps.sh', + // External Terminal + 'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt', + // Terminal shell integration 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1', 'out-build/vs/workbench/contrib/terminal/common/scripts/CodeTabExpansion.psm1', From 9ee30e50da91f818e60c1c3ccf156f64c097bff1 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 20 Dec 2024 14:05:31 -0600 Subject: [PATCH 0322/3587] fix terminal completion issues with `replacementIndex` (#236728) fix replacement index weirdness + more --- .../suggest/browser/terminalSuggestAddon.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index f750d76f089a..52d7c1b8c37a 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -64,7 +64,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest private _leadingLineContent?: string; private _cursorIndexDelta: number = 0; private _requestedCompletionsIndex: number = 0; - private _providerReplacementIndex: number = 0; private _lastUserData?: string; static lastAcceptedCompletionTimestamp: number = 0; @@ -171,11 +170,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } this._onDidReceiveCompletions.fire(); - // ATM, the two providers calculate the same replacement index / prefix, so we can just take the first one - // TODO: figure out if we can add support for multiple replacement indices - const replacementIndices = [...new Set(providedCompletions.map(c => c.replacementIndex))]; - const replacementIndex = replacementIndices.length === 1 ? replacementIndices[0] : 0; - this._providerReplacementIndex = replacementIndex; this._requestedCompletionsIndex = this._promptInputModel.cursorIndex; this._currentPromptInputState = { @@ -186,7 +180,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest ghostTextIndex: this._promptInputModel.ghostTextIndex }; - this._leadingLineContent = this._currentPromptInputState.prefix.substring(replacementIndex, replacementIndex + this._promptInputModel.cursorIndex + this._cursorIndexDelta); + this._leadingLineContent = this._currentPromptInputState.prefix.substring(0, this._requestedCompletionsIndex + this._cursorIndexDelta); const completions = providedCompletions.flat(); if (!completions?.length) { @@ -339,7 +333,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest if (this._terminalSuggestWidgetVisibleContextKey.get()) { this._cursorIndexDelta = this._currentPromptInputState.cursorIndex - (this._requestedCompletionsIndex); - let normalizedLeadingLineContent = this._currentPromptInputState.value.substring(this._providerReplacementIndex, this._requestedCompletionsIndex + this._cursorIndexDelta); + let normalizedLeadingLineContent = this._currentPromptInputState.value.substring(0, this._requestedCompletionsIndex + this._cursorIndexDelta); if (this._isFilteringDirectories) { normalizedLeadingLineContent = normalizePathSeparator(normalizedLeadingLineContent, this._pathSeparator); } @@ -458,7 +452,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest // The replacement text is any text after the replacement index for the completions, this // includes any text that was there before the completions were requested and any text added // since to refine the completion. - const replacementText = currentPromptInputState.value.substring(suggestion.item.completion.replacementIndex ?? this._providerReplacementIndex, currentPromptInputState.cursorIndex); + const replacementText = currentPromptInputState.value.substring(suggestion.item.completion.replacementIndex, currentPromptInputState.cursorIndex); // Right side of replacement text in the same word let rightSideReplacementText = ''; From 3751af286f68efe9a7ef11315ed0bfb06b8b8c14 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 20 Dec 2024 22:25:17 +0100 Subject: [PATCH 0323/3587] Implements inline edit indicator hover (#236738) --- .../browser/view/inlineCompletionsView.ts | 2 +- .../view/inlineEdits/gutterIndicatorMenu.ts | 152 ++++++++++++ .../view/inlineEdits/gutterIndicatorView.ts | 83 ++++++- .../view/inlineEdits/sideBySideDiff.ts | 4 +- .../browser/view/inlineEdits/utils.ts | 229 +++++++++++------- .../browser/view/inlineEdits/view.css | 21 +- .../browser/view/inlineEdits/view.ts | 4 +- 7 files changed, 395 insertions(+), 100 deletions(-) create mode 100644 src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts index baaf3c08205c..fa303f3fc6e3 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts @@ -48,7 +48,7 @@ export class InlineCompletionsView extends Disposable { constructor( private readonly _editor: ICodeEditor, private readonly _model: IObservable, - @IInstantiationService private readonly _instantiationService: IInstantiationService + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts new file mode 100644 index 000000000000..95168505a402 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { KeybindingLabel, unthemedKeybindingLabelOptions } from '../../../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { ResolvedKeybinding } from '../../../../../../base/common/keybindings.js'; +import { IObservable, autorun, constObservable, derived, derivedWithStore, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; +import { OS } from '../../../../../../base/common/platform.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { localize } from '../../../../../../nls.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; +import { Command } from '../../../../../common/languages.js'; +import { AcceptInlineCompletion, HideInlineCompletion, JumpToNextInlineEdit } from '../../controller/commands.js'; +import { ChildNode, FirstFnArg, LiveElement, n } from './utils.js'; + +export class GutterIndicatorMenuContent { + constructor( + private readonly _selectionOverride: IObservable<'jump' | 'accept' | undefined>, + private readonly _close: (focusEditor: boolean) => void, + private readonly _extensionCommands: IObservable, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @ICommandService private readonly _commandService: ICommandService, + ) { + } + + public toDisposableLiveElement(): LiveElement { + return this._createHoverContent().toDisposableLiveElement(); + } + + private _createHoverContent() { + const activeElement = observableValue('active', undefined); + const activeElementOrDefault = derived(reader => this._selectionOverride.read(reader) ?? activeElement.read(reader)); + + const createOptionArgs = (options: { id: string; title: string; icon: ThemeIcon; commandId: string; commandArgs?: unknown[] }): FirstFnArg => { + return { + title: options.title, + icon: options.icon, + keybinding: this._getKeybinding(options.commandArgs ? undefined : options.commandId), + isActive: activeElementOrDefault.map(v => v === options.id), + onHoverChange: v => activeElement.set(v ? options.id : undefined, undefined), + onAction: () => { + this._close(true); + return this._commandService.executeCommand(options.commandId, ...(options.commandArgs ?? [])); + }, + }; + }; + + // TODO make this menu contributable! + return hoverContent([ + // TODO: make header dynamic, get from extension + header(localize('inlineEdit', "Inline Edit")), + option(createOptionArgs({ id: 'jump', title: localize('jump', "Jump"), icon: Codicon.arrowRight, commandId: new JumpToNextInlineEdit().id })), + option(createOptionArgs({ id: 'accept', title: localize('accept', "Accept"), icon: Codicon.check, commandId: new AcceptInlineCompletion().id })), + option(createOptionArgs({ id: 'reject', title: localize('reject', "Reject"), icon: Codicon.close, commandId: new HideInlineCompletion().id })), + separator(), + this._extensionCommands?.map(c => c && c.length > 0 ? [ + ...c.map(c => option(createOptionArgs({ id: c.id, title: c.title, icon: Codicon.symbolEvent, commandId: c.id, commandArgs: c.arguments }))), + separator() + ] : []), + option(createOptionArgs({ id: 'settings', title: localize('settings', "Settings"), icon: Codicon.gear, commandId: 'workbench.action.openSettings', commandArgs: ['inlineSuggest.edits'] })), + ]); + } + + private _getKeybinding(commandId: string | undefined) { + if (!commandId) { + return constObservable(undefined); + } + return observableFromEvent(this._contextKeyService.onDidChangeContext, () => this._keybindingService.lookupKeybinding(commandId, this._contextKeyService, true)); + } +} + +function hoverContent(content: ChildNode) { + return n.div({ + class: 'content', + style: { + margin: 4, + minWidth: 150, + } + }, content); +} + +function header(title: string) { + return n.div({ + class: 'header', + style: { + color: 'var(--vscode-descriptionForeground)', + fontSize: '12px', + fontWeight: '600', + padding: '0 10px', + lineHeight: 26, + } + }, [title]); +} + +function option(props: { + title: string; + icon: ThemeIcon; + keybinding: IObservable; + isActive?: IObservable; + onHoverChange?: (isHovered: boolean) => void; + onAction?: () => void; +}) { + return derivedWithStore((_reader, store) => n.div({ + class: ['monaco-menu-option', props.isActive?.map(v => v && 'active')], + onmouseenter: () => props.onHoverChange?.(true), + onmouseleave: () => props.onHoverChange?.(false), + onclick: props.onAction, + onkeydown: e => { + if (e.key === 'Enter') { + props.onAction?.(); + } + }, + tabIndex: 0, + }, [ + n.elem('span', { + style: { + fontSize: 16, + display: 'flex', + } + }, [renderIcon(props.icon)]), + n.elem('span', {}, [props.title]), + n.div({ + style: { marginLeft: 'auto', opacity: '0.6' }, + ref: elem => { + const keybindingLabel = store.add(new KeybindingLabel(elem, OS, { disableTitle: true, ...unthemedKeybindingLabelOptions })); + store.add(autorun(reader => { + keybindingLabel.set(props.keybinding.read(reader)); + })); + } + }) + ])); +} + +function separator() { + return n.div({ + class: 'menu-separator', + style: { + color: 'var(--vscode-editorActionList-foreground)', + padding: '2px 0', + } + }, n.div({ + style: { + borderBottom: '1px solid var(--vscode-editorHoverWidget-border)', + } + })); +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index a3d7dcddbc10..e0a1ce126d0e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -6,16 +6,21 @@ import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { IObservable, constObservable, derived, observableFromEvent } from '../../../../../../base/common/observable.js'; +import { IObservable, autorun, constObservable, derived, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; +import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { buttonBackground, buttonForeground, buttonSecondaryBackground, buttonSecondaryForeground } from '../../../../../../platform/theme/common/colorRegistry.js'; import { registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; import { ObservableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { Rect } from '../../../../../browser/rect.js'; +import { HoverService } from '../../../../../browser/services/hoverService/hoverService.js'; +import { HoverWidget } from '../../../../../browser/services/hoverService/hoverWidget.js'; import { EditorOption } from '../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../common/core/lineRange.js'; import { OffsetRange } from '../../../../../common/core/offsetRange.js'; import { StickyScrollController } from '../../../../stickyScroll/browser/stickyScrollController.js'; import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; +import { GutterIndicatorMenuContent } from './gutterIndicatorMenu.js'; import { mapOutFalsy, n, rectToProps } from './utils.js'; import { localize } from '../../../../../../nls.js'; export const inlineEditIndicatorPrimaryForeground = registerColor( @@ -62,12 +67,14 @@ export const inlineEditIndicatorBackground = registerColor( localize('inlineEdit.gutterIndicator.background', 'Background color for the inline edit gutter indicator.') ); - export class InlineEditsGutterIndicator extends Disposable { constructor( private readonly _editorObs: ObservableCodeEditor, private readonly _originalRange: IObservable, private readonly _model: IObservable, + private readonly _shouldShowHover: IObservable, + @IHoverService private readonly _hoverService: HoverService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); @@ -77,6 +84,14 @@ export class InlineEditsGutterIndicator extends Disposable { allowEditorOverflow: false, minContentWidthInPx: constObservable(0), })); + + this._register(autorun(reader => { + if (this._shouldShowHover.read(reader)) { + this._showHover(); + } else { + this._hoverService.hideHover(); + } + })); } private readonly _originalRangeObs = mapOutFalsy(this._originalRange); @@ -132,44 +147,88 @@ export class InlineEditsGutterIndicator extends Disposable { private readonly _tabAction = derived(this, reader => { const m = this._model.read(reader); - if (m && m.tabShouldJumpToInlineEdit.read(reader)) { return 'jump' as const; } - if (m && m.tabShouldAcceptInlineEdit.read(reader)) { return 'accept' as const; } + if (this._editorObs.isFocused.read(reader)) { + if (m && m.tabShouldJumpToInlineEdit.read(reader)) { return 'jump' as const; } + if (m && m.tabShouldAcceptInlineEdit.read(reader)) { return 'accept' as const; } + } return 'inactive' as const; }); private readonly _onClickAction = derived(this, reader => { if (this._layout.map(d => d && d.docked).read(reader)) { return { - label: 'Click to accept inline edit', + selectionOverride: 'accept' as const, action: () => { this._model.get()?.accept(); } }; } else { return { - label: 'Click to jump to inline edit', + selectionOverride: 'jump' as const, action: () => { this._model.get()?.jump(); } }; } }); + private readonly _iconRef = n.ref(); + private _hoverVisible: boolean = false; + private readonly _isHoveredOverIcon = observableValue(this, false); + private readonly _hoverSelectionOverride = derived(this, reader => this._isHoveredOverIcon.read(reader) ? this._onClickAction.read(reader).selectionOverride : undefined); + + private _showHover(): void { + if (this._hoverVisible) { + return; + } + + const content = this._instantiationService.createInstance( + GutterIndicatorMenuContent, + this._hoverSelectionOverride, + (focusEditor) => { + h?.dispose(); + if (focusEditor) { + this._editorObs.editor.focus(); + } + }, + this._model.map((m, r) => m?.state.read(r)?.inlineCompletion?.inlineCompletion.source.inlineCompletions.commands), + ).toDisposableLiveElement(); + const h = this._hoverService.showHover({ + target: this._iconRef.element, + content: content.element, + }) as HoverWidget | undefined; + if (h) { + this._hoverVisible = true; + h.onDispose(() => { + content.dispose(); + this._hoverVisible = false; + }); + } else { + content.dispose(); + } + } + private readonly _indicator = n.div({ class: 'inline-edits-view-gutter-indicator', onclick: () => this._onClickAction.get().action(), - title: this._onClickAction.map(a => a.label), style: { position: 'absolute', overflow: 'visible', }, - }, mapOutFalsy(this._layout).map(l => !l ? [] : [ + }, mapOutFalsy(this._layout).map(layout => !layout ? [] : [ n.div({ style: { position: 'absolute', background: 'var(--vscode-inlineEdit-gutterIndicator-background)', borderRadius: '4px', - ...rectToProps(reader => l.read(reader).rect), + ...rectToProps(reader => layout.read(reader).rect), } }), n.div({ class: 'icon', + ref: this._iconRef, + onmouseenter: () => { + // TODO show hover when hovering ghost text etc. + this._isHoveredOverIcon.set(true, undefined); + this._showHover(); + }, + onmouseleave: () => { this._isHoveredOverIcon.set(false, undefined); }, style: { cursor: 'pointer', zIndex: '1000', @@ -192,12 +251,12 @@ export class InlineEditsGutterIndicator extends Disposable { display: 'flex', justifyContent: 'center', transition: 'background-color 0.2s ease-in-out', - ...rectToProps(reader => l.read(reader).iconRect), + ...rectToProps(reader => layout.read(reader).iconRect), } }, [ n.div({ style: { - rotate: l.map(l => { + rotate: layout.map(l => { switch (l.arrowDirection) { case 'right': return '0deg'; case 'bottom': return '90deg'; @@ -207,7 +266,7 @@ export class InlineEditsGutterIndicator extends Disposable { transition: 'rotate 0.2s ease-in-out', } }, [ - renderIcon(Codicon.arrowRight), + renderIcon(Codicon.arrowRight) ]) ]), ])).keepUpdated(this._store); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index 7198e4870e92..e1feaa2eb8b7 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -180,13 +180,15 @@ export class InlineEditsSideBySideDiff extends Disposable { private readonly _editorContainerTopLeft = observableValue | undefined>(this, undefined); private readonly _editorContainer = n.div({ - class: 'editorContainer', + class: ['editorContainer', this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !v.edits.experimental.useGutterIndicator && 'showHover')], style: { position: 'absolute' }, }, [ n.div({ class: 'preview', style: {}, ref: this.previewRef }), n.div({ class: 'toolbar', style: {}, ref: this.toolbarRef }), ]).keepUpdated(this._store); + public readonly isHovered = this._editorContainer.getIsHovered(this._store); + protected readonly _toolbar = this._register(this._instantiationService.createInstance(CustomizedMenuWorkbenchToolBar, this.toolbarRef.element, MenuId.InlineEditsActions, { menuOptions: { renderShortTitle: true }, toolbarOptions: { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts index a70a0721491a..57e0eb3393e0 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getDomNodePagePosition, h, isSVGElement } from '../../../../../../base/browser/dom.js'; +import { addDisposableListener, getDomNodePagePosition, h, isSVGElement } from '../../../../../../base/browser/dom.js'; import { KeybindingLabel, unthemedKeybindingLabelOptions } from '../../../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; import { numberComparator } from '../../../../../../base/common/arrays.js'; import { findFirstMin } from '../../../../../../base/common/arraysFind.js'; import { BugIndicatingError } from '../../../../../../base/common/errors.js'; -import { Disposable, DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { DisposableStore, IDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { derived, derivedObservableWithCache, IObservable, IReader, observableValue, transaction } from '../../../../../../base/common/observable.js'; import { OS } from '../../../../../../base/common/platform.js'; import { getIndentationLength, splitLines } from '../../../../../../base/common/strings.js'; @@ -164,7 +164,7 @@ export class PathBuilder { } type Value = T | IObservable; -type ValueOrList = Value | Value[]; +type ValueOrList = Value | ValueOrList[]; type ValueOrList2 = ValueOrList | ValueOrList>; type Element = HTMLElement | SVGElement; @@ -203,16 +203,18 @@ type SVGElementTagNameMap2 = { type DomTagCreateFn> = ( tag: TTag, - attributes: ElementAttributeKeys & { class?: Value; ref?: IRef }, - children?: ValueOrList2, + attributes: ElementAttributeKeys & { class?: ValueOrList; ref?: IRef }, + children?: ChildNode, ) => ObserverNode; type DomCreateFn = ( - attributes: ElementAttributeKeys & { class?: Value; ref?: IRef }, - children?: ValueOrList2, + attributes: ElementAttributeKeys & { class?: ValueOrList; ref?: IRef }, + children?: ChildNode, ) => ObserverNode; +export type ChildNode = ValueOrList2; + export namespace n { function nodeNs>(elementNs: string | undefined = undefined): DomTagCreateFn { return (tag, attributes, children) => { @@ -233,35 +235,37 @@ export namespace n { } export const div: DomCreateFn = node('div'); + + export const elem = nodeNs(undefined); + export const svg: DomCreateFn = node('svg', 'http://www.w3.org/2000/svg'); export const svgElem = nodeNs('http://www.w3.org/2000/svg'); - export function ref(): Ref { - return new Ref(); + export function ref(): IRefWithVal { + let value: T | undefined = undefined; + const result: IRef = function (val: T) { + value = val; + }; + Object.defineProperty(result, 'element', { + get() { + if (!value) { + throw new BugIndicatingError('Make sure the ref is set before accessing the element. Maybe wrong initialization order?'); + } + return value; + } + }); + return result as any; } } -export interface IRef { - setValue(value: T): void; -} - -export class Ref implements IRef { - private _value: T | undefined = undefined; - - public setValue(value: T): void { - this._value = value; - } +export type IRef = (value: T) => void; - public get element(): T { - if (!this._value) { - throw new BugIndicatingError('Make sure the ref is set before accessing the element. Maybe wrong initialization order?'); - } - return this._value; - } +export interface IRefWithVal extends IRef { + readonly element: T; } -export abstract class ObserverNode extends Disposable { +export abstract class ObserverNode { private readonly _deriveds: (IObservable)[] = []; protected readonly _element: T; @@ -270,48 +274,23 @@ export abstract class ObserverNode extends Disposab tag: string, ref: IRef | undefined, ns: string | undefined, - className: Value | undefined, + className: ValueOrList | undefined, attributes: ElementAttributeKeys, - children: ValueOrList2 | undefined, + children: ChildNode, ) { - super(); - this._element = (ns ? document.createElementNS(ns, tag) : document.createElement(tag)) as unknown as T; if (ref) { - ref.setValue(this._element); - } - - function setClassName(domNode: Element, className: string | string[]) { - if (isSVGElement(domNode)) { - if (Array.isArray(className)) { - domNode.setAttribute('class', className.join(' ')); - } else { - domNode.setAttribute('class', className); - } - } else { - if (Array.isArray(className)) { - domNode.className = className.join(' '); - } else { - domNode.className = className; - } - } + ref(this._element); } if (className) { - if (isObservable(className)) { + if (hasObservable(className)) { this._deriveds.push(derived(this, reader => { - setClassName(this._element, className.read(reader)); + setClassName(this._element, getClassName(className, reader)); })); } else { - setClassName(this._element, className); - } - } - - function convertCssValue(value: any): string { - if (typeof value === 'number') { - return value + 'px'; + setClassName(this._element, getClassName(className, undefined)); } - return value; } for (const [key, value] of Object.entries(attributes)) { @@ -347,36 +326,26 @@ export abstract class ObserverNode extends Disposab } } - function getChildren(reader: IReader | undefined, children: ValueOrList2): (Element | string)[] { - if (isObservable(children)) { - return getChildren(reader, children.read(reader)); - } - if (Array.isArray(children)) { - return children.flatMap(c => getChildren(reader, c)); - } - if (children instanceof ObserverNode) { - if (reader) { - children.readEffect(reader); + if (children) { + function getChildren(reader: IReader | undefined, children: ValueOrList2): (Element | string)[] { + if (isObservable(children)) { + return getChildren(reader, children.read(reader)); } - return [children._element]; - } - if (children) { - return [children]; - } - return []; - } - - function childrenIsObservable(children: ValueOrList2): boolean { - if (isObservable(children)) { - return true; - } - if (Array.isArray(children)) { - return children.some(c => childrenIsObservable(c)); + if (Array.isArray(children)) { + return children.flatMap(c => getChildren(reader, c)); + } + if (children instanceof ObserverNode) { + if (reader) { + children.readEffect(reader); + } + return [children._element]; + } + if (children) { + return [children]; + } + return []; } - return false; - } - if (children) { const d = derived(this, reader => { this._element.replaceChildren(...getChildren(reader, children)); }); @@ -399,12 +368,102 @@ export abstract class ObserverNode extends Disposab }).recomputeInitiallyAndOnChange(store); return this as unknown as ObserverNodeWithElement; } + + /** + * Creates a live element that will keep the element updated as long as the returned object is not disposed. + */ + toDisposableLiveElement() { + const store = new DisposableStore(); + this.keepUpdated(store); + return new LiveElement(this._element, store); + } +} + + + +function setClassName(domNode: Element, className: string) { + if (isSVGElement(domNode)) { + domNode.setAttribute('class', className); + } else { + domNode.className = className; + } +} + +function resolve(value: ValueOrList, reader: IReader | undefined, cb: (val: T) => void): void { + if (isObservable(value)) { + cb(value.read(reader)); + } + if (Array.isArray(value)) { + for (const v of value) { + resolve(v, reader, cb); + } + } + cb(value as any); +} + +function getClassName(className: ValueOrList | undefined, reader: IReader | undefined): string { + let result = ''; + resolve(className, reader, val => { + if (val) { + if (result.length === 0) { + result = val; + } else { + result += ' ' + val; + } + } + }); + return result; +} + +function hasObservable(value: ValueOrList): boolean { + if (isObservable(value)) { + return true; + } + if (Array.isArray(value)) { + return value.some(v => hasObservable(v)); + } + return false; +} +function convertCssValue(value: any): string { + if (typeof value === 'number') { + return value + 'px'; + } + return value; +} + + +function childrenIsObservable(children: ValueOrList2): boolean { + if (isObservable(children)) { + return true; + } + if (Array.isArray(children)) { + return children.some(c => childrenIsObservable(c)); + } + return false; +} + +export class LiveElement { + constructor( + public readonly element: T, + private readonly _disposable: IDisposable, + ) { } + + dispose() { + this._disposable.dispose(); + } } export class ObserverNodeWithElement extends ObserverNode { public get element() { return this._element; } + + public getIsHovered(store: DisposableStore): IObservable { + const hovered = observableValue('hovered', false); + store.add(addDisposableListener(this._element, 'mouseenter', () => hovered.set(true, undefined))); + store.add(addDisposableListener(this._element, 'mouseleave', () => hovered.set(false, undefined))); + return hovered; + } } function setOrRemoveAttribute(element: Element, key: string, value: unknown) { @@ -479,3 +538,5 @@ export function rectToProps(fn: (reader: IReader) => Rect) { height: derived(reader => fn(reader).bottom - fn(reader).top), }; } + +export type FirstFnArg = T extends (arg: infer U) => any ? U : never; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index e5531dc7b4b7..0a46a1490424 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -77,7 +77,7 @@ } .inline-edits-view { - &.toolbarDropdownVisible, .editorContainer:hover { + &.toolbarDropdownVisible, .editorContainer.showHover:hover { .toolbar { display: block; } @@ -165,3 +165,22 @@ border-left: solid var(--vscode-inlineEdit-modifiedChangedTextBackground) 3px; } } + +.monaco-menu-option { + color: var(--vscode-editorActionList-foreground); + font-size: 13px; + padding: 0 10px; + line-height: 26px; + display: flex; + gap: 8px; + align-items: center; + border-radius: 4px; + cursor: pointer; + + &.active { + background: var(--vscode-editorActionList-focusBackground); + color: var(--vscode-editorActionList-focusForeground); + outline: 1px solid var(--vscode-menu-selectionBorder, transparent); + outline-offset: -1px; + } +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index dfb11c8bebf2..4282852af531 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -137,10 +137,12 @@ export class InlineEditsView extends Disposable { protected readonly _indicator = this._register(autorunWithStore((reader, store) => { if (this._useGutterIndicator.read(reader)) { - store.add(new InlineEditsGutterIndicator( + store.add(this._instantiationService.createInstance( + InlineEditsGutterIndicator, this._editorObs, this._uiState.map(s => s && s.originalDisplayRange), this._model, + this._sideBySide.isHovered, )); } else { store.add(new InlineEditsIndicator( From 93148e5b285efb8a2713ad64b45ab50ad29e2d84 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 20 Dec 2024 13:48:51 -0800 Subject: [PATCH 0324/3587] Fix extension and skipLibCheck for now --- src/bootstrap-node.ts | 2 +- src/tsconfig.tsec.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bootstrap-node.ts b/src/bootstrap-node.ts index 0fcd81f9a623..1f4b3a2f6a60 100644 --- a/src/bootstrap-node.ts +++ b/src/bootstrap-node.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import * as fs from 'fs'; import { fileURLToPath } from 'url'; import { createRequire } from 'node:module'; -import type { IProductConfiguration } from './vs/base/common/product.ts'; +import type { IProductConfiguration } from './vs/base/common/product.js'; const require = createRequire(import.meta.url); const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/src/tsconfig.tsec.json b/src/tsconfig.tsec.json index d822b0a4e897..64550458ab70 100644 --- a/src/tsconfig.tsec.json +++ b/src/tsconfig.tsec.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "noEmit": true, + "skipLibCheck": true, "plugins": [ { "name": "tsec", From 0c51035590b20878d940ceb3cac0d54c4eb54bbc Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 20 Dec 2024 13:59:08 -0800 Subject: [PATCH 0325/3587] testing: fix errors peeks being oversized relative to their contents (#236763) Fixes #214011 --- src/vs/base/browser/ui/list/listView.ts | 2 + .../contrib/zoneWidget/browser/zoneWidget.ts | 14 ++-- .../contrib/debug/browser/callStackWidget.ts | 8 +++ .../testResultsView/testMessageStack.ts | 49 ------------- .../testResultsView/testResultsViewContent.ts | 37 +++++++--- .../testing/browser/testingOutputPeek.ts | 70 +++++++++++-------- 6 files changed, 85 insertions(+), 95 deletions(-) delete mode 100644 src/vs/workbench/contrib/testing/browser/testResultsView/testMessageStack.ts diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 36355a377ba5..e305b7de4b9a 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -567,6 +567,8 @@ export class ListView implements IListView { if (this.supportDynamicHeights) { this._rerender(this.lastRenderTop, this.lastRenderHeight); + } else { + this._onDidChangeContentHeight.fire(this.contentHeight); // otherwise fired in _rerender() } } diff --git a/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts b/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts index 51988af7d173..a38f1639eea6 100644 --- a/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts +++ b/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts @@ -29,7 +29,6 @@ export interface IOptions { frameColor?: Color | string; arrowColor?: Color; keepEditorSelection?: boolean; - allowUnlimitedHeight?: boolean; ordinal?: number; showInHiddenAreas?: boolean; } @@ -376,6 +375,11 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { return result; } + /** Gets the maximum widget height in lines. */ + protected _getMaximumHeightInLines(): number | undefined { + return Math.max(12, (this.editor.getLayoutInfo().height / this.editor.getOption(EditorOption.lineHeight)) * 0.8); + } + private _showImpl(where: Range, heightInLines: number): void { const position = where.getStartPosition(); const layoutInfo = this.editor.getLayoutInfo(); @@ -389,8 +393,8 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { const lineHeight = this.editor.getOption(EditorOption.lineHeight); // adjust heightInLines to viewport - if (!this.options.allowUnlimitedHeight) { - const maxHeightInLines = Math.max(12, (this.editor.getLayoutInfo().height / lineHeight) * 0.8); + const maxHeightInLines = this._getMaximumHeightInLines(); + if (maxHeightInLines !== undefined) { heightInLines = Math.min(heightInLines, maxHeightInLines); } @@ -493,7 +497,9 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { // implement in subclass } - protected _relayout(newHeightInLines: number): void { + protected _relayout(_newHeightInLines: number): void { + const maxHeightInLines = this._getMaximumHeightInLines(); + const newHeightInLines = maxHeightInLines === undefined ? _newHeightInLines : Math.min(maxHeightInLines, _newHeightInLines); if (this._viewZone && this._viewZone.heightInLines !== newHeightInLines) { this.editor.changeViewZones(accessor => { if (this._viewZone) { diff --git a/src/vs/workbench/contrib/debug/browser/callStackWidget.ts b/src/vs/workbench/contrib/debug/browser/callStackWidget.ts index dc2c12dd7306..2948bbb715b0 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackWidget.ts @@ -119,10 +119,18 @@ export class CallStackWidget extends Disposable { private readonly currentFramesDs = this._register(new DisposableStore()); private cts?: CancellationTokenSource; + public get onDidChangeContentHeight() { + return this.list.onDidChangeContentHeight; + } + public get onDidScroll() { return this.list.onDidScroll; } + public get contentHeight() { + return this.list.contentHeight; + } + constructor( container: HTMLElement, containingEditor: ICodeEditor | undefined, diff --git a/src/vs/workbench/contrib/testing/browser/testResultsView/testMessageStack.ts b/src/vs/workbench/contrib/testing/browser/testResultsView/testMessageStack.ts deleted file mode 100644 index fe0592cb567f..000000000000 --- a/src/vs/workbench/contrib/testing/browser/testResultsView/testMessageStack.ts +++ /dev/null @@ -1,49 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { AnyStackFrame, CallStackFrame, CallStackWidget } from '../../../debug/browser/callStackWidget.js'; -import { ITestMessageStackFrame } from '../../common/testTypes.js'; - -export class TestResultStackWidget extends Disposable { - private readonly widget: CallStackWidget; - - public get onDidScroll() { - return this.widget.onDidScroll; - } - - constructor( - private readonly container: HTMLElement, - containingEditor: ICodeEditor | undefined, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - - this.widget = this._register(instantiationService.createInstance( - CallStackWidget, - container, - containingEditor, - )); - } - - public collapseAll() { - this.widget.collapseAll(); - } - - public update(messageFrame: AnyStackFrame, stack: ITestMessageStackFrame[]) { - this.widget.setFrames([messageFrame, ...stack.map(frame => new CallStackFrame( - frame.label, - frame.uri, - frame.position?.lineNumber, - frame.position?.column, - ))]); - } - - public layout(height?: number, width?: number) { - this.widget.layout(height ?? this.container.clientHeight, width); - } -} diff --git a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts index 421afad78f20..5f2990a413a4 100644 --- a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts +++ b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts @@ -14,7 +14,6 @@ import { Emitter, Event, Relay } from '../../../../../base/common/event.js'; import { KeyCode } from '../../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { observableValue } from '../../../../../base/common/observable.js'; -import './testResultsViewContent.css'; import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { localize } from '../../../../../nls.js'; @@ -28,12 +27,7 @@ import { IInstantiationService, ServicesAccessor } from '../../../../../platform import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js'; -import { CustomStackFrame } from '../../../debug/browser/callStackWidget.js'; -import * as icons from '../icons.js'; -import { TestResultStackWidget } from './testMessageStack.js'; -import { DiffContentProvider, IPeekOutputRenderer, MarkdownTestMessagePeek, PlainTextMessagePeek, TerminalMessagePeek } from './testResultsOutput.js'; -import { equalsSubject, getSubjectTestItem, InspectSubject, MessageSubject, TaskSubject, TestOutputSubject } from './testResultsSubject.js'; -import { OutputPeekTree } from './testResultsTree.js'; +import { AnyStackFrame, CallStackFrame, CallStackWidget, CustomStackFrame } from '../../../debug/browser/callStackWidget.js'; import { TestCommandId } from '../../common/constants.js'; import { IObservableValue } from '../../common/observableValue.js'; import { capabilityContextKeys, ITestProfileService } from '../../common/testProfileService.js'; @@ -41,6 +35,11 @@ import { LiveTestResult } from '../../common/testResult.js'; import { ITestFollowup, ITestService } from '../../common/testService.js'; import { ITestMessageStackFrame, TestRunProfileBitset } from '../../common/testTypes.js'; import { TestingContextKeys } from '../../common/testingContextKeys.js'; +import * as icons from '../icons.js'; +import { DiffContentProvider, IPeekOutputRenderer, MarkdownTestMessagePeek, PlainTextMessagePeek, TerminalMessagePeek } from './testResultsOutput.js'; +import { equalsSubject, getSubjectTestItem, InspectSubject, MessageSubject, TaskSubject, TestOutputSubject } from './testResultsSubject.js'; +import { OutputPeekTree } from './testResultsTree.js'; +import './testResultsViewContent.css'; const enum SubView { Diff = 0, @@ -183,7 +182,7 @@ export class TestResultsViewContent extends Disposable { private contextKeyTestMessage!: IContextKey; private contextKeyResultOutdated!: IContextKey; private stackContainer!: HTMLElement; - private callStackWidget!: TestResultStackWidget; + private callStackWidget!: CallStackWidget; private currentTopFrame?: MessageStackFrame; private isDoingLayoutUpdate?: boolean; @@ -209,6 +208,14 @@ export class TestResultsViewContent extends Disposable { }; } + public get onDidChangeContentHeight() { + return this.callStackWidget.onDidChangeContentHeight; + } + + public get contentHeight() { + return this.callStackWidget?.contentHeight || 0; + } + constructor( private readonly editor: ICodeEditor | undefined, private readonly options: { @@ -233,7 +240,7 @@ export class TestResultsViewContent extends Disposable { const messageContainer = this.messageContainer = dom.$('.test-output-peek-message-container'); this.stackContainer = dom.append(containerElement, dom.$('.test-output-call-stack-container')); - this.callStackWidget = this._register(this.instantiationService.createInstance(TestResultStackWidget, this.stackContainer, this.editor)); + this.callStackWidget = this._register(this.instantiationService.createInstance(CallStackWidget, this.stackContainer, this.editor)); this.followupWidget = this._register(this.instantiationService.createInstance(FollowupActionWidget, this.editor)); this.onCloseEmitter.input = this.followupWidget.onClose; @@ -315,13 +322,22 @@ export class TestResultsViewContent extends Disposable { this.currentSubjectStore.clear(); const callFrames = this.getCallFrames(opts.subject) || []; const topFrame = await this.prepareTopFrame(opts.subject, callFrames); - this.callStackWidget.update(topFrame, callFrames); + this.setCallStackFrames(topFrame, callFrames); this.followupWidget.show(opts.subject); this.populateFloatingClick(opts.subject); }); } + private setCallStackFrames(messageFrame: AnyStackFrame, stack: ITestMessageStackFrame[]) { + this.callStackWidget.setFrames([messageFrame, ...stack.map(frame => new CallStackFrame( + frame.label, + frame.uri, + frame.position?.lineNumber, + frame.position?.column, + ))]); + } + /** * Collapses all displayed stack frames. */ @@ -375,7 +391,6 @@ export class TestResultsViewContent extends Disposable { })); } - if (provider.onDidContentSizeChange) { this.currentSubjectStore.add(provider.onDidContentSizeChange(() => { if (this.dimension && !this.isDoingLayoutUpdate) { diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 3ea9c37de5ca..808e68a6ca9a 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -6,16 +6,16 @@ import * as dom from '../../../../base/browser/dom.js'; import { alert } from '../../../../base/browser/ui/aria/aria.js'; import { IAction } from '../../../../base/common/actions.js'; +import { RunOnceScheduler } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Color } from '../../../../base/common/color.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; +import { Event } from '../../../../base/common/event.js'; import { stripIcons } from '../../../../base/common/iconLabels.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { derived, disposableObservableValue, observableValue } from '../../../../base/common/observable.js'; -import { count } from '../../../../base/common/strings.js'; import { URI } from '../../../../base/common/uri.js'; import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { EditorAction2 } from '../../../../editor/browser/editorExtensions.js'; @@ -678,10 +678,8 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo class TestResultsPeek extends PeekViewWidget { - private static lastHeightInLines?: number; - - private readonly visibilityChange = this._disposables.add(new Emitter()); public readonly current = observableValue('testPeekCurrent', undefined); + private resizeOnNextContentHeightUpdate = false; private content!: TestResultsViewContent; private scopedContextKeyService!: IContextKeyService; private dimension?: dom.Dimension; @@ -701,10 +699,22 @@ class TestResultsPeek extends PeekViewWidget { super(editor, { showFrame: true, frameWidth: 1, showArrow: true, isResizeable: true, isAccessible: true, className: 'test-output-peek' }, instantiationService); this._disposables.add(themeService.onDidColorThemeChange(this.applyTheme, this)); - this._disposables.add(this.onDidClose(() => this.visibilityChange.fire(false))); peekViewService.addExclusiveWidget(editor, this); } + protected override _getMaximumHeightInLines(): number | undefined { + const defaultMaxHeight = super._getMaximumHeightInLines(); + const contentHeight = this.content?.contentHeight; + if (!contentHeight) { // undefined or 0 + return defaultMaxHeight; + } + + const lineHeight = this.editor.getOption(EditorOption.lineHeight); + // 41 is experimentally determined to be the overhead of the peek view itself + // to avoid showing scrollbars by default in its content. + return Math.min(defaultMaxHeight || Infinity, (contentHeight + 41) / lineHeight); + } + private applyTheme() { const theme = this.themeService.getColorTheme(); const current = this.current.get(); @@ -764,6 +774,27 @@ class TestResultsPeek extends PeekViewWidget { protected override _fillBody(containerElement: HTMLElement): void { this.content.fillBody(containerElement); + + // Resize on height updates for a short time to allow any heights made + // by editor contributions to come into effect before. + const contentHeightSettleTimer = this._disposables.add(new RunOnceScheduler(() => { + this.resizeOnNextContentHeightUpdate = false; + }, 500)); + + this._disposables.add(this.content.onDidChangeContentHeight(height => { + if (!this.resizeOnNextContentHeightUpdate || !height) { + return; + } + + const displayed = this._getMaximumHeightInLines(); + if (displayed) { + this._relayout(Math.min(displayed, this.getVisibleEditorLines() / 2)); + if (!contentHeightSettleTimer.isScheduled()) { + contentHeightSettleTimer.schedule(); + } + } + })); + this._disposables.add(this.content.onDidRequestReveal(sub => { TestingOutputPeekController.get(this.editor)?.show(sub instanceof MessageSubject ? sub.messageUri @@ -780,7 +811,6 @@ class TestResultsPeek extends PeekViewWidget { return this.showInPlace(subject); } - const message = subject.message; const previous = this.current; const revealLocation = subject.revealLocation?.range.getStartPosition(); if (!revealLocation && !previous) { @@ -792,13 +822,8 @@ class TestResultsPeek extends PeekViewWidget { return this.showInPlace(subject); } - // If there is a stack we want to display, ensure the default size is large-ish - const peekLines = TestResultsPeek.lastHeightInLines || Math.max( - inspectSubjectHasStack(subject) ? Math.ceil(this.getVisibleEditorLines() / 2) : 0, - hintMessagePeekHeight(message) - ); - - this.show(revealLocation, peekLines); + this.resizeOnNextContentHeightUpdate = true; + this.show(revealLocation, 10); // 10 is just a random number, we resize once content is available this.editor.revealRangeNearTopIfOutsideViewport(Range.fromPositions(revealLocation), ScrollType.Smooth); return this.showInPlace(subject); @@ -832,11 +857,6 @@ class TestResultsPeek extends PeekViewWidget { await this.content.reveal({ subject, preserveFocus: false }); } - protected override _relayout(newHeightInLines: number): void { - super._relayout(newHeightInLines); - TestResultsPeek.lastHeightInLines = newHeightInLines; - } - /** @override */ protected override _doLayoutBody(height: number, width: number) { super._doLayoutBody(height, width); @@ -919,23 +939,11 @@ export class TestResultsView extends ViewPane { } } -const hintMessagePeekHeight = (msg: ITestMessage) => { - const msgHeight = ITestMessage.isDiffable(msg) - ? Math.max(hintPeekStrHeight(msg.actual), hintPeekStrHeight(msg.expected)) - : hintPeekStrHeight(typeof msg.message === 'string' ? msg.message : msg.message.value); - - // add 8ish lines for the size of the title and decorations in the peek. - return msgHeight + 8; -}; - const firstLine = (str: string) => { const index = str.indexOf('\n'); return index === -1 ? str : str.slice(0, index); }; - -const hintPeekStrHeight = (str: string) => Math.min(count(str, '\n'), 24); - function getOuterEditorFromDiffEditor(codeEditorService: ICodeEditorService): ICodeEditor | null { const diffEditors = codeEditorService.listDiffEditors(); From 13d2a615cb98a1dc3371dcea2f03d8e65b2d83e6 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 20 Dec 2024 14:33:52 -0800 Subject: [PATCH 0326/3587] fix: don't register Cloud Changes action if not configured in product.json (#236764) --- .../contrib/editSessions/browser/editSessionsStorageService.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts index b056dbf64a27..1bb5eb337370 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts @@ -455,6 +455,9 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes } private registerSignInAction() { + if (!this.serverConfiguration?.url) { + return; + } const that = this; const id = 'workbench.editSessions.actions.signIn'; const when = ContextKeyExpr.and(ContextKeyExpr.equals(EDIT_SESSIONS_PENDING_KEY, false), ContextKeyExpr.equals(EDIT_SESSIONS_SIGNED_IN_KEY, false)); From 68252d3d7b25bc50c67abad5f32c885012deb985 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 20 Dec 2024 20:22:20 -0800 Subject: [PATCH 0327/3587] fix: don't leak listener in share contribution (#236767) --- .../contrib/share/browser/share.contribution.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/share/browser/share.contribution.ts b/src/vs/workbench/contrib/share/browser/share.contribution.ts index cd1cfc176e01..3566792aa9d5 100644 --- a/src/vs/workbench/contrib/share/browser/share.contribution.ts +++ b/src/vs/workbench/contrib/share/browser/share.contribution.ts @@ -32,7 +32,7 @@ import { IProgressService, ProgressLocation } from '../../../../platform/progres import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; import { workbenchConfigurationNodeBase } from '../../../common/configuration.js'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; const targetMenus = [ MenuId.EditorContextShare, @@ -44,7 +44,7 @@ const targetMenus = [ MenuId.ExplorerContextShare ]; -class ShareWorkbenchContribution { +class ShareWorkbenchContribution extends Disposable { private static SHARE_ENABLED_SETTING = 'workbench.experimental.share.enabled'; private _disposables: DisposableStore | undefined; @@ -53,10 +53,12 @@ class ShareWorkbenchContribution { @IShareService private readonly shareService: IShareService, @IConfigurationService private readonly configurationService: IConfigurationService ) { + super(); + if (this.configurationService.getValue(ShareWorkbenchContribution.SHARE_ENABLED_SETTING)) { this.registerActions(); } - this.configurationService.onDidChangeConfiguration(e => { + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(ShareWorkbenchContribution.SHARE_ENABLED_SETTING)) { const settingValue = this.configurationService.getValue(ShareWorkbenchContribution.SHARE_ENABLED_SETTING); if (settingValue === true && this._disposables === undefined) { @@ -66,7 +68,12 @@ class ShareWorkbenchContribution { this._disposables = undefined; } } - }); + })); + } + + override dispose(): void { + super.dispose(); + this._disposables?.dispose(); } private registerActions() { From d4de5ceba46823bafe63733df924e63990afef72 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 20 Dec 2024 20:23:36 -0800 Subject: [PATCH 0328/3587] test: adopt `ensureNoDisposablesAreLeakedInTestSuite` (#236766) * test: adopt `ensureNoDisposablesAreLeakedInTestSuite` * test: adopt `ensureNoDisposablesAreLeakedInTestSuite` --- eslint.config.js | 1 - .../editSessions/test/browser/editSessions.test.ts | 9 +++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/eslint.config.js b/eslint.config.js index aa9fd4930c51..db83d096edde 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -229,7 +229,6 @@ export default tseslint.config( 'src/vs/workbench/api/test/node/extHostTunnelService.test.ts', 'src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts', 'src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts', - 'src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts', 'src/vs/workbench/contrib/extensions/test/common/extensionQuery.test.ts', 'src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts', 'src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts', diff --git a/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts index 38aeb6c161b9..359c7be57d51 100644 --- a/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts +++ b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts @@ -50,11 +50,13 @@ import { TestStorageService } from '../../../../test/common/workbenchTestService import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js'; import { UriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentityService.js'; import { IWorkspaceIdentityService, WorkspaceIdentityService } from '../../../../services/workspaces/common/workspaceIdentityService.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; const folderName = 'test-folder'; const folderUri = URI.file(`/${folderName}`); suite('Edit session sync', () => { + let instantiationService: TestInstantiationService; let editSessionsContribution: EditSessionsContribution; let fileService: FileService; @@ -63,6 +65,7 @@ suite('Edit session sync', () => { const disposables = new DisposableStore(); suiteSetup(() => { + sandbox = sinon.createSandbox(); instantiationService = new TestInstantiationService(); @@ -172,6 +175,10 @@ suite('Edit session sync', () => { disposables.clear(); }); + suiteTeardown(() => { + disposables.dispose(); + }); + test('Can apply edit session', async function () { const fileUri = joinPath(folderUri, 'dir1', 'README.md'); const fileContents = '# readme'; @@ -218,4 +225,6 @@ suite('Edit session sync', () => { // Verify that we did not attempt to write the edit session assert.equal(writeStub.called, false); }); + + ensureNoDisposablesAreLeakedInTestSuite(); }); From d6a59b79698b81cc1f0a74113189b7106453bb78 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Sat, 21 Dec 2024 12:59:52 -0800 Subject: [PATCH 0329/3587] debug: fix mismatched indentation for folders in loaded scripts (#236750) * debug: fix mismatched indentation for folders in loaded scripts Fixes #228241 * fix test --- .../contrib/debug/browser/baseDebugView.ts | 2 +- .../debug/browser/loadedScriptsView.ts | 78 ++++++++++--------- .../debug/test/browser/baseDebugView.test.ts | 2 +- 3 files changed, 44 insertions(+), 38 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index 9576b7e5bb78..d5ba5f0c8561 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -51,7 +51,7 @@ export interface IVariableTemplateData { export function renderViewTree(container: HTMLElement): HTMLElement { const treeContainer = $('.'); - treeContainer.classList.add('debug-view-content'); + treeContainer.classList.add('debug-view-content', 'file-icon-themable-tree'); container.appendChild(treeContainer); return treeContainer; } diff --git a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts index 3c6e37f7bc0f..4876cc3a7977 100644 --- a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts @@ -3,47 +3,46 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from '../../../../nls.js'; -import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; -import { normalize, isAbsolute, posix } from '../../../../base/common/path.js'; -import { ViewPane, ViewAction } from '../../../browser/parts/views/viewPane.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { renderViewTree } from './baseDebugView.js'; -import { IDebugSession, IDebugService, CONTEXT_LOADED_SCRIPTS_ITEM_TYPE, LOADED_SCRIPTS_VIEW_ID } from '../common/debug.js'; -import { Source } from '../common/debugSource.js'; -import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js'; -import { IContextKey, IContextKeyService, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { normalizeDriveLetter, tildify } from '../../../../base/common/labels.js'; -import { isWindows } from '../../../../base/common/platform.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ltrim } from '../../../../base/common/strings.js'; -import { RunOnceScheduler } from '../../../../base/common/async.js'; -import { ResourceLabels, IResourceLabelProps, IResourceLabelOptions, IResourceLabel } from '../../../browser/labels.js'; -import { FileKind } from '../../../../platform/files/common/files.js'; import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; -import { ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, ITreeElement } from '../../../../base/browser/ui/tree/tree.js'; import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { WorkbenchCompressibleObjectTree } from '../../../../platform/list/browser/listService.js'; -import { dispose } from '../../../../base/common/lifecycle.js'; -import { createMatches, FuzzyScore } from '../../../../base/common/filters.js'; -import { DebugContentProvider } from '../common/debugContentProvider.js'; -import { ILabelService } from '../../../../platform/label/common/label.js'; +import { TreeFindMode } from '../../../../base/browser/ui/tree/abstractTree.js'; import type { ICompressedTreeNode } from '../../../../base/browser/ui/tree/compressedObjectTreeModel.js'; import type { ICompressibleTreeRenderer } from '../../../../base/browser/ui/tree/objectTree.js'; -import { registerAction2, MenuId } from '../../../../platform/actions/common/actions.js'; +import { ITreeElement, ITreeFilter, ITreeNode, TreeFilterResult, TreeVisibility } from '../../../../base/browser/ui/tree/tree.js'; +import { RunOnceScheduler } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; - -import { IViewDescriptorService } from '../../../common/views.js'; +import { createMatches, FuzzyScore } from '../../../../base/common/filters.js'; +import { normalizeDriveLetter, tildify } from '../../../../base/common/labels.js'; +import { dispose } from '../../../../base/common/lifecycle.js'; +import { isAbsolute, normalize, posix } from '../../../../base/common/path.js'; +import { isWindows } from '../../../../base/common/platform.js'; +import { ltrim } from '../../../../base/common/strings.js'; +import { URI } from '../../../../base/common/uri.js'; +import * as nls from '../../../../nls.js'; +import { MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { FileKind } from '../../../../platform/files/common/files.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { ILabelService } from '../../../../platform/label/common/label.js'; +import { WorkbenchCompressibleObjectTree } from '../../../../platform/list/browser/listService.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { IFileIconTheme, IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js'; +import { IResourceLabel, IResourceLabelOptions, IResourceLabelProps, ResourceLabels } from '../../../browser/labels.js'; +import { ViewAction, ViewPane } from '../../../browser/parts/views/viewPane.js'; +import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; +import { IViewDescriptorService } from '../../../common/views.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IPathService } from '../../../services/path/common/pathService.js'; -import { TreeFindMode } from '../../../../base/browser/ui/tree/abstractTree.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { CONTEXT_LOADED_SCRIPTS_ITEM_TYPE, IDebugService, IDebugSession, LOADED_SCRIPTS_VIEW_ID } from '../common/debug.js'; +import { DebugContentProvider } from '../common/debugContentProvider.js'; +import { Source } from '../common/debugSource.js'; +import { renderViewTree } from './baseDebugView.js'; const NEW_STYLE_COMPRESS = true; @@ -439,7 +438,7 @@ export class LoadedScriptsView extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, - @IHoverService hoverService: IHoverService + @IHoverService hoverService: IHoverService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); this.loadedScriptsItemType = CONTEXT_LOADED_SCRIPTS_ITEM_TYPE.bindTo(contextKeyService); @@ -449,8 +448,7 @@ export class LoadedScriptsView extends ViewPane { super.renderBody(container); this.element.classList.add('debug-pane'); - container.classList.add('debug-loaded-scripts'); - container.classList.add('show-file-icons'); + container.classList.add('debug-loaded-scripts', 'show-file-icons'); this.treeContainer = renderViewTree(container); @@ -461,6 +459,14 @@ export class LoadedScriptsView extends ViewPane { this.treeLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); this._register(this.treeLabels); + const onFileIconThemeChange = (fileIconTheme: IFileIconTheme) => { + this.treeContainer.classList.toggle('align-icons-and-twisties', fileIconTheme.hasFileIcons && !fileIconTheme.hasFolderIcons); + this.treeContainer.classList.toggle('hide-arrows', fileIconTheme.hidesExplorerArrows === true); + }; + + this._register(this.themeService.onDidFileIconThemeChange(onFileIconThemeChange)); + onFileIconThemeChange(this.themeService.getFileIconTheme()); + this.tree = this.instantiationService.createInstance(WorkbenchCompressibleObjectTree, 'LoadedScriptsView', this.treeContainer, diff --git a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts index fcefad3c5167..9122412e421e 100644 --- a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts @@ -106,7 +106,7 @@ suite('Debug - Base Debug View', () => { const container = $('.container'); const treeContainer = renderViewTree(container); - assert.strictEqual(treeContainer.className, 'debug-view-content'); + assert.strictEqual(treeContainer.className, 'debug-view-content file-icon-themable-tree'); assert.strictEqual(container.childElementCount, 1); assert.strictEqual(container.firstChild, treeContainer); assert.strictEqual(dom.isHTMLDivElement(treeContainer), true); From d953d84d9018aef819147320ed58e5b517d2b0a2 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Sat, 21 Dec 2024 13:01:00 -0800 Subject: [PATCH 0330/3587] debug: allow filter/search on debug values (#236768) * debug: allow filter/search on debug values I think this is something that never worked, or at least not for a long while. Implementing match highlighting in values in the era of linkification and ANSI support was a little more complex, but this works well now. ![](https://memes.peet.io/img/24-12-b1f699dc-f20c-4c93-a5ce-f768473fff62.png) Fixes #230945 * fix test --- .../contrib/debug/browser/baseDebugView.ts | 30 ++++++- .../debug/browser/debugANSIHandling.ts | 29 ++++-- .../debug/browser/debugExpressionRenderer.ts | 13 +-- .../contrib/debug/browser/linkDetector.ts | 89 +++++++++++++++---- .../debug/browser/media/debugViewlet.css | 6 ++ .../contrib/debug/browser/variablesView.ts | 4 +- .../debug/browser/watchExpressionsView.ts | 4 +- .../test/browser/debugANSIHandling.test.ts | 8 +- .../debug/test/browser/linkDetector.test.ts | 64 +++++++++++++ 9 files changed, 208 insertions(+), 39 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index d5ba5f0c8561..efb22ae81c13 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -9,19 +9,21 @@ import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { HighlightedLabel, IHighlight } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IInputValidationOptions, InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js'; +import { IKeyboardNavigationLabelProvider } from '../../../../base/browser/ui/list/list.js'; import { IAsyncDataSource, ITreeNode, ITreeRenderer } from '../../../../base/browser/ui/tree/tree.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { FuzzyScore, createMatches } from '../../../../base/common/filters.js'; import { createSingleCallFunction } from '../../../../base/common/functional.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { DisposableStore, IDisposable, dispose, toDisposable } from '../../../../base/common/lifecycle.js'; +import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { IDebugService, IExpression } from '../common/debug.js'; +import { IDebugService, IExpression, IScope } from '../common/debug.js'; import { Variable } from '../common/debugModel.js'; import { IDebugVisualizerService } from '../common/debugVisualizers.js'; import { LinkDetector } from './linkDetector.js'; @@ -78,6 +80,32 @@ export interface IExpressionTemplateData { currentElement: IExpression | undefined; } +/** Splits highlights based on matching of the {@link expressionAndScopeLabelProvider} */ +export const splitExpressionOrScopeHighlights = (e: IExpression | IScope, highlights: IHighlight[]) => { + const nameEndsAt = e.name.length; + const labelBeginsAt = e.name.length + 2; + const name: IHighlight[] = []; + const value: IHighlight[] = []; + for (const hl of highlights) { + if (hl.start < nameEndsAt) { + name.push({ start: hl.start, end: Math.min(hl.end, nameEndsAt) }); + } + if (hl.end > labelBeginsAt) { + value.push({ start: Math.max(hl.start - labelBeginsAt, 0), end: hl.end - labelBeginsAt }); + } + } + + return { name, value }; +}; + +/** Keyboard label provider for expression and scope tree elements. */ +export const expressionAndScopeLabelProvider: IKeyboardNavigationLabelProvider = { + getKeyboardNavigationLabel(e) { + const stripAnsi = e.getSession()?.rememberedCapabilities?.supportsANSIStyling; + return `${e.name}: ${stripAnsi ? removeAnsiEscapeCodes(e.value) : e.value}`; + }, +}; + export abstract class AbstractExpressionDataSource implements IAsyncDataSource { constructor( @IDebugService protected debugService: IDebugService, diff --git a/src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts b/src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts index 16923a73f61d..4db2d7eb8c77 100644 --- a/src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts +++ b/src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IHighlight } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js'; import { Color, RGBA } from '../../../../base/common/color.js'; import { isDefined } from '../../../../base/common/types.js'; import { editorHoverBackground, listActiveSelectionBackground, listFocusBackground, listInactiveFocusBackground, listInactiveSelectionBackground } from '../../../../platform/theme/common/colorRegistry.js'; @@ -16,7 +17,7 @@ import { ILinkDetector } from './linkDetector.js'; * @param text The content to stylize. * @returns An {@link HTMLSpanElement} that contains the potentially stylized text. */ -export function handleANSIOutput(text: string, linkDetector: ILinkDetector, workspaceFolder: IWorkspaceFolder | undefined): HTMLSpanElement { +export function handleANSIOutput(text: string, linkDetector: ILinkDetector, workspaceFolder: IWorkspaceFolder | undefined, highlights: IHighlight[] | undefined): HTMLSpanElement { const root: HTMLSpanElement = document.createElement('span'); const textLength: number = text.length; @@ -27,6 +28,7 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, work let customUnderlineColor: RGBA | string | undefined; let colorsInverted: boolean = false; let currentPos: number = 0; + let unprintedChars = 0; let buffer: string = ''; while (currentPos < textLength) { @@ -58,8 +60,10 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, work if (sequenceFound) { + unprintedChars += 2 + ansiSequence.length; + // Flush buffer with previous styles. - appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, workspaceFolder, customFgColor, customBgColor, customUnderlineColor); + appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, workspaceFolder, customFgColor, customBgColor, customUnderlineColor, highlights, currentPos - buffer.length - unprintedChars); buffer = ''; @@ -105,7 +109,7 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, work // Flush remaining text buffer if not empty. if (buffer) { - appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, workspaceFolder, customFgColor, customBgColor, customUnderlineColor); + appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, workspaceFolder, customFgColor, customBgColor, customUnderlineColor, highlights, currentPos - buffer.length); } return root; @@ -395,6 +399,8 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, work * @param customTextColor If provided, will apply custom color with inline style. * @param customBackgroundColor If provided, will apply custom backgroundColor with inline style. * @param customUnderlineColor If provided, will apply custom textDecorationColor with inline style. + * @param highlights The ranges to highlight. + * @param offset The starting index of the stringContent in the original text. */ export function appendStylizedStringToContainer( root: HTMLElement, @@ -402,15 +408,24 @@ export function appendStylizedStringToContainer( cssClasses: string[], linkDetector: ILinkDetector, workspaceFolder: IWorkspaceFolder | undefined, - customTextColor?: RGBA | string, - customBackgroundColor?: RGBA | string, - customUnderlineColor?: RGBA | string, + customTextColor: RGBA | string | undefined, + customBackgroundColor: RGBA | string | undefined, + customUnderlineColor: RGBA | string | undefined, + highlights: IHighlight[] | undefined, + offset: number, ): void { if (!root || !stringContent) { return; } - const container = linkDetector.linkify(stringContent, true, workspaceFolder); + const container = linkDetector.linkify( + stringContent, + true, + workspaceFolder, + undefined, + undefined, + highlights?.map(h => ({ start: h.start - offset, end: h.end - offset, extraClasses: h.extraClasses })), + ); container.className = cssClasses.join(' '); if (customTextColor) { diff --git a/src/vs/workbench/contrib/debug/browser/debugExpressionRenderer.ts b/src/vs/workbench/contrib/debug/browser/debugExpressionRenderer.ts index 767950dc826c..0835365d4081 100644 --- a/src/vs/workbench/contrib/debug/browser/debugExpressionRenderer.ts +++ b/src/vs/workbench/contrib/debug/browser/debugExpressionRenderer.ts @@ -16,7 +16,7 @@ import { observableConfigValue } from '../../../../platform/observable/common/pl import { IDebugSession, IExpressionValue } from '../common/debug.js'; import { Expression, ExpressionContainer, Variable } from '../common/debugModel.js'; import { ReplEvaluationResult } from '../common/replModel.js'; -import { IVariableTemplateData } from './baseDebugView.js'; +import { IVariableTemplateData, splitExpressionOrScopeHighlights } from './baseDebugView.js'; import { handleANSIOutput } from './debugANSIHandling.js'; import { COPY_EVALUATE_PATH_ID, COPY_VALUE_ID } from './debugCommands.js'; import { DebugLinkHoverBehavior, DebugLinkHoverBehaviorTypeData, ILinkDetector, LinkDetector } from './linkDetector.js'; @@ -32,6 +32,7 @@ export interface IRenderValueOptions { /** If not false, a rich hover will be shown on the element. */ hover?: false | IValueHoverOptions; colorize?: boolean; + highlights?: IHighlight[]; /** * Indicates areas where VS Code implicitly always supported ANSI escape @@ -90,6 +91,7 @@ export class DebugExpressionRenderer { renderVariable(data: IVariableTemplateData, variable: Variable, options: IRenderVariableOptions = {}): IDisposable { const displayType = this.displayType.get(); + const highlights = splitExpressionOrScopeHighlights(variable, options.highlights || []); if (variable.available) { data.type.textContent = ''; @@ -103,7 +105,7 @@ export class DebugExpressionRenderer { } } - data.label.set(text, options.highlights, variable.type && !displayType ? variable.type : variable.name); + data.label.set(text, highlights.name, variable.type && !displayType ? variable.type : variable.name); data.name.classList.toggle('virtual', variable.presentationHint?.kind === 'virtual'); data.name.classList.toggle('internal', variable.presentationHint?.visibility === 'internal'); } else if (variable.value && typeof variable.name === 'string' && variable.name) { @@ -122,6 +124,7 @@ export class DebugExpressionRenderer { showChanged: options.showChanged, maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, hover: { commands }, + highlights: highlights.value, colorize: true, session: variable.getSession(), }); @@ -184,9 +187,9 @@ export class DebugExpressionRenderer { } if (supportsANSI) { - container.appendChild(handleANSIOutput(value, linkDetector, session ? session.root : undefined)); + container.appendChild(handleANSIOutput(value, linkDetector, session ? session.root : undefined, options.highlights)); } else { - container.appendChild(linkDetector.linkify(value, false, session?.root, true, hoverBehavior)); + container.appendChild(linkDetector.linkify(value, false, session?.root, true, hoverBehavior, options.highlights)); } if (options.hover !== false) { @@ -199,7 +202,7 @@ export class DebugExpressionRenderer { if (supportsANSI) { // note: intentionally using `this.linkDetector` so we don't blindly linkify the // entire contents and instead only link file paths that it contains. - hoverContentsPre.appendChild(handleANSIOutput(value, this.linkDetector, session ? session.root : undefined)); + hoverContentsPre.appendChild(handleANSIOutput(value, this.linkDetector, session ? session.root : undefined, options.highlights)); } else { hoverContentsPre.textContent = value; } diff --git a/src/vs/workbench/contrib/debug/browser/linkDetector.ts b/src/vs/workbench/contrib/debug/browser/linkDetector.ts index 98a89453a5ad..8d08e6f1be93 100644 --- a/src/vs/workbench/contrib/debug/browser/linkDetector.ts +++ b/src/vs/workbench/contrib/debug/browser/linkDetector.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getWindow } from '../../../../base/browser/dom.js'; +import { getWindow, isHTMLElement, reset } from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; @@ -23,6 +23,8 @@ import { IDebugSession } from '../common/debug.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IPathService } from '../../../services/path/common/pathService.js'; +import { IHighlight } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js'; +import { Iterable } from '../../../../base/common/iterator.js'; const CONTROL_CODES = '\\u0000-\\u0020\\u007f-\\u009f'; const WEB_LINK_REGEX = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|data:|www\\.)[^\\s' + CONTROL_CODES + '"]{2,}[^\\s' + CONTROL_CODES + '"\')}\\],:;.!?]', 'ug'); @@ -42,6 +44,7 @@ type LinkPart = { kind: LinkKind; value: string; captures: string[]; + index: number; }; export const enum DebugLinkHoverBehavior { @@ -61,7 +64,7 @@ export type DebugLinkHoverBehaviorTypeData = { type: DebugLinkHoverBehavior.None | { type: DebugLinkHoverBehavior.Rich; store: DisposableStore }; export interface ILinkDetector { - linkify(text: string, splitLines?: boolean, workspaceFolder?: IWorkspaceFolder, includeFulltext?: boolean, hoverBehavior?: DebugLinkHoverBehaviorTypeData): HTMLElement; + linkify(text: string, splitLines?: boolean, workspaceFolder?: IWorkspaceFolder, includeFulltext?: boolean, hoverBehavior?: DebugLinkHoverBehaviorTypeData, highlights?: IHighlight[]): HTMLElement; linkifyLocation(text: string, locationReference: number, session: IDebugSession, hoverBehavior?: DebugLinkHoverBehaviorTypeData): HTMLElement; } @@ -88,11 +91,11 @@ export class LinkDetector implements ILinkDetector { * If a `hoverBehavior` is passed, hovers may be added using the workbench hover service. * This should be preferred for new code where hovers are desirable. */ - linkify(text: string, splitLines?: boolean, workspaceFolder?: IWorkspaceFolder, includeFulltext?: boolean, hoverBehavior?: DebugLinkHoverBehaviorTypeData): HTMLElement { - return this._linkify(text, splitLines, workspaceFolder, includeFulltext, hoverBehavior); + linkify(text: string, splitLines?: boolean, workspaceFolder?: IWorkspaceFolder, includeFulltext?: boolean, hoverBehavior?: DebugLinkHoverBehaviorTypeData, highlights?: IHighlight[]): HTMLElement { + return this._linkify(text, splitLines, workspaceFolder, includeFulltext, hoverBehavior, highlights); } - private _linkify(text: string, splitLines?: boolean, workspaceFolder?: IWorkspaceFolder, includeFulltext?: boolean, hoverBehavior?: DebugLinkHoverBehaviorTypeData, defaultRef?: { locationReference: number; session: IDebugSession }): HTMLElement { + private _linkify(text: string, splitLines?: boolean, workspaceFolder?: IWorkspaceFolder, includeFulltext?: boolean, hoverBehavior?: DebugLinkHoverBehaviorTypeData, highlights?: IHighlight[], defaultRef?: { locationReference: number; session: IDebugSession }): HTMLElement { if (splitLines) { const lines = text.split('\n'); for (let i = 0; i < lines.length - 1; i++) { @@ -102,7 +105,7 @@ export class LinkDetector implements ILinkDetector { // Remove the last element ('') that split added. lines.pop(); } - const elements = lines.map(line => this._linkify(line, false, workspaceFolder, includeFulltext, hoverBehavior, defaultRef)); + const elements = lines.map(line => this._linkify(line, false, workspaceFolder, includeFulltext, hoverBehavior, highlights, defaultRef)); if (elements.length === 1) { // Do not wrap single line with extra span. return elements[0]; @@ -115,21 +118,26 @@ export class LinkDetector implements ILinkDetector { const container = document.createElement('span'); for (const part of this.detectLinks(text)) { try { + let node: Node; switch (part.kind) { case 'text': - container.appendChild(defaultRef ? this.linkifyLocation(part.value, defaultRef.locationReference, defaultRef.session, hoverBehavior) : document.createTextNode(part.value)); + node = defaultRef ? this.linkifyLocation(part.value, defaultRef.locationReference, defaultRef.session, hoverBehavior) : document.createTextNode(part.value); break; case 'web': - container.appendChild(this.createWebLink(includeFulltext ? text : undefined, part.value, hoverBehavior)); + node = this.createWebLink(includeFulltext ? text : undefined, part.value, hoverBehavior); break; case 'path': { const path = part.captures[0]; const lineNumber = part.captures[1] ? Number(part.captures[1]) : 0; const columnNumber = part.captures[2] ? Number(part.captures[2]) : 0; - container.appendChild(this.createPathLink(includeFulltext ? text : undefined, part.value, path, lineNumber, columnNumber, workspaceFolder, hoverBehavior)); + node = this.createPathLink(includeFulltext ? text : undefined, part.value, path, lineNumber, columnNumber, workspaceFolder, hoverBehavior); break; } + default: + node = document.createTextNode(part.value); } + + container.append(...this.applyHighlights(node, part.index, part.value.length, highlights)); } catch (e) { container.appendChild(document.createTextNode(part.value)); } @@ -137,6 +145,50 @@ export class LinkDetector implements ILinkDetector { return container; } + private applyHighlights(node: Node, startIndex: number, length: number, highlights: IHighlight[] | undefined): Iterable { + const children: (Node | string)[] = []; + let currentIndex = startIndex; + const endIndex = startIndex + length; + + for (const highlight of highlights || []) { + if (highlight.end <= currentIndex || highlight.start >= endIndex) { + continue; + } + + if (highlight.start > currentIndex) { + children.push(node.textContent!.substring(currentIndex - startIndex, highlight.start - startIndex)); + currentIndex = highlight.start; + } + + const highlightEnd = Math.min(highlight.end, endIndex); + const highlightedText = node.textContent!.substring(currentIndex - startIndex, highlightEnd - startIndex); + const highlightSpan = document.createElement('span'); + highlightSpan.classList.add('highlight'); + if (highlight.extraClasses) { + highlightSpan.classList.add(...highlight.extraClasses); + } + highlightSpan.textContent = highlightedText; + children.push(highlightSpan); + currentIndex = highlightEnd; + } + + if (currentIndex === startIndex) { + return Iterable.single(node); // no changes made + } + + if (currentIndex < endIndex) { + children.push(node.textContent!.substring(currentIndex - startIndex)); + } + + // reuse the element if it's a link + if (isHTMLElement(node)) { + reset(node, ...children); + return Iterable.single(node); + } + + return children; + } + /** * Linkifies a location reference. */ @@ -161,8 +213,8 @@ export class LinkDetector implements ILinkDetector { */ makeReferencedLinkDetector(locationReference: number, session: IDebugSession): ILinkDetector { return { - linkify: (text, splitLines, workspaceFolder, includeFulltext, hoverBehavior) => - this._linkify(text, splitLines, workspaceFolder, includeFulltext, hoverBehavior, { locationReference, session }), + linkify: (text, splitLines, workspaceFolder, includeFulltext, hoverBehavior, highlights) => + this._linkify(text, splitLines, workspaceFolder, includeFulltext, hoverBehavior, highlights, { locationReference, session }), linkifyLocation: this.linkifyLocation.bind(this), }; } @@ -295,16 +347,16 @@ export class LinkDetector implements ILinkDetector { private detectLinks(text: string): LinkPart[] { if (text.length > MAX_LENGTH) { - return [{ kind: 'text', value: text, captures: [] }]; + return [{ kind: 'text', value: text, captures: [], index: 0 }]; } const regexes: RegExp[] = [WEB_LINK_REGEX, PATH_LINK_REGEX]; const kinds: LinkKind[] = ['web', 'path']; const result: LinkPart[] = []; - const splitOne = (text: string, regexIndex: number) => { + const splitOne = (text: string, regexIndex: number, baseIndex: number) => { if (regexIndex >= regexes.length) { - result.push({ value: text, kind: 'text', captures: [] }); + result.push({ value: text, kind: 'text', captures: [], index: baseIndex }); return; } const regex = regexes[regexIndex]; @@ -314,23 +366,24 @@ export class LinkDetector implements ILinkDetector { while ((match = regex.exec(text)) !== null) { const stringBeforeMatch = text.substring(currentIndex, match.index); if (stringBeforeMatch) { - splitOne(stringBeforeMatch, regexIndex + 1); + splitOne(stringBeforeMatch, regexIndex + 1, baseIndex + currentIndex); } const value = match[0]; result.push({ value: value, kind: kinds[regexIndex], - captures: match.slice(1) + captures: match.slice(1), + index: baseIndex + match.index }); currentIndex = match.index + value.length; } const stringAfterMatches = text.substring(currentIndex); if (stringAfterMatches) { - splitOne(stringAfterMatches, regexIndex + 1); + splitOne(stringAfterMatches, regexIndex + 1, baseIndex + currentIndex); } }; - splitOne(text, 0); + splitOne(text, 0, 0); return result; } } diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index 90dfb51bc600..f795c6c22ebd 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -274,6 +274,12 @@ font-family: var(--monaco-monospace-font); font-weight: normal; } +.debug-view-content .monaco-tl-contents .highlight { + color: unset !important; + background-color: var(--vscode-list-filterMatchBackground); + outline: 1px dotted var(--vscode-list-filterMatchBorder); + outline-offset: -1px; +} /* Breakpoints */ diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 16d53c4f87ee..74c22d9ee976 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -46,7 +46,7 @@ import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS import { getContextForVariable } from '../common/debugContext.js'; import { ErrorScope, Expression, Scope, StackFrame, Variable, VisualizedExpression, getUriForDebugMemory } from '../common/debugModel.js'; import { DebugVisualizer, IDebugVisualizerService } from '../common/debugVisualizers.js'; -import { AbstractExpressionDataSource, AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderViewTree } from './baseDebugView.js'; +import { AbstractExpressionDataSource, AbstractExpressionsRenderer, expressionAndScopeLabelProvider, IExpressionTemplateData, IInputBoxOptions, renderViewTree } from './baseDebugView.js'; import { ADD_TO_WATCH_ID, ADD_TO_WATCH_LABEL, COPY_EVALUATE_PATH_ID, COPY_EVALUATE_PATH_LABEL, COPY_VALUE_ID, COPY_VALUE_LABEL } from './debugCommands.js'; import { DebugExpressionRenderer } from './debugExpressionRenderer.js'; @@ -138,7 +138,7 @@ export class VariablesView extends ViewPane implements IDebugViewWithVariables { this.instantiationService.createInstance(VariablesDataSource), { accessibilityProvider: new VariablesAccessibilityProvider(), identityProvider: { getId: (element: IExpression | IScope) => element.getId() }, - keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IExpression | IScope) => e.name }, + keyboardNavigationLabelProvider: expressionAndScopeLabelProvider, overrideStyles: this.getLocationBasedColors().listOverrideStyles }); diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index b325c138b0a3..a56d4aae4623 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -31,7 +31,7 @@ import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.j import { IViewDescriptorService } from '../../../common/views.js'; import { CONTEXT_CAN_VIEW_MEMORY, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_WATCH_EXPRESSIONS_EXIST, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_WATCH_ITEM_TYPE, IDebugConfiguration, IDebugService, IDebugViewWithVariables, IExpression, WATCH_VIEW_ID } from '../common/debug.js'; import { Expression, Variable, VisualizedExpression } from '../common/debugModel.js'; -import { AbstractExpressionDataSource, AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderViewTree } from './baseDebugView.js'; +import { AbstractExpressionDataSource, AbstractExpressionsRenderer, expressionAndScopeLabelProvider, IExpressionTemplateData, IInputBoxOptions, renderViewTree } from './baseDebugView.js'; import { DebugExpressionRenderer } from './debugExpressionRenderer.js'; import { watchExpressionsAdd, watchExpressionsRemoveAll } from './debugIcons.js'; import { VariablesRenderer, VisualizedVariableRenderer } from './variablesView.js'; @@ -109,7 +109,7 @@ export class WatchExpressionsView extends ViewPane implements IDebugViewWithVari return undefined; } - return e.name; + return expressionAndScopeLabelProvider.getKeyboardNavigationLabel(e); } }, dnd: new WatchExpressionsDragAndDrop(this.debugService), diff --git a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts index eb31a388b2ed..17d23dc63032 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts @@ -51,8 +51,8 @@ suite('Debug - ANSI Handling', () => { assert.strictEqual(0, root.children.length); - appendStylizedStringToContainer(root, 'content1', ['class1', 'class2'], linkDetector, session.root); - appendStylizedStringToContainer(root, 'content2', ['class2', 'class3'], linkDetector, session.root); + appendStylizedStringToContainer(root, 'content1', ['class1', 'class2'], linkDetector, session.root, undefined, undefined, undefined, undefined, 0); + appendStylizedStringToContainer(root, 'content2', ['class2', 'class3'], linkDetector, session.root, undefined, undefined, undefined, undefined, 0); assert.strictEqual(2, root.children.length); @@ -82,7 +82,7 @@ suite('Debug - ANSI Handling', () => { * @returns An {@link HTMLSpanElement} that contains the stylized text. */ function getSequenceOutput(sequence: string): HTMLSpanElement { - const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, session.root); + const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, session.root, []); assert.strictEqual(1, root.children.length); const child: Node = root.lastChild!; if (isHTMLSpanElement(child)) { @@ -395,7 +395,7 @@ suite('Debug - ANSI Handling', () => { if (elementsExpected === undefined) { elementsExpected = assertions.length; } - const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, session.root); + const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, session.root, []); assert.strictEqual(elementsExpected, root.children.length); for (let i = 0; i < elementsExpected; i++) { const child: Node = root.children[i]; diff --git a/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts b/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts index c078c4a371e5..9e2d0c8c767b 100644 --- a/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts @@ -13,6 +13,7 @@ import { ITunnelService } from '../../../../../platform/tunnel/common/tunnel.js' import { WorkspaceFolder } from '../../../../../platform/workspace/common/workspace.js'; import { LinkDetector } from '../../browser/linkDetector.js'; import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { IHighlight } from '../../../../../base/browser/ui/highlightedlabel/highlightedLabel.js'; suite('Debug - Link Detector', () => { @@ -168,4 +169,67 @@ suite('Debug - Link Detector', () => { assertElementIsLink(output.children[1].children[0]); assert.strictEqual(isWindows ? 'C:/foo/bar.js:12:34' : '/Users/foo/bar.js:12:34', output.children[1].children[0].textContent); }); + + test('highlightNoLinks', () => { + const input = 'I am a string'; + const highlights: IHighlight[] = [{ start: 2, end: 5 }]; + const expectedOutput = 'I am a string'; + const output = linkDetector.linkify(input, false, undefined, false, undefined, highlights); + + assert.strictEqual(1, output.children.length); + assert.strictEqual('SPAN', output.tagName); + assert.strictEqual(expectedOutput, output.outerHTML); + }); + + test('highlightWithLink', () => { + const input = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; + const highlights: IHighlight[] = [{ start: 0, end: 5 }]; + const expectedOutput = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; + const output = linkDetector.linkify(input, false, undefined, false, undefined, highlights); + + assert.strictEqual(1, output.children.length); + assert.strictEqual('SPAN', output.tagName); + assert.strictEqual('A', output.firstElementChild!.tagName); + assert.strictEqual(expectedOutput, output.outerHTML); + assertElementIsLink(output.firstElementChild!); + }); + + test('highlightOverlappingLinkStart', () => { + const input = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; + const highlights: IHighlight[] = [{ start: 0, end: 10 }]; + const expectedOutput = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; + const output = linkDetector.linkify(input, false, undefined, false, undefined, highlights); + + assert.strictEqual(1, output.children.length); + assert.strictEqual('SPAN', output.tagName); + assert.strictEqual('A', output.firstElementChild!.tagName); + assert.strictEqual(expectedOutput, output.outerHTML); + assertElementIsLink(output.firstElementChild!); + }); + + test('highlightOverlappingLinkEnd', () => { + const input = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; + const highlights: IHighlight[] = [{ start: 10, end: 20 }]; + const expectedOutput = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; + const output = linkDetector.linkify(input, false, undefined, false, undefined, highlights); + + assert.strictEqual(1, output.children.length); + assert.strictEqual('SPAN', output.tagName); + assert.strictEqual('A', output.firstElementChild!.tagName); + assert.strictEqual(expectedOutput, output.outerHTML); + assertElementIsLink(output.firstElementChild!); + }); + + test('highlightOverlappingLinkStartAndEnd', () => { + const input = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; + const highlights: IHighlight[] = [{ start: 5, end: 15 }]; + const expectedOutput = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; + const output = linkDetector.linkify(input, false, undefined, false, undefined, highlights); + + assert.strictEqual(1, output.children.length); + assert.strictEqual('SPAN', output.tagName); + assert.strictEqual('A', output.firstElementChild!.tagName); + assert.strictEqual(expectedOutput, output.outerHTML); + assertElementIsLink(output.firstElementChild!); + }); }); From 151ef3514e76629f4e7bf3951439b1e0dae0a6e5 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:09:59 +0100 Subject: [PATCH 0331/3587] SCM - disable actions for resource groups that do not have any resources (#236813) --- extensions/git/package.json | 14 ++++++------- src/vs/workbench/contrib/scm/browser/menus.ts | 20 ++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 53aa0fad747e..4556255d3dac 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -163,14 +163,14 @@ "title": "%command.stageAll%", "category": "Git", "icon": "$(add)", - "enablement": "!operationInProgress" + "enablement": "!operationInProgress && scmResourceGroupResourceCount > 0" }, { "command": "git.stageAllTracked", "title": "%command.stageAllTracked%", "category": "Git", "icon": "$(add)", - "enablement": "!operationInProgress" + "enablement": "!operationInProgress && scmResourceGroupResourceCount > 0" }, { "command": "git.stageAllUntracked", @@ -243,7 +243,7 @@ "title": "%command.unstageAll%", "category": "Git", "icon": "$(remove)", - "enablement": "!operationInProgress" + "enablement": "!operationInProgress && scmResourceGroupResourceCount > 0" }, { "command": "git.unstageSelectedRanges", @@ -270,14 +270,14 @@ "title": "%command.cleanAll%", "category": "Git", "icon": "$(discard)", - "enablement": "!operationInProgress" + "enablement": "!operationInProgress && scmResourceGroupResourceCount > 0" }, { "command": "git.cleanAllTracked", "title": "%command.cleanAllTracked%", "category": "Git", "icon": "$(discard)", - "enablement": "!operationInProgress" + "enablement": "!operationInProgress && scmResourceGroupResourceCount > 0" }, { "command": "git.cleanAllUntracked", @@ -889,14 +889,14 @@ "title": "%command.viewChanges%", "icon": "$(diff-multiple)", "category": "Git", - "enablement": "!operationInProgress" + "enablement": "!operationInProgress && scmResourceGroupResourceCount > 0" }, { "command": "git.viewStagedChanges", "title": "%command.viewStagedChanges%", "icon": "$(diff-multiple)", "category": "Git", - "enablement": "!operationInProgress" + "enablement": "!operationInProgress && scmResourceGroupResourceCount > 0" }, { "command": "git.viewUntrackedChanges", diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index f14d8ea79f67..352f63b6f3ec 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -6,7 +6,7 @@ import { IAction } from '../../../../base/common/actions.js'; import { equals } from '../../../../base/common/arrays.js'; import { Emitter } from '../../../../base/common/event.js'; -import { DisposableStore, IDisposable, dispose } from '../../../../base/common/lifecycle.js'; +import { DisposableStore, IDisposable, MutableDisposable, dispose } from '../../../../base/common/lifecycle.js'; import './media/scm.css'; import { localize } from '../../../../nls.js'; import { getActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; @@ -70,13 +70,14 @@ interface IContextualResourceMenuItem { class SCMMenusItem implements IDisposable { - private _resourceGroupMenu: IMenu | undefined; + private readonly _resourceGroupMenu = new MutableDisposable(); get resourceGroupMenu(): IMenu { - if (!this._resourceGroupMenu) { - this._resourceGroupMenu = this.menuService.createMenu(MenuId.SCMResourceGroupContext, this.contextKeyService); - } + const contextKeyService = this.contextKeyService.createOverlay([ + ['scmResourceGroupResourceCount', this.group.resources.length], + ]); - return this._resourceGroupMenu; + this._resourceGroupMenu.value = this.menuService.createMenu(MenuId.SCMResourceGroupContext, contextKeyService); + return this._resourceGroupMenu.value; } private _resourceFolderMenu: IMenu | undefined; @@ -92,8 +93,9 @@ class SCMMenusItem implements IDisposable { private contextualResourceMenus: Map | undefined; constructor( - private contextKeyService: IContextKeyService, - private menuService: IMenuService + private readonly group: ISCMResourceGroup, + private readonly contextKeyService: IContextKeyService, + private readonly menuService: IMenuService ) { } getResourceMenu(resource: ISCMResource): IMenu { @@ -206,7 +208,7 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { ['multiDiffEditorEnableViewChanges', group.multiDiffEditorEnableViewChanges], ]); - result = new SCMMenusItem(contextKeyService, this.menuService); + result = new SCMMenusItem(group, contextKeyService, this.menuService); this.resourceGroupMenusItems.set(group, result); } From 2e5cbd49c8252d6e1dc44de2ce8f63899a8774f7 Mon Sep 17 00:00:00 2001 From: Robo Date: Mon, 23 Dec 2024 19:22:49 +0900 Subject: [PATCH 0332/3587] chore: update electron@32.2.7 (#236843) * chore: update electron@32.2.7 * chore: bump distro --- .npmrc | 4 +- build/checksums/electron.txt | 150 +++++++++++++++++------------------ cgmanifest.json | 4 +- package-lock.json | 8 +- package.json | 4 +- 5 files changed, 85 insertions(+), 85 deletions(-) diff --git a/.npmrc b/.npmrc index 22256e5d8e7b..27692c2409ee 100644 --- a/.npmrc +++ b/.npmrc @@ -1,6 +1,6 @@ disturl="https://electronjs.org/headers" -target="32.2.6" -ms_build_id="10629634" +target="32.2.7" +ms_build_id="10660205" runtime="electron" build_from_source="true" legacy-peer-deps="true" diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 17ec96faa2a0..293250496af8 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -bb4164f7b554606b2c4daaf43e81bf2e2b5cf0d4441cfdd74f04653237fcf655 *chromedriver-v32.2.6-darwin-arm64.zip -a0fc3df1c6cd17bfe62ffbb1eba3655ca625dea5046e5d2b3dbb0e9e349cd10e *chromedriver-v32.2.6-darwin-x64.zip -671d6dab890747ea73ba5589327eef7612670950a20e5f88c7d8a301b5491e26 *chromedriver-v32.2.6-linux-arm64.zip -55bfd4e33fef1506261d4cb3074988e1970c2a762ca76a8f1197512a1766723c *chromedriver-v32.2.6-linux-armv7l.zip -d3c7a45c8c75152db927b3596f506995e72631df870b302b7dbcbd3399e54a3a *chromedriver-v32.2.6-linux-x64.zip -567f77d09708942901c6cdce6708b995f6ac779faceebb4ed383ca5003e2de4e *chromedriver-v32.2.6-mas-arm64.zip -b3a28181b1d077742f1be632a802e15b5a36a260b1cfe0e429735de9f52d074a *chromedriver-v32.2.6-mas-x64.zip -a113f5bd747b6eeb033f4d6ea2f53cf332d9b45d6340af514dd938bac7f99419 *chromedriver-v32.2.6-win32-arm64.zip -3b3237a788fad0a6be63a69b93c28b6052db23aeaa1a75d2589be15b4c2c0f2f *chromedriver-v32.2.6-win32-ia32.zip -1096c131cf8e3f98a01525e93d573eaf4fd23492d8dd78a211e39c448e69e463 *chromedriver-v32.2.6-win32-x64.zip -8e6fcf3171c3fcdcb117f641ec968bb53be3d38696e388636bf34f04c10b987d *electron-api.json -b1b20784a97e64992c92480e69af828a110d834372479b26759f1559b3da80fc *electron-v32.2.6-darwin-arm64-dsym-snapshot.zip -f11dd5a84229430ec59b4335415a4b308dc4330ff7b9febae20165fbdd862e92 *electron-v32.2.6-darwin-arm64-dsym.zip -cc96cf91f6b108dc927d8f7daee2fe27ae8a492c932993051508aa779e816445 *electron-v32.2.6-darwin-arm64-symbols.zip -fcb6bbb6aa3c1020b4045dbe9f2a5286173d5025248550f55631e70568e91775 *electron-v32.2.6-darwin-arm64.zip -d2bfeea27fc91936b4f71d0f5c577e5ad0ea094edba541dfa348948fd65c3331 *electron-v32.2.6-darwin-x64-dsym-snapshot.zip -5e7684cc12c0dee11fb933b68301d0fe68d3198d1daeadd5e1b4cf52743f79bf *electron-v32.2.6-darwin-x64-dsym.zip -6d2a7d41ab14fc7d3c5e4b35d5d425edb2d13978dcc332e781ec8b7bcfe6a794 *electron-v32.2.6-darwin-x64-symbols.zip -c0964ee5fdcefb1003ffd7ef574b07e5147856f3a94bb4335f78c409f8cf2eca *electron-v32.2.6-darwin-x64.zip -604d88b9d434ea66ddf234dd129dcef3d468b95b0da47e2f1555a682c8d0de28 *electron-v32.2.6-linux-arm64-debug.zip -f448e91df42fc84177bcd46378e49ee648f6114984fc57af4a84690a5197c23e *electron-v32.2.6-linux-arm64-symbols.zip -9e4f9345cae06e8e5679b228e7b7ac21b8733e3fcda8903e3dcbc8171c53f8be *electron-v32.2.6-linux-arm64.zip -604d88b9d434ea66ddf234dd129dcef3d468b95b0da47e2f1555a682c8d0de28 *electron-v32.2.6-linux-armv7l-debug.zip -c85d5ca3f38dc4140040bcde6a37ac9c7510bb542f12e1ffce695a35f68e3c13 *electron-v32.2.6-linux-armv7l-symbols.zip -df4b490a9c501d83c5305f20b2a9d1aa100d2e878e59ebafde477f21d35e3300 *electron-v32.2.6-linux-armv7l.zip -a5aa67da85ee318ff0770d55a506f62e6e5a10e967dfab272a94bcd91922e075 *electron-v32.2.6-linux-x64-debug.zip -671eb342a58e056f0dee5a6e9c69a6a96ee2141f81d306fa1a0e2635e22aa7c0 *electron-v32.2.6-linux-x64-symbols.zip -a3231409db7f8ac2cc446708f17e2abac0f8549c166eaab2f427e8d0f864208d *electron-v32.2.6-linux-x64.zip -8b8d0aeadcf21633216a9cce87d323ad6aa21e38ec82092cd5f65bf171be025f *electron-v32.2.6-mas-arm64-dsym-snapshot.zip -298931236955b83d174738d3325931e9672a8333bf854fc5f471168b0f7e70be *electron-v32.2.6-mas-arm64-dsym.zip -53e4c666a6f5f87aa150b1c2f34532e3711e00b0237fb103dcbef64d65979429 *electron-v32.2.6-mas-arm64-symbols.zip -bedbc78acc3bc6cb30e5fe1f133562104152022273ec21a83cd32a0eece9003f *electron-v32.2.6-mas-arm64.zip -9ba3def756c90460867d968af5417996991bf3b4b306dba4c9fbde43de2f771d *electron-v32.2.6-mas-x64-dsym-snapshot.zip -e4a3f9392934bb4ef82a214fec2ce1f319ea409b89bf03b2a2a4ab7a55688d0c *electron-v32.2.6-mas-x64-dsym.zip -55c54fd01faf5767493183001a440b837b63f8b8d64c36f7b42fa7217af36dcd *electron-v32.2.6-mas-x64-symbols.zip -1efe974cd426a288d617750c864e6edbf28495c3b851a5bc806af19cda7a274d *electron-v32.2.6-mas-x64.zip -88c50d34dc48a55a11014349d2d278f895f1615405614dbfcf27dff5f5030cf1 *electron-v32.2.6-win32-arm64-pdb.zip -b0b2211bf0f11924bcd693b6783627c7f6c9a066117bcf05c10766064c79794c *electron-v32.2.6-win32-arm64-symbols.zip -48b81d28fdceb4ab3ca27650d79bab910a1a19dbda72271882bfdc877c71975f *electron-v32.2.6-win32-arm64-toolchain-profile.zip -2b7962348f23410863cb6562d79654ce534666bab9f75965b5c8ebee61f49657 *electron-v32.2.6-win32-arm64.zip -d248ab4ec8b4a5aec414c7a404e0efd9b9a73083f7c5cb6c657c2ed58c4cbe94 *electron-v32.2.6-win32-ia32-pdb.zip -2ef98197d66d94aee4978047f22ba07538d259b25a8f5f301d9564a13649e72c *electron-v32.2.6-win32-ia32-symbols.zip -48b81d28fdceb4ab3ca27650d79bab910a1a19dbda72271882bfdc877c71975f *electron-v32.2.6-win32-ia32-toolchain-profile.zip -e85dbf85d58cab5b06cdb8e76fde3a25306e7c1808f5bb30925ba7e29ff14d13 *electron-v32.2.6-win32-ia32.zip -9d76b0c0d475cc062b2a951fbfb3b17837880102fb6cc042e2736dfebbfa0e5d *electron-v32.2.6-win32-x64-pdb.zip -3d7b93bafc633429f5c9f820bcb4843d504b12e454b3ecebb1b69c15b5f4080f *electron-v32.2.6-win32-x64-symbols.zip -48b81d28fdceb4ab3ca27650d79bab910a1a19dbda72271882bfdc877c71975f *electron-v32.2.6-win32-x64-toolchain-profile.zip -77d5e5b76b49767e6a3ad292dc315fbc7cdccd557ac38da9093b8ac6da9262d5 *electron-v32.2.6-win32-x64.zip -a52935712eb4fc2c12ac4ae611a57e121da3b6165c2de1abd7392ed4261287e2 *electron.d.ts -6345ea55fda07544434c90c276cdceb2662044b9e0355894db67ca95869af22a *ffmpeg-v32.2.6-darwin-arm64.zip -4c1347e8653727513a22be013008c2760d19200977295b98506b3b9947e74090 *ffmpeg-v32.2.6-darwin-x64.zip -3f1eafaf4cd90ab43ba0267429189be182435849a166a2cbe1faefc0d07217c4 *ffmpeg-v32.2.6-linux-arm64.zip -3db919bc57e1a5bf7c1bae1d7aeacf4a331990ea82750391c0b24a046d9a2812 *ffmpeg-v32.2.6-linux-armv7l.zip -fe7d779dddbfb5da5999a7607fc5e3c7a6ab7c65e8da9fee1384918865231612 *ffmpeg-v32.2.6-linux-x64.zip -e09ae881113d1b3103aec918e7c95c36f82b2db63657320c380c94386f689138 *ffmpeg-v32.2.6-mas-arm64.zip -ee316e435662201a81fcededc62582dc87a0bd5c9fd0f6a8a55235eca806652f *ffmpeg-v32.2.6-mas-x64.zip -415395968d31e13056cefcb589ed550a0e80d7c3d0851ee3ba29e4dcdf174210 *ffmpeg-v32.2.6-win32-arm64.zip -17f61a5293b707c984cee52b57a7e505cde994d22828e31c016434521638e419 *ffmpeg-v32.2.6-win32-ia32.zip -f9b47951a553eec21636b3cc15eccf0a2ba272567146ec8a6e2eeb91a985fd62 *ffmpeg-v32.2.6-win32-x64.zip -7d2b596bd94e4d5c7befba11662dc563a02f18c183da12ebd56f38bb6f4382e9 *hunspell_dictionaries.zip -06bca9a33142b5834fbca6d10d7f3303b0b5c52e7222fe783db109cd4c5260ed *libcxx-objects-v32.2.6-linux-arm64.zip -7ab8ff5a4e1d3a6639978ed718d2732df9c1a4dd4dcd3e18f6746a2168d353a9 *libcxx-objects-v32.2.6-linux-armv7l.zip -ba96792896751e11fdba63e5e336e323979986176aa1848122ca3757c854352e *libcxx-objects-v32.2.6-linux-x64.zip -1f858d484f4ce27f9f3c4a120b2881f88c17c81129ca10e495b50398fb2eed64 *libcxx_headers.zip -4566afb06a6dd8bd895dba5350a5705868203321116a1ae0a216d5a4a6bfb289 *libcxxabi_headers.zip -350f5419c14aede5802c4f0ef5204ddbbe0eb7d5263524da38d19f43620d8530 *mksnapshot-v32.2.6-darwin-arm64.zip -9f4e4943df4502943994ffa17525b7b5b9a1d889dbc5aeb28bfc40f9146323ec *mksnapshot-v32.2.6-darwin-x64.zip -6295ad1a4ab3b24ac99ec85d35ebfce3861e58185b94d20077e364e81ad935f8 *mksnapshot-v32.2.6-linux-arm64-x64.zip -ed9e1931165a2ff85c1af9f10b3bf8ba05d2dae31331d1d4f5ff1512078e4411 *mksnapshot-v32.2.6-linux-armv7l-x64.zip -6680c63b11e4638708d88c130474ceb2ee92fc58c62cfcd3bf33dda9fee771c2 *mksnapshot-v32.2.6-linux-x64.zip -5a4c45b755b7bbdcad51345f5eb2935ba988987650f9b8540c7ef04015207a2f *mksnapshot-v32.2.6-mas-arm64.zip -1e6243d6a1b68f327457b9dae244ffaeadc265b07a99d9c36f1abcff31e36856 *mksnapshot-v32.2.6-mas-x64.zip -407099537b17860ce7dcea6ba582a854314c29dc152a6df0abcc77ebb0187b4c *mksnapshot-v32.2.6-win32-arm64-x64.zip -f9107536378e19ae9305386dacf6fae924c7ddaad0cf0a6f49694e1e8af6ded5 *mksnapshot-v32.2.6-win32-ia32.zip -82a142db76a2cc5dfb9a89e4cd721cfebc0f3077d2415d8f3d86bb7411f0fe27 *mksnapshot-v32.2.6-win32-x64.zip +0729d2cb830425c4591b40d7189c2f7da020f5adb887a49a4faa022d551b1e6f *chromedriver-v32.2.7-darwin-arm64.zip +a7d61c68d3b3522c0ec383915b6ab3d9f269d9ada0e09aa87a4e7d9fc24fe928 *chromedriver-v32.2.7-darwin-x64.zip +45314c8c7127f6083469c2c067aa78beb20055ba4d7a63eb2108b27e594a647a *chromedriver-v32.2.7-linux-arm64.zip +7e976a7131dcfd55f781c073ae59c8a24a1393119d831fbac13c6a335eb71467 *chromedriver-v32.2.7-linux-armv7l.zip +3437feb5d8e7157476d2e7a6558346061cd7e46506874bc7870eed8a3a43642a *chromedriver-v32.2.7-linux-x64.zip +3737301add80a936374acb17b84bb3a715fab9fbce049816ea7a51fa53d3b35e *chromedriver-v32.2.7-mas-arm64.zip +8b8b62f48a5e8b8a340b47348a2cc5dd4ba38789f76bc5567c039587538081a9 *chromedriver-v32.2.7-mas-x64.zip +24b666e3ab41eb1c66ab0f2361af0529b2b8e1e5ef153cfcef36adc700f9ed6d *chromedriver-v32.2.7-win32-arm64.zip +6d40251661afb1835adbef85e317fd520c0f1378156614c82befb348c3122c2d *chromedriver-v32.2.7-win32-ia32.zip +483012da9903d8d75e5e251a3262667c9a0a012a492b93dbe1237c7827eba778 *chromedriver-v32.2.7-win32-x64.zip +1e9b2b9011f56fa26f4d9fa57254ef1d0bdb34405a9bdf83a652f6258347e46d *electron-api.json +c0ea4a21f2e7e946300bf587a4e31f72ef996c497eaa94e6b8f788b917b896e5 *electron-v32.2.7-darwin-arm64-dsym-snapshot.zip +1e6e84e56cfb3a2473cab41577c160d3afcbda8337dda17c5295da90266433c9 *electron-v32.2.7-darwin-arm64-dsym.zip +9f460100fb71ef098bec26e9a09978ec1b1663165d6a358bfc398f5548a844c3 *electron-v32.2.7-darwin-arm64-symbols.zip +71e76a0a81a0c1c10e9e4862caf96437ba85a18c8fa7d8e15d59e3c057b893bd *electron-v32.2.7-darwin-arm64.zip +e406d690365f332826843c86c6a1b5c0320a84b0527ad8700a0e995b12a35f8c *electron-v32.2.7-darwin-x64-dsym-snapshot.zip +53d6fb64d717af80f024284161a432aaffb47631ef7548f18f33016c3376871a *electron-v32.2.7-darwin-x64-dsym.zip +3c9187db2cc0570d7b01651fa78294df7d451c87c361335cee80a61c1c561b67 *electron-v32.2.7-darwin-x64-symbols.zip +34310ed51d32b6c02ba3e3f447b0807ea85804d1f2b239e02a9de58b9080fbf8 *electron-v32.2.7-darwin-x64.zip +e884f2f9f3d001488888929b8affe053a60a7a780af7d0ec8d7023023f40ca52 *electron-v32.2.7-linux-arm64-debug.zip +fd88e47e7b564b006f68641b5c328721bbc8d87cfc9e569d9733354d263cddee *electron-v32.2.7-linux-arm64-symbols.zip +fb6e1f24385c3058844bd768320d5b332b4cbd011ab930e7252dc330c8ee17b3 *electron-v32.2.7-linux-arm64.zip +e884f2f9f3d001488888929b8affe053a60a7a780af7d0ec8d7023023f40ca52 *electron-v32.2.7-linux-armv7l-debug.zip +f338ea7ea592c3ccdad1bb788e9b936610f825ac69290e48d394790d5266dce3 *electron-v32.2.7-linux-armv7l-symbols.zip +4a95643e88cadfb011354d25cafe242cdb8c5327d65f86b4fbabe64717367ed2 *electron-v32.2.7-linux-armv7l.zip +e6e0fce9f6d95a84653b537b741967cae48c4c70c8026c02293c979074225b46 *electron-v32.2.7-linux-x64-debug.zip +122cc565d0ccd2774e298645473869752d27d2632aa97583d93b499e9b02f22b *electron-v32.2.7-linux-x64-symbols.zip +98007545e1d3700b32de5cb5eebcc10b9d105fb0dad6396155fdab1b40abb638 *electron-v32.2.7-linux-x64.zip +556d9ca239ee1206c9d67affa836ebb651db88eea6bee48cb7b43fa75851c72d *electron-v32.2.7-mas-arm64-dsym-snapshot.zip +662a3742b94fcbf7ab91a7c20e1430825ae7852e915fcb558d6357a310d631c6 *electron-v32.2.7-mas-arm64-dsym.zip +edd0763ead7ffd5bf5072539e5ca0be9252b9590e674e6e44e69b2057c329d79 *electron-v32.2.7-mas-arm64-symbols.zip +a4483f5246ecadfa48b1fc671d92b5dfbc09fbd88fe386f2ce48f10de79f2127 *electron-v32.2.7-mas-arm64.zip +a9aad4c413d4851fa3463eeef7015e3a3e77a501192965db1c5b870fa31a9660 *electron-v32.2.7-mas-x64-dsym-snapshot.zip +96c20e5c4b73febd3458679e9cc939f5f8255a327b06f49188ab2e3fe8311ea3 *electron-v32.2.7-mas-x64-dsym.zip +6ac844957373114e04411d3af1cb6507e35174d1dc279cce41cb92bbf2ea5d26 *electron-v32.2.7-mas-x64-symbols.zip +888b830b991dab6cf2c4351e112a48f24a4748efefcd763d693a79161199e65a *electron-v32.2.7-mas-x64.zip +27759db6bcdd16d4ff5548684361ba4372d885d3142bf02db59837c3634b1934 *electron-v32.2.7-win32-arm64-pdb.zip +6019e6ec58e9b6da335f20874efebc42d034a179163180b3b6faedf2963ae577 *electron-v32.2.7-win32-arm64-symbols.zip +48b81d28fdceb4ab3ca27650d79bab910a1a19dbda72271882bfdc877c71975f *electron-v32.2.7-win32-arm64-toolchain-profile.zip +2c755fdd4f9fda618b2db6b8c7210c5f3106a88b1e87b83e8433b4ab4a628cc2 *electron-v32.2.7-win32-arm64.zip +4dce0b21d1c2093cc4f7c0eaf9453a38377e0076d811da3c7391f105fc1d6afb *electron-v32.2.7-win32-ia32-pdb.zip +9a0a9c3746cd40ddc9c926755633b16676714e2138d7a2d888f658a26f617039 *electron-v32.2.7-win32-ia32-symbols.zip +48b81d28fdceb4ab3ca27650d79bab910a1a19dbda72271882bfdc877c71975f *electron-v32.2.7-win32-ia32-toolchain-profile.zip +6c338c5cd0b0587349ab0f119ca8f7d2728b1c3a43fe241741087f5fdf139c9c *electron-v32.2.7-win32-ia32.zip +fa240d324c5376aa12ed2aef26597764d9bfc2fdd0d16d7f76afc2c3e3c65a29 *electron-v32.2.7-win32-x64-pdb.zip +f645b53771cbcdfaa041d9cf9581348821d82c1b185ddb913759e2d62ee2410a *electron-v32.2.7-win32-x64-symbols.zip +48b81d28fdceb4ab3ca27650d79bab910a1a19dbda72271882bfdc877c71975f *electron-v32.2.7-win32-x64-toolchain-profile.zip +819ab19b7111dfd39dff506b3cb5cd2e1d8f4bb17f96ba74b987b2eac14b6c63 *electron-v32.2.7-win32-x64.zip +ce41b10c28bd43249cd3b409e081b1c83a2b691381bdd2e3bf208ec40ca176b8 *electron.d.ts +d2491071a641ce2e0f63c1f52e3a412856dd83ca17d021af1166d6e5b4de5638 *ffmpeg-v32.2.7-darwin-arm64.zip +5c5589b2c93f834e595eb692aa768b934245d2631df69bc4cad3a6602bba0e67 *ffmpeg-v32.2.7-darwin-x64.zip +3f1eafaf4cd90ab43ba0267429189be182435849a166a2cbe1faefc0d07217c4 *ffmpeg-v32.2.7-linux-arm64.zip +3db919bc57e1a5bf7c1bae1d7aeacf4a331990ea82750391c0b24a046d9a2812 *ffmpeg-v32.2.7-linux-armv7l.zip +fe7d779dddbfb5da5999a7607fc5e3c7a6ab7c65e8da9fee1384918865231612 *ffmpeg-v32.2.7-linux-x64.zip +feeef1ab10543c813f730cc7a482b43eda35d40f1285b950e1a6d7805db2332a *ffmpeg-v32.2.7-mas-arm64.zip +96ef45180589c854fedf2d0601a20e70a65220c0820c45d0dfd4ec64724c58e0 *ffmpeg-v32.2.7-mas-x64.zip +ab4ab9cd62e40c4d3064004caa9de680cb72d8180d4facc1be06bdc886c23410 *ffmpeg-v32.2.7-win32-arm64.zip +90b5e2ebd4ff683eda97cc43ebbdee9b133b27edd2a34ae7ef37e7969d1d68be *ffmpeg-v32.2.7-win32-ia32.zip +8452085c0a650035f30a4b76e2ce1791f9b392ea7262109d29f7fe383fc41ddb *ffmpeg-v32.2.7-win32-x64.zip +78b415ebb9040dacabb6eb776a8d4837dda9a9b1ec9d64ee15db28dbb8598862 *hunspell_dictionaries.zip +a30057c37e6be5732944084575a2278616297242ae51bd474c683263cbc0c3e4 *libcxx-objects-v32.2.7-linux-arm64.zip +f9e9d1ff1a03a3e609ab8e727b1f89e77934509a4afdb849698b70e701c2176f *libcxx-objects-v32.2.7-linux-armv7l.zip +bb66e3b48f8e0706126b2b8b08827a4adda6f56c509eae4d136fcffd5414c353 *libcxx-objects-v32.2.7-linux-x64.zip +5181518d7da83fea5d8b033ab4fb7ed300f73bd8d20b8c26b624128233bd6ab2 *libcxx_headers.zip +6030ad099859b62cbdd9021b2cdb453a744a2751cb1dab30519e3e8708ad72d6 *libcxxabi_headers.zip +d3dcc4925a6bd55bc305fd41805ffee77dc8821730ac75cf4ee9ed2ca4ebdccb *mksnapshot-v32.2.7-darwin-arm64.zip +e6dfad3c30f4f38509b2fc972dd05cef06142c4832d931edba19742e06161279 *mksnapshot-v32.2.7-darwin-x64.zip +25ba5be47a721700f16af10945e71408ed86ffd6800b5d5ef04d38c0d77aa446 *mksnapshot-v32.2.7-linux-arm64-x64.zip +f7e8b50691712206587d81844bd63271f2dd49253c946a5b66bd6f169ccf94d6 *mksnapshot-v32.2.7-linux-armv7l-x64.zip +a0b119abe93c0231601b6c699cce4b78e89def766c24f9a8a06cfab3feca8f6c *mksnapshot-v32.2.7-linux-x64.zip +e3e8a496a1eaf6c8ce623fa4b139e5458cf3ce3702ea3560cded839087b60792 *mksnapshot-v32.2.7-mas-arm64.zip +c03219273c82022c29e277d07ce1d0980d25c22d39269fa3eef9547f57ec410b *mksnapshot-v32.2.7-mas-x64.zip +7684cb9c6f621db05b6e68080fade81f46d0ff8eeac94080bd635f035069d13e *mksnapshot-v32.2.7-win32-arm64-x64.zip +f7ca1d557e3d0f878b13f57dc0e00932f7a97f3dd0f0cc3bbbd565a06718bd17 *mksnapshot-v32.2.7-win32-ia32.zip +d9d8dd33561eb648e5ebd00f99418122d9a915ec63fe967e7cb0ff64ef8ee199 *mksnapshot-v32.2.7-win32-x64.zip diff --git a/cgmanifest.json b/cgmanifest.json index 832a3f604b78..1795c44c9372 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "10e0ce069260b2cc984c301d5a7ecb3492f0c2f0" + "commitHash": "3007f859dad930ae80bafffc6042a146a45e4e4d" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "32.2.6" + "version": "32.2.7" }, { "component": { diff --git a/package-lock.json b/package-lock.json index 5f2b14c3e397..23a24c728bdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,7 +95,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "32.2.6", + "electron": "32.2.7", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", @@ -6103,9 +6103,9 @@ "dev": true }, "node_modules/electron": { - "version": "32.2.6", - "resolved": "https://registry.npmjs.org/electron/-/electron-32.2.6.tgz", - "integrity": "sha512-aGG1MLvWCf+ECUFBCmaCF52F8312OPAJfph2D0FSsFmlbfnJuNevZCbty2lFzsiIMtU7/QRo6d0ksbgR4s7y3w==", + "version": "32.2.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-32.2.7.tgz", + "integrity": "sha512-y8jbQRG3xogF70XPlk5c+dWe5iRfUBo28o2NMpKd/CcW7ENIaWtBlGima8/8nmRdAaYTy1+yIt6KB0Lon9H8cA==", "dev": true, "hasInstallScript": true, "license": "MIT", diff --git a/package.json b/package.json index 3316ae225ea8..ba5004eed3e1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.97.0", - "distro": "45ffe59ad7c51acf28b541a2aa76cc73437ff118", + "distro": "baf3347105f6082ae1df942fd1c052cd06f7a7f0", "author": { "name": "Microsoft Corporation" }, @@ -153,7 +153,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "32.2.6", + "electron": "32.2.7", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", From fca210cd103a496f25c23786b861a67f4d1ee16b Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:56:24 +0100 Subject: [PATCH 0333/3587] Git - escape shell-sensitive characters (#236849) --- extensions/git/src/git.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 62bae1422df0..5bdfd655dbc4 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -348,10 +348,15 @@ function getGitErrorCode(stderr: string): string | undefined { return undefined; } -// https://github.com/microsoft/vscode/issues/89373 -// https://github.com/git-for-windows/git/issues/2478 function sanitizePath(path: string): string { - return path.replace(/^([a-z]):\\/i, (_, letter) => `${letter.toUpperCase()}:\\`); + return path + // Drive letter + // https://github.com/microsoft/vscode/issues/89373 + // https://github.com/git-for-windows/git/issues/2478 + .replace(/^([a-z]):\\/i, (_, letter) => `${letter.toUpperCase()}:\\`) + // Shell-sensitive characters + // https://github.com/microsoft/vscode/issues/133566 + .replace(/(["'\\\$!><#()\[\]*&^| ;{}?`])/g, '\\$1'); } const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%D%n%B'; From 4fa5611d67dc84e105e9cd155a746f2d7813d9a0 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 24 Dec 2024 02:02:40 +0100 Subject: [PATCH 0334/3587] Git - handle the diff editor for untracked files now that we throw `FileNotFound` if the file does not exist (#236863) --- extensions/git/src/fileSystemProvider.ts | 3 ++- extensions/git/src/repository.ts | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/extensions/git/src/fileSystemProvider.ts b/extensions/git/src/fileSystemProvider.ts index 24ae4e6df9a7..0847fe8d745a 100644 --- a/extensions/git/src/fileSystemProvider.ts +++ b/extensions/git/src/fileSystemProvider.ts @@ -192,7 +192,8 @@ export class GitFileSystemProvider implements FileSystemProvider { try { return await repository.buffer(sanitizeRef(ref, path, repository), path); } catch (err) { - // File does not exist in git (ex: git ignored) + // File does not exist in git. This could be + // because the file is untracked or ignored throw FileSystemError.FileNotFound(); } } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index ec66d510c720..4bf7fa32ef0e 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -560,13 +560,11 @@ class ResourceCommandResolver { switch (resource.type) { case Status.INDEX_MODIFIED: case Status.INDEX_RENAMED: - case Status.INDEX_ADDED: case Status.INTENT_TO_RENAME: case Status.TYPE_CHANGED: return { original: toGitUri(resource.original, 'HEAD') }; case Status.MODIFIED: - case Status.UNTRACKED: return { original: toGitUri(resource.resourceUri, '~') }; case Status.DELETED_BY_US: From 5cc442a4186e7cf97d3809ddc23031b8839f2fce Mon Sep 17 00:00:00 2001 From: Dmitry Sonder Date: Wed, 25 Dec 2024 14:13:11 +0700 Subject: [PATCH 0335/3587] refactor: use EventType constants for events --- src/vs/editor/browser/editorDom.ts | 6 +++--- .../electron-sandbox/parts/titlebar/titlebarPart.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/browser/editorDom.ts b/src/vs/editor/browser/editorDom.ts index 38100ab8407d..e42959d81b6f 100644 --- a/src/vs/editor/browser/editorDom.ts +++ b/src/vs/editor/browser/editorDom.ts @@ -150,13 +150,13 @@ export class EditorMouseEventFactory { } public onContextMenu(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable { - return dom.addDisposableListener(target, 'contextmenu', (e: MouseEvent) => { + return dom.addDisposableListener(target, dom.EventType.CONTEXT_MENU, (e: MouseEvent) => { callback(this._create(e)); }); } public onMouseUp(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable { - return dom.addDisposableListener(target, 'mouseup', (e: MouseEvent) => { + return dom.addDisposableListener(target, dom.EventType.MOUSE_UP, (e: MouseEvent) => { callback(this._create(e)); }); } @@ -180,7 +180,7 @@ export class EditorMouseEventFactory { } public onMouseMove(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable { - return dom.addDisposableListener(target, 'mousemove', (e) => callback(this._create(e))); + return dom.addDisposableListener(target, dom.EventType.MOUSE_MOVE, (e) => callback(this._create(e))); } } diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index a4f1a964afe3..04095aab5d76 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts @@ -200,7 +200,7 @@ export class NativeTitlebarPart extends BrowserTitlebarPart { } const zoomFactor = getZoomFactor(getWindow(this.element)); - this.onContextMenu(new MouseEvent('mouseup', { clientX: x / zoomFactor, clientY: y / zoomFactor }), MenuId.TitleBarContext); + this.onContextMenu(new MouseEvent(EventType.MOUSE_UP, { clientX: x / zoomFactor, clientY: y / zoomFactor }), MenuId.TitleBarContext); })); } From 9d83bf14f1d6d329ec72bd657e24f710192459fc Mon Sep 17 00:00:00 2001 From: Mohankumar Ramachandran Date: Thu, 26 Dec 2024 10:03:02 +0000 Subject: [PATCH 0336/3587] Change identifier to id --- src/vs/workbench/contrib/chat/common/languageModels.ts | 4 ++-- .../workbench/contrib/chat/test/common/languageModels.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index b3310ea5332d..66fee6ef6c27 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -101,7 +101,7 @@ export interface ILanguageModelChat { export interface ILanguageModelChatSelector { readonly name?: string; - readonly identifier?: string; + readonly id?: string; readonly vendor?: string; readonly version?: string; readonly family?: string; @@ -264,7 +264,7 @@ export class LanguageModelsService implements ILanguageModelsService { if ((selector.vendor === undefined || model.metadata.vendor === selector.vendor) && (selector.family === undefined || model.metadata.family === selector.family) && (selector.version === undefined || model.metadata.version === selector.version) - && (selector.identifier === undefined || model.metadata.id === selector.identifier) + && (selector.id === undefined || model.metadata.id === selector.id) && (!model.metadata.targetExtensions || model.metadata.targetExtensions.some(candidate => ExtensionIdentifier.equals(candidate, selector.extension))) ) { result.push(identifier); diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts index bc148fcd6935..06560d694d6e 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -144,7 +144,7 @@ suite('LanguageModels', function () { } })); - const models = await languageModels.selectLanguageModels({ identifier: 'actual-lm' }); + const models = await languageModels.selectLanguageModels({ id: 'actual-lm' }); assert.ok(models.length === 1); const first = models[0]; From 7da68c033c76bd21d5faabe830f255550a5c7710 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 27 Dec 2024 16:50:29 +1100 Subject: [PATCH 0337/3587] Remove console.log (#236006) --- .../browser/contrib/chatEdit/notebookSynchronizerService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts index 1e4cf151fae6..9788828cf870 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts @@ -28,7 +28,6 @@ class NotebookSynchronizerSaveParticipant extends NotebookSaveParticipant { } override async participate(workingCopy: IStoredFileWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress, token: CancellationToken): Promise { - console.log('notebook synchronizer participate'); const session = this._chatEditingService.currentEditingSessionObs.get(); if (!session) { From 0a66dc39ff0a3476dd5f711df232cba4d713a57d Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 27 Dec 2024 14:31:53 -0500 Subject: [PATCH 0338/3587] Adopt concept of flows in Microsoft Auth (#237006) And only use Loopback flow when not running in Remote Extension Host. --- .../src/node/authProvider.ts | 125 ++++++++---------- .../src/node/flows.ts | 105 +++++++++++++++ 2 files changed, 159 insertions(+), 71 deletions(-) create mode 100644 extensions/microsoft-authentication/src/node/flows.ts diff --git a/extensions/microsoft-authentication/src/node/authProvider.ts b/extensions/microsoft-authentication/src/node/authProvider.ts index cc8eb2bc5c7d..0a352c8eb868 100644 --- a/extensions/microsoft-authentication/src/node/authProvider.ts +++ b/extensions/microsoft-authentication/src/node/authProvider.ts @@ -3,18 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { AccountInfo, AuthenticationResult, ClientAuthError, ClientAuthErrorCodes, ServerError } from '@azure/msal-node'; -import { AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationProviderSessionOptions, AuthenticationSession, AuthenticationSessionAccountInformation, CancellationError, env, EventEmitter, ExtensionContext, l10n, LogOutputChannel, Uri, window } from 'vscode'; +import { AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationProviderSessionOptions, AuthenticationSession, AuthenticationSessionAccountInformation, CancellationError, EventEmitter, ExtensionContext, ExtensionKind, l10n, LogOutputChannel, window } from 'vscode'; import { Environment } from '@azure/ms-rest-azure-env'; import { CachedPublicClientApplicationManager } from './publicClientCache'; -import { UriHandlerLoopbackClient } from '../common/loopbackClientAndOpener'; import { UriEventHandler } from '../UriEventHandler'; import { ICachedPublicClientApplication } from '../common/publicClientCache'; import { MicrosoftAccountType, MicrosoftAuthenticationTelemetryReporter } from '../common/telemetryReporter'; -import { loopbackTemplate } from './loopbackTemplate'; import { ScopeData } from '../common/scopeData'; import { EventBufferer } from '../common/event'; import { BetterTokenStorage } from '../betterSecretStorage'; import { IStoredSession } from '../AADHelper'; +import { ExtensionHost, getMsalFlows } from './flows'; const redirectUri = 'https://vscode.dev/redirect'; const MSA_TID = '9188040d-6c67-4c5b-b112-36a304b66dad'; @@ -187,86 +186,70 @@ export class MsalAuthProvider implements AuthenticationProvider { this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'starting'); const cachedPca = await this.getOrCreatePublicClientApplication(scopeData.clientId, scopeData.tenant); - let result: AuthenticationResult | undefined; - - try { - const windowHandle = window.nativeHandle ? Buffer.from(window.nativeHandle) : undefined; - result = await cachedPca.acquireTokenInteractive({ - openBrowser: async (url: string) => { await env.openExternal(Uri.parse(url)); }, - scopes: scopeData.scopesToSend, - // The logic for rendering one or the other of these templates is in the - // template itself, so we pass the same one for both. - successTemplate: loopbackTemplate, - errorTemplate: loopbackTemplate, - // Pass the label of the account to the login hint so that we prefer signing in to that account - loginHint: options.account?.label, - // If we aren't logging in to a specific account, then we can use the prompt to make sure they get - // the option to choose a different account. - prompt: options.account?.label ? undefined : 'select_account', - windowHandle - }); - } catch (e) { - if (e instanceof CancellationError) { - const yes = l10n.t('Yes'); - const result = await window.showErrorMessage( - l10n.t('Having trouble logging in?'), - { - modal: true, - detail: l10n.t('Would you like to try a different way to sign in to your Microsoft account? ({0})', 'protocol handler') - }, - yes - ); - if (!result) { - this._telemetryReporter.sendLoginFailedEvent(); - throw e; - } + + // Used for showing a friendlier message to the user when the explicitly cancel a flow. + let userCancelled: boolean | undefined; + const yes = l10n.t('Yes'); + const no = l10n.t('No'); + const promptToContinue = async (mode: string) => { + if (userCancelled === undefined) { + // We haven't had a failure yet so wait to prompt + return; } - // This error comes from the backend and is likely not due to the user's machine - // failing to open a port or something local that would require us to try the - // URL handler loopback client. - if (e instanceof ServerError) { - this._telemetryReporter.sendLoginFailedEvent(); - throw e; + const message = userCancelled + ? l10n.t('Having trouble logging in? Would you like to try a different way? ({0})', mode) + : l10n.t('You have not yet finished authorizing this extension to use your Microsoft Account. Would you like to try a different way? ({0})', mode); + const result = await window.showWarningMessage(message, yes, no); + if (result !== yes) { + throw new CancellationError(); } + }; - // The user closed the modal window - if ((e as ClientAuthError).errorCode === ClientAuthErrorCodes.userCanceled) { - this._telemetryReporter.sendLoginFailedEvent(); - throw e; - } + const flows = getMsalFlows({ + extensionHost: typeof navigator === 'undefined' + ? this._context.extension.extensionKind === ExtensionKind.UI ? ExtensionHost.Local : ExtensionHost.Remote + : ExtensionHost.WebWorker, + }); - // The user wants to try the loopback client or we got an error likely due to spinning up the server - const loopbackClient = new UriHandlerLoopbackClient(this._uriHandler, redirectUri, this._logger); + let lastError: Error | undefined; + for (const flow of flows) { + if (flow !== flows[0]) { + try { + await promptToContinue(flow.label); + } finally { + this._telemetryReporter.sendLoginFailedEvent(); + } + } try { - const windowHandle = window.nativeHandle ? Buffer.from(window.nativeHandle) : undefined; - result = await cachedPca.acquireTokenInteractive({ - openBrowser: (url: string) => loopbackClient.openBrowser(url), + const result = await flow.trigger({ + cachedPca, scopes: scopeData.scopesToSend, - loopbackClient, loginHint: options.account?.label, - prompt: options.account?.label ? undefined : 'select_account', - windowHandle + windowHandle: window.nativeHandle ? Buffer.from(window.nativeHandle) : undefined, + logger: this._logger, + uriHandler: this._uriHandler }); + + const session = this.sessionFromAuthenticationResult(result, scopeData.originalScopes); + this._telemetryReporter.sendLoginEvent(session.scopes); + this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'returned session'); + this._onDidChangeSessionsEmitter.fire({ added: [session], changed: [], removed: [] }); + return session; } catch (e) { - this._telemetryReporter.sendLoginFailedEvent(); - throw e; + lastError = e; + if (e instanceof ServerError || (e as ClientAuthError)?.errorCode === ClientAuthErrorCodes.userCanceled) { + this._telemetryReporter.sendLoginFailedEvent(); + throw e; + } + // Continue to next flow + if (e instanceof CancellationError) { + userCancelled = true; + } } } - if (!result) { - this._telemetryReporter.sendLoginFailedEvent(); - throw new Error('No result returned from MSAL'); - } - - const session = this.sessionFromAuthenticationResult(result, scopeData.originalScopes); - this._telemetryReporter.sendLoginEvent(session.scopes); - this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'returned session'); - // This is the only scenario in which we need to fire the _onDidChangeSessionsEmitter out of band... - // the badge flow (when the client passes no options in to getSession) will only remove a badge if a session - // was created that _matches the scopes_ that that badge requests. See `onDidChangeSessions` for more info. - // TODO: This should really be fixed in Core. - this._onDidChangeSessionsEmitter.fire({ added: [session], changed: [], removed: [] }); - return session; + this._telemetryReporter.sendLoginFailedEvent(); + throw lastError ?? new Error('No auth flow succeeded'); } async removeSession(sessionId: string): Promise { diff --git a/extensions/microsoft-authentication/src/node/flows.ts b/extensions/microsoft-authentication/src/node/flows.ts new file mode 100644 index 000000000000..3e1d8c513f08 --- /dev/null +++ b/extensions/microsoft-authentication/src/node/flows.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AuthenticationResult } from '@azure/msal-node'; +import { Uri, LogOutputChannel, env } from 'vscode'; +import { ICachedPublicClientApplication } from '../common/publicClientCache'; +import { UriHandlerLoopbackClient } from '../common/loopbackClientAndOpener'; +import { UriEventHandler } from '../UriEventHandler'; +import { loopbackTemplate } from './loopbackTemplate'; + +const redirectUri = 'https://vscode.dev/redirect'; + +export const enum ExtensionHost { + WebWorker, + Remote, + Local +} + +interface IMsalFlowOptions { + supportsRemoteExtensionHost: boolean; + supportsWebWorkerExtensionHost: boolean; +} + +interface IMsalFlowTriggerOptions { + cachedPca: ICachedPublicClientApplication; + scopes: string[]; + loginHint?: string; + windowHandle?: Buffer; + logger: LogOutputChannel; + uriHandler: UriEventHandler; +} + +interface IMsalFlow { + readonly label: string; + readonly options: IMsalFlowOptions; + trigger(options: IMsalFlowTriggerOptions): Promise; +} + +class DefaultLoopbackFlow implements IMsalFlow { + label = 'default'; + options: IMsalFlowOptions = { + supportsRemoteExtensionHost: true, + supportsWebWorkerExtensionHost: true + }; + + async trigger({ cachedPca, scopes, loginHint, windowHandle, logger }: IMsalFlowTriggerOptions): Promise { + logger.info('Trying default msal flow...'); + return await cachedPca.acquireTokenInteractive({ + openBrowser: async (url: string) => { await env.openExternal(Uri.parse(url)); }, + scopes, + successTemplate: loopbackTemplate, + errorTemplate: loopbackTemplate, + loginHint, + prompt: loginHint ? undefined : 'select_account', + windowHandle + }); + } +} + +class UrlHandlerFlow implements IMsalFlow { + label = 'protocol handler'; + options: IMsalFlowOptions = { + supportsRemoteExtensionHost: false, + supportsWebWorkerExtensionHost: false + }; + + async trigger({ cachedPca, scopes, loginHint, windowHandle, logger, uriHandler }: IMsalFlowTriggerOptions): Promise { + logger.info('Trying protocol handler flow...'); + const loopbackClient = new UriHandlerLoopbackClient(uriHandler, redirectUri, logger); + return await cachedPca.acquireTokenInteractive({ + openBrowser: (url: string) => loopbackClient.openBrowser(url), + scopes, + loopbackClient, + loginHint, + prompt: loginHint ? undefined : 'select_account', + windowHandle + }); + } +} + +const allFlows: IMsalFlow[] = [ + new DefaultLoopbackFlow(), + new UrlHandlerFlow() +]; + +export interface IMsalFlowQuery { + extensionHost: ExtensionHost; +} + +export function getMsalFlows(query: IMsalFlowQuery): IMsalFlow[] { + return allFlows.filter(flow => { + let useFlow: boolean = true; + switch (query.extensionHost) { + case ExtensionHost.Remote: + useFlow &&= flow.options.supportsRemoteExtensionHost; + break; + case ExtensionHost.WebWorker: + useFlow &&= flow.options.supportsWebWorkerExtensionHost; + break; + } + return useFlow; + }); +} From 1410d77f6fdd80ceb2e522449096fc97e821aa01 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Sat, 28 Dec 2024 03:36:05 -0500 Subject: [PATCH 0339/3587] reverse cancellation and sequencer (#237029) When we cancel, then the promise should be cancelled. If we don't do this, we hang on the first interaction request until we timeout. Fixes the 2nd point in https://github.com/microsoft/vscode/issues/236825#issuecomment-2563882150 --- .../src/node/cachedPublicClientApplication.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts index 0f27c2c0e4d6..616b3e1120d5 100644 --- a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts +++ b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts @@ -133,11 +133,11 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica cancellable: true, title: l10n.t('Signing in to Microsoft...') }, - (_process, token) => raceCancellationAndTimeoutError( - this._sequencer.queue(() => this._pca.acquireTokenInteractive(request)), + (_process, token) => this._sequencer.queue(() => raceCancellationAndTimeoutError( + this._pca.acquireTokenInteractive(request), token, 1000 * 60 * 5 - ) + )) ); // this._setupRefresh(result); if (this._isBrokerAvailable) { From a3261eae42b18ff8cb685cf649c27c8b27985eec Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Sun, 29 Dec 2024 08:44:19 +0100 Subject: [PATCH 0340/3587] Fix default tree find modes (#237057) fix #236770 --- src/vs/base/browser/ui/tree/abstractTree.ts | 2 +- src/vs/base/browser/ui/tree/asyncDataTree.ts | 27 ++++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 09ce418bda94..cff0e6614d3a 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -947,7 +947,7 @@ interface IAbstractFindControllerOptions extends IFindWidgetOptions { showNotFoundMessage?: boolean; } -interface IFindControllerOptions extends IAbstractFindControllerOptions { +export interface IFindControllerOptions extends IAbstractFindControllerOptions { defaultFindMode?: TreeFindMode; defaultFindMatchType?: TreeFindMatchType; } diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index dd6059051f08..f6c47bb9283e 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -7,7 +7,7 @@ import { IDragAndDropData } from '../../dnd.js'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction, IListVirtualDelegate } from '../list/list.js'; import { ElementsDragAndDropData, ListViewTargetSector } from '../list/listView.js'; import { IListStyles } from '../list/listWidget.js'; -import { ComposedTreeDelegate, TreeFindMode as TreeFindMode, IAbstractTreeOptions, IAbstractTreeOptionsUpdate, TreeFindMatchType, AbstractTreePart, LabelFuzzyScore, FindFilter, FindController, ITreeFindToggleChangeEvent } from './abstractTree.js'; +import { ComposedTreeDelegate, TreeFindMode as TreeFindMode, IAbstractTreeOptions, IAbstractTreeOptionsUpdate, TreeFindMatchType, AbstractTreePart, LabelFuzzyScore, FindFilter, FindController, ITreeFindToggleChangeEvent, IFindControllerOptions } from './abstractTree.js'; import { ICompressedTreeElement, ICompressedTreeNode } from './compressedObjectTreeModel.js'; import { getVisibleState, isFilterResult } from './indexTreeModel.js'; import { CompressibleObjectTree, ICompressibleKeyboardNavigationLabelProvider, ICompressibleObjectTreeOptions, ICompressibleTreeRenderer, IObjectTreeOptions, IObjectTreeSetChildrenOptions, ObjectTree } from './objectTree.js'; @@ -629,7 +629,12 @@ export class AsyncDataTree implements IDisposable this.tree.onDidChangeCollapseState(this._onDidChangeCollapseState, this, this.disposables); if (asyncFindEnabled) { - const findOptions = { styles: options.findWidgetStyles, showNotFoundMessage: options.showNotFoundMessage }; + const findOptions: IFindControllerOptions = { + styles: options.findWidgetStyles, + showNotFoundMessage: options.showNotFoundMessage, + defaultFindMatchType: options.defaultFindMatchType, + defaultFindMode: options.defaultFindMode, + }; this.findController = this.disposables.add(new AsyncFindController(this.tree, options.findProvider!, findFilter!, this.tree.options.contextViewProvider!, findOptions)); this.focusNavigationFilter = node => this.findController!.shouldFocusWhenNavigating(node); @@ -657,8 +662,18 @@ export class AsyncDataTree implements IDisposable return new ObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions); } - updateOptions(options: IAsyncDataTreeOptionsUpdate = {}): void { - this.tree.updateOptions(options); + updateOptions(optionsUpdate: IAsyncDataTreeOptionsUpdate = {}): void { + if (this.findController) { + if (optionsUpdate.defaultFindMode !== undefined) { + this.findController.mode = optionsUpdate.defaultFindMode; + } + + if (optionsUpdate.defaultFindMatchType !== undefined) { + this.findController.matchType = optionsUpdate.defaultFindMatchType; + } + } + + this.tree.updateOptions(optionsUpdate); } get options(): IAsyncDataTreeOptions { @@ -1513,10 +1528,6 @@ export class CompressibleAsyncDataTree extends As }; } - override updateOptions(options: ICompressibleAsyncDataTreeOptionsUpdate = {}): void { - this.tree.updateOptions(options); - } - override getViewState(): IAsyncDataTreeViewState { if (!this.identityProvider) { throw new TreeError(this.user, 'Can\'t get tree view state without an identity provider'); From a91e6b684ba2cc23ce4da04874004b4cf9b2e952 Mon Sep 17 00:00:00 2001 From: aslezar <97354675+aslezar@users.noreply.github.com> Date: Sun, 29 Dec 2024 15:24:38 +0530 Subject: [PATCH 0341/3587] feat: support custom js switch-case indentation --- extensions/typescript-language-features/package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 51ac7bd20c94..0ad561675b63 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -532,6 +532,12 @@ "description": "%format.indentSwitchCase%", "scope": "resource" }, + "javascript.format.indentSwitchCase": { + "type": "boolean", + "default": true, + "description": "%format.indentSwitchCase%", + "scope": "resource" + }, "javascript.validate.enable": { "type": "boolean", "default": true, From 6d6cfdc3a6a1836a29a7034d88958c0b91df5def Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 30 Dec 2024 21:10:42 +0100 Subject: [PATCH 0342/3587] Git - add git blame editor decoration hover provider (#237102) * Initial implementation * Refactor editor decoration type --- extensions/git/src/blame.ts | 190 +++++++++++++++++++++++++++--------- 1 file changed, 145 insertions(+), 45 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 5c65fbcf1fcc..753af17766ab 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString } from 'vscode'; +import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString, languages, HoverProvider, CancellationToken, Hover, TextDocument } from 'vscode'; import { Model } from './model'; import { dispose, fromNow, IDisposable } from './util'; import { Repository } from './repository'; import { throttle } from './decorators'; -import { BlameInformation } from './git'; +import { BlameInformation, Commit } from './git'; import { fromGitUri, isGitUri } from './uri'; import { emojify, ensureEmojis } from './emoji'; import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation } from './staging'; @@ -55,6 +55,15 @@ function mapModifiedLineNumberToOriginalLineNumber(lineNumber: number, changes: return lineNumber; } +function getEditorDecorationRange(lineNumber: number): Range { + const position = new Position(lineNumber, Number.MAX_SAFE_INTEGER); + return new Range(position, position); +} + +function isBlameInformation(object: any): object is BlameInformation { + return Array.isArray((object as BlameInformation).ranges); +} + type BlameInformationTemplateTokens = { readonly hash: string; readonly hashShort: string; @@ -191,32 +200,63 @@ export class GitBlameController { }); } - getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation | string): MarkdownString { - if (typeof blameInformation === 'string') { - return new MarkdownString(blameInformation, true); + async getBlameInformationDetailedHover(documentUri: Uri, blameInformation: BlameInformation): Promise { + const repository = this._model.getRepository(documentUri); + if (!repository) { + return this.getBlameInformationHover(documentUri, blameInformation); + } + + try { + const commit = await repository.getCommit(blameInformation.hash); + return this.getBlameInformationHover(documentUri, commit); + } catch { + return this.getBlameInformationHover(documentUri, blameInformation); } + } + getBlameInformationHover(documentUri: Uri, blameInformationOrCommit: BlameInformation | Commit): MarkdownString { const markdownString = new MarkdownString(); - markdownString.supportThemeIcons = true; markdownString.isTrusted = true; + markdownString.supportHtml = true; + markdownString.supportThemeIcons = true; - if (blameInformation.authorName) { - markdownString.appendMarkdown(`$(account) **${blameInformation.authorName}**`); + if (blameInformationOrCommit.authorName) { + markdownString.appendMarkdown(`$(account) **${blameInformationOrCommit.authorName}**`); - if (blameInformation.authorDate) { - const dateString = new Date(blameInformation.authorDate).toLocaleString(undefined, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); - markdownString.appendMarkdown(`, $(history) ${fromNow(blameInformation.authorDate, true, true)} (${dateString})`); + if (blameInformationOrCommit.authorDate) { + const dateString = new Date(blameInformationOrCommit.authorDate).toLocaleString(undefined, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); + markdownString.appendMarkdown(`, $(history) ${fromNow(blameInformationOrCommit.authorDate, true, true)} (${dateString})`); } markdownString.appendMarkdown('\n\n'); } - markdownString.appendMarkdown(`${emojify(blameInformation.subject ?? '')}\n\n`); + markdownString.appendMarkdown(`${emojify(isBlameInformation(blameInformationOrCommit) ? blameInformationOrCommit.subject ?? '' : blameInformationOrCommit.message)}\n\n`); markdownString.appendMarkdown(`---\n\n`); - markdownString.appendMarkdown(`[$(eye) View Commit](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformation.hash]))} "${l10n.t('View Commit')}")`); + if (!isBlameInformation(blameInformationOrCommit) && blameInformationOrCommit.shortStat) { + markdownString.appendMarkdown(`${blameInformationOrCommit.shortStat.files === 1 ? + l10n.t('{0} file changed', blameInformationOrCommit.shortStat.files) : + l10n.t('{0} files changed', blameInformationOrCommit.shortStat.files)}`); + + if (blameInformationOrCommit.shortStat.insertions) { + markdownString.appendMarkdown(`, ${blameInformationOrCommit.shortStat.insertions === 1 ? + l10n.t('{0} insertion{1}', blameInformationOrCommit.shortStat.insertions, '(+)') : + l10n.t('{0} insertions{1}', blameInformationOrCommit.shortStat.insertions, '(+)')}`); + } + + if (blameInformationOrCommit.shortStat.deletions) { + markdownString.appendMarkdown(`, ${blameInformationOrCommit.shortStat.deletions === 1 ? + l10n.t('{0} deletion{1}', blameInformationOrCommit.shortStat.deletions, '(-)') : + l10n.t('{0} deletions{1}', blameInformationOrCommit.shortStat.deletions, '(-)')}`); + } + + markdownString.appendMarkdown(`\n\n---\n\n`); + } + + markdownString.appendMarkdown(`[$(eye) View Commit](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformationOrCommit.hash]))} "${l10n.t('View Commit')}")`); markdownString.appendMarkdown('    '); - markdownString.appendMarkdown(`[$(copy) ${blameInformation.hash.substring(0, 8)}](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformation.hash))} "${l10n.t('Copy Commit Hash')}")`); + markdownString.appendMarkdown(`[$(copy) ${blameInformationOrCommit.hash.substring(0, 8)}](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformationOrCommit.hash))} "${l10n.t('Copy Commit Hash')}")`); return markdownString; } @@ -411,36 +451,81 @@ export class GitBlameController { } } -class GitBlameEditorDecoration { - private readonly _decorationType: TextEditorDecorationType; +class GitBlameEditorDecoration implements HoverProvider { + private _decoration: TextEditorDecorationType | undefined; + private get decoration(): TextEditorDecorationType { + if (!this._decoration) { + this._decoration = window.createTextEditorDecorationType({ + after: { + color: new ThemeColor('git.blame.editorDecorationForeground') + } + }); + } + + return this._decoration; + } + + private _hoverDisposable: IDisposable | undefined; private _disposables: IDisposable[] = []; constructor(private readonly _controller: GitBlameController) { - this._decorationType = window.createTextEditorDecorationType({ - after: { - color: new ThemeColor('git.blame.editorDecorationForeground') - } - }); - this._disposables.push(this._decorationType); - workspace.onDidChangeConfiguration(this._onDidChangeConfiguration, this, this._disposables); window.onDidChangeActiveTextEditor(this._onDidChangeActiveTextEditor, this, this._disposables); - this._controller.onDidChangeBlameInformation(e => this._updateDecorations(e), this, this._disposables); + + this._onDidChangeConfiguration(); } - private _onDidChangeConfiguration(e: ConfigurationChangeEvent): void { - if (!e.affectsConfiguration('git.blame.editorDecoration.enabled') && + async provideHover(document: TextDocument, position: Position, token: CancellationToken): Promise { + if (token.isCancellationRequested) { + return undefined; + } + + const textEditor = window.activeTextEditor; + if (!textEditor) { + return undefined; + } + + // Position must be at the end of the line + if (position.character !== document.lineAt(position.line).range.end.character) { + return undefined; + } + + // Get blame information + const blameInformation = this._controller.textEditorBlameInformation + .get(textEditor)?.find(blame => blame.lineNumber === position.line); + + if (!blameInformation || typeof blameInformation.blameInformation === 'string') { + return undefined; + } + + const contents = await this._controller.getBlameInformationDetailedHover(textEditor.document.uri, blameInformation.blameInformation); + + if (!contents || token.isCancellationRequested) { + return undefined; + } + + return { range: getEditorDecorationRange(position.line), contents: [contents] }; + } + + private _onDidChangeConfiguration(e?: ConfigurationChangeEvent): void { + if (e && + !e.affectsConfiguration('git.blame.editorDecoration.enabled') && !e.affectsConfiguration('git.blame.editorDecoration.template')) { return; } - for (const textEditor of window.visibleTextEditors) { - if (this._getConfiguration().enabled) { - this._updateDecorations(textEditor); - } else { - textEditor.setDecorations(this._decorationType, []); + if (this._getConfiguration().enabled) { + if (window.activeTextEditor) { + this._registerHoverProvider(); + this._updateDecorations(window.activeTextEditor); } + } else { + this._decoration?.dispose(); + this._decoration = undefined; + + this._hoverDisposable?.dispose(); + this._hoverDisposable = undefined; } } @@ -449,11 +534,15 @@ class GitBlameEditorDecoration { return; } + // Clear decorations for (const editor of window.visibleTextEditors) { if (editor !== window.activeTextEditor) { - editor.setDecorations(this._decorationType, []); + editor.setDecorations(this.decoration, []); } } + + // Register hover provider + this._registerHoverProvider(); } private _getConfiguration(): { enabled: boolean; template: string } { @@ -472,14 +561,14 @@ class GitBlameEditorDecoration { // Only support resources with `file` and `git` schemes if (textEditor.document.uri.scheme !== 'file' && !isGitUri(textEditor.document.uri)) { - textEditor.setDecorations(this._decorationType, []); + textEditor.setDecorations(this.decoration, []); return; } // Get blame information const blameInformation = this._controller.textEditorBlameInformation.get(textEditor); if (!blameInformation) { - textEditor.setDecorations(this._decorationType, []); + textEditor.setDecorations(this.decoration, []); return; } @@ -488,32 +577,43 @@ class GitBlameEditorDecoration { const contentText = typeof blame.blameInformation !== 'string' ? this._controller.formatBlameInformationMessage(template, blame.blameInformation) : blame.blameInformation; - const hoverMessage = typeof blame.blameInformation !== 'string' - ? this._controller.getBlameInformationHover(textEditor.document.uri, blame.blameInformation) - : undefined; - return this._createDecoration(blame.lineNumber, contentText, hoverMessage); + return this._createDecoration(blame.lineNumber, contentText); }); - textEditor.setDecorations(this._decorationType, decorations); + textEditor.setDecorations(this.decoration, decorations); } - private _createDecoration(lineNumber: number, contentText: string, hoverMessage: MarkdownString | undefined): DecorationOptions { - const position = new Position(lineNumber, Number.MAX_SAFE_INTEGER); - + private _createDecoration(lineNumber: number, contentText: string): DecorationOptions { return { - hoverMessage, - range: new Range(position, position), + range: getEditorDecorationRange(lineNumber), renderOptions: { after: { - contentText: `${contentText}`, + contentText, margin: '0 0 0 50px' } }, }; } + private _registerHoverProvider(): void { + this._hoverDisposable?.dispose(); + + if (window.activeTextEditor?.document.uri.scheme === 'file' || + window.activeTextEditor?.document.uri.scheme === 'git') { + this._hoverDisposable = languages.registerHoverProvider({ + pattern: window.activeTextEditor.document.uri.fsPath + }, this); + } + } + dispose() { + this._decoration?.dispose(); + this._decoration = undefined; + + this._hoverDisposable?.dispose(); + this._hoverDisposable = undefined; + this._disposables = dispose(this._disposables); } } From 97a5f18d0d41171eb00d7a3aa7c1056c5bd35a5f Mon Sep 17 00:00:00 2001 From: aslezar <97354675+aslezar@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:06:32 +0530 Subject: [PATCH 0343/3587] fix: update descriptions for switch-case indentation settings in TypeScript and JavaScript --- extensions/typescript-language-features/package.json | 4 ++-- extensions/typescript-language-features/package.nls.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 0ad561675b63..a58e678e3281 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -529,13 +529,13 @@ "typescript.format.indentSwitchCase": { "type": "boolean", "default": true, - "description": "%format.indentSwitchCase%", + "description": "%typescript.format.indentSwitchCase%", "scope": "resource" }, "javascript.format.indentSwitchCase": { "type": "boolean", "default": true, - "description": "%format.indentSwitchCase%", + "description": "%javascript.format.indentSwitchCase%", "scope": "resource" }, "javascript.validate.enable": { diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 6a7dc26a24d6..d5ea70614987 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -40,7 +40,8 @@ "format.semicolons.ignore": "Don't insert or remove any semicolons.", "format.semicolons.insert": "Insert semicolons at statement ends.", "format.semicolons.remove": "Remove unnecessary semicolons.", - "format.indentSwitchCase": "Indent case clauses in switch statements. Requires using TypeScript 5.1+ in the workspace.", + "typescript.format.indentSwitchCase": "Indent case clauses in switch statements in TypeScript Files. Requires using TypeScript 5.1+ in the workspace.", + "javascript.format.indentSwitchCase": "Indent case clauses in switch statements in JavaScript Files. Requires using TypeScript 5.1+ in the workspace.", "javascript.validate.enable": "Enable/disable JavaScript validation.", "javascript.goToProjectConfig.title": "Go to Project Configuration (jsconfig / tsconfig)", "typescript.goToProjectConfig.title": "Go to Project Configuration (tsconfig)", From ba56ac5291229995c0e0907127668461551911e7 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:44:45 +0100 Subject: [PATCH 0344/3587] Git - refactor git blame code (#237185) Refactor git blame code --- extensions/git/src/blame.ts | 259 ++++++++++++++++++------------------ 1 file changed, 127 insertions(+), 132 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 753af17766ab..3893b4bff4fb 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -64,6 +64,10 @@ function isBlameInformation(object: any): object is BlameInformation { return Array.isArray((object as BlameInformation).ranges); } +function isResourceSchemeSupported(uri: Uri): boolean { + return uri.scheme === 'file' || isGitUri(uri); +} + type BlameInformationTemplateTokens = { readonly hash: string; readonly hashShort: string; @@ -156,28 +160,30 @@ class GitBlameInformationCache { export class GitBlameController { private readonly _subjectMaxLength = 50; - private readonly _onDidChangeBlameInformation = new EventEmitter(); + private readonly _onDidChangeBlameInformation = new EventEmitter(); public readonly onDidChangeBlameInformation = this._onDidChangeBlameInformation.event; - readonly textEditorBlameInformation = new Map(); + private _textEditorBlameInformation: LineBlameInformation[] | undefined; + get textEditorBlameInformation(): readonly LineBlameInformation[] | undefined { + return this._textEditorBlameInformation; + } + private set textEditorBlameInformation(blameInformation: LineBlameInformation[] | undefined) { + this._textEditorBlameInformation = blameInformation; + this._onDidChangeBlameInformation.fire(); + } private readonly _repositoryBlameCache = new GitBlameInformationCache(); + private _editorDecoration: GitBlameEditorDecoration | undefined; + private _statusBarItem: GitBlameStatusBarItem | undefined; + private _repositoryDisposables = new Map(); + private _enablementDisposables: IDisposable[] = []; private _disposables: IDisposable[] = []; constructor(private readonly _model: Model) { - this._disposables.push(new GitBlameEditorDecoration(this)); - this._disposables.push(new GitBlameStatusBarItem(this)); - - this._model.onDidOpenRepository(this._onDidOpenRepository, this, this._disposables); - this._model.onDidCloseRepository(this._onDidCloseRepository, this, this._disposables); - - window.onDidChangeActiveTextEditor(e => this._updateTextEditorBlameInformation(e), this, this._disposables); - window.onDidChangeTextEditorSelection(e => this._updateTextEditorBlameInformation(e.textEditor, true), this, this._disposables); - window.onDidChangeTextEditorDiffInformation(e => this._updateTextEditorBlameInformation(e.textEditor), this, this._disposables); - - this._updateTextEditorBlameInformation(window.activeTextEditor); + workspace.onDidChangeConfiguration(this._onDidChangeConfiguration, this, this._disposables); + this._onDidChangeConfiguration(); } formatBlameInformationMessage(template: string, blameInformation: BlameInformation): string { @@ -261,6 +267,57 @@ export class GitBlameController { return markdownString; } + private _onDidChangeConfiguration(e?: ConfigurationChangeEvent): void { + if (e && + !e.affectsConfiguration('git.blame.editorDecoration.enabled') && + !e.affectsConfiguration('git.blame.statusBarItem.enabled')) { + return; + } + + const config = workspace.getConfiguration('git'); + const editorDecorationEnabled = config.get('blame.editorDecoration.enabled') === true; + const statusBarItemEnabled = config.get('blame.statusBarItem.enabled') === true; + + // Editor decoration + if (editorDecorationEnabled) { + if (!this._editorDecoration) { + this._editorDecoration = new GitBlameEditorDecoration(this); + } + } else { + this._editorDecoration?.dispose(); + this._editorDecoration = undefined; + } + + // StatusBar item + if (statusBarItemEnabled) { + if (!this._statusBarItem) { + this._statusBarItem = new GitBlameStatusBarItem(this); + } + } else { + this._statusBarItem?.dispose(); + this._statusBarItem = undefined; + } + + // Listeners + if (editorDecorationEnabled || statusBarItemEnabled) { + if (this._enablementDisposables.length === 0) { + this._model.onDidOpenRepository(this._onDidOpenRepository, this, this._enablementDisposables); + this._model.onDidCloseRepository(this._onDidCloseRepository, this, this._enablementDisposables); + for (const repository of this._model.repositories) { + this._onDidOpenRepository(repository); + } + + window.onDidChangeActiveTextEditor(e => this._updateTextEditorBlameInformation(e), this, this._enablementDisposables); + window.onDidChangeTextEditorSelection(e => this._updateTextEditorBlameInformation(e.textEditor, true), this, this._enablementDisposables); + window.onDidChangeTextEditorDiffInformation(e => this._updateTextEditorBlameInformation(e.textEditor), this, this._enablementDisposables); + } + } else { + this._enablementDisposables = dispose(this._enablementDisposables); + } + + this._updateTextEditorBlameInformation(window.activeTextEditor); + } + private _onDidOpenRepository(repository: Repository): void { const repositoryDisposables: IDisposable[] = []; repository.onDidRunGitStatus(() => this._onDidRunGitStatus(repository), this, repositoryDisposables); @@ -290,9 +347,7 @@ export class GitBlameController { this._repositoryBlameCache.deleteBlameInformation(repository, 'file'); this._repositoryBlameCache.setRepositoryHEAD(repository, repository.HEAD.commit); - for (const textEditor of window.visibleTextEditors) { - this._updateTextEditorBlameInformation(textEditor); - } + this._updateTextEditorBlameInformation(window.activeTextEditor); } } @@ -320,7 +375,12 @@ export class GitBlameController { @throttle private async _updateTextEditorBlameInformation(textEditor: TextEditor | undefined, showBlameInformationForPositionZero = false): Promise { - if (!textEditor?.diffInformation || textEditor !== window.activeTextEditor) { + if (textEditor) { + if (!textEditor.diffInformation || textEditor !== window.activeTextEditor) { + return; + } + } else { + this.textEditorBlameInformation = undefined; return; } @@ -329,14 +389,19 @@ export class GitBlameController { return; } + // Only support resources with `file` and `git` schemes + if (!isResourceSchemeSupported(textEditor.document.uri)) { + this.textEditorBlameInformation = undefined; + return; + } + // Do not show blame information when there is a single selection and it is at the beginning // of the file [0, 0, 0, 0] unless the user explicitly navigates the cursor there. We do this // to avoid showing blame information when the editor is not focused. if (!showBlameInformationForPositionZero && textEditor.selections.length === 1 && textEditor.selections[0].start.line === 0 && textEditor.selections[0].start.character === 0 && textEditor.selections[0].end.line === 0 && textEditor.selections[0].end.character === 0) { - this.textEditorBlameInformation.set(textEditor, []); - this._onDidChangeBlameInformation.fire(textEditor); + this.textEditorBlameInformation = undefined; return; } @@ -437,8 +502,7 @@ export class GitBlameController { } } - this.textEditorBlameInformation.set(textEditor, lineBlameInformation); - this._onDidChangeBlameInformation.fire(textEditor); + this.textEditorBlameInformation = lineBlameInformation; } dispose() { @@ -452,26 +516,22 @@ export class GitBlameController { } class GitBlameEditorDecoration implements HoverProvider { - private _decoration: TextEditorDecorationType | undefined; - private get decoration(): TextEditorDecorationType { - if (!this._decoration) { - this._decoration = window.createTextEditorDecorationType({ - after: { - color: new ThemeColor('git.blame.editorDecorationForeground') - } - }); - } - - return this._decoration; - } + private _decoration: TextEditorDecorationType; private _hoverDisposable: IDisposable | undefined; private _disposables: IDisposable[] = []; constructor(private readonly _controller: GitBlameController) { + this._decoration = window.createTextEditorDecorationType({ + after: { + color: new ThemeColor('git.blame.editorDecorationForeground') + } + }); + this._disposables.push(this._decoration); + workspace.onDidChangeConfiguration(this._onDidChangeConfiguration, this, this._disposables); window.onDidChangeActiveTextEditor(this._onDidChangeActiveTextEditor, this, this._disposables); - this._controller.onDidChangeBlameInformation(e => this._updateDecorations(e), this, this._disposables); + this._controller.onDidChangeBlameInformation(() => this._onDidChangeBlameInformation(), this, this._disposables); this._onDidChangeConfiguration(); } @@ -492,14 +552,14 @@ class GitBlameEditorDecoration implements HoverProvider { } // Get blame information - const blameInformation = this._controller.textEditorBlameInformation - .get(textEditor)?.find(blame => blame.lineNumber === position.line); + const blameInformation = this._controller.textEditorBlameInformation; + const lineBlameInformation = blameInformation?.find(blame => blame.lineNumber === position.line); - if (!blameInformation || typeof blameInformation.blameInformation === 'string') { + if (!lineBlameInformation || typeof lineBlameInformation.blameInformation === 'string') { return undefined; } - const contents = await this._controller.getBlameInformationDetailedHover(textEditor.document.uri, blameInformation.blameInformation); + const contents = await this._controller.getBlameInformationDetailedHover(textEditor.document.uri, lineBlameInformation.blameInformation); if (!contents || token.isCancellationRequested) { return undefined; @@ -509,35 +569,19 @@ class GitBlameEditorDecoration implements HoverProvider { } private _onDidChangeConfiguration(e?: ConfigurationChangeEvent): void { - if (e && - !e.affectsConfiguration('git.blame.editorDecoration.enabled') && - !e.affectsConfiguration('git.blame.editorDecoration.template')) { + if (e && !e.affectsConfiguration('git.blame.editorDecoration.template')) { return; } - if (this._getConfiguration().enabled) { - if (window.activeTextEditor) { - this._registerHoverProvider(); - this._updateDecorations(window.activeTextEditor); - } - } else { - this._decoration?.dispose(); - this._decoration = undefined; - - this._hoverDisposable?.dispose(); - this._hoverDisposable = undefined; - } + this._registerHoverProvider(); + this._onDidChangeBlameInformation(); } private _onDidChangeActiveTextEditor(): void { - if (!this._getConfiguration().enabled) { - return; - } - // Clear decorations for (const editor of window.visibleTextEditors) { if (editor !== window.activeTextEditor) { - editor.setDecorations(this.decoration, []); + editor.setDecorations(this._decoration, []); } } @@ -545,34 +589,23 @@ class GitBlameEditorDecoration implements HoverProvider { this._registerHoverProvider(); } - private _getConfiguration(): { enabled: boolean; template: string } { - const config = workspace.getConfiguration('git'); - const enabled = config.get('blame.editorDecoration.enabled', false); - const template = config.get('blame.editorDecoration.template', '${subject}, ${authorName} (${authorDateAgo})'); - - return { enabled, template }; - } - - private _updateDecorations(textEditor: TextEditor): void { - const { enabled, template } = this._getConfiguration(); - if (!enabled) { - return; - } - - // Only support resources with `file` and `git` schemes - if (textEditor.document.uri.scheme !== 'file' && !isGitUri(textEditor.document.uri)) { - textEditor.setDecorations(this.decoration, []); + private _onDidChangeBlameInformation(): void { + const textEditor = window.activeTextEditor; + if (!textEditor) { return; } // Get blame information - const blameInformation = this._controller.textEditorBlameInformation.get(textEditor); + const blameInformation = this._controller.textEditorBlameInformation; if (!blameInformation) { - textEditor.setDecorations(this.decoration, []); + textEditor.setDecorations(this._decoration, []); return; } // Set decorations for the editor + const config = workspace.getConfiguration('git'); + const template = config.get('blame.editorDecoration.template', '${subject}, ${authorName} (${authorDateAgo})'); + const decorations = blameInformation.map(blame => { const contentText = typeof blame.blameInformation !== 'string' ? this._controller.formatBlameInformationMessage(template, blame.blameInformation) @@ -581,7 +614,7 @@ class GitBlameEditorDecoration implements HoverProvider { return this._createDecoration(blame.lineNumber, contentText); }); - textEditor.setDecorations(this.decoration, decorations); + textEditor.setDecorations(this._decoration, decorations); } private _createDecoration(lineNumber: number, contentText: string): DecorationOptions { @@ -599,8 +632,7 @@ class GitBlameEditorDecoration implements HoverProvider { private _registerHoverProvider(): void { this._hoverDisposable?.dispose(); - if (window.activeTextEditor?.document.uri.scheme === 'file' || - window.activeTextEditor?.document.uri.scheme === 'git') { + if (window.activeTextEditor && isResourceSchemeSupported(window.activeTextEditor.document.uri)) { this._hoverDisposable = languages.registerHoverProvider({ pattern: window.activeTextEditor.document.uri.fsPath }, this); @@ -608,9 +640,6 @@ class GitBlameEditorDecoration implements HoverProvider { } dispose() { - this._decoration?.dispose(); - this._decoration = undefined; - this._hoverDisposable?.dispose(); this._hoverDisposable = undefined; @@ -619,70 +648,33 @@ class GitBlameEditorDecoration implements HoverProvider { } class GitBlameStatusBarItem { - private _statusBarItem: StatusBarItem | undefined; - + private _statusBarItem: StatusBarItem; private _disposables: IDisposable[] = []; constructor(private readonly _controller: GitBlameController) { - workspace.onDidChangeConfiguration(this._onDidChangeConfiguration, this, this._disposables); - window.onDidChangeActiveTextEditor(this._onDidChangeActiveTextEditor, this, this._disposables); + this._statusBarItem = window.createStatusBarItem('git.blame', StatusBarAlignment.Right, 200); + this._statusBarItem.name = l10n.t('Git Blame Information'); + this._disposables.push(this._statusBarItem); - this._controller.onDidChangeBlameInformation(e => this._updateStatusBarItem(e), this, this._disposables); + workspace.onDidChangeConfiguration(this._onDidChangeConfiguration, this, this._disposables); + this._controller.onDidChangeBlameInformation(() => this._onDidChangeBlameInformation(), this, this._disposables); } private _onDidChangeConfiguration(e: ConfigurationChangeEvent): void { - if (!e.affectsConfiguration('git.blame.statusBarItem.enabled') && - !e.affectsConfiguration('git.blame.statusBarItem.template')) { + if (!e.affectsConfiguration('git.blame.statusBarItem.template')) { return; } - if (this._getConfiguration().enabled) { - if (window.activeTextEditor) { - this._updateStatusBarItem(window.activeTextEditor); - } - } else { - this._statusBarItem?.dispose(); - this._statusBarItem = undefined; - } + this._onDidChangeBlameInformation(); } - private _onDidChangeActiveTextEditor(): void { - if (!this._getConfiguration().enabled) { - return; - } - + private _onDidChangeBlameInformation(): void { if (!window.activeTextEditor) { - this._statusBarItem?.hide(); - } - } - - private _getConfiguration(): { enabled: boolean; template: string } { - const config = workspace.getConfiguration('git'); - const enabled = config.get('blame.statusBarItem.enabled', false); - const template = config.get('blame.statusBarItem.template', '${authorName} (${authorDateAgo})'); - - return { enabled, template }; - } - - private _updateStatusBarItem(textEditor: TextEditor): void { - const { enabled, template } = this._getConfiguration(); - if (!enabled || textEditor !== window.activeTextEditor) { - return; - } - - if (!this._statusBarItem) { - this._statusBarItem = window.createStatusBarItem('git.blame', StatusBarAlignment.Right, 200); - this._statusBarItem.name = l10n.t('Git Blame Information'); - this._disposables.push(this._statusBarItem); - } - - // Only support resources with `file` and `git` schemes - if (textEditor.document.uri.scheme !== 'file' && !isGitUri(textEditor.document.uri)) { this._statusBarItem.hide(); return; } - const blameInformation = this._controller.textEditorBlameInformation.get(textEditor); + const blameInformation = this._controller.textEditorBlameInformation; if (!blameInformation || blameInformation.length === 0) { this._statusBarItem.hide(); return; @@ -693,12 +685,15 @@ class GitBlameStatusBarItem { this._statusBarItem.tooltip = l10n.t('Git Blame Information'); this._statusBarItem.command = undefined; } else { + const config = workspace.getConfiguration('git'); + const template = config.get('blame.statusBarItem.template', '${authorName} (${authorDateAgo})'); + this._statusBarItem.text = `$(git-commit) ${this._controller.formatBlameInformationMessage(template, blameInformation[0].blameInformation)}`; - this._statusBarItem.tooltip = this._controller.getBlameInformationHover(textEditor.document.uri, blameInformation[0].blameInformation); + this._statusBarItem.tooltip = this._controller.getBlameInformationHover(window.activeTextEditor.document.uri, blameInformation[0].blameInformation); this._statusBarItem.command = { title: l10n.t('View Commit'), command: 'git.blameStatusBarItem.viewCommit', - arguments: [textEditor.document.uri, blameInformation[0].blameInformation.hash] + arguments: [window.activeTextEditor.document.uri, blameInformation[0].blameInformation.hash] } satisfies Command; } From 92bae09f4dba58bb5eed52d75ba22ffd4424e1c6 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:45:45 +0100 Subject: [PATCH 0345/3587] Git - update git blame settings (#237187) --- extensions/git/package.json | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 4556255d3dac..748ac40f1ae1 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -3203,38 +3203,22 @@ "git.blame.editorDecoration.enabled": { "type": "boolean", "default": false, - "markdownDescription": "%config.blameEditorDecoration.enabled%", - "scope": "resource", - "tags": [ - "experimental" - ] + "markdownDescription": "%config.blameEditorDecoration.enabled%" }, "git.blame.editorDecoration.template": { "type": "string", "default": "${subject}, ${authorName} (${authorDateAgo})", - "markdownDescription": "%config.blameEditorDecoration.template%", - "scope": "resource", - "tags": [ - "experimental" - ] + "markdownDescription": "%config.blameEditorDecoration.template%" }, "git.blame.statusBarItem.enabled": { "type": "boolean", "default": false, - "markdownDescription": "%config.blameStatusBarItem.enabled%", - "scope": "resource", - "tags": [ - "experimental" - ] + "markdownDescription": "%config.blameStatusBarItem.enabled%" }, "git.blame.statusBarItem.template": { "type": "string", "default": "${authorName} (${authorDateAgo})", - "markdownDescription": "%config.blameStatusBarItem.template%", - "scope": "resource", - "tags": [ - "experimental" - ] + "markdownDescription": "%config.blameStatusBarItem.template%" } } }, From 2bdb3e9b41bd72048ea2067a350d8536c82fc7f6 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:46:09 +0100 Subject: [PATCH 0346/3587] Git - git blame hover commands polish (#237190) --- extensions/git/src/blame.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 3893b4bff4fb..3c7d61dd1e1f 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -260,9 +260,11 @@ export class GitBlameController { markdownString.appendMarkdown(`\n\n---\n\n`); } - markdownString.appendMarkdown(`[$(eye) View Commit](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformationOrCommit.hash]))} "${l10n.t('View Commit')}")`); - markdownString.appendMarkdown('    '); - markdownString.appendMarkdown(`[$(copy) ${blameInformationOrCommit.hash.substring(0, 8)}](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformationOrCommit.hash))} "${l10n.t('Copy Commit Hash')}")`); + markdownString.appendMarkdown(`[\`$(git-commit) ${blameInformationOrCommit.hash.substring(0, 8)} \`](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformationOrCommit.hash]))} "${l10n.t('View Commit')}")`); + markdownString.appendMarkdown(' '); + markdownString.appendMarkdown(`[$(copy)](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformationOrCommit.hash))} "${l10n.t('Copy Commit Hash')}")`); + markdownString.appendMarkdown('  |  '); + markdownString.appendMarkdown(`[$(gear)](command:workbench.action.openSettings?%5B%22git.blame%22%5D "${l10n.t('Open Settings')}")`); return markdownString; } From d79d9f03199bb20c30e1a121b680bcc83f7e54c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20K=C3=BChner?= Date: Fri, 3 Jan 2025 09:36:33 +0100 Subject: [PATCH 0347/3587] support svg's in image preview --- extensions/media-preview/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/media-preview/package.json b/extensions/media-preview/package.json index e7ddad4354e5..7e2b70293fc3 100644 --- a/extensions/media-preview/package.json +++ b/extensions/media-preview/package.json @@ -50,7 +50,7 @@ "priority": "builtin", "selector": [ { - "filenamePattern": "*.{jpg,jpe,jpeg,png,bmp,gif,ico,webp,avif}" + "filenamePattern": "*.{jpg,jpe,jpeg,png,bmp,gif,ico,webp,avif,svg}" } ] }, From d8d429c47285632fa46c4acd5728a3ef23363675 Mon Sep 17 00:00:00 2001 From: Andrew Suzuki Date: Fri, 3 Jan 2025 12:49:45 -0500 Subject: [PATCH 0348/3587] Fix new Color in string typo --- src/vs/editor/common/core/editorColorRegistry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/core/editorColorRegistry.ts b/src/vs/editor/common/core/editorColorRegistry.ts index 1bcf72524438..15679ff7ba94 100644 --- a/src/vs/editor/common/core/editorColorRegistry.ts +++ b/src/vs/editor/common/core/editorColorRegistry.ts @@ -80,7 +80,7 @@ export const editorBracketHighlightingForeground4 = registerColor('editorBracket export const editorBracketHighlightingForeground5 = registerColor('editorBracketHighlight.foreground5', '#00000000', nls.localize('editorBracketHighlightForeground5', 'Foreground color of brackets (5). Requires enabling bracket pair colorization.')); export const editorBracketHighlightingForeground6 = registerColor('editorBracketHighlight.foreground6', '#00000000', nls.localize('editorBracketHighlightForeground6', 'Foreground color of brackets (6). Requires enabling bracket pair colorization.')); -export const editorBracketHighlightingUnexpectedBracketForeground = registerColor('editorBracketHighlight.unexpectedBracket.foreground', { dark: new Color(new RGBA(255, 18, 18, 0.8)), light: new Color(new RGBA(255, 18, 18, 0.8)), hcDark: 'new Color(new RGBA(255, 50, 50, 1))', hcLight: '#B5200D' }, nls.localize('editorBracketHighlightUnexpectedBracketForeground', 'Foreground color of unexpected brackets.')); +export const editorBracketHighlightingUnexpectedBracketForeground = registerColor('editorBracketHighlight.unexpectedBracket.foreground', { dark: new Color(new RGBA(255, 18, 18, 0.8)), light: new Color(new RGBA(255, 18, 18, 0.8)), hcDark: new Color(new RGBA(255, 50, 50, 1)), hcLight: '#B5200D' }, nls.localize('editorBracketHighlightUnexpectedBracketForeground', 'Foreground color of unexpected brackets.')); export const editorBracketPairGuideBackground1 = registerColor('editorBracketPairGuide.background1', '#00000000', nls.localize('editorBracketPairGuide.background1', 'Background color of inactive bracket pair guides (1). Requires enabling bracket pair guides.')); export const editorBracketPairGuideBackground2 = registerColor('editorBracketPairGuide.background2', '#00000000', nls.localize('editorBracketPairGuide.background2', 'Background color of inactive bracket pair guides (2). Requires enabling bracket pair guides.')); From aa6a38114c3487606f6f3f8c234b4aae89e9ee57 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Sat, 4 Jan 2025 19:15:35 +0100 Subject: [PATCH 0349/3587] Git - add stage/unstage commands to editor title (#237257) --- extensions/git/package.json | 12 +++++++++++- extensions/git/src/model.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 748ac40f1ae1..cdb7c77a03a1 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -2050,9 +2050,19 @@ "group": "navigation", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInNotebookTextDiffEditor && resourceScheme =~ /^git$|^file$/" }, + { + "command": "git.stage", + "group": "navigation@1", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && git.activeResourceHasUnstagedChanges" + }, + { + "command": "git.unstage", + "group": "navigation@1", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && git.activeResourceHasStagedChanges" + }, { "command": "git.openChange", - "group": "navigation", + "group": "navigation@2", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && scmActiveResourceHasChanges" }, { diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index 48e66d5e9ff6..142d073914f3 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -272,6 +272,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables); window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables); + window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, this.disposables); workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); const fsWatcher = workspace.createFileSystemWatcher('**'); @@ -519,6 +520,31 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } } + private onDidChangeActiveTextEditor(): void { + const textEditor = window.activeTextEditor; + + if (textEditor === undefined) { + commands.executeCommand('setContext', 'git.activeResourceHasUnstagedChanges', false); + commands.executeCommand('setContext', 'git.activeResourceHasStagedChanges', false); + return; + } + + const repository = this.getRepository(textEditor.document.uri); + if (!repository) { + commands.executeCommand('setContext', 'git.activeResourceHasUnstagedChanges', false); + commands.executeCommand('setContext', 'git.activeResourceHasStagedChanges', false); + return; + } + + const indexResource = repository.indexGroup.resourceStates + .find(resource => pathEquals(resource.resourceUri.fsPath, textEditor.document.uri.fsPath)); + const workingTreeResource = repository.workingTreeGroup.resourceStates + .find(resource => pathEquals(resource.resourceUri.fsPath, textEditor.document.uri.fsPath)); + + commands.executeCommand('setContext', 'git.activeResourceHasStagedChanges', indexResource !== undefined); + commands.executeCommand('setContext', 'git.activeResourceHasUnstagedChanges', workingTreeResource !== undefined); + } + @sequentialize async openRepository(repoPath: string, openIfClosed = false): Promise { this.logger.trace(`[Model][openRepository] Repository: ${repoPath}`); @@ -727,8 +753,10 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi const statusListener = repository.onDidRunGitStatus(() => { checkForSubmodules(); updateMergeChanges(); + this.onDidChangeActiveTextEditor(); }); checkForSubmodules(); + this.onDidChangeActiveTextEditor(); const updateOperationInProgressContext = () => { let operationInProgress = false; From 4cafefb526fdc21e4456c02226c4a6566bc7f9d7 Mon Sep 17 00:00:00 2001 From: Misode Date: Sun, 5 Jan 2025 05:17:26 +0100 Subject: [PATCH 0350/3587] Fix missing uri parsing in json schema loading --- .../json-language-features/server/src/node/jsonServerMain.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/json-language-features/server/src/node/jsonServerMain.ts b/extensions/json-language-features/server/src/node/jsonServerMain.ts index c22cd14834d3..71da2377c77f 100644 --- a/extensions/json-language-features/server/src/node/jsonServerMain.ts +++ b/extensions/json-language-features/server/src/node/jsonServerMain.ts @@ -8,6 +8,7 @@ import { formatError } from '../utils/runner'; import { RequestService, RuntimeEnvironment, startServer } from '../jsonServer'; import { xhr, XHRResponse, configure as configureHttpRequests, getErrorStatusDescription } from 'request-light'; +import { URI as Uri } from 'vscode-uri'; import { promises as fs } from 'fs'; import * as l10n from '@vscode/l10n'; @@ -38,7 +39,8 @@ function getFileRequestService(): RequestService { return { async getContent(location: string, encoding?: BufferEncoding) { try { - return (await fs.readFile(location, encoding)).toString(); + const uri = Uri.parse(location); + return (await fs.readFile(uri.fsPath, encoding)).toString(); } catch (e) { if (e.code === 'ENOENT') { throw new Error(l10n.t('Schema not found: {0}', location)); From 08c8f1a330274cb42964a8fb5813a2e593965b82 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 6 Jan 2025 14:58:08 +1100 Subject: [PATCH 0351/3587] Sort cell metadata for notebook cell diffview (#237302) --- .../notebook/browser/diff/diffComponents.ts | 4 +-- .../notebook/browser/notebook.contribution.ts | 6 ++--- .../common/model/notebookCellTextModel.ts | 25 +++++++++++++++++-- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index 3ef8a33b0883..a0a5557ddab7 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -847,7 +847,7 @@ abstract class AbstractElementRenderer extends Disposable { return; } - const modifiedMetadataSource = getFormattedMetadataJSON(this.notebookEditor.textModel?.transientOptions.transientCellMetadata, this.cell.modified?.metadata || {}, this.cell.modified?.language); + const modifiedMetadataSource = getFormattedMetadataJSON(this.notebookEditor.textModel?.transientOptions.transientCellMetadata, this.cell.modified?.metadata || {}, this.cell.modified?.language, true); modifiedMetadataModel.object.textEditorModel.setValue(modifiedMetadataSource); })); @@ -869,7 +869,7 @@ abstract class AbstractElementRenderer extends Disposable { const originalMetadataSource = getFormattedMetadataJSON(this.notebookEditor.textModel?.transientOptions.transientCellMetadata, this.cell.type === 'insert' ? this.cell.modified!.metadata || {} - : this.cell.original!.metadata || {}); + : this.cell.original!.metadata || {}, undefined, true); const uri = this.cell.type === 'insert' ? this.cell.modified!.uri : this.cell.original!.uri; diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index a09069d24cfc..17568757a132 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -452,7 +452,7 @@ class CellInfoContentProvider { for (const cell of ref.object.notebook.cells) { if (cell.handle === data.handle) { const cellIndex = ref.object.notebook.cells.indexOf(cell); - const metadataSource = getFormattedMetadataJSON(ref.object.notebook.transientOptions.transientCellMetadata, cell.metadata, cell.language); + const metadataSource = getFormattedMetadataJSON(ref.object.notebook.transientOptions.transientCellMetadata, cell.metadata, cell.language, true); result = this._modelService.createModel( metadataSource, mode, @@ -460,9 +460,9 @@ class CellInfoContentProvider { ); this._disposables.push(disposables.add(ref.object.notebook.onDidChangeContent(e => { if (result && e.rawEvents.some(event => (event.kind === NotebookCellsChangeType.ChangeCellMetadata || event.kind === NotebookCellsChangeType.ChangeCellLanguage) && event.index === cellIndex)) { - const value = getFormattedMetadataJSON(ref.object.notebook.transientOptions.transientCellMetadata, cell.metadata, cell.language); + const value = getFormattedMetadataJSON(ref.object.notebook.transientOptions.transientCellMetadata, cell.metadata, cell.language, true); if (result.getValue() !== value) { - result.setValue(getFormattedMetadataJSON(ref.object.notebook.transientOptions.transientCellMetadata, cell.metadata, cell.language)); + result.setValue(value); } } }))); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 018aa490c9a0..ac481402ceda 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -516,7 +516,7 @@ function computeRunStartTimeAdjustment(oldMetadata: NotebookCellInternalMetadata } -export function getFormattedMetadataJSON(transientCellMetadata: TransientCellMetadata | undefined, metadata: NotebookCellMetadata, language?: string) { +export function getFormattedMetadataJSON(transientCellMetadata: TransientCellMetadata | undefined, metadata: NotebookCellMetadata, language?: string, sortKeys?: boolean): string { let filteredMetadata: { [key: string]: any } = {}; if (transientCellMetadata) { @@ -541,7 +541,28 @@ export function getFormattedMetadataJSON(transientCellMetadata: TransientCellMet if (language) { obj.language = language; } - const metadataSource = toFormattedString(obj, {}); + const metadataSource = toFormattedString(sortKeys ? sortObjectPropertiesRecursively(obj) : obj, {}); return metadataSource; } + + +/** + * Sort the JSON to ensure when diffing, the JSON keys are sorted & matched correctly in diff view. + */ +export function sortObjectPropertiesRecursively(obj: any): any { + if (Array.isArray(obj)) { + return obj.map(sortObjectPropertiesRecursively); + } + if (obj !== undefined && obj !== null && typeof obj === 'object' && Object.keys(obj).length > 0) { + return ( + Object.keys(obj) + .sort() + .reduce>((sortedObj, prop) => { + sortedObj[prop] = sortObjectPropertiesRecursively(obj[prop]); + return sortedObj; + }, {}) as any + ); + } + return obj; +} From 8cc255e03ac7c51804cea1194b1ea864bbe2ccb6 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 6 Jan 2025 14:58:34 +1100 Subject: [PATCH 0352/3587] Ensure execution_count is cleared when clearing outputs (#237301) --- extensions/ipynb/src/deserializers.ts | 8 +- .../ipynb/src/notebookModelStoreSync.ts | 7 + .../ipynb/src/test/clearOutputs.test.ts | 736 ++++++++++++++++++ extensions/ipynb/src/test/serializers.test.ts | 13 +- .../notebook/browser/notebookEditorWidget.ts | 2 +- 5 files changed, 762 insertions(+), 4 deletions(-) create mode 100644 extensions/ipynb/src/test/clearOutputs.test.ts diff --git a/extensions/ipynb/src/deserializers.ts b/extensions/ipynb/src/deserializers.ts index de467f660776..ad99a4fcea82 100644 --- a/extensions/ipynb/src/deserializers.ts +++ b/extensions/ipynb/src/deserializers.ts @@ -149,8 +149,12 @@ function getNotebookCellMetadata(cell: nbformat.IBaseCell): { // We put this only for VSC to display in diff view. // Else we don't use this. const cellMetadata: CellMetadata = {}; - if (cell.cell_type === 'code' && typeof cell['execution_count'] === 'number') { - cellMetadata.execution_count = cell['execution_count']; + if (cell.cell_type === 'code') { + if (typeof cell['execution_count'] === 'number') { + cellMetadata.execution_count = cell['execution_count']; + } else { + cellMetadata.execution_count = null; + } } if (cell['metadata']) { diff --git a/extensions/ipynb/src/notebookModelStoreSync.ts b/extensions/ipynb/src/notebookModelStoreSync.ts index dc1bae1de2ff..836e1c8afc5d 100644 --- a/extensions/ipynb/src/notebookModelStoreSync.ts +++ b/extensions/ipynb/src/notebookModelStoreSync.ts @@ -165,6 +165,13 @@ function onDidChangeNotebookCells(e: NotebookDocumentChangeEventEx) { metadata.execution_count = null; metadataUpdated = true; pendingCellUpdates.delete(e.cell); + } else if (!e.executionSummary?.executionOrder && !e.executionSummary?.success && !e.executionSummary?.timing + && !e.metadata && !e.outputs && currentMetadata.execution_count && !pendingCellUpdates.has(e.cell)) { + // This is a result of the cell without outupts but has execution count being cleared + // Create two cells, one that produces output and one that doesn't. Run both and then clear the output or all cells. + // This condition will be satisfied for first cell without outputs. + metadata.execution_count = null; + metadataUpdated = true; } if (e.document?.languageId && e.document?.languageId !== preferredCellLanguage && e.document?.languageId !== languageIdInMetadata) { diff --git a/extensions/ipynb/src/test/clearOutputs.test.ts b/extensions/ipynb/src/test/clearOutputs.test.ts new file mode 100644 index 000000000000..0437e9838bb5 --- /dev/null +++ b/extensions/ipynb/src/test/clearOutputs.test.ts @@ -0,0 +1,736 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as sinon from 'sinon'; +import type * as nbformat from '@jupyterlab/nbformat'; +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { jupyterNotebookModelToNotebookData } from '../deserializers'; +import { activate } from '../notebookModelStoreSync'; + + +suite(`ipynb Clear Outputs`, () => { + const disposables: vscode.Disposable[] = []; + const context = { subscriptions: disposables } as vscode.ExtensionContext; + setup(() => { + disposables.length = 0; + activate(context); + }); + teardown(async () => { + disposables.forEach(d => d.dispose()); + disposables.length = 0; + sinon.restore(); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('Clear outputs after opening Notebook', async () => { + const cells: nbformat.ICell[] = [ + { + cell_type: 'code', + execution_count: 10, + outputs: [{ output_type: 'stream', name: 'stdout', text: ['Hello'] }], + source: 'print(1)', + metadata: {} + }, + { + cell_type: 'code', + outputs: [], + source: 'print(2)', + metadata: {} + }, + { + cell_type: 'markdown', + source: '# HEAD', + metadata: {} + } + ]; + const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python'); + + const notebookDocument = await vscode.workspace.openNotebookDocument('jupyter-notebook', notebook); + await vscode.window.showNotebookDocument(notebookDocument); + + assert.strictEqual(notebookDocument.cellCount, 3); + assert.strictEqual(notebookDocument.cellAt(0).metadata.execution_count, 10); + assert.strictEqual(notebookDocument.cellAt(1).metadata.execution_count, null); + assert.strictEqual(notebookDocument.cellAt(2).metadata.execution_count, undefined); + + // Clear all outputs + await vscode.commands.executeCommand('notebook.clearAllCellsOutputs'); + + // Wait for all changes to be applied, could take a few ms. + const verifyMetadataChanges = () => { + assert.strictEqual(notebookDocument.cellAt(0).metadata.execution_count, null); + assert.strictEqual(notebookDocument.cellAt(1).metadata.execution_count, null); + assert.strictEqual(notebookDocument.cellAt(2).metadata.execution_count, undefined); + }; + + vscode.workspace.onDidChangeNotebookDocument(() => verifyMetadataChanges(), undefined, disposables); + + await new Promise((resolve, reject) => { + const interval = setInterval(() => { + try { + verifyMetadataChanges(); + clearInterval(interval); + resolve(); + } catch { + // Ignore + } + }, 50); + disposables.push({ dispose: () => clearInterval(interval) }); + const timeout = setTimeout(() => { + try { + verifyMetadataChanges(); + resolve(); + } catch (ex) { + reject(ex); + } + }, 1000); + disposables.push({ dispose: () => clearTimeout(timeout) }); + }); + }); + + + // test('Serialize', async () => { + // const markdownCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# header1', 'markdown'); + // markdownCell.metadata = { + // attachments: { + // 'image.png': { + // 'image/png': 'abc' + // } + // }, + // id: '123', + // metadata: { + // foo: 'bar' + // } + // }; + + // const cellMetadata = getCellMetadata({ cell: markdownCell }); + // assert.deepStrictEqual(cellMetadata, { + // id: '123', + // metadata: { + // foo: 'bar', + // }, + // attachments: { + // 'image.png': { + // 'image/png': 'abc' + // } + // } + // }); + + // const markdownCell2 = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# header1', 'markdown'); + // markdownCell2.metadata = { + // id: '123', + // metadata: { + // foo: 'bar' + // }, + // attachments: { + // 'image.png': { + // 'image/png': 'abc' + // } + // } + // }; + + // const nbMarkdownCell = createMarkdownCellFromNotebookCell(markdownCell); + // const nbMarkdownCell2 = createMarkdownCellFromNotebookCell(markdownCell2); + // assert.deepStrictEqual(nbMarkdownCell, nbMarkdownCell2); + + // assert.deepStrictEqual(nbMarkdownCell, { + // cell_type: 'markdown', + // source: ['# header1'], + // metadata: { + // foo: 'bar', + // }, + // attachments: { + // 'image.png': { + // 'image/png': 'abc' + // } + // }, + // id: '123' + // }); + // }); + + // suite('Outputs', () => { + // function validateCellOutputTranslation( + // outputs: nbformat.IOutput[], + // expectedOutputs: vscode.NotebookCellOutput[], + // propertiesToExcludeFromComparison: string[] = [] + // ) { + // const cells: nbformat.ICell[] = [ + // { + // cell_type: 'code', + // execution_count: 10, + // outputs, + // source: 'print(1)', + // metadata: {} + // } + // ]; + // const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python'); + + // // OutputItems contain an `id` property generated by VSC. + // // Exclude that property when comparing. + // const propertiesToExclude = propertiesToExcludeFromComparison.concat(['id']); + // const actualOuts = notebook.cells[0].outputs; + // deepStripProperties(actualOuts, propertiesToExclude); + // deepStripProperties(expectedOutputs, propertiesToExclude); + // assert.deepStrictEqual(actualOuts, expectedOutputs); + // } + + // test('Empty output', () => { + // validateCellOutputTranslation([], []); + // }); + + // test('Stream output', () => { + // validateCellOutputTranslation( + // [ + // { + // output_type: 'stream', + // name: 'stderr', + // text: 'Error' + // }, + // { + // output_type: 'stream', + // name: 'stdout', + // text: 'NoError' + // } + // ], + // [ + // new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('Error')], { + // outputType: 'stream' + // }), + // new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('NoError')], { + // outputType: 'stream' + // }) + // ] + // ); + // }); + // test('Stream output and line endings', () => { + // validateCellOutputTranslation( + // [ + // { + // output_type: 'stream', + // name: 'stdout', + // text: [ + // 'Line1\n', + // '\n', + // 'Line3\n', + // 'Line4' + // ] + // } + // ], + // [ + // new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Line1\n\nLine3\nLine4')], { + // outputType: 'stream' + // }) + // ] + // ); + // validateCellOutputTranslation( + // [ + // { + // output_type: 'stream', + // name: 'stdout', + // text: [ + // 'Hello\n', + // 'Hello\n', + // 'Hello\n', + // 'Hello\n', + // 'Hello\n', + // 'Hello\n' + // ] + // } + // ], + // [ + // new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Hello\nHello\nHello\nHello\nHello\nHello\n')], { + // outputType: 'stream' + // }) + // ] + // ); + // }); + // test('Multi-line Stream output', () => { + // validateCellOutputTranslation( + // [ + // { + // name: 'stdout', + // output_type: 'stream', + // text: [ + // 'Epoch 1/5\n', + // '...\n', + // 'Epoch 2/5\n', + // '...\n', + // 'Epoch 3/5\n', + // '...\n', + // 'Epoch 4/5\n', + // '...\n', + // 'Epoch 5/5\n', + // '...\n' + // ] + // } + // ], + // [ + // new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout(['Epoch 1/5\n', + // '...\n', + // 'Epoch 2/5\n', + // '...\n', + // 'Epoch 3/5\n', + // '...\n', + // 'Epoch 4/5\n', + // '...\n', + // 'Epoch 5/5\n', + // '...\n'].join(''))], { + // outputType: 'stream' + // }) + // ] + // ); + // }); + + // test('Multi-line Stream output (last empty line should not be saved in ipynb)', () => { + // validateCellOutputTranslation( + // [ + // { + // name: 'stderr', + // output_type: 'stream', + // text: [ + // 'Epoch 1/5\n', + // '...\n', + // 'Epoch 2/5\n', + // '...\n', + // 'Epoch 3/5\n', + // '...\n', + // 'Epoch 4/5\n', + // '...\n', + // 'Epoch 5/5\n', + // '...\n' + // ] + // } + // ], + // [ + // new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr(['Epoch 1/5\n', + // '...\n', + // 'Epoch 2/5\n', + // '...\n', + // 'Epoch 3/5\n', + // '...\n', + // 'Epoch 4/5\n', + // '...\n', + // 'Epoch 5/5\n', + // '...\n', + // // This last empty line should not be saved in ipynb. + // '\n'].join(''))], { + // outputType: 'stream' + // }) + // ] + // ); + // }); + + // test('Streamed text with Ansi characters', async () => { + // validateCellOutputTranslation( + // [ + // { + // name: 'stderr', + // text: '\u001b[K\u001b[33m✅ \u001b[0m Loading\n', + // output_type: 'stream' + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [vscode.NotebookCellOutputItem.stderr('\u001b[K\u001b[33m✅ \u001b[0m Loading\n')], + // { + // outputType: 'stream' + // } + // ) + // ] + // ); + // }); + + // test('Streamed text with angle bracket characters', async () => { + // validateCellOutputTranslation( + // [ + // { + // name: 'stderr', + // text: '1 is < 2', + // output_type: 'stream' + // } + // ], + // [ + // new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('1 is < 2')], { + // outputType: 'stream' + // }) + // ] + // ); + // }); + + // test('Streamed text with angle bracket characters and ansi chars', async () => { + // validateCellOutputTranslation( + // [ + // { + // name: 'stderr', + // text: '1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n', + // output_type: 'stream' + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [vscode.NotebookCellOutputItem.stderr('1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n')], + // { + // outputType: 'stream' + // } + // ) + // ] + // ); + // }); + + // test('Error', async () => { + // validateCellOutputTranslation( + // [ + // { + // ename: 'Error Name', + // evalue: 'Error Value', + // traceback: ['stack1', 'stack2', 'stack3'], + // output_type: 'error' + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [ + // vscode.NotebookCellOutputItem.error({ + // name: 'Error Name', + // message: 'Error Value', + // stack: ['stack1', 'stack2', 'stack3'].join('\n') + // }) + // ], + // { + // outputType: 'error', + // originalError: { + // ename: 'Error Name', + // evalue: 'Error Value', + // traceback: ['stack1', 'stack2', 'stack3'], + // output_type: 'error' + // } + // } + // ) + // ] + // ); + // }); + + // ['display_data', 'execute_result'].forEach(output_type => { + // suite(`Rich output for output_type = ${output_type}`, () => { + // // Properties to exclude when comparing. + // let propertiesToExcludeFromComparison: string[] = []; + // setup(() => { + // if (output_type === 'display_data') { + // // With display_data the execution_count property will never exist in the output. + // // We can ignore that (as it will never exist). + // // But we leave it in the case of `output_type === 'execute_result'` + // propertiesToExcludeFromComparison = ['execution_count', 'executionCount']; + // } + // }); + + // test('Text mimeType output', async () => { + // validateCellOutputTranslation( + // [ + // { + // data: { + // 'text/plain': 'Hello World!' + // }, + // output_type, + // metadata: {}, + // execution_count: 1 + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [new vscode.NotebookCellOutputItem(Buffer.from('Hello World!', 'utf8'), 'text/plain')], + // { + // outputType: output_type, + // metadata: {}, // display_data & execute_result always have metadata. + // executionCount: 1 + // } + // ) + // ], + // propertiesToExcludeFromComparison + // ); + // }); + + // test('png,jpeg images', async () => { + // validateCellOutputTranslation( + // [ + // { + // execution_count: 1, + // data: { + // 'image/png': base64EncodedImage, + // 'image/jpeg': base64EncodedImage + // }, + // metadata: {}, + // output_type + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [ + // new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png'), + // new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/jpeg') + // ], + // { + // executionCount: 1, + // outputType: output_type, + // metadata: {} // display_data & execute_result always have metadata. + // } + // ) + // ], + // propertiesToExcludeFromComparison + // ); + // }); + + // test('png image with a light background', async () => { + // validateCellOutputTranslation( + // [ + // { + // execution_count: 1, + // data: { + // 'image/png': base64EncodedImage + // }, + // metadata: { + // needs_background: 'light' + // }, + // output_type + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + // { + // executionCount: 1, + // metadata: { + // needs_background: 'light' + // }, + // outputType: output_type + // } + // ) + // ], + // propertiesToExcludeFromComparison + // ); + // }); + + // test('png image with a dark background', async () => { + // validateCellOutputTranslation( + // [ + // { + // execution_count: 1, + // data: { + // 'image/png': base64EncodedImage + // }, + // metadata: { + // needs_background: 'dark' + // }, + // output_type + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + // { + // executionCount: 1, + // metadata: { + // needs_background: 'dark' + // }, + // outputType: output_type + // } + // ) + // ], + // propertiesToExcludeFromComparison + // ); + // }); + + // test('png image with custom dimensions', async () => { + // validateCellOutputTranslation( + // [ + // { + // execution_count: 1, + // data: { + // 'image/png': base64EncodedImage + // }, + // metadata: { + // 'image/png': { height: '111px', width: '999px' } + // }, + // output_type + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + // { + // executionCount: 1, + // metadata: { + // 'image/png': { height: '111px', width: '999px' } + // }, + // outputType: output_type + // } + // ) + // ], + // propertiesToExcludeFromComparison + // ); + // }); + + // test('png allowed to scroll', async () => { + // validateCellOutputTranslation( + // [ + // { + // execution_count: 1, + // data: { + // 'image/png': base64EncodedImage + // }, + // metadata: { + // unconfined: true, + // 'image/png': { width: '999px' } + // }, + // output_type + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + // { + // executionCount: 1, + // metadata: { + // unconfined: true, + // 'image/png': { width: '999px' } + // }, + // outputType: output_type + // } + // ) + // ], + // propertiesToExcludeFromComparison + // ); + // }); + // }); + // }); + // }); + + // suite('Output Order', () => { + // test('Verify order of outputs', async () => { + // const dataAndExpectedOrder: { output: nbformat.IDisplayData; expectedMimeTypesOrder: string[] }[] = [ + // { + // output: { + // data: { + // 'application/vnd.vegalite.v4+json': 'some json', + // 'text/html': 'Hello' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: ['application/vnd.vegalite.v4+json', 'text/html'] + // }, + // { + // output: { + // data: { + // 'application/vnd.vegalite.v4+json': 'some json', + // 'application/javascript': 'some js', + // 'text/plain': 'some text', + // 'text/html': 'Hello' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: [ + // 'application/vnd.vegalite.v4+json', + // 'text/html', + // 'application/javascript', + // 'text/plain' + // ] + // }, + // { + // output: { + // data: { + // 'application/vnd.vegalite.v4+json': '', // Empty, should give preference to other mimetypes. + // 'application/javascript': 'some js', + // 'text/plain': 'some text', + // 'text/html': 'Hello' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: [ + // 'text/html', + // 'application/javascript', + // 'text/plain', + // 'application/vnd.vegalite.v4+json' + // ] + // }, + // { + // output: { + // data: { + // 'text/plain': 'some text', + // 'text/html': 'Hello' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: ['text/html', 'text/plain'] + // }, + // { + // output: { + // data: { + // 'application/javascript': 'some js', + // 'text/plain': 'some text' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: ['application/javascript', 'text/plain'] + // }, + // { + // output: { + // data: { + // 'image/svg+xml': 'some svg', + // 'text/plain': 'some text' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: ['image/svg+xml', 'text/plain'] + // }, + // { + // output: { + // data: { + // 'text/latex': 'some latex', + // 'text/plain': 'some text' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: ['text/latex', 'text/plain'] + // }, + // { + // output: { + // data: { + // 'application/vnd.jupyter.widget-view+json': 'some widget', + // 'text/plain': 'some text' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: ['application/vnd.jupyter.widget-view+json', 'text/plain'] + // }, + // { + // output: { + // data: { + // 'text/plain': 'some text', + // 'image/svg+xml': 'some svg', + // 'image/png': 'some png' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: ['image/png', 'image/svg+xml', 'text/plain'] + // } + // ]; + + // dataAndExpectedOrder.forEach(({ output, expectedMimeTypesOrder }) => { + // const sortedOutputs = jupyterCellOutputToCellOutput(output); + // const mimeTypes = sortedOutputs.items.map((item) => item.mime).join(','); + // assert.equal(mimeTypes, expectedMimeTypesOrder.join(',')); + // }); + // }); + // }); +}); diff --git a/extensions/ipynb/src/test/serializers.test.ts b/extensions/ipynb/src/test/serializers.test.ts index cb461539c5d0..e132b6b2b1d1 100644 --- a/extensions/ipynb/src/test/serializers.test.ts +++ b/extensions/ipynb/src/test/serializers.test.ts @@ -41,6 +41,12 @@ suite(`ipynb serializer`, () => { source: 'print(1)', metadata: {} }, + { + cell_type: 'code', + outputs: [], + source: 'print(2)', + metadata: {} + }, { cell_type: 'markdown', source: '# HEAD', @@ -55,13 +61,18 @@ suite(`ipynb serializer`, () => { expectedCodeCell.metadata = { execution_count: 10, metadata: {} }; expectedCodeCell.executionSummary = { executionOrder: 10 }; + const expectedCodeCell2 = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, 'print(2)', 'python'); + expectedCodeCell2.outputs = []; + expectedCodeCell2.metadata = { execution_count: null, metadata: {} }; + expectedCodeCell2.executionSummary = {}; + const expectedMarkdownCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# HEAD', 'markdown'); expectedMarkdownCell.outputs = []; expectedMarkdownCell.metadata = { metadata: {} }; - assert.deepStrictEqual(notebook.cells, [expectedCodeCell, expectedMarkdownCell]); + assert.deepStrictEqual(notebook.cells, [expectedCodeCell, expectedCodeCell2, expectedMarkdownCell]); }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index b08b816b95af..b528b21b2a6b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -282,7 +282,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } get visibleRanges() { - return this._list.visibleRanges || []; + return this._list ? (this._list.visibleRanges || []) : []; } private _baseCellEditorOptions = new Map(); From 70140a270ccb3918a3d2e030ccd9ebbe5664ff99 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 6 Jan 2025 11:15:40 +0100 Subject: [PATCH 0353/3587] Add a setting to warn when collapsing unsubmitted comments (#236589) Fixes https://github.com/microsoft/vscode-pull-request-github/issues/1455 --- .../comments/browser/commentThreadBody.ts | 5 ++- .../comments/browser/commentThreadHeader.ts | 8 ++-- .../comments/browser/commentThreadWidget.ts | 44 ++++++++++++++----- .../comments/browser/comments.contribution.ts | 6 +++ 4 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts index fa68fe093031..035b88131fe7 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts @@ -41,7 +41,6 @@ export class CommentThreadBody extends D return this._commentElements.filter(node => node.isEditing)[0]; } - constructor( private readonly _parentEditor: LayoutableEditor, readonly owner: string, @@ -77,6 +76,10 @@ export class CommentThreadBody extends D this._commentsElement.focus(); } + hasCommentsInEditMode() { + return this._commentElements.some(commentNode => commentNode.isEditing); + } + ensureFocusIntoNewEditingComment() { if (this._commentElements.length === 1 && this._commentElements[0].isEditing) { this._commentElements[0].setFocus(true); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts index 3f42e0cdf323..cb721233760a 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts @@ -44,9 +44,9 @@ export class CommentThreadHeader extends Disposable { private _delegate: { collapse: () => void }, private _commentMenus: CommentMenus, private _commentThread: languages.CommentThread, - private _contextKeyService: IContextKeyService, - private instantiationService: IInstantiationService, - private _contextMenuService: IContextMenuService + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IContextMenuService private readonly _contextMenuService: IContextMenuService ) { super(); this._headElement = dom.$('.head'); @@ -63,7 +63,7 @@ export class CommentThreadHeader extends Disposable { const actionsContainer = dom.append(this._headElement, dom.$('.review-actions')); this._actionbarWidget = new ActionBar(actionsContainer, { - actionViewItemProvider: createActionViewItem.bind(undefined, this.instantiationService) + actionViewItemProvider: createActionViewItem.bind(undefined, this._instantiationService) }); this._register(this._actionbarWidget); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 94578614d7bf..41fbd153cfd0 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -5,6 +5,7 @@ import './media/review.css'; import * as dom from '../../../../base/browser/dom.js'; +import * as nls from '../../../../nls.js'; import * as domStylesheets from '../../../../base/browser/domStylesheets.js'; import { Emitter } from '../../../../base/common/event.js'; import { Disposable, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; @@ -28,7 +29,6 @@ import { IRange, Range } from '../../../../editor/common/core/range.js'; import { commentThreadStateBackgroundColorVar, commentThreadStateColorVar } from './commentColors.js'; import { ICellRange } from '../../notebook/common/notebookRange.js'; import { FontInfo } from '../../../../editor/common/config/fontInfo.js'; -import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { COMMENTS_SECTION, ICommentsConfiguration } from '../common/commentsConfiguration.js'; @@ -38,6 +38,8 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js'; import { LayoutableEditor } from './simpleCommentEditor.js'; import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import Severity from '../../../../base/common/severity.js'; export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; @@ -76,10 +78,11 @@ export class CommentThreadWidget extends actionRunner: (() => void) | null; collapse: () => void; }, - @ICommentService private commentService: ICommentService, - @IContextMenuService contextMenuService: IContextMenuService, - @IConfigurationService private configurationService: IConfigurationService, - @IKeybindingService private _keybindingService: IKeybindingService + @ICommentService private readonly commentService: ICommentService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IDialogService private readonly _dialogService: IDialogService ) { super(); @@ -89,16 +92,14 @@ export class CommentThreadWidget extends this._commentMenus = this.commentService.getCommentMenus(this._owner); - this._register(this._header = new CommentThreadHeader( + this._register(this._header = this._scopedInstantiationService.createInstance( + CommentThreadHeader, container, { - collapse: this.collapse.bind(this) + collapse: this.collapseAction.bind(this) }, this._commentMenus, - this._commentThread, - this._contextKeyService, - this._scopedInstantiationService, - contextMenuService + this._commentThread )); this._header.updateCommentThread(this._commentThread); @@ -159,6 +160,21 @@ export class CommentThreadWidget extends this.currentThreadListeners(); } + private async confirmCollapse(): Promise { + const confirmSetting = this._configurationService.getValue<'whenHasUnsubmittedComments' | 'never'>('comments.thread.confirmOnCollapse'); + + const hasUnsubmitted = !!this._commentReply?.commentEditor.getValue() || this._body.hasCommentsInEditMode(); + if (confirmSetting === 'whenHasUnsubmittedComments' && hasUnsubmitted) { + const result = await this._dialogService.confirm({ + message: nls.localize('confirmCollapse', "This comment thread has unsubmitted comments. Do you want to collapse it?"), + primaryButton: nls.localize('collapse', "Collapse"), + type: Severity.Warning + }); + return result.confirmed; + } + return true; + } + private _setAriaLabel(): void { let ariaLabel = localize('commentLabel', "Comment"); let keybinding: string | undefined; @@ -375,6 +391,12 @@ export class CommentThreadWidget extends } } + private async collapseAction() { + if (await this.confirmCollapse()) { + this.collapse(); + } + } + collapse() { if (Range.isIRange(this.commentThread.range) && isCodeEditor(this._parentEditor)) { this._parentEditor.setSelection(this.commentThread.range); diff --git a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index d2cfe1a5680a..3cf98a0cde92 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -138,6 +138,12 @@ Registry.as(ConfigurationExtensions.Configuration).regis type: 'boolean', default: true, description: nls.localize('collapseOnResolve', "Controls whether the comment thread should collapse when the thread is resolved.") + }, + 'comments.thread.confirmOnCollapse': { + type: 'string', + enum: ['whenHasUnsubmittedComments', 'never'], + default: 'never', + description: nls.localize('confirmOnCollapse', "Controls whether a confirmation dialog is shown when collapsing a comment thread with unsubmitted comments.") } } }); From e8b620fc968e78136c3417aab4ef1cc9950b55ed Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 6 Jan 2025 11:25:43 +0100 Subject: [PATCH 0354/3587] Settings progress bar is too eager to appear (fix #200547) (#237322) --- src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 8e3388a7008e..43214c917773 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -1606,7 +1606,7 @@ export class SettingsEditor2 extends EditorPane { } private async triggerSearch(query: string): Promise { - const progressRunner = this.editorProgressService.show(true); + const progressRunner = this.editorProgressService.show(true, 800); this.viewState.tagFilters = new Set(); this.viewState.extensionFilters = new Set(); this.viewState.featureFilters = new Set(); From dc1cfc044d68746517d5fefd976eb7d13781b170 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 6 Jan 2025 11:27:00 +0100 Subject: [PATCH 0355/3587] Setup: allow a command link that opens setup view (fix microsoft/vscode-copilot#11301) (#237323) --- .../contrib/chat/browser/chatSetup.ts | 18 ++++++++++- .../extensions/browser/extensionUrlHandler.ts | 30 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 53a86ec5245a..125673651263 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -60,6 +60,7 @@ import { IHostService } from '../../../services/host/browser/host.js'; import Severity from '../../../../base/common/severity.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { isWeb } from '../../../../base/common/platform.js'; +import { ExtensionUrlHandlerOverrideRegistry } from '../../../services/extensions/browser/extensionUrlHandler.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -109,7 +110,9 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr constructor( @IProductService private readonly productService: IProductService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ICommandService private readonly commandService: ICommandService, + @ITelemetryService private readonly telemetryService: ITelemetryService ) { super(); @@ -122,6 +125,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr this.registerChatWelcome(); this.registerActions(); + this.registerUrlLinkHandler(); } private registerChatWelcome(): void { @@ -292,6 +296,18 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr registerAction2(ChatSetupHideAction); registerAction2(UpgradePlanAction); } + + private registerUrlLinkHandler(): void { + this._register(ExtensionUrlHandlerOverrideRegistry.registerHandler(URI.parse(`${this.productService.urlProtocol}://${defaultChat.chatExtensionId}`), { + handleURL: async () => { + this.telemetryService.publicLog2('workbenchActionExecuted', { id: TRIGGER_SETUP_COMMAND_ID, from: 'url' }); + + await this.commandService.executeCommand(TRIGGER_SETUP_COMMAND_ID); + + return true; + } + })); + } } //#endregion diff --git a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index ac3c7ed50063..04aa6fe51f84 100644 --- a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize, localize2 } from '../../../../nls.js'; -import { IDisposable, combinedDisposable } from '../../../../base/common/lifecycle.js'; +import { IDisposable, combinedDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; @@ -27,6 +27,7 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { isCancellationError } from '../../../../base/common/errors.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; +import { ResourceMap } from '../../../../base/common/map.js'; const FIVE_MINUTES = 5 * 60 * 1000; const THIRTY_SECONDS = 30 * 1000; @@ -99,6 +100,25 @@ type ExtensionUrlReloadHandlerClassification = { comment: 'This is used to understand the drop funnel of extension URI handling by the OS & VS Code.'; }; +export interface IExtensionUrlHandlerOverride { + handleURL(uri: URI): Promise; +} + +export class ExtensionUrlHandlerOverrideRegistry { + + private static readonly handlers = new ResourceMap(); + + static registerHandler(uri: URI, handler: IExtensionUrlHandlerOverride): IDisposable { + this.handlers.set(uri, handler); + + return toDisposable(() => this.handlers.delete(uri)); + } + + static getHandler(uri: URI): IExtensionUrlHandlerOverride | undefined { + return this.handlers.get(uri); + } +} + /** * This class handles URLs which are directed towards extensions. * If a URL is directed towards an inactive extension, it buffers it, @@ -153,6 +173,14 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { return false; } + const overrideHandler = ExtensionUrlHandlerOverrideRegistry.getHandler(uri); + if (overrideHandler) { + const handled = await overrideHandler.handleURL(uri); + if (handled) { + return handled; + } + } + const extensionId = uri.authority; this.telemetryService.publicLog2('uri_invoked/start', { extensionId }); From 567779ca5154518823ac35f2cdb0efa36fe1cba7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 6 Jan 2025 14:34:23 +0100 Subject: [PATCH 0356/3587] Status bar tooltip - add support for specifying a list of commands and lazy contents (#234339) (#237336) Core changes. --- .../browser/parts/statusbar/statusbarItem.ts | 25 +++++++++++++++---- .../services/statusbar/browser/statusbar.ts | 20 +++++++++++++-- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts index 3de50cb8937f..149ee4cbff52 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts @@ -8,7 +8,7 @@ import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle import { SimpleIconLabel } from '../../../../base/browser/ui/iconLabel/simpleIconLabel.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IStatusbarEntry, ShowTooltipCommand, StatusbarEntryKinds } from '../../../services/statusbar/browser/statusbar.js'; +import { IStatusbarEntry, isTooltipWithCommands, ShowTooltipCommand, StatusbarEntryKinds, TooltipContent } from '../../../services/statusbar/browser/statusbar.js'; import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from '../../../../base/common/actions.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { ThemeColor } from '../../../../base/common/themables.js'; @@ -24,7 +24,7 @@ import { spinningLoading, syncing } from '../../../../platform/theme/common/icon import { isMarkdownString, markdownStringEqual } from '../../../../base/common/htmlContent.js'; import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { Gesture, EventType as TouchEventType } from '../../../../base/browser/touch.js'; -import type { IManagedHover } from '../../../../base/browser/ui/hover/hover.js'; +import { IManagedHover, IManagedHoverOptions } from '../../../../base/browser/ui/hover/hover.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; export class StatusbarEntryItem extends Disposable { @@ -116,11 +116,26 @@ export class StatusbarEntryItem extends Disposable { // Update: Hover if (!this.entry || !this.isEqualTooltip(this.entry, entry)) { - const hoverContents = isMarkdownString(entry.tooltip) ? { markdown: entry.tooltip, markdownNotSupportedFallback: undefined } : entry.tooltip; + let hoverOptions: IManagedHoverOptions | undefined; + let hoverTooltip: TooltipContent | undefined; + if (isTooltipWithCommands(entry.tooltip)) { + hoverTooltip = entry.tooltip.content; + hoverOptions = { + actions: entry.tooltip.commands.map(command => ({ + commandId: command.id, + label: command.title, + run: () => this.executeCommand(command) + })) + }; + } else { + hoverTooltip = entry.tooltip; + } + + const hoverContents = isMarkdownString(hoverTooltip) ? { markdown: hoverTooltip, markdownNotSupportedFallback: undefined } : hoverTooltip; if (this.hover) { - this.hover.update(hoverContents); + this.hover.update(hoverContents, hoverOptions); } else { - this.hover = this._register(this.hoverService.setupManagedHover(this.hoverDelegate, this.container, hoverContents)); + this.hover = this._register(this.hoverService.setupManagedHover(this.hoverDelegate, this.container, hoverContents, hoverOptions)); } } diff --git a/src/vs/workbench/services/statusbar/browser/statusbar.ts b/src/vs/workbench/services/statusbar/browser/statusbar.ts index 3b3f1f1fe173..7c81333e4162 100644 --- a/src/vs/workbench/services/statusbar/browser/statusbar.ts +++ b/src/vs/workbench/services/statusbar/browser/statusbar.ts @@ -8,6 +8,7 @@ import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle. import { ThemeColor } from '../../../../base/common/themables.js'; import { Command } from '../../../../editor/common/languages.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; +import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; import { ColorIdentifier } from '../../../../platform/theme/common/colorRegistry.js'; import { IAuxiliaryStatusbarPart, IStatusbarEntryContainer } from '../../../browser/parts/statusbar/statusbarPart.js'; @@ -111,6 +112,19 @@ export interface IStatusbarStyleOverride { export type StatusbarEntryKind = 'standard' | 'warning' | 'error' | 'prominent' | 'remote' | 'offline'; export const StatusbarEntryKinds: StatusbarEntryKind[] = ['standard', 'warning', 'error', 'prominent', 'remote', 'offline']; +export type TooltipContent = string | IMarkdownString | IManagedHoverTooltipMarkdownString | HTMLElement; + +export interface ITooltipWithCommands { + readonly content: TooltipContent; + readonly commands: Command[]; +} + +export function isTooltipWithCommands(thing: unknown): thing is ITooltipWithCommands { + const candidate = thing as ITooltipWithCommands | undefined; + + return !!candidate?.content && Array.isArray(candidate?.commands); +} + /** * A declarative way of describing a status bar entry */ @@ -141,9 +155,11 @@ export interface IStatusbarEntry { readonly role?: string; /** - * An optional tooltip text to show when you hover over the entry + * An optional tooltip text to show when you hover over the entry. + * + * Use `ITooltipWithCommands` to show a tooltip with commands in hover footer area. */ - readonly tooltip?: string | IMarkdownString | HTMLElement; + readonly tooltip?: TooltipContent | ITooltipWithCommands; /** * An optional color to use for the entry. From b7f437d2a1974e160f3ec00b9e72526d27bc4fb9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 6 Jan 2025 15:37:07 +0100 Subject: [PATCH 0357/3587] Allow custom titlebar on Linux as experiment (microsoft/vscode-internalbacklog#4857) (#237337) --- src/vs/code/electron-main/app.ts | 12 +++++-- src/vs/platform/native/common/native.ts | 9 +++-- .../electron-main/nativeHostMainService.ts | 16 +++++++-- src/vs/platform/window/common/window.ts | 20 ++++++++++- .../electron-sandbox/desktop.contribution.ts | 1 + .../parts/titlebar/titlebarPart.ts | 34 +++++++++++++++++-- .../electron-sandbox/workbenchTestServices.ts | 1 + 7 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 11ca66e8767c..7c7fc636e21d 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -83,7 +83,7 @@ import { NativeURLService } from '../../platform/url/common/urlService.js'; import { ElectronURLListener } from '../../platform/url/electron-main/electronUrlListener.js'; import { IWebviewManagerService } from '../../platform/webview/common/webviewManagerService.js'; import { WebviewMainService } from '../../platform/webview/electron-main/webviewMainService.js'; -import { isFolderToOpen, isWorkspaceToOpen, IWindowOpenable } from '../../platform/window/common/window.js'; +import { isFolderToOpen, isWorkspaceToOpen, IWindowOpenable, TitlebarStyle, overrideDefaultTitlebarStyle } from '../../platform/window/common/window.js'; import { IWindowsMainService, OpenContext } from '../../platform/windows/electron-main/windows.js'; import { ICodeWindow } from '../../platform/window/electron-main/window.js'; import { WindowsMainService } from '../../platform/windows/electron-main/windowsMainService.js'; @@ -593,6 +593,14 @@ export class CodeApplication extends Disposable { // Services const appInstantiationService = await this.initServices(machineId, sqmId, devDeviceId, sharedProcessReady); + // Linux (stable only): custom title default style override + if (isLinux && this.productService.quality === 'stable') { + const titleBarDefaultStyleOverride = this.stateService.getItem('window.titleBarStyleOverride'); + if (titleBarDefaultStyleOverride === TitlebarStyle.CUSTOM || titleBarDefaultStyleOverride === TitlebarStyle.NATIVE) { + overrideDefaultTitlebarStyle(titleBarDefaultStyleOverride); + } + } + // Auth Handler appInstantiationService.invokeFunction(accessor => accessor.get(IProxyAuthService)); @@ -605,7 +613,7 @@ export class CodeApplication extends Disposable { // Setup Protocol URL Handlers const initialProtocolUrls = await appInstantiationService.invokeFunction(accessor => this.setupProtocolUrlHandlers(accessor, mainProcessElectronServer)); - // Setup vscode-remote-resource protocol handler. + // Setup vscode-remote-resource protocol handler this.setupManagedRemoteResourceUrlHandler(mainProcessElectronServer); // Signal phase: ready - before opening first window diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 2fdfb63e0b55..1f8d0adabe7b 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -113,6 +113,9 @@ export interface ICommonNativeHostService { */ focusWindow(options?: INativeHostOptions & { force?: boolean }): Promise; + // Titlebar default style override + overrideDefaultTitlebarStyle(style: 'native' | 'custom' | undefined): Promise; + // Dialogs showMessageBox(options: MessageBoxOptions & INativeHostOptions): Promise; showSaveDialog(options: SaveDialogOptions & INativeHostOptions): Promise; @@ -143,10 +146,6 @@ export interface ICommonNativeHostService { hasWSLFeatureInstalled(): Promise; // Screenshots - - /** - * Gets a screenshot of the currently active Electron window. - */ getScreenshot(): Promise; // Process @@ -199,7 +198,7 @@ export interface ICommonNativeHostService { loadCertificates(): Promise; findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride?: number): Promise; - // Registry (windows only) + // Registry (Windows only) windowsGetStringRegKey(hive: 'HKEY_CURRENT_USER' | 'HKEY_LOCAL_MACHINE' | 'HKEY_CLASSES_ROOT' | 'HKEY_USERS' | 'HKEY_CURRENT_CONFIG', path: string, name: string): Promise; } diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 794c5aed5753..8e5c129ea4f2 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -33,7 +33,7 @@ import { IProductService } from '../../product/common/productService.js'; import { IPartsSplash } from '../../theme/common/themeService.js'; import { IThemeMainService } from '../../theme/electron-main/themeMainService.js'; import { defaultWindowState, ICodeWindow } from '../../window/electron-main/window.js'; -import { IColorScheme, IOpenedAuxiliaryWindow, IOpenedMainWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IPoint, IRectangle, IWindowOpenable } from '../../window/common/window.js'; +import { IColorScheme, IOpenedAuxiliaryWindow, IOpenedMainWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IPoint, IRectangle, IWindowOpenable, overrideDefaultTitlebarStyle } from '../../window/common/window.js'; import { defaultBrowserWindowOptions, IWindowsMainService, OpenContext } from '../../windows/electron-main/windows.js'; import { isWorkspaceIdentifier, toWorkspaceIdentifier } from '../../workspace/common/workspace.js'; import { IWorkspacesManagementMainService } from '../../workspaces/electron-main/workspacesManagementMainService.js'; @@ -48,6 +48,7 @@ import { IConfigurationService } from '../../configuration/common/configuration. import { IProxyAuthService } from './auth.js'; import { AuthInfo, Credentials, IRequestService } from '../../request/common/request.js'; import { randomPath } from '../../../base/common/extpath.js'; +import { IStateService } from '../../state/node/state.js'; export interface INativeHostMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } @@ -70,7 +71,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain @IConfigurationService private readonly configurationService: IConfigurationService, @IRequestService private readonly requestService: IRequestService, @IProxyAuthService private readonly proxyAuthService: IProxyAuthService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IStateService private readonly stateService: IStateService ) { super(); } @@ -324,6 +326,15 @@ export class NativeHostMainService extends Disposable implements INativeHostMain this.themeMainService.saveWindowSplash(windowId, splash); } + async overrideDefaultTitlebarStyle(windowId: number | undefined, style: 'native' | 'custom' | undefined): Promise { + if (typeof style === 'string') { + this.stateService.setItem('window.titleBarStyleOverride', style); + } else { + this.stateService.removeItem('window.titleBarStyleOverride'); + } + overrideDefaultTitlebarStyle(style); + } + //#endregion @@ -697,6 +708,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async getScreenshot(windowId: number | undefined, options?: INativeHostOptions): Promise { const window = this.windowById(options?.targetWindowId, windowId); const captured = await window?.win?.webContents.capturePage(); + return captured?.toJPEG(95); } diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 3a03481e55a5..02e4152376da 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -187,10 +187,23 @@ export const enum CustomTitleBarVisibility { NEVER = 'never', } +export let titlebarStyleDefaultOverride: TitlebarStyle | undefined = undefined; +export function overrideDefaultTitlebarStyle(style: 'native' | 'custom' | undefined): void { + switch (style) { + case 'native': + titlebarStyleDefaultOverride = TitlebarStyle.NATIVE; + break; + case 'custom': + titlebarStyleDefaultOverride = TitlebarStyle.CUSTOM; + break; + default: + titlebarStyleDefaultOverride = undefined; + } +} + export function hasCustomTitlebar(configurationService: IConfigurationService, titleBarStyle?: TitlebarStyle): boolean { // Returns if it possible to have a custom title bar in the curren session // Does not imply that the title bar is visible - return true; } @@ -198,6 +211,7 @@ export function hasNativeTitlebar(configurationService: IConfigurationService, t if (!titleBarStyle) { titleBarStyle = getTitleBarStyle(configurationService); } + return titleBarStyle === TitlebarStyle.NATIVE; } @@ -224,6 +238,10 @@ export function getTitleBarStyle(configurationService: IConfigurationService): T } } + if (titlebarStyleDefaultOverride) { + return titlebarStyleDefaultOverride; + } + return isLinux && product.quality === 'stable' ? TitlebarStyle.NATIVE : TitlebarStyle.CUSTOM; // default to custom on all OS except Linux stable (for now) } diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index 19a8cf11e841..da72519dee35 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -234,6 +234,7 @@ import product from '../../platform/product/common/product.js'; 'type': 'string', 'enum': ['native', 'custom'], 'default': isLinux && product.quality === 'stable' ? 'native' : 'custom', + 'tags': isLinux && product.quality === 'stable' ? ['onExP'] : undefined, 'scope': ConfigurationScope.APPLICATION, 'description': localize('titleBarStyle', "Adjust the appearance of the window title bar to be native by the OS or custom. On Linux and Windows, this setting also affects the application and context menu appearances. Changes require a full restart to apply."), }, diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index 04095aab5d76..0d7a06de173d 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts @@ -27,6 +27,7 @@ import { IEditorGroupsContainer, IEditorGroupsService } from '../../../services/ import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { CodeWindow, mainWindow } from '../../../../base/browser/window.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; export class NativeTitlebarPart extends BrowserTitlebarPart { @@ -70,7 +71,7 @@ export class NativeTitlebarPart extends BrowserTitlebarPart { @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IContextKeyService contextKeyService: IContextKeyService, @IHostService hostService: IHostService, - @INativeHostService private readonly nativeHostService: INativeHostService, + @INativeHostService protected readonly nativeHostService: INativeHostService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @IEditorService editorService: IEditorService, @IMenuService menuService: IMenuService, @@ -287,9 +288,38 @@ export class MainNativeTitlebarPart extends NativeTitlebarPart { @IEditorGroupsService editorGroupService: IEditorGroupsService, @IEditorService editorService: IEditorService, @IMenuService menuService: IMenuService, - @IKeybindingService keybindingService: IKeybindingService + @IKeybindingService keybindingService: IKeybindingService, + @IProductService productService: IProductService ) { super(Parts.TITLEBAR_PART, mainWindow, 'main', contextMenuService, configurationService, environmentService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, nativeHostService, editorGroupService, editorService, menuService, keybindingService); + + if (isLinux && productService.quality === 'stable') { + this.handleDefaultTitlebarStyle(); // TODO@bpasero remove me eventually once settled + } + } + + private handleDefaultTitlebarStyle(): void { + this.updateDefaultTitlebarStyle(); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('window.titleBarStyle')) { + this.updateDefaultTitlebarStyle(); + } + })); + } + + private updateDefaultTitlebarStyle(): void { + const titleBarStyle = this.configurationService.inspect('window.titleBarStyle'); + + let titleBarStyleOverride: 'custom' | undefined; + if (titleBarStyle.applicationValue || titleBarStyle.userValue || titleBarStyle.userLocalValue) { + // configured by user or application: clear override + titleBarStyleOverride = undefined; + } else { + // not configured: set override if experiment is active + titleBarStyleOverride = titleBarStyle.defaultValue === 'native' ? undefined : 'custom'; + } + + this.nativeHostService.overrideDefaultTitlebarStyle(titleBarStyleOverride); } } diff --git a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts index 9bc958141ff7..ecec6ce983de 100644 --- a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts @@ -162,6 +162,7 @@ export class TestNativeHostService implements INativeHostService { async windowsGetStringRegKey(hive: 'HKEY_CURRENT_USER' | 'HKEY_LOCAL_MACHINE' | 'HKEY_CLASSES_ROOT' | 'HKEY_USERS' | 'HKEY_CURRENT_CONFIG', path: string, name: string): Promise { return undefined; } async profileRenderer(): Promise { throw new Error(); } async getScreenshot(): Promise { return undefined; } + async overrideDefaultTitlebarStyle(style: 'native' | 'custom' | undefined): Promise { } } export class TestExtensionTipsService extends AbstractNativeExtensionTipsService { From d0dfb42e3468b5b0aa87bd825ec5da20ac1684d2 Mon Sep 17 00:00:00 2001 From: Robo Date: Tue, 7 Jan 2025 00:33:12 +0900 Subject: [PATCH 0358/3587] ci: fix broken monaco editor action (#237347) --- .github/workflows/monaco-editor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/monaco-editor.yml b/.github/workflows/monaco-editor.yml index 426999ce43b4..1f5694faec2b 100644 --- a/.github/workflows/monaco-editor.yml +++ b/.github/workflows/monaco-editor.yml @@ -45,11 +45,11 @@ jobs: path: ${{ steps.npmCacheDirPath.outputs.dir }} key: ${{ runner.os }}-npmCacheDir-${{ steps.nodeModulesCacheKey.outputs.value }} restore-keys: ${{ runner.os }}-npmCacheDir- - - name: Install libkrb5-dev + - name: Install system dependencies if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} run: | sudo apt update - sudo apt install -y libkrb5-dev + sudo apt install -y libxkbfile-dev pkg-config libkrb5-dev libxss1 - name: Execute npm if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} env: From aaa576acca01852119f6a6b0260cf5aa74a30c58 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 6 Jan 2025 16:54:06 +0100 Subject: [PATCH 0359/3587] Add time information to chatinstallentitlement event (fix microsoft/vscode-copilot#11476) (#237350) --- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 125673651263..52bb5bea1b0f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -318,6 +318,7 @@ type EntitlementClassification = { entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating the chat entitlement state' }; quotaChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat completions available to the user' }; quotaCompletions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat completions available to the user' }; + quotaResetDate: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The date the quota will reset' }; owner: 'bpasero'; comment: 'Reporting chat setup entitlements'; }; @@ -326,6 +327,7 @@ type EntitlementEvent = { entitlement: ChatEntitlement; quotaChat: number | undefined; quotaCompletions: number | undefined; + quotaResetDate: string | undefined; }; interface IEntitlementsResponse { @@ -536,7 +538,8 @@ class ChatSetupRequests extends Disposable { this.telemetryService.publicLog2('chatInstallEntitlement', { entitlement: entitlements.entitlement, quotaChat: entitlementsResponse.limited_user_quotas?.chat, - quotaCompletions: entitlementsResponse.limited_user_quotas?.completions + quotaCompletions: entitlementsResponse.limited_user_quotas?.completions, + quotaResetDate: entitlementsResponse.limited_user_reset_date }); return entitlements; From 4e2ba23aed553047bd7b5726f1b92520adeda749 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 6 Jan 2025 16:59:10 +0100 Subject: [PATCH 0360/3587] chat setup - restore copilot menu on extension install (#237348) --- .../browser/actions/chatGettingStarted.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts b/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts index 8dc7d4dee91f..ea35e44e0169 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts @@ -16,6 +16,7 @@ import { IDefaultChatAgent } from '../../../../../base/common/product.js'; import { IViewDescriptorService } from '../../../../common/views.js'; import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; import { ensureSideBarChatViewSize } from '../chat.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; export class ChatGettingStartedContribution extends Disposable implements IWorkbenchContribution { @@ -32,6 +33,7 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb @IStorageService private readonly storageService: IStorageService, @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); @@ -60,14 +62,25 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb if (ExtensionIdentifier.equals(defaultChatAgent.extensionId, ext.value)) { const extensionStatus = this.extensionService.getExtensionsStatus(); if (extensionStatus[ext.value].activationTimes && this.recentlyInstalled) { - await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID); - ensureSideBarChatViewSize(400, this.viewDescriptorService, this.layoutService); - this.storageService.store(ChatGettingStartedContribution.hideWelcomeView, true, StorageScope.APPLICATION, StorageTarget.MACHINE); - this.recentlyInstalled = false; + this.onDidInstallChat(); return; } } } })); } + + private async onDidInstallChat() { + + // Enable chat command center if previously disabled + this.configurationService.updateValue('chat.commandCenter.enabled', true); + + // Open and configure chat view + await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID); + ensureSideBarChatViewSize(400, this.viewDescriptorService, this.layoutService); + + // Only do this once + this.storageService.store(ChatGettingStartedContribution.hideWelcomeView, true, StorageScope.APPLICATION, StorageTarget.MACHINE); + this.recentlyInstalled = false; + } } From 3548eae0e119c7bdb948d138be0c129dad71493f Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:05:37 +0100 Subject: [PATCH 0361/3587] Git - add `git.commitShortHashLength` setting (#237343) --- extensions/git/package.json | 8 ++++++++ extensions/git/package.nls.json | 5 +++-- extensions/git/src/blame.ts | 19 +++++++++++-------- extensions/git/src/commands.ts | 15 +++++++++------ extensions/git/src/historyProvider.ts | 22 ++++++++++++---------- extensions/git/src/repository.ts | 6 +++--- extensions/git/src/util.ts | 8 +++++++- 7 files changed, 53 insertions(+), 30 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index cdb7c77a03a1..064dfe3f1cfb 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -3229,6 +3229,14 @@ "type": "string", "default": "${authorName} (${authorDateAgo})", "markdownDescription": "%config.blameStatusBarItem.template%" + }, + "git.commitShortHashLength": { + "type": "number", + "default": 7, + "minimum": 7, + "maximum": 40, + "markdownDescription": "%config.commitShortHashLength%", + "scope": "resource" } } }, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 6db253137e15..afbb44ba48f8 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -278,9 +278,10 @@ "config.publishBeforeContinueOn.prompt": "Prompt to publish unpublished Git state when using Continue Working On from a Git repository", "config.similarityThreshold": "Controls the threshold of the similarity index (the amount of additions/deletions compared to the file's size) for changes in a pair of added/deleted files to be considered a rename. **Note:** Requires Git version `2.18.0` or later.", "config.blameEditorDecoration.enabled": "Controls whether to show blame information in the editor using editor decorations.", - "config.blameEditorDecoration.template": "Template for the blame information editor decoration. Supported variables:\n\n* `hash`: Commit hash\n\n* `hashShort`: First 8 characters of the commit hash\n\n* `subject`: First line of the commit message\n\n* `authorName`: Author name\n\n* `authorEmail`: Author email\n\n* `authorDate`: Author date\n\n* `authorDateAgo`: Time difference between now and the author date\n\n", + "config.blameEditorDecoration.template": "Template for the blame information editor decoration. Supported variables:\n\n* `hash`: Commit hash\n\n* `hashShort`: First N characters of the commit hash according to `#git.commitShortHashLength#`\n\n* `subject`: First line of the commit message\n\n* `authorName`: Author name\n\n* `authorEmail`: Author email\n\n* `authorDate`: Author date\n\n* `authorDateAgo`: Time difference between now and the author date\n\n", "config.blameStatusBarItem.enabled": "Controls whether to show blame information in the status bar.", - "config.blameStatusBarItem.template": "Template for the blame information status bar item. Supported variables:\n\n* `hash`: Commit hash\n\n* `hashShort`: First 8 characters of the commit hash\n\n* `subject`: First line of the commit message\n\n* `authorName`: Author name\n\n* `authorEmail`: Author email\n\n* `authorDate`: Author date\n\n* `authorDateAgo`: Time difference between now and the author date\n\n", + "config.blameStatusBarItem.template": "Template for the blame information status bar item. Supported variables:\n\n* `hash`: Commit hash\n\n* `hashShort`: First N characters of the commit hash according to `#git.commitShortHashLength#`\n\n* `subject`: First line of the commit message\n\n* `authorName`: Author name\n\n* `authorEmail`: Author email\n\n* `authorDate`: Author date\n\n* `authorDateAgo`: Time difference between now and the author date\n\n", + "config.commitShortHashLength": "Controls the length of the commit short hash.", "submenu.explorer": "Git", "submenu.commit": "Commit", "submenu.commit.amend": "Amend", diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 3c7d61dd1e1f..0dbdca92b326 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -5,7 +5,7 @@ import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString, languages, HoverProvider, CancellationToken, Hover, TextDocument } from 'vscode'; import { Model } from './model'; -import { dispose, fromNow, IDisposable } from './util'; +import { dispose, fromNow, getCommitShortHash, IDisposable } from './util'; import { Repository } from './repository'; import { throttle } from './decorators'; import { BlameInformation, Commit } from './git'; @@ -186,14 +186,14 @@ export class GitBlameController { this._onDidChangeConfiguration(); } - formatBlameInformationMessage(template: string, blameInformation: BlameInformation): string { + formatBlameInformationMessage(documentUri: Uri, template: string, blameInformation: BlameInformation): string { const subject = blameInformation.subject && blameInformation.subject.length > this._subjectMaxLength ? `${blameInformation.subject.substring(0, this._subjectMaxLength)}\u2026` : blameInformation.subject; const templateTokens = { hash: blameInformation.hash, - hashShort: blameInformation.hash.substring(0, 8), + hashShort: getCommitShortHash(documentUri, blameInformation.hash), subject: emojify(subject ?? ''), authorName: blameInformation.authorName ?? '', authorEmail: blameInformation.authorEmail ?? '', @@ -260,7 +260,7 @@ export class GitBlameController { markdownString.appendMarkdown(`\n\n---\n\n`); } - markdownString.appendMarkdown(`[\`$(git-commit) ${blameInformationOrCommit.hash.substring(0, 8)} \`](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformationOrCommit.hash]))} "${l10n.t('View Commit')}")`); + markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, blameInformationOrCommit.hash)} \`](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformationOrCommit.hash]))} "${l10n.t('View Commit')}")`); markdownString.appendMarkdown(' '); markdownString.appendMarkdown(`[$(copy)](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformationOrCommit.hash))} "${l10n.t('Copy Commit Hash')}")`); markdownString.appendMarkdown('  |  '); @@ -571,7 +571,9 @@ class GitBlameEditorDecoration implements HoverProvider { } private _onDidChangeConfiguration(e?: ConfigurationChangeEvent): void { - if (e && !e.affectsConfiguration('git.blame.editorDecoration.template')) { + if (e && + !e.affectsConfiguration('git.commitShortHashLength') && + !e.affectsConfiguration('git.blame.editorDecoration.template')) { return; } @@ -610,7 +612,7 @@ class GitBlameEditorDecoration implements HoverProvider { const decorations = blameInformation.map(blame => { const contentText = typeof blame.blameInformation !== 'string' - ? this._controller.formatBlameInformationMessage(template, blame.blameInformation) + ? this._controller.formatBlameInformationMessage(textEditor.document.uri, template, blame.blameInformation) : blame.blameInformation; return this._createDecoration(blame.lineNumber, contentText); @@ -663,7 +665,8 @@ class GitBlameStatusBarItem { } private _onDidChangeConfiguration(e: ConfigurationChangeEvent): void { - if (!e.affectsConfiguration('git.blame.statusBarItem.template')) { + if (!e.affectsConfiguration('git.commitShortHashLength') && + !e.affectsConfiguration('git.blame.statusBarItem.template')) { return; } @@ -690,7 +693,7 @@ class GitBlameStatusBarItem { const config = workspace.getConfiguration('git'); const template = config.get('blame.statusBarItem.template', '${authorName} (${authorDateAgo})'); - this._statusBarItem.text = `$(git-commit) ${this._controller.formatBlameInformationMessage(template, blameInformation[0].blameInformation)}`; + this._statusBarItem.text = `$(git-commit) ${this._controller.formatBlameInformationMessage(window.activeTextEditor.document.uri, template, blameInformation[0].blameInformation)}`; this._statusBarItem.tooltip = this._controller.getBlameInformationHover(window.activeTextEditor.document.uri, blameInformation[0].blameInformation); this._statusBarItem.command = { title: l10n.t('View Commit'), diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 95ab9ada0713..f7476a232142 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -14,7 +14,7 @@ import { Model } from './model'; import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository'; import { DiffEditorSelectionHunkToolbarContext, applyLineChanges, getModifiedRange, getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges } from './staging'; import { fromGitUri, toGitUri, isGitUri, toMergeUris, toMultiFileDiffEditorUris } from './uri'; -import { dispose, grep, isDefined, isDescendant, pathEquals, relativePath, truncate } from './util'; +import { dispose, getCommitShortHash, grep, isDefined, isDescendant, pathEquals, relativePath, truncate } from './util'; import { GitTimelineItem } from './timelineProvider'; import { ApiRepository } from './api/api1'; import { getRemoteSourceActions, pickRemoteSource } from './remoteSource'; @@ -4286,16 +4286,17 @@ export class CommandCenter { let title: string | undefined; let historyItemParentId: string | undefined; + const rootUri = Uri.file(repository.root); // If historyItem2 is not provided, we are viewing a single commit. If historyItem2 is // provided, we are viewing a range and we have to include both start and end commits. // TODO@lszomoru - handle the case when historyItem2 is the first commit in the repository if (!historyItem2) { const commit = await repository.getCommit(historyItem1.id); - title = `${historyItem1.id.substring(0, 8)} - ${truncate(commit.message)}`; + title = `${getCommitShortHash(rootUri, historyItem1.id)} - ${truncate(commit.message)}`; historyItemParentId = historyItem1.parentIds.length > 0 ? historyItem1.parentIds[0] : `${historyItem1.id}^`; } else { - title = l10n.t('All Changes ({0} ↔ {1})', historyItem2.id.substring(0, 8), historyItem1.id.substring(0, 8)); + title = l10n.t('All Changes ({0} ↔ {1})', getCommitShortHash(rootUri, historyItem2.id), getCommitShortHash(rootUri, historyItem1.id)); historyItemParentId = historyItem2.parentIds.length > 0 ? historyItem2.parentIds[0] : `${historyItem2.id}^`; } @@ -4310,8 +4311,9 @@ export class CommandCenter { return; } - const modifiedShortRef = historyItem.id.substring(0, 8); - const originalShortRef = historyItem.parentIds.length > 0 ? historyItem.parentIds[0].substring(0, 8) : `${modifiedShortRef}^`; + const rootUri = Uri.file(repository.root); + const modifiedShortRef = getCommitShortHash(rootUri, historyItem.id); + const originalShortRef = historyItem.parentIds.length > 0 ? getCommitShortHash(rootUri, historyItem.parentIds[0]) : `${modifiedShortRef}^`; const title = l10n.t('All Changes ({0} ↔ {1})', originalShortRef, modifiedShortRef); const multiDiffSourceUri = toGitUri(Uri.file(repository.root), historyItem.id, { scheme: 'git-changes' }); @@ -4350,8 +4352,9 @@ export class CommandCenter { return; } + const rootUri = Uri.file(repository.root); const commit = await repository.getCommit(historyItemId); - const title = `${historyItemId.substring(0, 8)} - ${truncate(commit.message)}`; + const title = `${getCommitShortHash(rootUri, historyItemId)} - ${truncate(commit.message)}`; const historyItemParentId = commit.parents.length > 0 ? commit.parents[0] : `${historyItemId}^`; const multiDiffSourceUri = Uri.from({ scheme: 'scm-history-item', path: `${repository.root}/${historyItemParentId}..${historyItemId}` }); diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 896c46851c49..250cf80560dd 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -6,20 +6,22 @@ import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent } from 'vscode'; import { Repository, Resource } from './repository'; -import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent } from './util'; +import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent, getCommitShortHash } from './util'; import { toGitUri } from './uri'; import { Branch, LogOptions, Ref, RefType } from './api/git'; import { emojify, ensureEmojis } from './emoji'; import { Commit } from './git'; import { OperationKind, OperationResult } from './operation'; -function toSourceControlHistoryItemRef(ref: Ref): SourceControlHistoryItemRef { +function toSourceControlHistoryItemRef(repository: Repository, ref: Ref): SourceControlHistoryItemRef { + const rootUri = Uri.file(repository.root); + switch (ref.type) { case RefType.RemoteHead: return { id: `refs/remotes/${ref.name}`, name: ref.name ?? '', - description: ref.commit ? l10n.t('Remote branch at {0}', ref.commit.substring(0, 8)) : undefined, + description: ref.commit ? l10n.t('Remote branch at {0}', getCommitShortHash(rootUri, ref.commit)) : undefined, revision: ref.commit, icon: new ThemeIcon('cloud'), category: l10n.t('remote branches') @@ -28,7 +30,7 @@ function toSourceControlHistoryItemRef(ref: Ref): SourceControlHistoryItemRef { return { id: `refs/tags/${ref.name}`, name: ref.name ?? '', - description: ref.commit ? l10n.t('Tag at {0}', ref.commit.substring(0, 8)) : undefined, + description: ref.commit ? l10n.t('Tag at {0}', getCommitShortHash(rootUri, ref.commit)) : undefined, revision: ref.commit, icon: new ThemeIcon('tag'), category: l10n.t('tags') @@ -37,7 +39,7 @@ function toSourceControlHistoryItemRef(ref: Ref): SourceControlHistoryItemRef { return { id: `refs/heads/${ref.name}`, name: ref.name ?? '', - description: ref.commit ? ref.commit.substring(0, 8) : undefined, + description: ref.commit ? getCommitShortHash(rootUri, ref.commit) : undefined, revision: ref.commit, icon: new ThemeIcon('git-branch'), category: l10n.t('branches') @@ -178,7 +180,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec // Refs (alphabetically) const historyItemRefs = this.repository.refs - .map(ref => toSourceControlHistoryItemRef(ref)) + .map(ref => toSourceControlHistoryItemRef(this.repository, ref)) .sort((a, b) => a.id.localeCompare(b.id)); // Auto-fetch @@ -207,13 +209,13 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec for (const ref of refs) { switch (ref.type) { case RefType.RemoteHead: - remoteBranches.push(toSourceControlHistoryItemRef(ref)); + remoteBranches.push(toSourceControlHistoryItemRef(this.repository, ref)); break; case RefType.Tag: - tags.push(toSourceControlHistoryItemRef(ref)); + tags.push(toSourceControlHistoryItemRef(this.repository, ref)); break; default: - branches.push(toSourceControlHistoryItemRef(ref)); + branches.push(toSourceControlHistoryItemRef(this.repository, ref)); break; } } @@ -259,7 +261,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec message: emojify(commit.message), author: commit.authorName, icon: new ThemeIcon('git-commit'), - displayId: commit.hash.substring(0, 8), + displayId: getCommitShortHash(Uri.file(this.repository.root), commit.hash), timestamp: commit.authorDate?.getTime(), statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, references: references.length !== 0 ? references : undefined diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 4bf7fa32ef0e..e1b71d87f0ad 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -23,7 +23,7 @@ import { IPushErrorHandlerRegistry } from './pushError'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { StatusBarCommands } from './statusbar'; import { toGitUri } from './uri'; -import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util'; +import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; import { detectEncoding } from './encoding'; @@ -1657,7 +1657,7 @@ export class Repository implements Disposable { } async checkout(treeish: string, opts?: { detached?: boolean; pullBeforeCheckout?: boolean }): Promise { - const refLabel = opts?.detached ? treeish.substring(0, 8) : treeish; + const refLabel = opts?.detached ? getCommitShortHash(Uri.file(this.root), treeish) : treeish; await this.run(Operation.Checkout(refLabel), async () => { @@ -1675,7 +1675,7 @@ export class Repository implements Disposable { } async checkoutTracking(treeish: string, opts: { detached?: boolean } = {}): Promise { - const refLabel = opts.detached ? treeish.substring(0, 8) : treeish; + const refLabel = opts.detached ? getCommitShortHash(Uri.file(this.root), treeish) : treeish; await this.run(Operation.CheckoutTracking(refLabel), () => this.repository.checkout(treeish, [], { ...opts, track: true })); } diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index 8fb85493a9b8..759ccdf82def 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef, l10n } from 'vscode'; +import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef, l10n, workspace, Uri } from 'vscode'; import { dirname, sep, relative } from 'path'; import { Readable } from 'stream'; import { promises as fs, createReadStream } from 'fs'; @@ -766,3 +766,9 @@ export function fromNow(date: number | Date, appendAgoLabel?: boolean, useFullTi } } } + +export function getCommitShortHash(scope: Uri, hash: string): string { + const config = workspace.getConfiguration('git', scope); + const shortHashLength = config.get('commitShortHashLength', 7); + return hash.substring(0, shortHashLength); +} From e3bea63a7a854fc5e912c1cb05d3390c85bc38f9 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 6 Jan 2025 17:14:40 +0100 Subject: [PATCH 0362/3587] clean up in extensions scanning (#237344) --- .../common/extensionsProfileScannerService.ts | 6 +- .../common/extensionsScannerService.ts | 88 ++++++++++--------- .../node/extensionManagementService.ts | 73 +++++---------- .../extensionsProfileScannerService.test.ts | 31 ++++--- .../node/extensionsScannerService.test.ts | 56 ++++++------ src/vs/server/node/remoteExtensionsScanner.ts | 2 +- .../cachedExtensionScanner.ts | 4 +- 7 files changed, 122 insertions(+), 138 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index ced131827bee..162f11b5c968 100644 --- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -85,7 +85,7 @@ export interface IExtensionsProfileScannerService { scanProfileExtensions(profileLocation: URI, options?: IProfileExtensionsScanOptions): Promise; addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI, keepExistingVersions?: boolean): Promise; updateMetadata(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise; - removeExtensionFromProfile(extension: IExtension, profileLocation: URI): Promise; + removeExtensionsFromProfile(extensions: IExtensionIdentifier[], profileLocation: URI): Promise; } export abstract class AbstractExtensionsProfileScannerService extends Disposable implements IExtensionsProfileScannerService { @@ -193,13 +193,13 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable return updatedExtensions; } - async removeExtensionFromProfile(extension: IExtension, profileLocation: URI): Promise { + async removeExtensionsFromProfile(extensions: IExtensionIdentifier[], profileLocation: URI): Promise { const extensionsToRemove: IScannedProfileExtension[] = []; try { await this.withProfileExtensions(profileLocation, profileExtensions => { const result: IScannedProfileExtension[] = []; for (const e of profileExtensions) { - if (areSameExtensions(e.identifier, extension.identifier)) { + if (extensions.some(extension => areSameExtensions(e.identifier, extension))) { extensionsToRemove.push(e); } else { result.push(e); diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index a186b6fa0456..f852d912536e 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -100,16 +100,24 @@ interface IBuiltInExtensionControl { [name: string]: 'marketplace' | 'disabled' | string; } -export type ScanOptions = { - readonly profileLocation?: URI; - readonly includeInvalid?: boolean; - readonly includeAllVersions?: boolean; +export type SystemExtensionsScanOptions = { readonly checkControlFile?: boolean; readonly language?: string; +}; + +export type UserExtensionsScanOptions = { + readonly profileLocation: URI; + readonly includeInvalid?: boolean; + readonly language?: string; readonly useCache?: boolean; readonly productVersion?: IProductVersion; }; +export type ScanOptions = { + readonly includeInvalid?: boolean; + readonly language?: string; +}; + export const IExtensionsScannerService = createDecorator('IExtensionsScannerService'); export interface IExtensionsScannerService { readonly _serviceBrand: undefined; @@ -118,17 +126,16 @@ export interface IExtensionsScannerService { readonly userExtensionsLocation: URI; readonly onDidChangeCache: Event; - getTargetPlatform(): Promise; + scanAllExtensions(systemScanOptions: SystemExtensionsScanOptions, userScanOptions: UserExtensionsScanOptions): Promise; + scanSystemExtensions(scanOptions: SystemExtensionsScanOptions): Promise; + scanUserExtensions(scanOptions: UserExtensionsScanOptions): Promise; + scanAllUserExtensions(): Promise; - scanAllExtensions(systemScanOptions: ScanOptions, userScanOptions: ScanOptions, includeExtensionsUnderDev: boolean): Promise; - scanSystemExtensions(scanOptions: ScanOptions): Promise; - scanUserExtensions(scanOptions: ScanOptions): Promise; - scanExtensionsUnderDevelopment(scanOptions: ScanOptions, existingExtensions: IScannedExtension[]): Promise; + scanExtensionsUnderDevelopment(existingExtensions: IScannedExtension[], scanOptions: ScanOptions): Promise; scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise; - scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise; scanMultipleExtensions(extensionLocations: URI[], extensionType: ExtensionType, scanOptions: ScanOptions): Promise; + scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise; - scanMetadata(extensionLocation: URI): Promise; updateMetadata(extensionLocation: URI, metadata: Partial): Promise; initializeDefaultProfileExtensions(): Promise; } @@ -167,35 +174,33 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } private _targetPlatformPromise: Promise | undefined; - getTargetPlatform(): Promise { + private getTargetPlatform(): Promise { if (!this._targetPlatformPromise) { this._targetPlatformPromise = computeTargetPlatform(this.fileService, this.logService); } return this._targetPlatformPromise; } - async scanAllExtensions(systemScanOptions: ScanOptions, userScanOptions: ScanOptions, includeExtensionsUnderDev: boolean): Promise { + async scanAllExtensions(systemScanOptions: SystemExtensionsScanOptions, userScanOptions: UserExtensionsScanOptions): Promise { const [system, user] = await Promise.all([ this.scanSystemExtensions(systemScanOptions), this.scanUserExtensions(userScanOptions), ]); - const development = includeExtensionsUnderDev ? await this.scanExtensionsUnderDevelopment(systemScanOptions, [...system, ...user]) : []; - return this.dedupExtensions(system, user, development, await this.getTargetPlatform(), true); + return this.dedupExtensions(system, user, [], await this.getTargetPlatform(), true); } - async scanSystemExtensions(scanOptions: ScanOptions): Promise { + async scanSystemExtensions(scanOptions: SystemExtensionsScanOptions): Promise { const promises: Promise[] = []; - promises.push(this.scanDefaultSystemExtensions(!!scanOptions.useCache, scanOptions.language)); + promises.push(this.scanDefaultSystemExtensions(scanOptions.language)); promises.push(this.scanDevSystemExtensions(scanOptions.language, !!scanOptions.checkControlFile)); const [defaultSystemExtensions, devSystemExtensions] = await Promise.all(promises); - return this.applyScanOptions([...defaultSystemExtensions, ...devSystemExtensions], ExtensionType.System, scanOptions, false); + return this.applyScanOptions([...defaultSystemExtensions, ...devSystemExtensions], ExtensionType.System, { pickLatest: false }); } - async scanUserExtensions(scanOptions: ScanOptions): Promise { - const location = scanOptions.profileLocation ?? this.userExtensionsLocation; - this.logService.trace('Started scanning user extensions', location); + async scanUserExtensions(scanOptions: UserExtensionsScanOptions): Promise { + this.logService.trace('Started scanning user extensions', scanOptions.profileLocation); const profileScanOptions: IProfileExtensionsScanOptions | undefined = this.uriIdentityService.extUri.isEqual(scanOptions.profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource) ? { bailOutWhenFileNotFound: true } : undefined; - const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(scanOptions.profileLocation, true, ExtensionType.User, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion()); const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode ? this.userExtensionsCachedScanner : this.extensionsScanner; let extensions: IRelaxedScannedExtension[]; try { @@ -208,16 +213,22 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem throw error; } } - extensions = await this.applyScanOptions(extensions, ExtensionType.User, scanOptions, true); + extensions = await this.applyScanOptions(extensions, ExtensionType.User, { includeInvalid: scanOptions.includeInvalid, pickLatest: true }); this.logService.trace('Scanned user extensions:', extensions.length); return extensions; } - async scanExtensionsUnderDevelopment(scanOptions: ScanOptions, existingExtensions: IScannedExtension[]): Promise { + async scanAllUserExtensions(scanOptions: { includeAllVersions?: boolean; includeInvalid: boolean } = { includeInvalid: true, includeAllVersions: true }): Promise { + const extensionsScannerInput = await this.createExtensionScannerInput(this.userExtensionsLocation, false, ExtensionType.User, undefined, true, undefined, this.getProductVersion()); + const extensions = await this.extensionsScanner.scanExtensions(extensionsScannerInput); + return this.applyScanOptions(extensions, ExtensionType.User, { includeAllVersions: scanOptions.includeAllVersions, includeInvalid: scanOptions.includeInvalid }); + } + + async scanExtensionsUnderDevelopment(existingExtensions: IScannedExtension[], scanOptions: ScanOptions): Promise { if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionDevelopmentLocationURI) { const extensions = (await Promise.all(this.environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file) .map(async extensionDevelopmentLocationURI => { - const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, scanOptions.language, false /* do not validate */, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, scanOptions.language, false /* do not validate */, undefined, this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(input); return extensions.map(extension => { // Override the extension type from the existing extensions @@ -227,13 +238,13 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem }); }))) .flat(); - return this.applyScanOptions(extensions, 'development', scanOptions, true); + return this.applyScanOptions(extensions, 'development', { includeInvalid: scanOptions.includeInvalid, pickLatest: true }); } return []; } async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, this.getProductVersion()); const extension = await this.extensionsScanner.scanExtension(extensionsScannerInput); if (!extension) { return null; @@ -245,9 +256,9 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(extensionsScannerInput); - return this.applyScanOptions(extensions, extensionType, scanOptions, true); + return this.applyScanOptions(extensions, extensionType, { includeInvalid: scanOptions.includeInvalid, pickLatest: true }); } async scanMultipleExtensions(extensionLocations: URI[], extensionType: ExtensionType, scanOptions: ScanOptions): Promise { @@ -256,14 +267,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem const scannedExtensions = await this.scanOneOrMultipleExtensions(extensionLocation, extensionType, scanOptions); extensions.push(...scannedExtensions); })); - return this.applyScanOptions(extensions, extensionType, scanOptions, true); - } - - async scanMetadata(extensionLocation: URI): Promise { - const manifestLocation = joinPath(extensionLocation, 'package.json'); - const content = (await this.fileService.readFile(manifestLocation)).value.toString(); - const manifest: IScannedExtensionManifest = JSON.parse(content); - return manifest.__metadata; + return this.applyScanOptions(extensions, extensionType, { includeInvalid: scanOptions.includeInvalid, pickLatest: true }); } async updateMetadata(extensionLocation: URI, metaData: Partial): Promise { @@ -301,7 +305,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem this.initializeDefaultProfileExtensionsPromise = (async () => { try { this.logService.info('Started initializing default profile extensions in extensions installation folder.', this.userExtensionsLocation.toString()); - const userExtensions = await this.scanUserExtensions({ includeInvalid: true }); + const userExtensions = await this.scanAllUserExtensions({ includeInvalid: true }); if (userExtensions.length) { await this.extensionsProfileScannerService.addExtensionsToProfile(userExtensions.map(e => [e, e.metadata]), this.userDataProfilesService.defaultProfile.extensionsResource); } else { @@ -324,9 +328,9 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem return this.initializeDefaultProfileExtensionsPromise; } - private async applyScanOptions(extensions: IRelaxedScannedExtension[], type: ExtensionType | 'development', scanOptions: ScanOptions, pickLatest: boolean): Promise { + private async applyScanOptions(extensions: IRelaxedScannedExtension[], type: ExtensionType | 'development', scanOptions: { includeAllVersions?: boolean; includeInvalid?: boolean; pickLatest?: boolean } = {}): Promise { if (!scanOptions.includeAllVersions) { - extensions = this.dedupExtensions(type === ExtensionType.System ? extensions : undefined, type === ExtensionType.User ? extensions : undefined, type === 'development' ? extensions : undefined, await this.getTargetPlatform(), pickLatest); + extensions = this.dedupExtensions(type === ExtensionType.System ? extensions : undefined, type === ExtensionType.User ? extensions : undefined, type === 'development' ? extensions : undefined, await this.getTargetPlatform(), !!scanOptions.pickLatest); } if (!scanOptions.includeInvalid) { extensions = extensions.filter(extension => extension.isValid); @@ -399,10 +403,10 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem return [...result.values()]; } - private async scanDefaultSystemExtensions(useCache: boolean, language: string | undefined): Promise { + private async scanDefaultSystemExtensions(language: string | undefined): Promise { this.logService.trace('Started scanning system extensions'); const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, language, true, undefined, this.getProductVersion()); - const extensionsScanner = useCache && !extensionsScannerInput.devMode ? this.systemExtensionsCachedScanner : this.extensionsScanner; + const extensionsScanner = !extensionsScannerInput.devMode ? this.systemExtensionsCachedScanner : this.extensionsScanner; const result = await extensionsScanner.scanExtensions(extensionsScannerInput); this.logService.trace('Scanned system extensions:', result.length); return result; diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 762da10967f2..e9b38bc69f2f 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -17,7 +17,7 @@ import { Schemas } from '../../../base/common/network.js'; import * as path from '../../../base/common/path.js'; import { joinPath } from '../../../base/common/resources.js'; import * as semver from '../../../base/common/semver/semver.js'; -import { isBoolean } from '../../../base/common/types.js'; +import { isBoolean, isDefined, isUndefined } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { generateUuid } from '../../../base/common/uuid.js'; import * as pfs from '../../../base/node/pfs.js'; @@ -37,7 +37,7 @@ import { } from '../common/extensionManagement.js'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from '../common/extensionManagementUtil.js'; import { IExtensionsProfileScannerService, IScannedProfileExtension } from '../common/extensionsProfileScannerService.js'; -import { IExtensionsScannerService, IScannedExtension, ScanOptions } from '../common/extensionsScannerService.js'; +import { IExtensionsScannerService, IScannedExtension, UserExtensionsScanOptions } from '../common/extensionsScannerService.js'; import { ExtensionsDownloader } from './extensionDownloader.js'; import { ExtensionsLifecycle } from './extensionLifecycle.js'; import { fromExtractError, getManifest } from './extensionManagementUtil.js'; @@ -132,7 +132,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } scanAllUserInstalledExtensions(): Promise { - return this.extensionsScanner.scanAllUserExtensions(false); + return this.extensionsScanner.scanAllUserExtensions(); } scanInstalledExtensionAtLocation(location: URI): Promise { @@ -298,23 +298,6 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi const local = await this.extensionsScanner.extractUserExtension( extensionKey, location.fsPath, - { - id: gallery.identifier.uuid, - publisherId: gallery.publisherId, - publisherDisplayName: gallery.publisherDisplayName, - targetPlatform: gallery.properties.targetPlatform, - isApplicationScoped: options.isApplicationScoped, - isMachineScoped: options.isMachineScoped, - isBuiltin: options.isBuiltin, - isPreReleaseVersion: gallery.properties.isPreReleaseVersion, - hasPreReleaseVersion: gallery.properties.isPreReleaseVersion, - installedTimestamp: Date.now(), - pinned: options.installGivenVersion ? true : !!options.pinned, - preRelease: isBoolean(options.preRelease) - ? options.preRelease - : options.installPreReleaseVersion || gallery.properties.isPreReleaseVersion, - source: 'gallery', - }, false, token); @@ -382,14 +365,6 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi const local = await this.extensionsScanner.extractUserExtension( extensionKey, path.resolve(location.fsPath), - { - isApplicationScoped: options.isApplicationScoped, - isMachineScoped: options.isMachineScoped, - isBuiltin: options.isBuiltin, - installedTimestamp: Date.now(), - pinned: options.installGivenVersion ? true : !!options.pinned, - source: 'vsix', - }, isBoolean(options.keepExisting) ? !options.keepExisting : true, token); return { local }; @@ -561,17 +536,18 @@ export class ExtensionsScanner extends Disposable { async cleanUp(): Promise { await this.removeTemporarilyDeletedFolders(); await this.deleteExtensionsMarkedForRemoval(); - await this.initializeMetadata(); + //TODO: Remove this initiialization after coupe of releases + await this.initializeExtensionSize(); } async scanExtensions(type: ExtensionType | null, profileLocation: URI, productVersion: IProductVersion): Promise { try { - const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation, productVersion }; + const userScanOptions: UserExtensionsScanOptions = { includeInvalid: true, profileLocation, productVersion }; let scannedExtensions: IScannedExtension[] = []; if (type === null || type === ExtensionType.System) { let scanAllExtensionsPromise = this.scanAllExtensionPromise.get(profileLocation); if (!scanAllExtensionsPromise) { - scanAllExtensionsPromise = this.extensionsScannerService.scanAllExtensions({ includeInvalid: true, useCache: true }, userScanOptions, false) + scanAllExtensionsPromise = this.extensionsScannerService.scanAllExtensions({}, userScanOptions) .finally(() => this.scanAllExtensionPromise.delete(profileLocation)); this.scanAllExtensionPromise.set(profileLocation, scanAllExtensionsPromise); } @@ -592,9 +568,9 @@ export class ExtensionsScanner extends Disposable { } } - async scanAllUserExtensions(excludeOutdated: boolean): Promise { + async scanAllUserExtensions(): Promise { try { - const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); + const scannedExtensions = await this.extensionsScannerService.scanAllUserExtensions(); return await Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); } catch (error) { throw toExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); @@ -613,7 +589,7 @@ export class ExtensionsScanner extends Disposable { return null; } - async extractUserExtension(extensionKey: ExtensionKey, zipPath: string, metadata: Metadata, removeIfExists: boolean, token: CancellationToken): Promise { + async extractUserExtension(extensionKey: ExtensionKey, zipPath: string, removeIfExists: boolean, token: CancellationToken): Promise { const folderName = extensionKey.toString(); const tempLocation = URI.file(path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, `.${generateUuid()}`)); const extensionLocation = URI.file(path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, folderName)); @@ -648,6 +624,7 @@ export class ExtensionsScanner extends Disposable { throw fromExtractError(e); } + const metadata: Metadata = { installedTimestamp: Date.now() }; try { metadata.size = await computeSize(tempLocation, this.fileService); } catch (error) { @@ -691,13 +668,9 @@ export class ExtensionsScanner extends Disposable { return this.scanLocalExtension(extensionLocation, ExtensionType.User); } - async scanMetadata(local: ILocalExtension, profileLocation?: URI): Promise { - if (profileLocation) { - const extension = await this.getScannedExtension(local, profileLocation); - return extension?.metadata; - } else { - return this.extensionsScannerService.scanMetadata(local.location); - } + async scanMetadata(local: ILocalExtension, profileLocation: URI): Promise { + const extension = await this.getScannedExtension(local, profileLocation); + return extension?.metadata; } private async getScannedExtension(local: ILocalExtension, profileLocation: URI): Promise { @@ -763,7 +736,7 @@ export class ExtensionsScanner extends Disposable { await this.extensionsProfileScannerService.updateMetadata([[extension, { ...target.metadata, ...metadata }]], toProfileLocation); } else { const targetExtension = await this.scanLocalExtension(target.location, extension.type, toProfileLocation); - await this.extensionsProfileScannerService.removeExtensionFromProfile(targetExtension, toProfileLocation); + await this.extensionsProfileScannerService.removeExtensionsFromProfile([targetExtension.identifier], toProfileLocation); await this.extensionsProfileScannerService.addExtensionsToProfile([[extension, { ...target.metadata, ...metadata }]], toProfileLocation); } } else { @@ -890,11 +863,11 @@ export class ExtensionsScanner extends Disposable { }; } - private async initializeMetadata(): Promise { - const extensions = await this.extensionsScannerService.scanUserExtensions({ includeInvalid: true }); + private async initializeExtensionSize(): Promise { + const extensions = await this.extensionsScannerService.scanAllUserExtensions(); await Promise.all(extensions.map(async extension => { // set size if not set before - if (!extension.metadata?.size && extension.metadata?.source !== 'resource') { + if (isDefined(extension.metadata?.installedTimestamp) && isUndefined(extension.metadata?.size)) { const size = await computeSize(extension.location, this.fileService); await this.extensionsScannerService.updateMetadata(extension.location, { size }); } @@ -916,7 +889,7 @@ export class ExtensionsScanner extends Disposable { this.logService.debug(`Deleting extensions marked as removed:`, Object.keys(removed)); - const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeInvalid: true }); // All user extensions + const extensions = await this.scanAllUserExtensions(); const installed: Set = new Set(); for (const e of extensions) { if (!removed[ExtensionKey.create(e).toString()]) { @@ -930,14 +903,14 @@ export class ExtensionsScanner extends Disposable { await Promises.settled(byExtension.map(async e => { const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]; if (!installed.has(latest.identifier.id.toLowerCase())) { - await this.beforeRemovingExtension(await this.toLocalExtension(latest)); + await this.beforeRemovingExtension(latest); } })); } catch (error) { this.logService.error(error); } - const toRemove = extensions.filter(e => e.metadata /* Installed by System */ && removed[ExtensionKey.create(e).toString()]); + const toRemove = extensions.filter(e => e.installedTimestamp /* Installed by System */ && removed[ExtensionKey.create(e).toString()]); await Promise.allSettled(toRemove.map(e => this.deleteExtension(e, 'marked for removal'))); } @@ -1115,7 +1088,7 @@ class InstallExtensionInProfileTask extends AbstractExtensionTask ExtensionKey.create(i).equals(extensionKey)); } return undefined; @@ -1155,7 +1128,7 @@ class UninstallExtensionInProfileTask extends AbstractExtensionTask implem } protected doRun(token: CancellationToken): Promise { - return this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension, this.options.profileLocation); + return this.extensionsProfileScannerService.removeExtensionsFromProfile([this.extension.identifier], this.options.profileLocation); } } diff --git a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts index 0329b90263b4..a21b64a95a5e 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts @@ -356,7 +356,7 @@ suite('ExtensionsProfileScannerService', () => { assert.deepStrictEqual(((target2.args[0][0])).extensions[0].location.toString(), extension.location.toString()); }); - test('remove extension trigger events', async () => { + test('remove extensions trigger events', async () => { const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const target1 = sinon.stub(); const target2 = sinon.stub(); @@ -364,26 +364,33 @@ suite('ExtensionsProfileScannerService', () => { disposables.add(testObject.onDidRemoveExtensions(target2)); const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); - const extension = aExtension('pub.a', joinPath(ROOT, 'foo', 'pub.a-1.0.0')); - await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest); - await testObject.removeExtensionFromProfile(extension, extensionsManifest); + const extension1 = aExtension('pub.a', joinPath(ROOT, 'foo', 'pub.a-1.0.0')); + const extension2 = aExtension('pub.b', joinPath(ROOT, 'foo', 'pub.b-1.0.0')); + await testObject.addExtensionsToProfile([[extension1, undefined], [extension2, undefined]], extensionsManifest); + await testObject.removeExtensionsFromProfile([extension1.identifier, extension2.identifier], extensionsManifest); const actual = await testObject.scanProfileExtensions(extensionsManifest); assert.deepStrictEqual(actual.length, 0); assert.ok(target1.calledOnce); assert.deepStrictEqual(((target1.args[0][0])).profileLocation.toString(), extensionsManifest.toString()); - assert.deepStrictEqual(((target1.args[0][0])).extensions.length, 1); - assert.deepStrictEqual(((target1.args[0][0])).extensions[0].identifier, extension.identifier); - assert.deepStrictEqual(((target1.args[0][0])).extensions[0].version, extension.manifest.version); - assert.deepStrictEqual(((target1.args[0][0])).extensions[0].location.toString(), extension.location.toString()); + assert.deepStrictEqual(((target1.args[0][0])).extensions.length, 2); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].identifier, extension1.identifier); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].version, extension1.manifest.version); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].location.toString(), extension1.location.toString()); + assert.deepStrictEqual(((target1.args[0][0])).extensions[1].identifier, extension2.identifier); + assert.deepStrictEqual(((target1.args[0][0])).extensions[1].version, extension2.manifest.version); + assert.deepStrictEqual(((target1.args[0][0])).extensions[1].location.toString(), extension2.location.toString()); assert.ok(target2.calledOnce); assert.deepStrictEqual(((target2.args[0][0])).profileLocation.toString(), extensionsManifest.toString()); - assert.deepStrictEqual(((target2.args[0][0])).extensions.length, 1); - assert.deepStrictEqual(((target2.args[0][0])).extensions[0].identifier, extension.identifier); - assert.deepStrictEqual(((target2.args[0][0])).extensions[0].version, extension.manifest.version); - assert.deepStrictEqual(((target2.args[0][0])).extensions[0].location.toString(), extension.location.toString()); + assert.deepStrictEqual(((target2.args[0][0])).extensions.length, 2); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].identifier, extension1.identifier); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].version, extension1.manifest.version); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].location.toString(), extension1.location.toString()); + assert.deepStrictEqual(((target2.args[0][0])).extensions[1].identifier, extension2.identifier); + assert.deepStrictEqual(((target2.args[0][0])).extensions[1].version, extension2.manifest.version); + assert.deepStrictEqual(((target2.args[0][0])).extensions[1].location.toString(), extension2.location.toString()); }); test('add extension with same id but different version', async () => { diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts index 551ba576d445..ea4108b2c17e 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -105,12 +105,12 @@ suite('NativeExtensionsScanerService Test', () => { assert.deepStrictEqual(actual[0].manifest, manifest); }); - test('scan user extension', async () => { + test('scan user extensions', async () => { const manifest: Partial = anExtensionManifest({ 'name': 'name', 'publisher': 'pub', __metadata: { id: 'uuid' } }); const extensionLocation = await aUserExtension(manifest); const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - const actual = await testObject.scanUserExtensions({}); + const actual = await testObject.scanAllUserExtensions(); assert.deepStrictEqual(actual.length, 1); assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name', uuid: 'uuid' }); @@ -175,24 +175,24 @@ suite('NativeExtensionsScanerService Test', () => { assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' }); }); - test('scan user extension with different versions', async () => { + test('scan all user extensions with different versions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2' })); - const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - const actual = await testObject.scanUserExtensions({}); + const actual = await testObject.scanAllUserExtensions({ includeAllVersions: false, includeInvalid: false }); assert.deepStrictEqual(actual.length, 1); assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); assert.deepStrictEqual(actual[0].manifest.version, '1.0.2'); }); - test('scan user extension include all versions', async () => { + test('scan all user extensions include all versions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2' })); - const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - const actual = await testObject.scanUserExtensions({ includeAllVersions: true }); + const actual = await testObject.scanAllUserExtensions(); assert.deepStrictEqual(actual.length, 2); assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); @@ -201,35 +201,35 @@ suite('NativeExtensionsScanerService Test', () => { assert.deepStrictEqual(actual[1].manifest.version, '1.0.2'); }); - test('scan user extension with different versions and higher version is not compatible', async () => { + test('scan all user extensions with different versions and higher version is not compatible', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2', engines: { vscode: '^1.67.0' } })); - const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - const actual = await testObject.scanUserExtensions({}); + const actual = await testObject.scanAllUserExtensions({ includeAllVersions: false, includeInvalid: false }); assert.deepStrictEqual(actual.length, 1); assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); assert.deepStrictEqual(actual[0].manifest.version, '1.0.1'); }); - test('scan exclude invalid extensions', async () => { + test('scan all user extensions exclude invalid extensions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } })); - const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - const actual = await testObject.scanUserExtensions({}); + const actual = await testObject.scanAllUserExtensions({ includeAllVersions: false, includeInvalid: false }); assert.deepStrictEqual(actual.length, 1); assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); }); - test('scan include invalid extensions', async () => { + test('scan all user extensions include invalid extensions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } })); - const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - const actual = await testObject.scanUserExtensions({ includeInvalid: true }); + const actual = await testObject.scanAllUserExtensions({ includeAllVersions: false, includeInvalid: true }); assert.deepStrictEqual(actual.length, 2); assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); @@ -257,12 +257,12 @@ suite('NativeExtensionsScanerService Test', () => { assert.deepStrictEqual(actual[0].manifest.version, '1.0.0'); }); - test('scan extension with default nls replacements', async () => { + test('scan all user extensions with default nls replacements', async () => { const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', displayName: '%displayName%' })); await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' }))); - const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - const actual = await testObject.scanUserExtensions({}); + const actual = await testObject.scanAllUserExtensions(); assert.deepStrictEqual(actual.length, 1); assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); @@ -277,11 +277,11 @@ suite('NativeExtensionsScanerService Test', () => { const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); translations = { 'pub.name': nlsLocation.fsPath }; - const actual = await testObject.scanUserExtensions({ language: 'en' }); + const actual = await testObject.scanExistingExtension(extensionLocation, ExtensionType.User, { language: 'en' }); - assert.deepStrictEqual(actual.length, 1); - assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); - assert.deepStrictEqual(actual[0].manifest.displayName, 'Hello World EN'); + assert.ok(actual !== null); + assert.deepStrictEqual(actual!.identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual!.manifest.displayName, 'Hello World EN'); }); test('scan extension falls back to default nls replacements', async () => { @@ -292,11 +292,11 @@ suite('NativeExtensionsScanerService Test', () => { const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); translations = { 'pub.name2': nlsLocation.fsPath }; - const actual = await testObject.scanUserExtensions({ language: 'en' }); + const actual = await testObject.scanExistingExtension(extensionLocation, ExtensionType.User, { language: 'en' }); - assert.deepStrictEqual(actual.length, 1); - assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); - assert.deepStrictEqual(actual[0].manifest.displayName, 'Hello World'); + assert.ok(actual !== null); + assert.deepStrictEqual(actual!.identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual!.manifest.displayName, 'Hello World'); }); async function aUserExtension(manifest: Partial): Promise { diff --git a/src/vs/server/node/remoteExtensionsScanner.ts b/src/vs/server/node/remoteExtensionsScanner.ts index ee94de1090d2..3855e37ce941 100644 --- a/src/vs/server/node/remoteExtensionsScanner.ts +++ b/src/vs/server/node/remoteExtensionsScanner.ts @@ -139,7 +139,7 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS } private async _scanBuiltinExtensions(language: string): Promise { - const scannedExtensions = await this._extensionsScannerService.scanSystemExtensions({ language, useCache: true }); + const scannedExtensions = await this._extensionsScannerService.scanSystemExtensions({ language }); return scannedExtensions.map(e => toExtensionDescription(e, false)); } diff --git a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts index 04d4ff9703d3..9e6d46f78243 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts @@ -53,7 +53,7 @@ export class CachedExtensionScanner { try { const language = platform.language; const result = await Promise.allSettled([ - this._extensionsScannerService.scanSystemExtensions({ language, useCache: true, checkControlFile: true }), + this._extensionsScannerService.scanSystemExtensions({ language, checkControlFile: true }), this._extensionsScannerService.scanUserExtensions({ language, profileLocation: this._userDataProfileService.currentProfile.extensionsResource, useCache: true }), this._environmentService.remoteAuthority ? [] : this._extensionManagementService.getInstalledWorkspaceExtensions(false) ]); @@ -86,7 +86,7 @@ export class CachedExtensionScanner { } try { - scannedDevelopedExtensions = await this._extensionsScannerService.scanExtensionsUnderDevelopment({ language }, [...scannedSystemExtensions, ...scannedUserExtensions]); + scannedDevelopedExtensions = await this._extensionsScannerService.scanExtensionsUnderDevelopment([...scannedSystemExtensions, ...scannedUserExtensions], { language }); } catch (error) { this._logService.error(error); } From 85925efe02b1be4671c5f37af8386a75a8b35ecc Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 6 Jan 2025 17:20:28 +0100 Subject: [PATCH 0363/3587] Allow deleting files via the explorer when a folder is marked as readonly (fix #195701) (#237342) --- .../files/browser/fileActions.contribution.ts | 37 ++++++++--------- .../contrib/files/browser/fileActions.ts | 40 ++++++++++++++----- .../files/browser/views/explorerView.ts | 4 +- .../workbench/contrib/files/common/files.ts | 2 +- 4 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index be7dddf26ff9..6cd04336cfb4 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -14,7 +14,7 @@ import { COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMA import { CommandsRegistry, ICommandHandler } from '../../../../platform/commands/common/commands.js'; import { ContextKeyExpr, ContextKeyExpression } from '../../../../platform/contextkey/common/contextkey.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceNotReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerResourceAvailableEditorIdsContext, FoldersViewVisibleContext } from '../common/files.js'; +import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceWritableContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerResourceAvailableEditorIdsContext, FoldersViewVisibleContext } from '../common/files.js'; import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL } from '../../../browser/actions/workspaceCommands.js'; import { CLOSE_SAVED_EDITORS_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, REOPEN_WITH_COMMAND_ID } from '../../../browser/parts/editor/editorCommands.js'; import { AutoSaveAfterShortDelayContext } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; @@ -52,7 +52,7 @@ const RENAME_ID = 'renameFile'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: RENAME_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceWritableContext), primary: KeyCode.F2, mac: { primary: KeyCode.Enter @@ -64,7 +64,7 @@ const MOVE_FILE_TO_TRASH_ID = 'moveFileToTrash'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: MOVE_FILE_TO_TRASH_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext, ExplorerResourceMoveableToTrash), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceMoveableToTrash), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace, @@ -77,7 +77,7 @@ const DELETE_FILE_ID = 'deleteFile'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: DELETE_FILE_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext), + when: FilesExplorerFocusCondition, primary: KeyMod.Shift | KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Backspace @@ -88,7 +88,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: DELETE_FILE_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext, ExplorerResourceMoveableToTrash.toNegated()), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceMoveableToTrash.toNegated()), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace @@ -100,7 +100,7 @@ const CUT_FILE_ID = 'filesExplorer.cut'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CUT_FILE_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceWritableContext), primary: KeyMod.CtrlCmd | KeyCode.KeyX, handler: cutFileHandler, }); @@ -121,7 +121,7 @@ CommandsRegistry.registerCommand(PASTE_FILE_ID, pasteFileHandler); KeybindingsRegistry.registerKeybindingRule({ id: `^${PASTE_FILE_ID}`, // the `^` enables pasting files into the explorer by preventing default bubble up weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceWritableContext), primary: KeyMod.CtrlCmd | KeyCode.KeyV, }); @@ -479,7 +479,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { command: { id: NEW_FILE_COMMAND_ID, title: NEW_FILE_LABEL, - precondition: ExplorerResourceNotReadonlyContext + precondition: ExplorerResourceWritableContext }, when: ExplorerFolderContext }); @@ -490,7 +490,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { command: { id: NEW_FOLDER_COMMAND_ID, title: NEW_FOLDER_LABEL, - precondition: ExplorerResourceNotReadonlyContext + precondition: ExplorerResourceWritableContext }, when: ExplorerFolderContext }); @@ -540,7 +540,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { id: CUT_FILE_ID, title: nls.localize('cut', "Cut"), }, - when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext) + when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceWritableContext) }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { @@ -559,7 +559,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { command: { id: PASTE_FILE_ID, title: PASTE_FILE_LABEL, - precondition: ContextKeyExpr.and(ExplorerResourceNotReadonlyContext, FileCopiedContext) + precondition: ContextKeyExpr.and(ExplorerResourceWritableContext, FileCopiedContext) }, when: ExplorerFolderContext }); @@ -593,8 +593,8 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, ({ IsWebContext, // only on folders ExplorerFolderContext, - // only on editable folders - ExplorerResourceNotReadonlyContext + // only on writable folders + ExplorerResourceWritableContext ) })); @@ -638,7 +638,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { command: { id: RENAME_ID, title: TRIGGER_RENAME_LABEL, - precondition: ExplorerResourceNotReadonlyContext, + precondition: ExplorerResourceWritableContext, }, when: ExplorerRootContext.toNegated() }); @@ -648,13 +648,11 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { order: 20, command: { id: MOVE_FILE_TO_TRASH_ID, - title: MOVE_FILE_TO_TRASH_LABEL, - precondition: ContextKeyExpr.and(ExplorerResourceNotReadonlyContext), + title: MOVE_FILE_TO_TRASH_LABEL }, alt: { id: DELETE_FILE_ID, - title: nls.localize('deleteFile', "Delete Permanently"), - precondition: ContextKeyExpr.and(ExplorerResourceNotReadonlyContext), + title: nls.localize('deleteFile', "Delete Permanently") }, when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceMoveableToTrash) }); @@ -664,8 +662,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { order: 20, command: { id: DELETE_FILE_ID, - title: nls.localize('deleteFile', "Delete Permanently"), - precondition: ExplorerResourceNotReadonlyContext, + title: nls.localize('deleteFile', "Delete Permanently") }, when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceMoveableToTrash.toNegated()) }); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 635765f09b75..50f786ea2f7e 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -93,7 +93,7 @@ async function refreshIfSeparator(value: string, explorerService: IExplorerServi } } -async function deleteFiles(explorerService: IExplorerService, workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false, ignoreIfNotExists = false): Promise { +async function deleteFiles(explorerService: IExplorerService, workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, filesConfigurationService: IFilesConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false, ignoreIfNotExists = false): Promise { let primaryButton: string; if (useTrash) { primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash"); @@ -109,7 +109,7 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer dirtyWorkingCopies.add(dirtyWorkingCopy); } } - let confirmed = true; + if (dirtyWorkingCopies.size) { let message: string; if (distinctElements.length > 1) { @@ -132,18 +132,40 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer }); if (!response.confirmed) { - confirmed = false; + return; } else { skipConfirm = true; } } - // Check if file is dirty in editor and save it to avoid data loss - if (!confirmed) { - return; + // Handle readonly + if (!skipConfirm) { + const readonlyResources = distinctElements.filter(e => filesConfigurationService.isReadonly(e.resource)); + if (readonlyResources.length) { + let message: string; + if (readonlyResources.length > 1) { + message = nls.localize('readonlyMessageFilesDelete', "You are deleting files that are configured to be read-only. Do you want to continue?"); + } else if (readonlyResources[0].isDirectory) { + message = nls.localize('readonlyMessageFolderOneDelete', "You are deleting a folder {0} that is configured to be read-only. Do you want to continue?", distinctElements[0].name); + } else { + message = nls.localize('readonlyMessageFolderDelete', "You are deleting a file {0} that is configured to be read-only. Do you want to continue?", distinctElements[0].name); + } + + const response = await dialogService.confirm({ + type: 'warning', + message, + detail: nls.localize('continueDetail', "The read-only protection will be overridden if you continue."), + primaryButton: nls.localize('continueButtonLabel', "Continue") + }); + + if (!response.confirmed) { + return; + } + } } let confirmation: IConfirmationResult; + // We do not support undo of folders, so in that case the delete action is irreversible const deleteDetail = distinctElements.some(e => e.isDirectory) ? nls.localize('irreversible', "This action is irreversible!") : distinctElements.length > 1 ? nls.localize('restorePlural', "You can restore these files using the Undo command.") : nls.localize('restore', "You can restore this file using the Undo command."); @@ -234,7 +256,7 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer skipConfirm = true; ignoreIfNotExists = true; - return deleteFiles(explorerService, workingCopyFileService, dialogService, configurationService, elements, useTrash, skipConfirm, ignoreIfNotExists); + return deleteFiles(explorerService, workingCopyFileService, dialogService, configurationService, filesConfigurationService, elements, useTrash, skipConfirm, ignoreIfNotExists); } } } @@ -1020,7 +1042,7 @@ export const moveFileToTrashHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); const stats = explorerService.getContext(true).filter(s => !s.isRoot); if (stats.length) { - await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, true); + await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFilesConfigurationService), stats, true); } }; @@ -1029,7 +1051,7 @@ export const deleteFileHandler = async (accessor: ServicesAccessor) => { const stats = explorerService.getContext(true).filter(s => !s.isRoot); if (stats.length) { - await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, false); + await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFilesConfigurationService), stats, false); } }; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 1c7188375010..f91029d58d5e 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -8,7 +8,7 @@ import { URI } from '../../../../../base/common/uri.js'; import * as perf from '../../../../../base/common/performance.js'; import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from '../../../../../base/common/actions.js'; import { memoize } from '../../../../../base/common/decorators.js'; -import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, ExplorerResourceAvailableEditorIdsContext, VIEW_ID, ExplorerResourceNotReadonlyContext, ViewHasSomeCollapsibleRootItemContext, FoldersViewVisibleContext, ExplorerResourceParentReadOnlyContext, ExplorerFindProviderActive } from '../../common/files.js'; +import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, ExplorerResourceAvailableEditorIdsContext, VIEW_ID, ExplorerResourceWritableContext, ViewHasSomeCollapsibleRootItemContext, FoldersViewVisibleContext, ExplorerResourceParentReadOnlyContext, ExplorerFindProviderActive } from '../../common/files.js'; import { FileCopiedContext, NEW_FILE_COMMAND_ID, NEW_FOLDER_COMMAND_ID } from '../fileActions.js'; import * as DOM from '../../../../../base/browser/dom.js'; import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; @@ -988,7 +988,7 @@ export function createFileIconThemableTreeContainerScope(container: HTMLElement, const CanCreateContext = ContextKeyExpr.or( // Folder: can create unless readonly - ContextKeyExpr.and(ExplorerFolderContext, ExplorerResourceNotReadonlyContext), + ContextKeyExpr.and(ExplorerFolderContext, ExplorerResourceWritableContext), // File: can create unless parent is readonly ContextKeyExpr.and(ExplorerFolderContext.toNegated(), ExplorerResourceParentReadOnlyContext.toNegated()) ); diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index 56715560f15e..0eb091699a54 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -40,7 +40,7 @@ export const ExplorerViewletVisibleContext = new RawContextKey('explore export const FoldersViewVisibleContext = new RawContextKey('foldersViewVisible', true, { type: 'boolean', description: localize('foldersViewVisible', "True when the FOLDERS view (the file tree within the explorer view container) is visible.") }); export const ExplorerFolderContext = new RawContextKey('explorerResourceIsFolder', false, { type: 'boolean', description: localize('explorerResourceIsFolder', "True when the focused item in the EXPLORER is a folder.") }); export const ExplorerResourceReadonlyContext = new RawContextKey('explorerResourceReadonly', false, { type: 'boolean', description: localize('explorerResourceReadonly', "True when the focused item in the EXPLORER is read-only.") }); -export const ExplorerResourceNotReadonlyContext = ExplorerResourceReadonlyContext.toNegated(); +export const ExplorerResourceWritableContext = ExplorerResourceReadonlyContext.toNegated(); export const ExplorerResourceParentReadOnlyContext = new RawContextKey('explorerResourceParentReadonly', false, { type: 'boolean', description: localize('explorerResourceParentReadonly', "True when the focused item in the EXPLORER's parent is read-only.") }); /** From 3b92e711627e7da2a45f444afddb03f0359bd339 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 6 Jan 2025 17:32:21 +0100 Subject: [PATCH 0364/3587] /edits - accept hunk (#237341) * implement accept hunk * use reject over undo or discard --- .../chatEditingModifiedFileEntry.ts | 49 +++++++++++++-- .../contrib/chat/browser/chatEditorActions.ts | 28 +++++++++ .../chat/browser/chatEditorController.ts | 59 +++++++++++-------- .../contrib/chat/browser/chatEditorOverlay.ts | 4 ++ .../contrib/chat/common/chatEditingService.ts | 3 + 5 files changed, 114 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index aa105b2e7673..37a6b9a2b68d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -14,6 +14,7 @@ import { EditOperation, ISingleEditOperation } from '../../../../../editor/commo import { OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { IDocumentDiff, nullDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js'; +import { DetailedLineRangeMapping } from '../../../../../editor/common/diff/rangeMapping.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { IModelDeltaDecoration, ITextModel, OverviewRulerLane } from '../../../../../editor/common/model.js'; @@ -343,6 +344,41 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie }); } + async acceptHunk(change: DetailedLineRangeMapping): Promise { + if (!this._diffInfo.get().changes.includes(change)) { + // diffInfo should have model version ids and check them (instead of the caller doing that) + return false; + } + const edits: ISingleEditOperation[] = []; + for (const edit of change.innerChanges ?? []) { + const newText = this.modifiedModel.getValueInRange(edit.modifiedRange); + edits.push(EditOperation.replace(edit.originalRange, newText)); + } + this.docSnapshot.pushEditOperations(null, edits, _ => null); + await this._updateDiffInfoSeq(); + if (this.diffInfo.get().identical) { + this._stateObs.set(WorkingSetEntryState.Accepted, undefined); + } + return true; + } + + async rejectHunk(change: DetailedLineRangeMapping): Promise { + if (!this._diffInfo.get().changes.includes(change)) { + return false; + } + const edits: ISingleEditOperation[] = []; + for (const edit of change.innerChanges ?? []) { + const newText = this.docSnapshot.getValueInRange(edit.originalRange); + edits.push(EditOperation.replace(edit.modifiedRange, newText)); + } + this.doc.pushEditOperations(null, edits, _ => null); + await this._updateDiffInfoSeq(); + if (this.diffInfo.get().identical) { + this._stateObs.set(WorkingSetEntryState.Rejected, undefined); + } + return true; + } + private _applyEdits(edits: ISingleEditOperation[]) { // make the actual edit this._isEditFromUs = true; @@ -358,13 +394,14 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie } } - private _updateDiffInfoSeq() { + private async _updateDiffInfoSeq() { const myDiffOperationId = ++this._diffOperationIds; - Promise.resolve(this._diffOperation).then(() => { - if (this._diffOperationIds === myDiffOperationId) { - this._diffOperation = this._updateDiffInfo(); - } - }); + await Promise.resolve(this._diffOperation); + if (this._diffOperationIds === myDiffOperationId) { + const thisDiffOperation = this._updateDiffInfo(); + this._diffOperation = thisDiffOperation; + await thisDiffOperation; + } } private async _updateDiffInfo(): Promise { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts index b7f3ae1d0b4f..a8060733fff3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts @@ -217,6 +217,33 @@ class UndoHunkAction extends EditorAction2 { } } +class AcceptHunkAction extends EditorAction2 { + constructor() { + super({ + id: 'chatEditor.action.acceptHunk', + title: localize2('acceptHunk', 'Accept this Change'), + shortTitle: localize2('acceptHunk2', 'Accept'), + category: CHAT_CATEGORY, + precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), + icon: Codicon.check, + f1: true, + keybinding: { + when: EditorContextKeys.focus, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter + }, + menu: { + id: MenuId.ChatEditingEditorHunk, + order: 0 + } + }); + } + + override runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]) { + ChatEditorController.get(editor)?.acceptNearestChange(args[0]); + } +} + class OpenDiffFromHunkAction extends EditorAction2 { constructor() { super({ @@ -243,5 +270,6 @@ export function registerChatEditorActions() { registerAction2(AcceptAction); registerAction2(RejectAction); registerAction2(UndoHunkAction); + registerAction2(AcceptHunkAction); registerAction2(OpenDiffFromHunkAction); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index e2a186b5886b..bf0c199267f3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -12,7 +12,6 @@ import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPosi import { LineSource, renderLines, RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; import { diffAddDecoration, diffDeleteDecoration, diffWholeLineAddDecoration } from '../../../../editor/browser/widget/diffEditor/registrations.contribution.js'; import { EditorOption, IEditorStickyScrollOptions } from '../../../../editor/common/config/editorOptions.js'; -import { EditOperation, ISingleEditOperation } from '../../../../editor/common/core/editOperation.js'; import { Range } from '../../../../editor/common/core/range.js'; import { IDocumentDiff } from '../../../../editor/common/diff/documentDiffProvider.js'; import { IEditorContribution, ScrollType } from '../../../../editor/common/editorCommon.js'; @@ -31,6 +30,7 @@ import { Selection } from '../../../../editor/common/core/selection.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; import { minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../../scm/common/quickDiff.js'; +import { DetailedLineRangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; export const ctxHasEditorModification = new RawContextKey('chat.hasEditorModifications', undefined, localize('chat.hasEditorModifications', "The current editor contains chat modifications")); export const ctxHasRequestInProgress = new RawContextKey('chat.ctxHasRequestInProgress', false, localize('chat.ctxHasRequestInProgress', "The current editor shows a file from an edit session which is still in progress")); @@ -328,13 +328,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut } // Add content widget for each diff change - const undoEdits: ISingleEditOperation[] = []; - for (const c of diffEntry.innerChanges ?? []) { - const oldText = originalModel.getValueInRange(c.originalRange); - undoEdits.push(EditOperation.replace(c.modifiedRange, oldText)); - } - - const widget = this._instantiationService.createInstance(DiffHunkWidget, entry, undoEdits, this._editor.getModel()!.getVersionId(), this._editor, isCreatedContent ? 0 : result.heightInLines); + const widget = this._instantiationService.createInstance(DiffHunkWidget, entry, diffEntry, this._editor.getModel()!.getVersionId(), this._editor, isCreatedContent ? 0 : result.heightInLines); widget.layout(diffEntry.modified.startLineNumber); this._diffHunkWidgets.push(widget); @@ -492,28 +486,41 @@ export class ChatEditorController extends Disposable implements IEditorContribut return true; } - undoNearestChange(closestWidget: DiffHunkWidget | undefined): void { + private _findClosestWidget(): DiffHunkWidget | undefined { if (!this._editor.hasModel()) { - return; + return undefined; } const lineRelativeTop = this._editor.getTopForLineNumber(this._editor.getPosition().lineNumber) - this._editor.getScrollTop(); + let closestWidget: DiffHunkWidget | undefined; let closestDistance = Number.MAX_VALUE; - if (!(closestWidget instanceof DiffHunkWidget)) { - for (const widget of this._diffHunkWidgets) { - const widgetTop = (widget.getPosition()?.preference)?.top; - if (widgetTop !== undefined) { - const distance = Math.abs(widgetTop - lineRelativeTop); - if (distance < closestDistance) { - closestDistance = distance; - closestWidget = widget; - } + for (const widget of this._diffHunkWidgets) { + const widgetTop = (widget.getPosition()?.preference)?.top; + if (widgetTop !== undefined) { + const distance = Math.abs(widgetTop - lineRelativeTop); + if (distance < closestDistance) { + closestDistance = distance; + closestWidget = widget; } } } + return closestWidget; + } + + undoNearestChange(closestWidget: DiffHunkWidget | undefined): void { + closestWidget = closestWidget ?? this._findClosestWidget(); + if (closestWidget instanceof DiffHunkWidget) { + closestWidget.reject(); + this.revealNext(); + } + } + + acceptNearestChange(closestWidget: DiffHunkWidget | undefined): void { + closestWidget = closestWidget ?? this._findClosestWidget(); if (closestWidget instanceof DiffHunkWidget) { - closestWidget.undo(); + closestWidget.accept(); + this.revealNext(); } } @@ -570,7 +577,7 @@ class DiffHunkWidget implements IOverlayWidget { constructor( readonly entry: IModifiedFileEntry, - private readonly _undoEdits: ISingleEditOperation[], + private readonly _change: DetailedLineRangeMapping, private readonly _versionId: number, private readonly _editor: ICodeEditor, private readonly _lineDelta: number, @@ -641,9 +648,15 @@ class DiffHunkWidget implements IOverlayWidget { // --- - undo() { + reject(): void { + if (this._versionId === this._editor.getModel()?.getVersionId()) { + this.entry.rejectHunk(this._change); + } + } + + accept(): void { if (this._versionId === this._editor.getModel()?.getVersionId()) { - this._editor.executeEdits('chatEdits.undo', this._undoEdits); + this.entry.acceptHunk(this._change); } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index bb94d4e0fc91..4b0dd3c8111f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -310,6 +310,10 @@ export class ChatEditorOverlayController implements IEditorContribution { } const entry = entries[idx]; + if (entry.state.read(r) === WorkingSetEntryState.Accepted || entry.state.read(r) === WorkingSetEntryState.Rejected) { + widget.hide(); + return; + } widget.show(session, entry, entries[(idx + 1) % entries.length]); })); diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index ed970c30625f..57e0924ce482 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -10,6 +10,7 @@ import { ResourceMap } from '../../../../base/common/map.js'; import { IObservable, IReader, ITransaction } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { IDocumentDiff } from '../../../../editor/common/diff/documentDiffProvider.js'; +import { DetailedLineRangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; import { TextEdit } from '../../../../editor/common/languages.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { localize } from '../../../../nls.js'; @@ -120,6 +121,8 @@ export interface IModifiedFileEntry { readonly rewriteRatio: IObservable; readonly maxLineNumber: IObservable; readonly diffInfo: IObservable; + acceptHunk(change: DetailedLineRangeMapping): Promise; + rejectHunk(change: DetailedLineRangeMapping): Promise; readonly lastModifyingRequestId: string; accept(transaction: ITransaction | undefined): Promise; reject(transaction: ITransaction | undefined): Promise; From abe43ed1d5257afa420579370cb0b45818e1d89d Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 6 Jan 2025 20:31:52 +0100 Subject: [PATCH 0365/3587] Git - add author email to the blame/graph hover (#237360) --- extensions/git/src/blame.ts | 7 ++++++- extensions/git/src/historyProvider.ts | 1 + src/vs/workbench/api/common/extHost.protocol.ts | 1 + .../workbench/contrib/scm/browser/scmHistoryViewPane.ts | 8 ++++++-- src/vs/workbench/contrib/scm/common/history.ts | 1 + src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts | 1 + 6 files changed, 16 insertions(+), 3 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 0dbdca92b326..a39781cdc41b 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -227,7 +227,12 @@ export class GitBlameController { markdownString.supportThemeIcons = true; if (blameInformationOrCommit.authorName) { - markdownString.appendMarkdown(`$(account) **${blameInformationOrCommit.authorName}**`); + if (blameInformationOrCommit.authorEmail) { + const emailTitle = l10n.t('Email'); + markdownString.appendMarkdown(`$(account) [**${blameInformationOrCommit.authorName}**](mailto:${blameInformationOrCommit.authorEmail} "${emailTitle} ${blameInformationOrCommit.authorName}")`); + } else { + markdownString.appendMarkdown(`$(account) **${blameInformationOrCommit.authorName}**`); + } if (blameInformationOrCommit.authorDate) { const dateString = new Date(blameInformationOrCommit.authorDate).toLocaleString(undefined, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 250cf80560dd..b18923d56b7b 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -260,6 +260,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec parentIds: commit.parents, message: emojify(commit.message), author: commit.authorName, + authorEmail: commit.authorEmail, icon: new ThemeIcon('git-commit'), displayId: getCommitShortHash(Uri.file(this.repository.root), commit.hash), timestamp: commit.authorDate?.getTime(), diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 36e385c3e6ee..7bccc00f5b9c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1606,6 +1606,7 @@ export interface SCMHistoryItemDto { readonly message: string; readonly displayId?: string; readonly author?: string; + readonly authorEmail?: string; readonly timestamp?: number; readonly statistics?: { readonly files: number; diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index af17ed451bd0..1c3f71ac1ca9 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -459,10 +459,14 @@ class HistoryItemRenderer implements ITreeRenderer Date: Mon, 6 Jan 2025 11:44:04 -0800 Subject: [PATCH 0366/3587] fix: don't try to move chat editing session to a chat editor (#237362) --- .../workbench/contrib/chat/browser/actions/chatMoveActions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts index e927be89e773..e732c01efbbb 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts @@ -18,6 +18,7 @@ import { IEditorGroupsService } from '../../../../services/editor/common/editorG import { ACTIVE_GROUP, AUX_WINDOW_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { isChatViewTitleActionContext } from '../../common/chatActions.js'; +import { ChatAgentLocation } from '../../common/chatAgents.js'; enum MoveToNewLocation { Editor = 'Editor', @@ -99,7 +100,7 @@ async function executeMoveToAction(accessor: ServicesAccessor, moveTo: MoveToNew const widget = (_sessionId ? widgetService.getWidgetBySessionId(_sessionId) : undefined) ?? widgetService.lastFocusedWidget; - if (!widget || !('viewId' in widget.viewContext)) { + if (!widget || widget.location !== ChatAgentLocation.Panel) { await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true } }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); return; } From e22e3e729360d29e15b6bf3e0301b9a9eff4770e Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 6 Jan 2025 21:47:43 +0100 Subject: [PATCH 0367/3587] Git - improve timeline hover (#237365) --- extensions/git/src/blame.ts | 6 +-- extensions/git/src/commands.ts | 8 ++-- extensions/git/src/git.ts | 5 ++ extensions/git/src/timelineProvider.ts | 47 +++++++++++++++---- .../contrib/timeline/browser/timelinePane.ts | 2 +- 5 files changed, 52 insertions(+), 16 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index a39781cdc41b..c4ca348eef9f 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -265,9 +265,9 @@ export class GitBlameController { markdownString.appendMarkdown(`\n\n---\n\n`); } - markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, blameInformationOrCommit.hash)} \`](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformationOrCommit.hash]))} "${l10n.t('View Commit')}")`); + markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, blameInformationOrCommit.hash)} \`](command:git.viewCommit2?${encodeURIComponent(JSON.stringify([documentUri, blameInformationOrCommit.hash]))} "${l10n.t('View Commit')}")`); markdownString.appendMarkdown(' '); - markdownString.appendMarkdown(`[$(copy)](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformationOrCommit.hash))} "${l10n.t('Copy Commit Hash')}")`); + markdownString.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(blameInformationOrCommit.hash))} "${l10n.t('Copy Commit Hash')}")`); markdownString.appendMarkdown('  |  '); markdownString.appendMarkdown(`[$(gear)](command:workbench.action.openSettings?%5B%22git.blame%22%5D "${l10n.t('Open Settings')}")`); @@ -702,7 +702,7 @@ class GitBlameStatusBarItem { this._statusBarItem.tooltip = this._controller.getBlameInformationHover(window.activeTextEditor.document.uri, blameInformation[0].blameInformation); this._statusBarItem.command = { title: l10n.t('View Commit'), - command: 'git.blameStatusBarItem.viewCommit', + command: 'git.viewCommit2', arguments: [window.activeTextEditor.document.uri, blameInformation[0].blameInformation.hash] } satisfies Command; } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index f7476a232142..ab344ad50230 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -4346,8 +4346,8 @@ export class CommandCenter { env.clipboard.writeText(historyItem.message); } - @command('git.blameStatusBarItem.viewCommit', { repository: true }) - async viewStatusBarCommit(repository: Repository, historyItemId: string): Promise { + @command('git.viewCommit2', { repository: true }) + async viewCommit2(repository: Repository, historyItemId: string): Promise { if (!repository || !historyItemId) { return; } @@ -4365,8 +4365,8 @@ export class CommandCenter { await commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources }); } - @command('git.blameStatusBarItem.copyContent') - async blameStatusBarCopyContent(content: string): Promise { + @command('git.copyContentToClipboard') + async copyContentToClipboard(content: string): Promise { if (typeof content !== 'string') { return; } diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 5bdfd655dbc4..0ca691fda643 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -62,6 +62,7 @@ export interface LogFileOptions { /** Optional. Specifies whether to start retrieving log entries in reverse order. */ readonly reverse?: boolean; readonly sortByAuthorDate?: boolean; + readonly shortStats?: boolean; } function parseVersion(raw: string): string { @@ -1290,6 +1291,10 @@ export class Repository { } } + if (options?.shortStats) { + args.push('--shortstat'); + } + if (options?.sortByAuthorDate) { args.push('--author-date-order'); } diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 5788ecc53dd7..4f5c53f9e502 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -10,6 +10,8 @@ import { debounce } from './decorators'; import { emojify, ensureEmojis } from './emoji'; import { CommandCenter } from './commands'; import { OperationKind, OperationResult } from './operation'; +import { getCommitShortHash } from './util'; +import { CommitShortStat } from './git'; export class GitTimelineItem extends TimelineItem { static is(item: TimelineItem): item is GitTimelineItem { @@ -48,18 +50,46 @@ export class GitTimelineItem extends TimelineItem { return this.shortenRef(this.previousRef); } - setItemDetails(author: string, email: string | undefined, date: string, message: string): void { + setItemDetails(uri: Uri, hash: string | undefined, author: string, email: string | undefined, date: string, message: string, shortStat?: CommitShortStat): void { this.tooltip = new MarkdownString('', true); + this.tooltip.isTrusted = true; + this.tooltip.supportHtml = true; if (email) { const emailTitle = l10n.t('Email'); - this.tooltip.appendMarkdown(`$(account) [**${author}**](mailto:${email} "${emailTitle} ${author}")\n\n`); + this.tooltip.appendMarkdown(`$(account) [**${author}**](mailto:${email} "${emailTitle} ${author}")`); } else { - this.tooltip.appendMarkdown(`$(account) **${author}**\n\n`); + this.tooltip.appendMarkdown(`$(account) **${author}**`); } - this.tooltip.appendMarkdown(`$(history) ${date}\n\n`); - this.tooltip.appendMarkdown(message); + this.tooltip.appendMarkdown(`, $(history) ${date}\n\n`); + this.tooltip.appendMarkdown(`${message}\n\n`); + + if (shortStat) { + this.tooltip.appendMarkdown(`---\n\n`); + + if (shortStat.insertions) { + this.tooltip.appendMarkdown(`${shortStat.insertions === 1 ? + l10n.t('{0} insertion{1}', shortStat.insertions, '(+)') : + l10n.t('{0} insertions{1}', shortStat.insertions, '(+)')}`); + } + + if (shortStat.deletions) { + this.tooltip.appendMarkdown(`, ${shortStat.deletions === 1 ? + l10n.t('{0} deletion{1}', shortStat.deletions, '(-)') : + l10n.t('{0} deletions{1}', shortStat.deletions, '(-)')}`); + } + + this.tooltip.appendMarkdown(`\n\n`); + } + + if (hash) { + this.tooltip.appendMarkdown(`---\n\n`); + + this.tooltip.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(uri, hash)} \`](command:git.viewCommit2?${encodeURIComponent(JSON.stringify([uri, hash]))} "${l10n.t('View Commit')}")`); + this.tooltip.appendMarkdown(' '); + this.tooltip.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`); + } } private shortenRef(ref: string): string { @@ -153,6 +183,7 @@ export class GitTimelineProvider implements TimelineProvider { maxEntries: limit, hash: options.cursor, follow: true, + shortStats: true, // sortByAuthorDate: true }); @@ -184,7 +215,7 @@ export class GitTimelineProvider implements TimelineProvider { item.description = c.authorName; } - item.setItemDetails(c.authorName!, c.authorEmail, dateFormatter.format(date), message); + item.setItemDetails(uri, c.hash, c.authorName!, c.authorEmail, dateFormatter.format(date), message, c.shortStat); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { @@ -209,7 +240,7 @@ export class GitTimelineProvider implements TimelineProvider { // TODO@eamodio: Replace with a better icon -- reflecting its status maybe? item.iconPath = new ThemeIcon('git-commit'); item.description = ''; - item.setItemDetails(you, undefined, dateFormatter.format(date), Resource.getStatusText(index.type)); + item.setItemDetails(uri, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(index.type)); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { @@ -231,7 +262,7 @@ export class GitTimelineProvider implements TimelineProvider { const item = new GitTimelineItem('', index ? '~' : 'HEAD', l10n.t('Uncommitted Changes'), date.getTime(), 'working', 'git:file:working'); item.iconPath = new ThemeIcon('circle-outline'); item.description = ''; - item.setItemDetails(you, undefined, dateFormatter.format(date), Resource.getStatusText(working.type)); + item.setItemDetails(uri, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(working.type)); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index b97f92019532..7e18e20482c2 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -1162,7 +1162,7 @@ class TimelineTreeRenderer implements ITreeRenderer Date: Mon, 6 Jan 2025 21:48:07 +0100 Subject: [PATCH 0368/3587] fix #236194 (#237366) --- .../common/extensionsScannerService.ts | 136 ++++++++++-------- .../node/extensionManagementService.ts | 10 +- 2 files changed, 86 insertions(+), 60 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index f852d912536e..0800126329da 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -609,15 +609,16 @@ class ExtensionsScanner extends Disposable { if (!scannedProfileExtensions.length) { return []; } - const extensions = await Promise.all( - scannedProfileExtensions.map(async extensionInfo => { - if (filter(extensionInfo)) { - const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); - return this.scanExtension(extensionScannerInput, extensionInfo.metadata); - } - return null; - })); - return coalesce(extensions); + const extensions: IRelaxedScannedExtension[] = []; + await Promise.all(scannedProfileExtensions.map(async extensionInfo => { + if (!filter(extensionInfo)) { + return; + } + const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); + const extension = await this.scanExtension(extensionScannerInput, extensionInfo); + extensions.push(extension); + })); + return extensions; } async scanOneOrMultipleExtensions(input: ExtensionScannerInput): Promise { @@ -634,56 +635,76 @@ class ExtensionsScanner extends Disposable { } } - async scanExtension(input: ExtensionScannerInput, metadata?: Metadata): Promise { + async scanExtension(input: ExtensionScannerInput): Promise; + async scanExtension(input: ExtensionScannerInput, scannedProfileExtension: IScannedProfileExtension): Promise; + async scanExtension(input: ExtensionScannerInput, scannedProfileExtension?: IScannedProfileExtension): Promise { + const validations: [Severity, string][] = []; + let isValid = true; + let manifest: IScannedExtensionManifest; try { - let manifest = await this.scanExtensionManifest(input.location); - if (manifest) { - // allow publisher to be undefined to make the initial extension authoring experience smoother - if (!manifest.publisher) { - manifest.publisher = UNDEFINED_PUBLISHER; - } - metadata = metadata ?? manifest.__metadata; - if (metadata && !metadata?.size && manifest.__metadata?.size) { - metadata.size = manifest.__metadata?.size; - } - delete manifest.__metadata; - const id = getGalleryExtensionId(manifest.publisher, manifest.name); - const identifier = metadata?.id ? { id, uuid: metadata.id } : { id }; - const type = metadata?.isSystem ? ExtensionType.System : input.type; - const isBuiltin = type === ExtensionType.System || !!metadata?.isBuiltin; - manifest = await this.translateManifest(input.location, manifest, ExtensionScannerInput.createNlsConfiguration(input)); - let extension: IRelaxedScannedExtension = { - type, - identifier, - manifest, - location: input.location, - isBuiltin, - targetPlatform: metadata?.targetPlatform ?? TargetPlatform.UNDEFINED, - publisherDisplayName: metadata?.publisherDisplayName, - metadata, - isValid: true, - validations: [], - preRelease: !!metadata?.preRelease, + manifest = await this.scanExtensionManifest(input.location); + } catch (e) { + if (scannedProfileExtension) { + validations.push([Severity.Error, getErrorMessage(e)]); + isValid = false; + const [publisher, name] = scannedProfileExtension.identifier.id.split('.'); + manifest = { + name, + publisher, + version: scannedProfileExtension.version, + engines: { vscode: '' } }; - if (input.validate) { - extension = this.validate(extension, input); - } - if (manifest.enabledApiProposals && (!this.environmentService.isBuilt || this.extensionsEnabledWithApiProposalVersion.includes(id.toLowerCase()))) { - manifest.originalEnabledApiProposals = manifest.enabledApiProposals; - manifest.enabledApiProposals = parseEnabledApiProposalNames([...manifest.enabledApiProposals]); + } else { + if (input.type !== ExtensionType.System) { + this.logService.error(e); } - return extension; - } - } catch (e) { - if (input.type !== ExtensionType.System) { - this.logService.error(e); + return null; } } - return null; + + // allow publisher to be undefined to make the initial extension authoring experience smoother + if (!manifest.publisher) { + manifest.publisher = UNDEFINED_PUBLISHER; + } + const metadata = scannedProfileExtension?.metadata ?? manifest.__metadata; + if (metadata && !metadata?.size && manifest.__metadata?.size) { + metadata.size = manifest.__metadata?.size; + } + delete manifest.__metadata; + const id = getGalleryExtensionId(manifest.publisher, manifest.name); + const identifier = metadata?.id ? { id, uuid: metadata.id } : { id }; + const type = metadata?.isSystem ? ExtensionType.System : input.type; + const isBuiltin = type === ExtensionType.System || !!metadata?.isBuiltin; + try { + manifest = await this.translateManifest(input.location, manifest, ExtensionScannerInput.createNlsConfiguration(input)); + } catch (error) { + this.logService.warn('Failed to translate manifest', getErrorMessage(error)); + } + let extension: IRelaxedScannedExtension = { + type, + identifier, + manifest, + location: input.location, + isBuiltin, + targetPlatform: metadata?.targetPlatform ?? TargetPlatform.UNDEFINED, + publisherDisplayName: metadata?.publisherDisplayName, + metadata, + isValid, + validations, + preRelease: !!metadata?.preRelease, + }; + if (input.validate) { + extension = this.validate(extension, input); + } + if (manifest.enabledApiProposals && (!this.environmentService.isBuilt || this.extensionsEnabledWithApiProposalVersion.includes(id.toLowerCase()))) { + manifest.originalEnabledApiProposals = manifest.enabledApiProposals; + manifest.enabledApiProposals = parseEnabledApiProposalNames([...manifest.enabledApiProposals]); + } + return extension; } validate(extension: IRelaxedScannedExtension, input: ExtensionScannerInput): IRelaxedScannedExtension { - let isValid = true; + let isValid = extension.isValid; const validateApiVersion = this.environmentService.isBuilt && this.extensionsEnabledWithApiProposalVersion.includes(extension.identifier.id.toLowerCase()); const validations = validateExtensionManifest(input.productVersion, input.productDate, input.location, extension.manifest, extension.isBuiltin, validateApiVersion); for (const [severity, message] of validations) { @@ -693,11 +714,11 @@ class ExtensionsScanner extends Disposable { } } extension.isValid = isValid; - extension.validations = validations; + extension.validations = [...extension.validations, ...validations]; return extension; } - private async scanExtensionManifest(extensionLocation: URI): Promise { + private async scanExtensionManifest(extensionLocation: URI): Promise { const manifestLocation = joinPath(extensionLocation, 'package.json'); let content; try { @@ -706,7 +727,7 @@ class ExtensionsScanner extends Disposable { if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { this.logService.error(this.formatMessage(extensionLocation, localize('fileReadFail', "Cannot read file {0}: {1}.", manifestLocation.path, error.message))); } - return null; + throw error; } let manifest: IScannedExtensionManifest; try { @@ -718,11 +739,12 @@ class ExtensionsScanner extends Disposable { for (const e of errors) { this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseFail', "Failed to parse {0}: [{1}, {2}] {3}.", manifestLocation.path, e.offset, e.length, getParseErrorMessage(e.error)))); } - return null; + throw err; } if (getNodeType(manifest) !== 'object') { - this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseInvalidType', "Invalid manifest file {0}: Not a JSON object.", manifestLocation.path))); - return null; + const errorMessage = this.formatMessage(extensionLocation, localize('jsonParseInvalidType', "Invalid manifest file {0}: Not a JSON object.", manifestLocation.path)); + this.logService.error(errorMessage); + throw new Error(errorMessage); } return manifest; } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index e9b38bc69f2f..e954abf073b7 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -45,7 +45,7 @@ import { ExtensionsManifestCache } from './extensionsManifestCache.js'; import { DidChangeProfileExtensionsEvent, ExtensionsWatcher } from './extensionsWatcher.js'; import { ExtensionType, IExtension, IExtensionManifest, TargetPlatform } from '../../extensions/common/extensions.js'; import { isEngineValid } from '../../extensions/common/extensionValidator.js'; -import { FileChangesEvent, FileChangeType, FileOperationResult, IFileService, toFileOperationResult } from '../../files/common/files.js'; +import { FileChangesEvent, FileChangeType, FileOperationResult, IFileService, IFileStat, toFileOperationResult } from '../../files/common/files.js'; import { IInstantiationService, refineServiceDecorator } from '../../instantiation/common/instantiation.js'; import { ILogService } from '../../log/common/log.js'; import { IProductService } from '../../product/common/productService.js'; @@ -829,10 +829,14 @@ export class ExtensionsScanner extends Disposable { } private async toLocalExtension(extension: IScannedExtension): Promise { - const stat = await this.fileService.resolve(extension.location); + let stat: IFileStat | undefined; + try { + stat = await this.fileService.resolve(extension.location); + } catch (error) {/* ignore */ } + let readmeUrl: URI | undefined; let changelogUrl: URI | undefined; - if (stat.children) { + if (stat?.children) { readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource; changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource; } From fc4e78cbfe5c7e5e365ebe9fd43b7cf157d229c3 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 6 Jan 2025 22:17:09 +0100 Subject: [PATCH 0369/3587] Git - remove commands that are not used (#237368) Git - remove commands that are not used --- extensions/git/package.json | 11 ----- extensions/git/package.nls.json | 1 - extensions/git/src/blame.ts | 4 +- extensions/git/src/commands.ts | 61 +------------------------- extensions/git/src/timelineProvider.ts | 2 +- 5 files changed, 5 insertions(+), 74 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 064dfe3f1cfb..66e8dd4981d7 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -912,13 +912,6 @@ "category": "Git", "enablement": "!operationInProgress" }, - { - "command": "git.viewAllChanges", - "title": "%command.viewAllChanges%", - "icon": "$(diff-multiple)", - "category": "Git", - "enablement": "!operationInProgress" - }, { "command": "git.copyCommitId", "title": "%command.timelineCopyCommitId%", @@ -1444,10 +1437,6 @@ "command": "git.viewCommit", "when": "false" }, - { - "command": "git.viewAllChanges", - "when": "false" - }, { "command": "git.stageFile", "when": "false" diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index afbb44ba48f8..e7020f41890f 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -125,7 +125,6 @@ "command.viewChanges": "View Changes", "command.viewStagedChanges": "View Staged Changes", "command.viewUntrackedChanges": "View Untracked Changes", - "command.viewAllChanges": "View All Changes", "command.viewCommit": "View Commit", "command.api.getRepositories": "Get Repositories", "command.api.getRepositoryState": "Get Repository State", diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index c4ca348eef9f..c8f15ffdf947 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -265,7 +265,7 @@ export class GitBlameController { markdownString.appendMarkdown(`\n\n---\n\n`); } - markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, blameInformationOrCommit.hash)} \`](command:git.viewCommit2?${encodeURIComponent(JSON.stringify([documentUri, blameInformationOrCommit.hash]))} "${l10n.t('View Commit')}")`); + markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, blameInformationOrCommit.hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformationOrCommit.hash]))} "${l10n.t('View Commit')}")`); markdownString.appendMarkdown(' '); markdownString.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(blameInformationOrCommit.hash))} "${l10n.t('Copy Commit Hash')}")`); markdownString.appendMarkdown('  |  '); @@ -702,7 +702,7 @@ class GitBlameStatusBarItem { this._statusBarItem.tooltip = this._controller.getBlameInformationHover(window.activeTextEditor.document.uri, blameInformation[0].blameInformation); this._statusBarItem.command = { title: l10n.t('View Commit'), - command: 'git.viewCommit2', + command: 'git.viewCommit', arguments: [window.activeTextEditor.document.uri, blameInformation[0].blameInformation.hash] } satisfies Command; } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index ab344ad50230..b8fa9009e1f6 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -4271,63 +4271,6 @@ export class CommandCenter { }); } - @command('git.viewCommit', { repository: true }) - async viewCommit(repository: Repository, historyItem1: SourceControlHistoryItem, historyItem2?: SourceControlHistoryItem): Promise { - if (!repository || !historyItem1) { - return; - } - - if (historyItem2) { - const mergeBase = await repository.getMergeBase(historyItem1.id, historyItem2.id); - if (!mergeBase || (mergeBase !== historyItem1.id && mergeBase !== historyItem2.id)) { - return; - } - } - - let title: string | undefined; - let historyItemParentId: string | undefined; - const rootUri = Uri.file(repository.root); - - // If historyItem2 is not provided, we are viewing a single commit. If historyItem2 is - // provided, we are viewing a range and we have to include both start and end commits. - // TODO@lszomoru - handle the case when historyItem2 is the first commit in the repository - if (!historyItem2) { - const commit = await repository.getCommit(historyItem1.id); - title = `${getCommitShortHash(rootUri, historyItem1.id)} - ${truncate(commit.message)}`; - historyItemParentId = historyItem1.parentIds.length > 0 ? historyItem1.parentIds[0] : `${historyItem1.id}^`; - } else { - title = l10n.t('All Changes ({0} ↔ {1})', getCommitShortHash(rootUri, historyItem2.id), getCommitShortHash(rootUri, historyItem1.id)); - historyItemParentId = historyItem2.parentIds.length > 0 ? historyItem2.parentIds[0] : `${historyItem2.id}^`; - } - - const multiDiffSourceUri = Uri.from({ scheme: 'scm-history-item', path: `${repository.root}/${historyItemParentId}..${historyItem1.id}` }); - - await this._viewChanges(repository, historyItem1.id, historyItemParentId, multiDiffSourceUri, title); - } - - @command('git.viewAllChanges', { repository: true }) - async viewAllChanges(repository: Repository, historyItem: SourceControlHistoryItem): Promise { - if (!repository || !historyItem) { - return; - } - - const rootUri = Uri.file(repository.root); - const modifiedShortRef = getCommitShortHash(rootUri, historyItem.id); - const originalShortRef = historyItem.parentIds.length > 0 ? getCommitShortHash(rootUri, historyItem.parentIds[0]) : `${modifiedShortRef}^`; - const title = l10n.t('All Changes ({0} ↔ {1})', originalShortRef, modifiedShortRef); - - const multiDiffSourceUri = toGitUri(Uri.file(repository.root), historyItem.id, { scheme: 'git-changes' }); - - await this._viewChanges(repository, modifiedShortRef, originalShortRef, multiDiffSourceUri, title); - } - - async _viewChanges(repository: Repository, historyItemId: string, historyItemParentId: string, multiDiffSourceUri: Uri, title: string): Promise { - const changes = await repository.diffBetween(historyItemParentId, historyItemId); - const resources = changes.map(c => toMultiFileDiffEditorUris(c, historyItemParentId, historyItemId)); - - await commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources }); - } - @command('git.copyCommitId', { repository: true }) async copyCommitId(repository: Repository, historyItem: SourceControlHistoryItem): Promise { if (!repository || !historyItem) { @@ -4346,8 +4289,8 @@ export class CommandCenter { env.clipboard.writeText(historyItem.message); } - @command('git.viewCommit2', { repository: true }) - async viewCommit2(repository: Repository, historyItemId: string): Promise { + @command('git.viewCommit', { repository: true }) + async viewCommit(repository: Repository, historyItemId: string): Promise { if (!repository || !historyItemId) { return; } diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 4f5c53f9e502..2b3da08f82e1 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -86,7 +86,7 @@ export class GitTimelineItem extends TimelineItem { if (hash) { this.tooltip.appendMarkdown(`---\n\n`); - this.tooltip.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(uri, hash)} \`](command:git.viewCommit2?${encodeURIComponent(JSON.stringify([uri, hash]))} "${l10n.t('View Commit')}")`); + this.tooltip.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(uri, hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([uri, hash]))} "${l10n.t('View Commit')}")`); this.tooltip.appendMarkdown(' '); this.tooltip.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`); } From 70866d528727b9131fc07b0f14869f256059949b Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 6 Jan 2025 21:22:42 -0700 Subject: [PATCH 0370/3587] Enable "tools agent" (#237369) * Add an edit tool (doesn't work) * More * Properly wait on text edits to be done applying * Better editFile tool * Fixes * Be more insistent with editFile instructions * Add "agent mode" UI * Fix error thrown when calling tools sometimes * Persist chat agent mode state * Hide editing tools from other extensions for now * Fix test build issues * Allow disabling tools agent mode * Remove comment * Fix codeblock index properly * Cleanup * Cleanup * Remove ccreq check * Rename for clarity --- .../workbench/api/common/extHost.api.impl.ts | 4 +- .../api/common/extHostLanguageModelTools.ts | 27 ++- .../browser/actions/chatExecuteActions.ts | 49 +++++- .../contrib/chat/browser/chat.contribution.ts | 2 + .../chatMarkdownContentPart.ts | 25 +-- .../browser/chatParticipant.contribution.ts | 3 +- .../contrib/chat/browser/codeBlockPart.ts | 1 + .../browser/contrib/chatInputCompletions.ts | 17 +- .../chat/browser/languageModelToolsService.ts | 24 ++- .../contrib/chat/browser/tools/tools.ts | 157 ++++++++++++++++++ .../contrib/chat/common/chatAgents.ts | 48 +++++- .../contrib/chat/common/chatContextKeys.ts | 5 + .../common/chatParticipantContribTypes.ts | 1 + .../chatProgressTypes/chatToolInvocation.ts | 4 - .../contrib/chat/common/chatViewModel.ts | 18 +- .../chat/test/common/chatAgents.test.ts | 3 +- .../chat/test/common/voiceChatService.test.ts | 22 ++- 17 files changed, 351 insertions(+), 59 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/tools/tools.ts diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 868906908ea7..a2fc5b81c0f0 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1500,10 +1500,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostLanguageModelTools.registerTool(extension, name, tool); }, invokeTool(name: string, parameters: vscode.LanguageModelToolInvocationOptions, token?: vscode.CancellationToken) { - return extHostLanguageModelTools.invokeTool(name, parameters, token); + return extHostLanguageModelTools.invokeTool(extension, name, parameters, token); }, get tools() { - return extHostLanguageModelTools.tools; + return extHostLanguageModelTools.getTools(extension); }, fileIsIgnored(uri: vscode.Uri, token: vscode.CancellationToken) { return extHostLanguageModels.fileIsIgnored(extension, uri, token); diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index f078897e24ab..b2f25fb152f4 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -14,6 +14,7 @@ import { IExtensionDescription } from '../../../platform/extensions/common/exten import { IPreparedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js'; import * as typeConvert from './extHostTypeConverters.js'; +import { isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; @@ -45,17 +46,22 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape return await fn(input, token); } - async invokeTool(toolId: string, options: vscode.LanguageModelToolInvocationOptions, token?: CancellationToken): Promise { + async invokeTool(extension: IExtensionDescription, toolId: string, options: vscode.LanguageModelToolInvocationOptions, token?: CancellationToken): Promise { const callId = generateUuid(); if (options.tokenizationOptions) { this._tokenCountFuncs.set(callId, options.tokenizationOptions.countTokens); } - if (options.toolInvocationToken && !isToolInvocationContext(options.toolInvocationToken)) { - throw new Error(`Invalid tool invocation token`); - } - try { + if (options.toolInvocationToken && !isToolInvocationContext(options.toolInvocationToken)) { + throw new Error(`Invalid tool invocation token`); + } + + const tool = this._allTools.get(toolId); + if (tool?.tags?.includes('vscode_editing') && !isProposedApiEnabled(extension, 'chatParticipantPrivate')) { + throw new Error(`Invalid tool: ${toolId}`); + } + // Making the round trip here because not all tools were necessarily registered in this EH const result = await this._proxy.$invokeTool({ toolId, @@ -77,9 +83,16 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape } } - get tools(): vscode.LanguageModelToolInformation[] { + getTools(extension: IExtensionDescription): vscode.LanguageModelToolInformation[] { return Array.from(this._allTools.values()) - .map(tool => typeConvert.LanguageModelToolDescription.to(tool)); + .map(tool => typeConvert.LanguageModelToolDescription.to(tool)) + .filter(tool => { + if (tool.tags.includes('vscode_editing')) { + return isProposedApiEnabled(extension, 'chatParticipantPrivate'); + } + + return true; + }); } async $invokeTool(dto: IToolInvocation, token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 1b8f11d48511..6557d40e8c78 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -6,11 +6,11 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { URI } from '../../../../../base/common/uri.js'; -import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { localize, localize2 } from '../../../../../nls.js'; import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; @@ -75,6 +75,52 @@ export class ChatSubmitAction extends SubmitAction { } } +export const ToggleAgentModeActionId = 'workbench.action.chat.toggleAgentMode'; +export class ToggleAgentModeAction extends Action2 { + static readonly ID = ToggleAgentModeActionId; + + constructor() { + super({ + id: ToggleAgentModeAction.ID, + title: localize2('interactive.toggleAgent.label', "Toggle Agent Mode"), + f1: true, + category: CHAT_CATEGORY, + precondition: ContextKeyExpr.and( + ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), + ChatContextKeys.Editing.hasToolsAgent), + icon: Codicon.edit, + toggled: { + condition: ChatContextKeys.Editing.agentMode, + icon: Codicon.tools, + tooltip: localize('agentEnabled', "Agent Mode Enabled"), + }, + tooltip: localize('agentDisabled', "Agent Mode Disabled"), + keybinding: { + when: ContextKeyExpr.and( + ChatContextKeys.inChatInput, + ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)), + primary: KeyMod.CtrlCmd | KeyCode.Period, + weight: KeybindingWeight.EditorContrib + }, + menu: [ + { + id: MenuId.ChatExecute, + order: 1, + when: ContextKeyExpr.and( + ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), + ChatContextKeys.Editing.hasToolsAgent), + group: 'navigation', + }, + ] + }); + } + + override run(accessor: ServicesAccessor, ...args: any[]): void { + const agentService = accessor.get(IChatAgentService); + agentService.toggleToolsAgentMode(); + } +} + export class ChatEditingSessionSubmitAction extends SubmitAction { static readonly ID = 'workbench.action.edits.submit'; @@ -388,4 +434,5 @@ export function registerChatExecuteActions() { registerAction2(SendToNewChatAction); registerAction2(ChatSubmitSecondaryAgentAction); registerAction2(SendToChatEditingAction); + registerAction2(ToggleAgentModeAction); } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 456c8817cd46..86b780b350c8 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -81,6 +81,7 @@ import { Extensions, IConfigurationMigrationRegistry } from '../../../common/con import { ChatEditorOverlayController } from './chatEditorOverlay.js'; import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesContrib.js'; import { ChatQuotasService, ChatQuotasStatusBarEntry, IChatQuotasService } from './chatQuotasService.js'; +import { BuiltinToolsContribution } from './tools/tools.js'; import { ChatSetupContribution } from './chatSetup.js'; // Register configuration @@ -319,6 +320,7 @@ registerWorkbenchContribution2(ChatViewsWelcomeHandler.ID, ChatViewsWelcomeHandl registerWorkbenchContribution2(ChatGettingStartedContribution.ID, ChatGettingStartedContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatSetupContribution.ID, ChatSetupContribution, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatQuotasStatusBarEntry.ID, ChatQuotasStatusBarEntry, WorkbenchPhase.Eventually); +registerWorkbenchContribution2(BuiltinToolsContribution.ID, BuiltinToolsContribution, WorkbenchPhase.Eventually); registerChatActions(); registerChatCopyActions(); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index a281df1c40ce..aad084c2bb28 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -76,17 +76,22 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP // We release editors in order so that it's more likely that the same editor will be assigned if this element is re-rendered right away, like it often is during progressive rendering const orderedDisposablesList: IDisposable[] = []; - let codeBlockIndex = codeBlockStartIndex; + + // Need to track the index of the codeblock within the response so it can have a unique ID, + // and within this part to find it within the codeblocks array + let globalCodeBlockIndexStart = codeBlockStartIndex; + let thisPartCodeBlockIndexStart = 0; const result = this._register(renderer.render(markdown.content, { fillInIncompleteTokens, codeBlockRendererSync: (languageId, text, raw) => { - const isCodeBlockComplete = !isResponseVM(context.element) || context.element.isComplete || !raw || raw?.endsWith('```'); + const isCodeBlockComplete = !isResponseVM(context.element) || context.element.isComplete || !raw || raw?.trim().endsWith('```'); if ((!text || (text.startsWith('') && !text.includes('\n'))) && !isCodeBlockComplete && rendererOptions.renderCodeBlockPills) { const hideEmptyCodeblock = $('div'); hideEmptyCodeblock.style.display = 'none'; return hideEmptyCodeblock; } - const index = codeBlockIndex++; + const globalIndex = globalCodeBlockIndexStart++; + const thisPartIndex = thisPartCodeBlockIndexStart++; let textModel: Promise; let range: Range | undefined; let vulns: readonly IMarkdownVulnerability[] | undefined; @@ -101,15 +106,15 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP } } else { const sessionId = isResponseVM(element) || isRequestVM(element) ? element.sessionId : ''; - const modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, index); - const fastUpdateModelEntry = this.codeBlockModelCollection.updateSync(sessionId, element, index, { text, languageId, isComplete: isCodeBlockComplete }); + const modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, globalIndex); + const fastUpdateModelEntry = this.codeBlockModelCollection.updateSync(sessionId, element, globalIndex, { text, languageId, isComplete: isCodeBlockComplete }); vulns = modelEntry.vulns; codemapperUri = fastUpdateModelEntry.codemapperUri; textModel = modelEntry.model; } const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered; - const codeBlockInfo: ICodeBlockData = { languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: contextKeyService, vulns, codemapperUri }; + const codeBlockInfo: ICodeBlockData = { languageId, textModel, codeBlockIndex: globalIndex, codeBlockPartIndex: thisPartIndex, element, range, hideToolbar, parentContextKeyService: contextKeyService, vulns, codemapperUri }; if (!rendererOptions.renderCodeBlockPills || element.isCompleteAddedRequest || !codemapperUri) { const ref = this.renderCodeBlock(codeBlockInfo, text, isCodeBlockComplete, currentWidth); @@ -122,7 +127,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const ownerMarkdownPartId = this.id; const info: IChatCodeBlockInfo = new class { readonly ownerMarkdownPartId = ownerMarkdownPartId; - readonly codeBlockIndex = index; + readonly codeBlockIndex = globalIndex; readonly element = element; readonly isStreaming = !rendererOptions.renderCodeBlockPills; codemapperUri = undefined; // will be set async @@ -149,7 +154,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP // TODO@joyceerhl: remove this code when we change the codeblockUri API to make the URI available synchronously this.codeBlockModelCollection.update(codeBlockInfo.element.sessionId, codeBlockInfo.element, codeBlockInfo.codeBlockIndex, { text, languageId: codeBlockInfo.languageId, isComplete: isCodeBlockComplete }).then((e) => { // Update the existing object's codemapperUri - this.codeblocks[codeBlockInfo.codeBlockIndex].codemapperUri = e.codemapperUri; + this.codeblocks[codeBlockInfo.codeBlockPartIndex].codemapperUri = e.codemapperUri; this._onDidChangeHeight.fire(); }); } @@ -157,7 +162,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const ownerMarkdownPartId = this.id; const info: IChatCodeBlockInfo = new class { readonly ownerMarkdownPartId = ownerMarkdownPartId; - readonly codeBlockIndex = index; + readonly codeBlockIndex = globalIndex; readonly element = element; readonly isStreaming = !isCodeBlockComplete; readonly codemapperUri = codemapperUri; @@ -205,7 +210,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP if (isResponseVM(data.element)) { this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId, isComplete }).then((e) => { // Update the existing object's codemapperUri - this.codeblocks[data.codeBlockIndex].codemapperUri = e.codemapperUri; + this.codeblocks[data.codeBlockPartIndex].codemapperUri = e.codemapperUri; this._onDidChangeHeight.fire(); }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts index 3d16ee993b75..6949c7318038 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts @@ -199,7 +199,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { continue; } - if (providerDescriptor.isDefault && !isProposedApiEnabled(extension.description, 'defaultChatParticipant')) { + if ((providerDescriptor.isDefault || providerDescriptor.isAgent) && !isProposedApiEnabled(extension.description, 'defaultChatParticipant')) { this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: defaultChatParticipant.`); continue; } @@ -245,6 +245,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { name: providerDescriptor.name, fullName: providerDescriptor.fullName, isDefault: providerDescriptor.isDefault, + isToolsAgent: providerDescriptor.isAgent, locations: isNonEmptyArray(providerDescriptor.locations) ? providerDescriptor.locations.map(ChatAgentLocation.fromRaw) : [ChatAgentLocation.Panel], diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index 6f34a50203b3..29ed76b29c48 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -74,6 +74,7 @@ const $ = dom.$; export interface ICodeBlockData { readonly codeBlockIndex: number; + readonly codeBlockPartIndex: number; readonly element: unknown; readonly textModel: Promise; diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index bf5dfa67cef0..68e436eeeef5 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { coalesce } from '../../../../../base/common/arrays.js'; import { raceTimeout } from '../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { isPatternInWord } from '../../../../../base/common/filters.js'; @@ -258,7 +259,11 @@ class AgentCompletions extends Disposable { return { suggestions: justAgents.concat( - agents.flatMap(agent => agent.slashCommands.map((c, i) => { + coalesce(agents.flatMap(agent => agent.slashCommands.map((c, i) => { + if (agent.isDefault && this.chatAgentService.getDefaultAgent(widget.location)?.id !== agent.id) { + return; + } + const { label: agentLabel, isDupe } = this.getAgentCompletionDetails(agent); const label = `${agentLabel} ${chatSubcommandLeader}${c.name}`; const item: CompletionItem = { @@ -284,7 +289,7 @@ class AgentCompletions extends Disposable { } return item; - }))) + })))) }; } })); @@ -313,7 +318,11 @@ class AgentCompletions extends Disposable { .filter(a => a.locations.includes(widget.location)); return { - suggestions: agents.flatMap(agent => agent.slashCommands.map((c, i) => { + suggestions: coalesce(agents.flatMap(agent => agent.slashCommands.map((c, i) => { + if (agent.isDefault && this.chatAgentService.getDefaultAgent(widget.location)?.id !== agent.id) { + return; + } + const { label: agentLabel, isDupe } = this.getAgentCompletionDetails(agent); const withSlash = `${chatSubcommandLeader}${c.name}`; const extraSortText = agent.id === 'github.copilot.terminalPanel' ? `z` : ``; @@ -338,7 +347,7 @@ class AgentCompletions extends Disposable { } return item; - })) + }))) }; } })); diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index d0bce841451c..d53c0db6859a 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -13,6 +13,7 @@ import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from import { localize } from '../../../../nls.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { ChatModel } from '../common/chatModel.js'; @@ -46,6 +47,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo @IChatService private readonly _chatService: IChatService, @IDialogService private readonly _dialogService: IDialogService, @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ILogService private readonly _logService: ILogService, ) { super(); @@ -125,6 +127,8 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } async invokeTool(dto: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { + this._logService.trace(`[LanguageModelToolsService#invokeTool] Invoking tool ${dto.toolId} with parameters ${JSON.stringify(dto.parameters)}`); + // When invoking a tool, don't validate the "when" clause. An extension may have invoked a tool just as it was becoming disabled, and just let it go through rather than throw and break the chat. let tool = this._tools.get(dto.toolId); if (!tool) { @@ -165,12 +169,15 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo const source = new CancellationTokenSource(); store.add(toDisposable(() => { - toolInvocation!.confirmed.complete(false); source.dispose(true); })); store.add(token.onCancellationRequested(() => { + toolInvocation?.confirmed.complete(false); source.cancel(); })); + store.add(source.token.onCancellationRequested(() => { + toolInvocation?.confirmed.complete(false); + })); token = source.token; const prepared = tool.impl.prepareToolInvocation ? @@ -179,13 +186,14 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo const defaultMessage = localize('toolInvocationMessage', "Using {0}", `"${tool.data.displayName}"`); const invocationMessage = prepared?.invocationMessage ?? defaultMessage; - toolInvocation = new ChatToolInvocation(invocationMessage, prepared?.confirmationMessages); - - model.acceptResponseProgress(request, toolInvocation); - if (prepared?.confirmationMessages) { - const userConfirmed = await toolInvocation.confirmed.p; - if (!userConfirmed) { - throw new CancellationError(); + if (tool.data.id !== 'vscode_editFile') { + toolInvocation = new ChatToolInvocation(invocationMessage, prepared?.confirmationMessages); + model.acceptResponseProgress(request, toolInvocation); + if (prepared?.confirmationMessages) { + const userConfirmed = await toolInvocation.confirmed.p; + if (!userConfirmed) { + throw new CancellationError(); + } } } } else { diff --git a/src/vs/workbench/contrib/chat/browser/tools/tools.ts b/src/vs/workbench/contrib/chat/browser/tools/tools.ts new file mode 100644 index 000000000000..359b47eb4548 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/tools/tools.ts @@ -0,0 +1,157 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { IJSONSchema } from '../../../../../base/common/jsonSchema.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { localize } from '../../../../../nls.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IWorkbenchContribution } from '../../../../common/contributions.js'; +import { ICodeMapperService } from '../../common/chatCodeMapperService.js'; +import { IChatEditingService } from '../../common/chatEditingService.js'; +import { ChatModel } from '../../common/chatModel.js'; +import { IChatService } from '../../common/chatService.js'; +import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; + +export class BuiltinToolsContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'chat.builtinTools'; + + constructor( + @ILanguageModelToolsService toolsService: ILanguageModelToolsService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + const editTool = instantiationService.createInstance(EditTool); + this._register(toolsService.registerToolData(editTool)); + this._register(toolsService.registerToolImplementation(editTool.id, editTool)); + } +} + +interface EditToolParams { + filePath: string; + explanation: string; + code: string; +} + +const codeInstructions = ` +The user is very smart and can understand how to apply your edits to their files, you just need to provide minimal hints. +Avoid repeating existing code, instead use comments to represent regions of unchanged code. The user prefers that you are as concise as possible. For example: +// ...existing code... +{ changed code } +// ...existing code... +{ changed code } +// ...existing code... + +Here is an example of how you should format an edit to an existing Person class: +class Person { + // ...existing code... + age: number; + // ...existing code... + getAge() { + return this.age; + } +} +`; + +class EditTool implements IToolData, IToolImpl { + readonly id = 'vscode_editFile'; + readonly tags = ['vscode_editing']; + readonly displayName = localize('chat.tools.editFile', "Edit File"); + readonly modelDescription = `Edit a file in the workspace. Use this tool once per file that needs to be modified, even if there are multiple changes for a file. ${codeInstructions}`; + readonly inputSchema: IJSONSchema; + + constructor( + @IChatService private readonly chatService: IChatService, + @IChatEditingService private readonly chatEditingService: IChatEditingService, + @ICodeMapperService private readonly codeMapperService: ICodeMapperService + ) { + this.inputSchema = { + type: 'object', + properties: { + filePath: { + type: 'string', + description: 'An absolute path to the file to edit', + }, + explanation: { + type: 'string', + description: 'A short explanation of the edit being made. Can be the same as the explanation you showed to the user.', + }, + code: { + type: 'string', + description: 'The code change to apply to the file. ' + codeInstructions + } + }, + required: ['filePath', 'explanation', 'code'] + }; + } + + async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { + if (!invocation.context) { + throw new Error('toolInvocationToken is required for this tool'); + } + + + const parameters = invocation.parameters as EditToolParams; + if (!parameters.filePath || !parameters.explanation || !parameters.code) { + throw new Error(`Invalid tool input: ${JSON.stringify(parameters)}`); + } + + const model = this.chatService.getSession(invocation.context?.sessionId) as ChatModel; + const request = model.getRequests().at(-1)!; + + const uri = URI.file(parameters.filePath); + model.acceptResponseProgress(request, { + kind: 'markdownContent', + content: new MarkdownString('\n````\n') + }); + model.acceptResponseProgress(request, { + kind: 'codeblockUri', + uri + }); + model.acceptResponseProgress(request, { + kind: 'markdownContent', + content: new MarkdownString(parameters.code + '\n````\n') + }); + + if (this.chatEditingService.currentEditingSession?.chatSessionId !== model.sessionId) { + throw new Error('This tool must be called from within an editing session'); + } + + const result = await this.codeMapperService.mapCode({ + codeBlocks: [{ code: parameters.code, resource: uri, markdownBeforeBlock: parameters.explanation }], + conversation: [] + }, { + textEdit: (target, edits) => { + model.acceptResponseProgress(request, { kind: 'textEdit', uri: target, edits }); + } + }, token); + + model.acceptResponseProgress(request, { kind: 'textEdit', uri, edits: [], done: true }); + + if (result?.errorMessage) { + throw new Error(result.errorMessage); + } + + await new Promise((resolve) => { + autorun((r) => { + const currentEditingSession = this.chatEditingService.currentEditingSessionObs.read(r); + const entries = currentEditingSession?.entries.read(r); + const currentFile = entries?.find((e) => e.modifiedURI.toString() === uri.toString()); + if (currentFile && !currentFile.isCurrentlyBeingModified.read(r)) { + resolve(true); + } + }); + }); + + return { + content: [{ kind: 'text', value: 'Success' }] + }; + } +} diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index e8d88c925987..8045fc3d091d 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -70,6 +70,8 @@ export interface IChatAgentData { extensionDisplayName: string; /** The agent invoked when no agent is specified */ isDefault?: boolean; + /** The default agent when "agent-mode" is enabled */ + isToolsAgent?: boolean; /** This agent is not contributed in package.json, but is registered dynamically */ isDynamic?: boolean; metadata: IChatAgentMetadata; @@ -201,9 +203,11 @@ export interface IChatAgentCompletionItem { export interface IChatAgentService { _serviceBrand: undefined; /** - * undefined when an agent was removed IChatAgent + * undefined when an agent was removed */ readonly onDidChangeAgents: Event; + readonly toolsAgentModeEnabled: boolean; + toggleToolsAgentMode(): void; registerAgent(id: string, data: IChatAgentData): IDisposable; registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable; registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; @@ -235,6 +239,8 @@ export interface IChatAgentService { updateAgent(id: string, updateMetadata: IChatAgentMetadata): void; } +const ChatToolsAgentModeStorageKey = 'chat.toolsAgentMode'; + export class ChatAgentService extends Disposable implements IChatAgentService { public static readonly AGENT_LEADER = '@'; @@ -250,11 +256,14 @@ export class ChatAgentService extends Disposable implements IChatAgentService { private readonly _hasDefaultAgent: IContextKey; private readonly _defaultAgentRegistered: IContextKey; private readonly _editingAgentRegistered: IContextKey; + private readonly _agentModeContextKey: IContextKey; + private readonly _hasToolsAgentContextKey: IContextKey; private _chatParticipantDetectionProviders = new Map(); constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IStorageService private readonly storageService: IStorageService, ) { super(); this._hasDefaultAgent = ChatContextKeys.enabled.bindTo(this.contextKeyService); @@ -265,6 +274,13 @@ export class ChatAgentService extends Disposable implements IChatAgentService { this._updateContextKeys(); } })); + + this._agentModeContextKey = ChatContextKeys.Editing.agentMode.bindTo(contextKeyService); + this._hasToolsAgentContextKey = ChatContextKeys.Editing.hasToolsAgent.bindTo(contextKeyService); + this._agentModeContextKey.set( + this.storageService.getBoolean(ChatToolsAgentModeStorageKey, StorageScope.WORKSPACE, false)); + this._register( + this.storageService.onWillSaveState(() => this.storageService.store(ChatToolsAgentModeStorageKey, this._agentModeContextKey.get(), StorageScope.WORKSPACE, StorageTarget.USER))); } registerAgent(id: string, data: IChatAgentData): IDisposable { @@ -311,15 +327,23 @@ export class ChatAgentService extends Disposable implements IChatAgentService { private _updateContextKeys(): void { let editingAgentRegistered = false; let defaultAgentRegistered = false; + let toolsAgentRegistered = false; for (const agent of this.getAgents()) { if (agent.isDefault && agent.locations.includes(ChatAgentLocation.EditingSession)) { editingAgentRegistered = true; + if (agent.isToolsAgent) { + toolsAgentRegistered = true; + } } else if (agent.isDefault) { defaultAgentRegistered = true; } } this._editingAgentRegistered.set(editingAgentRegistered); this._defaultAgentRegistered.set(defaultAgentRegistered); + if (toolsAgentRegistered !== this._hasToolsAgentContextKey.get()) { + this._hasToolsAgentContextKey.set(toolsAgentRegistered); + this._onDidChangeAgents.fire(this.getDefaultAgent(ChatAgentLocation.EditingSession)); + } } registerAgentImplementation(id: string, agentImpl: IChatAgentImplementation): IDisposable { @@ -384,7 +408,22 @@ export class ChatAgentService extends Disposable implements IChatAgentService { } getDefaultAgent(location: ChatAgentLocation): IChatAgent | undefined { - return findLast(this.getActivatedAgents(), a => !!a.isDefault && a.locations.includes(location)); + return findLast(this.getActivatedAgents(), a => { + if (location === ChatAgentLocation.EditingSession && this.toolsAgentModeEnabled !== !!a.isToolsAgent) { + return false; + } + + return !!a.isDefault && a.locations.includes(location); + }); + } + + public get toolsAgentModeEnabled(): boolean { + return !!this._hasToolsAgentContextKey.get() && !!this._agentModeContextKey.get(); + } + + toggleToolsAgentMode(): void { + this._agentModeContextKey.set(!this._agentModeContextKey.get()); + this._onDidChangeAgents.fire(this.getDefaultAgent(ChatAgentLocation.EditingSession)); } getContributedDefaultAgent(location: ChatAgentLocation): IChatAgentData | undefined { @@ -404,8 +443,8 @@ export class ChatAgentService extends Disposable implements IChatAgentService { return this._agents.get(id)?.data; } - private _agentIsEnabled(id: string): boolean { - const entry = this._agents.get(id); + private _agentIsEnabled(idOrAgent: string | IChatAgentEntry): boolean { + const entry = typeof idOrAgent === 'string' ? this._agents.get(idOrAgent) : idOrAgent; return !entry?.data.when || this.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(entry.data.when)); } @@ -554,6 +593,7 @@ export class MergedChatAgent implements IChatAgent { get extensionPublisherDisplayName() { return this.data.publisherDisplayName; } get extensionDisplayName(): string { return this.data.extensionDisplayName; } get isDefault(): boolean | undefined { return this.data.isDefault; } + get isToolsAgent(): boolean | undefined { return this.data.isToolsAgent; } get metadata(): IChatAgentMetadata { return this.data.metadata; } get slashCommands(): IChatAgentCommand[] { return this.data.slashCommands; } get locations(): ChatAgentLocation[] { return this.data.locations; } diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index be03126dd2db..68c50c90600a 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -76,4 +76,9 @@ export namespace ChatContextKeys { export const chatQuotaExceeded = new RawContextKey('chatQuotaExceeded', false, true); export const completionsQuotaExceeded = new RawContextKey('completionsQuotaExceeded', false, true); + + export const Editing = { + hasToolsAgent: new RawContextKey('chatHasToolsAgent', false, { type: 'boolean', description: localize('chatEditingHasToolsAgent', "True when a tools agent is registered.") }), + agentMode: new RawContextKey('chatAgentMode', false, { type: 'boolean', description: localize('chatEditingAgentMode', "True when edits is in agent mode.") }), + }; } diff --git a/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts b/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts index 211d7e7e9c9f..401211b3c4f1 100644 --- a/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts @@ -22,6 +22,7 @@ export interface IRawChatParticipantContribution { when?: string; description?: string; isDefault?: boolean; + isAgent?: boolean; isSticky?: boolean; sampleRequest?: string; commands?: IRawChatCommandContribution[]; diff --git a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts index 7632ee2f0b6c..76e572c91df5 100644 --- a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts +++ b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts @@ -48,10 +48,6 @@ export class ChatToolInvocation implements IChatToolInvocation { this._confirmDeferred.p.then(confirmed => { this._isConfirmed = confirmed; this._confirmationMessages = undefined; - if (!confirmed) { - // Spinner -> check - this._isCompleteDeferred.complete(); - } }); this._isCompleteDeferred.p.then(() => { diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index b7cd3f361415..b078e519666b 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -147,7 +147,7 @@ export interface IChatCodeCitations { export type IChatRendererContent = IChatProgressRenderableResponseContent | IChatReferences | IChatCodeCitations; export interface IChatLiveUpdateData { - firstWordTime: number; + totalTime: number; lastUpdateTime: number; impliedWordLoadRate: number; lastWordCount: number; @@ -566,10 +566,10 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi if (!_model.isComplete) { this._contentUpdateTimings = { - firstWordTime: 0, + totalTime: 0, lastUpdateTime: Date.now(), impliedWordLoadRate: 0, - lastWordCount: 0 + lastWordCount: 0, }; } @@ -579,12 +579,14 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi const now = Date.now(); const wordCount = countWords(_model.response.getMarkdown()); - // Apply a min time difference, or the rate is typically too high for first few words - const timeDiff = Math.max(now - this._contentUpdateTimings.firstWordTime, 250); - const impliedWordLoadRate = this._contentUpdateTimings.lastWordCount / (timeDiff / 1000); - this.trace('onDidChange', `Update- got ${this._contentUpdateTimings.lastWordCount} words over last ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${wordCount} words are now available.`); + const timeDiff = Math.min(now - this._contentUpdateTimings.lastUpdateTime, 1000); + const newTotalTime = Math.max(this._contentUpdateTimings.totalTime + timeDiff, 250); + const impliedWordLoadRate = this._contentUpdateTimings.lastWordCount / (newTotalTime / 1000); + this.trace('onDidChange', `Update- got ${this._contentUpdateTimings.lastWordCount} words over last ${newTotalTime}ms = ${impliedWordLoadRate} words/s. ${wordCount} words are now available.`); this._contentUpdateTimings = { - firstWordTime: this._contentUpdateTimings.firstWordTime === 0 && this.response.value.some(v => v.kind === 'markdownContent') ? now : this._contentUpdateTimings.firstWordTime, + totalTime: this._contentUpdateTimings.totalTime !== 0 || this.response.value.some(v => v.kind === 'markdownContent') ? + newTotalTime : + this._contentUpdateTimings.totalTime, lastUpdateTime: now, impliedWordLoadRate, lastWordCount: wordCount diff --git a/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts b/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts index cec803118e1e..580fa44ce454 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts @@ -9,6 +9,7 @@ import { ContextKeyExpression } from '../../../../../platform/contextkey/common/ import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { ChatAgentService, IChatAgentData, IChatAgentImplementation } from '../../common/chatAgents.js'; +import { TestStorageService } from '../../../../test/common/workbenchTestServices.js'; const testAgentId = 'testAgent'; const testAgentData: IChatAgentData = { @@ -41,7 +42,7 @@ suite('ChatAgents', function () { let contextKeyService: TestingContextKeyService; setup(() => { contextKeyService = new TestingContextKeyService(); - chatAgentService = store.add(new ChatAgentService(contextKeyService)); + chatAgentService = store.add(new ChatAgentService(contextKeyService, store.add(new TestStorageService()))); }); test('registerAgent', async () => { diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts index 60e55441ce75..f84b3ba640b9 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts @@ -64,15 +64,6 @@ suite('VoiceChat', () => { ]; class TestChatAgentService implements IChatAgentService { - hasChatParticipantDetectionProviders(): boolean { - throw new Error('Method not implemented.'); - } - registerChatParticipantDetectionProvider(handle: number, provider: IChatParticipantDetectionProvider): IDisposable { - throw new Error('Method not implemented.'); - } - detectAgentOrCommand(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation }, token: CancellationToken): Promise<{ agent: IChatAgentData; command?: IChatAgentCommand } | undefined> { - throw new Error('Method not implemented.'); - } _serviceBrand: undefined; readonly onDidChangeAgents = Event.None; registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } @@ -93,6 +84,19 @@ suite('VoiceChat', () => { getAgentCompletionItems(id: string, query: string, token: CancellationToken): Promise { throw new Error('Method not implemented.'); } agentHasDupeName(id: string): boolean { throw new Error('Method not implemented.'); } getChatTitle(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error('Method not implemented.'); } + readonly toolsAgentModeEnabled: boolean = false; + toggleToolsAgentMode(): void { + throw new Error('Method not implemented.'); + } + hasChatParticipantDetectionProviders(): boolean { + throw new Error('Method not implemented.'); + } + registerChatParticipantDetectionProvider(handle: number, provider: IChatParticipantDetectionProvider): IDisposable { + throw new Error('Method not implemented.'); + } + detectAgentOrCommand(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation }, token: CancellationToken): Promise<{ agent: IChatAgentData; command?: IChatAgentCommand } | undefined> { + throw new Error('Method not implemented.'); + } } class TestSpeechService implements ISpeechService { From 47e4f1c45c8c70811d1ad7fcf8b22e781788f177 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 7 Jan 2025 08:26:58 +0100 Subject: [PATCH 0371/3587] Need to be clear on what the 30d free trial is when hitting quota (fix microsoft/vscode-copilot-release#3658) (#237374) --- src/vs/workbench/contrib/chat/browser/chatQuotasService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts b/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts index 62bf59aca5e6..6e2ab0a5b340 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts @@ -123,7 +123,7 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService message = localize('chatAndCompletionsQuotaExceeded', "You've reached the limit of the Copilot Free plan. These limits will reset on {0}.", dateFormatter.format(that.quotas.quotaResetDate)); } - const upgradeToPro = localize('upgradeToPro', "Here's what you can expect when upgrading to Copilot Pro:\n- Unlimited code completions\n- Unlimited chat messages\n- 30-day free trial"); + const upgradeToPro = localize('upgradeToPro', "Upgrade to Copilot Pro (your first 30 days are free) for:\n- Unlimited code completions\n- Unlimited chat messages\n- Access to additional models"); await dialogService.prompt({ type: 'none', From 3958e26f650620710d7bc02e70a580cc18c0cbc5 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Tue, 7 Jan 2025 10:00:03 +0100 Subject: [PATCH 0372/3587] Enable edit context (#235386) * Revert "Revert Enablement of EditContext on Insiders (#235062)" This reverts commit 45385e1c6f93a89a7992c0baacac62792434adac. * adding product import --- src/typings/editContext.d.ts | 4 +- src/vs/editor/common/config/editorOptions.ts | 3 +- .../services/driver/browser/driver.ts | 44 +++++++++++++------ test/automation/src/code.ts | 9 ++-- test/automation/src/debug.ts | 9 ++-- test/automation/src/editor.ts | 15 ++++--- test/automation/src/editors.ts | 3 +- test/automation/src/extensions.ts | 3 +- test/automation/src/notebook.ts | 7 +-- test/automation/src/scm.ts | 16 ++++--- test/automation/src/settings.ts | 14 ++++-- 11 files changed, 86 insertions(+), 41 deletions(-) diff --git a/src/typings/editContext.d.ts b/src/typings/editContext.d.ts index 5b5da0ac7e95..095858486671 100644 --- a/src/typings/editContext.d.ts +++ b/src/typings/editContext.d.ts @@ -58,8 +58,8 @@ interface EditContextEventHandlersEventMap { type EventHandler = (event: TEvent) => void; -interface TextUpdateEvent extends Event { - new(type: DOMString, options?: TextUpdateEventInit): TextUpdateEvent; +declare class TextUpdateEvent extends Event { + constructor(type: DOMString, options?: TextUpdateEventInit); readonly updateRangeStart: number; readonly updateRangeEnd: number; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 5fe2028e3666..1847c2ff3456 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -16,6 +16,7 @@ import { USUAL_WORD_SEPARATORS } from '../core/wordHelper.js'; import * as nls from '../../../nls.js'; import { AccessibilitySupport } from '../../../platform/accessibility/common/accessibility.js'; import { IConfigurationPropertySchema } from '../../../platform/configuration/common/configurationRegistry.js'; +import product from '../../../platform/product/common/product.js'; //#region typed options @@ -5822,7 +5823,7 @@ export const EditorOptions = { emptySelectionClipboard: register(new EditorEmptySelectionClipboard()), dropIntoEditor: register(new EditorDropIntoEditor()), experimentalEditContextEnabled: register(new EditorBooleanOption( - EditorOption.experimentalEditContextEnabled, 'experimentalEditContextEnabled', false, + EditorOption.experimentalEditContextEnabled, 'experimentalEditContextEnabled', product.quality !== 'stable', { description: nls.localize('experimentalEditContextEnabled', "Sets whether the new experimental edit context should be used instead of the text area."), included: platform.isChrome || platform.isEdge || platform.isNative diff --git a/src/vs/workbench/services/driver/browser/driver.ts b/src/vs/workbench/services/driver/browser/driver.ts index d78e55aa9737..ad689b9db6f3 100644 --- a/src/vs/workbench/services/driver/browser/driver.ts +++ b/src/vs/workbench/services/driver/browser/driver.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getClientArea, getTopLeftOffset } from '../../../../base/browser/dom.js'; +import { getClientArea, getTopLeftOffset, isHTMLDivElement, isHTMLTextAreaElement } from '../../../../base/browser/dom.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { language, locale } from '../../../../base/common/platform.js'; @@ -133,18 +133,36 @@ export class BrowserWindowDriver implements IWindowDriver { if (!element) { throw new Error(`Editor not found: ${selector}`); } - - const textarea = element as HTMLTextAreaElement; - const start = textarea.selectionStart; - const newStart = start + text.length; - const value = textarea.value; - const newValue = value.substr(0, start) + text + value.substr(start); - - textarea.value = newValue; - textarea.setSelectionRange(newStart, newStart); - - const event = new Event('input', { 'bubbles': true, 'cancelable': true }); - textarea.dispatchEvent(event); + if (isHTMLDivElement(element)) { + // Edit context is enabled + const editContext = element.editContext; + if (!editContext) { + throw new Error(`Edit context not found: ${selector}`); + } + const selectionStart = editContext.selectionStart; + const selectionEnd = editContext.selectionEnd; + const event = new TextUpdateEvent('textupdate', { + updateRangeStart: selectionStart, + updateRangeEnd: selectionEnd, + text, + selectionStart: selectionStart + text.length, + selectionEnd: selectionStart + text.length, + compositionStart: 0, + compositionEnd: 0 + }); + editContext.dispatchEvent(event); + } else if (isHTMLTextAreaElement(element)) { + const start = element.selectionStart; + const newStart = start + text.length; + const value = element.value; + const newValue = value.substr(0, start) + text + value.substr(start); + + element.value = newValue; + element.setSelectionRange(newStart, newStart); + + const event = new Event('input', { 'bubbles': true, 'cancelable': true }); + element.dispatchEvent(event); + } } async getTerminalBuffer(selector: string): Promise { diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index a925cdd65bc9..fd64b7cdeb1a 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -12,6 +12,7 @@ import { launch as launchPlaywrightBrowser } from './playwrightBrowser'; import { PlaywrightDriver } from './playwrightDriver'; import { launch as launchPlaywrightElectron } from './playwrightElectron'; import { teardown } from './processes'; +import { Quality } from './application'; export interface LaunchOptions { codePath?: string; @@ -28,6 +29,7 @@ export interface LaunchOptions { readonly tracing?: boolean; readonly headless?: boolean; readonly browser?: 'chromium' | 'webkit' | 'firefox'; + readonly quality: Quality; } interface ICodeInstance { @@ -77,7 +79,7 @@ export async function launch(options: LaunchOptions): Promise { const { serverProcess, driver } = await measureAndLog(() => launchPlaywrightBrowser(options), 'launch playwright (browser)', options.logger); registerInstance(serverProcess, options.logger, 'server'); - return new Code(driver, options.logger, serverProcess); + return new Code(driver, options.logger, serverProcess, options.quality); } // Electron smoke tests (playwright) @@ -85,7 +87,7 @@ export async function launch(options: LaunchOptions): Promise { const { electronProcess, driver } = await measureAndLog(() => launchPlaywrightElectron(options), 'launch playwright (electron)', options.logger); registerInstance(electronProcess, options.logger, 'electron'); - return new Code(driver, options.logger, electronProcess); + return new Code(driver, options.logger, electronProcess, options.quality); } } @@ -96,7 +98,8 @@ export class Code { constructor( driver: PlaywrightDriver, readonly logger: Logger, - private readonly mainProcess: cp.ChildProcess + private readonly mainProcess: cp.ChildProcess, + readonly quality: Quality ) { this.driver = new Proxy(driver, { get(target, prop) { diff --git a/test/automation/src/debug.ts b/test/automation/src/debug.ts index b7b7d427f4b0..e2e227fc35e1 100644 --- a/test/automation/src/debug.ts +++ b/test/automation/src/debug.ts @@ -9,6 +9,7 @@ import { Code, findElement } from './code'; import { Editors } from './editors'; import { Editor } from './editor'; import { IElement } from './driver'; +import { Quality } from './application'; const VIEWLET = 'div[id="workbench.view.debug"]'; const DEBUG_VIEW = `${VIEWLET}`; @@ -31,7 +32,8 @@ const CONSOLE_OUTPUT = `.repl .output.expression .value`; const CONSOLE_EVALUATION_RESULT = `.repl .evaluation-result.expression .value`; const CONSOLE_LINK = `.repl .value a.link`; -const REPL_FOCUSED = '.repl-input-wrapper .monaco-editor textarea'; +const REPL_FOCUSED_NATIVE_EDIT_CONTEXT = '.repl-input-wrapper .monaco-editor .native-edit-context'; +const REPL_FOCUSED_TEXTAREA = '.repl-input-wrapper .monaco-editor textarea'; export interface IStackFrame { name: string; @@ -127,8 +129,9 @@ export class Debug extends Viewlet { async waitForReplCommand(text: string, accept: (result: string) => boolean): Promise { await this.commands.runCommand('Debug: Focus on Debug Console View'); - await this.code.waitForActiveElement(REPL_FOCUSED); - await this.code.waitForSetValue(REPL_FOCUSED, text); + const selector = this.code.quality === Quality.Stable ? REPL_FOCUSED_TEXTAREA : REPL_FOCUSED_NATIVE_EDIT_CONTEXT; + await this.code.waitForActiveElement(selector); + await this.code.waitForSetValue(selector, text); // Wait for the keys to be picked up by the editor model such that repl evaluates what just got typed await this.editor.waitForEditorContents('debug:replinput', s => s.indexOf(text) >= 0); diff --git a/test/automation/src/editor.ts b/test/automation/src/editor.ts index 538866bfc060..dd6160795650 100644 --- a/test/automation/src/editor.ts +++ b/test/automation/src/editor.ts @@ -6,6 +6,7 @@ import { References } from './peek'; import { Commands } from './workbench'; import { Code } from './code'; +import { Quality } from './application'; const RENAME_BOX = '.monaco-editor .monaco-editor.rename-box'; const RENAME_INPUT = `${RENAME_BOX} .rename-input`; @@ -78,10 +79,10 @@ export class Editor { async waitForEditorFocus(filename: string, lineNumber: number, selectorPrefix = ''): Promise { const editor = [selectorPrefix || '', EDITOR(filename)].join(' '); const line = `${editor} .view-lines > .view-line:nth-child(${lineNumber})`; - const textarea = `${editor} textarea`; + const editContext = `${editor} ${this._editContextSelector()}`; await this.code.waitAndClick(line, 1, 1); - await this.code.waitForActiveElement(textarea); + await this.code.waitForActiveElement(editContext); } async waitForTypeInEditor(filename: string, text: string, selectorPrefix = ''): Promise { @@ -92,14 +93,18 @@ export class Editor { await this.code.waitForElement(editor); - const textarea = `${editor} textarea`; - await this.code.waitForActiveElement(textarea); + const editContext = `${editor} ${this._editContextSelector()}`; + await this.code.waitForActiveElement(editContext); - await this.code.waitForTypeInEditor(textarea, text); + await this.code.waitForTypeInEditor(editContext, text); await this.waitForEditorContents(filename, c => c.indexOf(text) > -1, selectorPrefix); } + private _editContextSelector() { + return this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'; + } + async waitForEditorContents(filename: string, accept: (contents: string) => boolean, selectorPrefix = ''): Promise { const selector = [selectorPrefix || '', `${EDITOR(filename)} .view-lines`].join(' '); return this.code.waitForTextContent(selector, undefined, c => accept(c.replace(/\u00a0/g, ' '))); diff --git a/test/automation/src/editors.ts b/test/automation/src/editors.ts index b3a914ffff02..472385c8534d 100644 --- a/test/automation/src/editors.ts +++ b/test/automation/src/editors.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Quality } from './application'; import { Code } from './code'; export class Editors { @@ -53,7 +54,7 @@ export class Editors { } async waitForActiveEditor(fileName: string, retryCount?: number): Promise { - const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] textarea`; + const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`; return this.code.waitForActiveElement(selector, retryCount); } diff --git a/test/automation/src/extensions.ts b/test/automation/src/extensions.ts index 2a481f9fe766..c881e4fd8dc6 100644 --- a/test/automation/src/extensions.ts +++ b/test/automation/src/extensions.ts @@ -8,6 +8,7 @@ import { Code } from './code'; import { ncp } from 'ncp'; import { promisify } from 'util'; import { Commands } from './workbench'; +import { Quality } from './application'; import path = require('path'); import fs = require('fs'); @@ -20,7 +21,7 @@ export class Extensions extends Viewlet { async searchForExtension(id: string): Promise { await this.commands.runCommand('Extensions: Focus on Extensions View', { exactLabelMatch: true }); - await this.code.waitForTypeInEditor('div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor textarea', `@id:${id}`); + await this.code.waitForTypeInEditor(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`, `@id:${id}`); await this.code.waitForTextContent(`div.part.sidebar div.composite.title h2`, 'Extensions: Marketplace'); let retrials = 1; diff --git a/test/automation/src/notebook.ts b/test/automation/src/notebook.ts index dff250027db7..cd46cbdb0dd4 100644 --- a/test/automation/src/notebook.ts +++ b/test/automation/src/notebook.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Quality } from './application'; import { Code } from './code'; import { QuickAccess } from './quickaccess'; import { QuickInput } from './quickinput'; @@ -46,10 +47,10 @@ export class Notebook { await this.code.waitForElement(editor); - const textarea = `${editor} textarea`; - await this.code.waitForActiveElement(textarea); + const editContext = `${editor} ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`; + await this.code.waitForActiveElement(editContext); - await this.code.waitForTypeInEditor(textarea, text); + await this.code.waitForTypeInEditor(editContext, text); await this._waitForActiveCellEditorContents(c => c.indexOf(text) > -1); } diff --git a/test/automation/src/scm.ts b/test/automation/src/scm.ts index 9f950f2b16a7..6489badbe8a4 100644 --- a/test/automation/src/scm.ts +++ b/test/automation/src/scm.ts @@ -6,9 +6,11 @@ import { Viewlet } from './viewlet'; import { IElement } from './driver'; import { findElement, findElements, Code } from './code'; +import { Quality } from './application'; const VIEWLET = 'div[id="workbench.view.scm"]'; -const SCM_INPUT = `${VIEWLET} .scm-editor textarea`; +const SCM_INPUT_NATIVE_EDIT_CONTEXT = `${VIEWLET} .scm-editor .native-edit-context`; +const SCM_INPUT_TEXTAREA = `${VIEWLET} .scm-editor textarea`; const SCM_RESOURCE = `${VIEWLET} .monaco-list-row .resource`; const REFRESH_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Refresh"]`; const COMMIT_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Commit"]`; @@ -44,7 +46,7 @@ export class SCM extends Viewlet { async openSCMViewlet(): Promise { await this.code.dispatchKeybinding('ctrl+shift+g'); - await this.code.waitForElement(SCM_INPUT); + await this.code.waitForElement(this._editContextSelector()); } async waitForChange(name: string, type?: string): Promise { @@ -71,9 +73,13 @@ export class SCM extends Viewlet { } async commit(message: string): Promise { - await this.code.waitAndClick(SCM_INPUT); - await this.code.waitForActiveElement(SCM_INPUT); - await this.code.waitForSetValue(SCM_INPUT, message); + await this.code.waitAndClick(this._editContextSelector()); + await this.code.waitForActiveElement(this._editContextSelector()); + await this.code.waitForSetValue(this._editContextSelector(), message); await this.code.waitAndClick(COMMIT_COMMAND); } + + private _editContextSelector(): string { + return this.code.quality === Quality.Stable ? SCM_INPUT_TEXTAREA : SCM_INPUT_NATIVE_EDIT_CONTEXT; + } } diff --git a/test/automation/src/settings.ts b/test/automation/src/settings.ts index 68401eb0edaa..8cf221b1487b 100644 --- a/test/automation/src/settings.ts +++ b/test/automation/src/settings.ts @@ -7,8 +7,10 @@ import { Editor } from './editor'; import { Editors } from './editors'; import { Code } from './code'; import { QuickAccess } from './quickaccess'; +import { Quality } from './application'; -const SEARCH_BOX = '.settings-editor .suggest-input-container .monaco-editor textarea'; +const SEARCH_BOX_NATIVE_EDIT_CONTEXT = '.settings-editor .suggest-input-container .monaco-editor .native-edit-context'; +const SEARCH_BOX_TEXTAREA = '.settings-editor .suggest-input-container .monaco-editor textarea'; export class SettingsEditor { constructor(private code: Code, private editors: Editors, private editor: Editor, private quickaccess: QuickAccess) { } @@ -57,13 +59,13 @@ export class SettingsEditor { async openUserSettingsUI(): Promise { await this.quickaccess.runCommand('workbench.action.openSettings2'); - await this.code.waitForActiveElement(SEARCH_BOX); + await this.code.waitForActiveElement(this._editContextSelector()); } async searchSettingsUI(query: string): Promise { await this.openUserSettingsUI(); - await this.code.waitAndClick(SEARCH_BOX); + await this.code.waitAndClick(this._editContextSelector()); if (process.platform === 'darwin') { await this.code.dispatchKeybinding('cmd+a'); } else { @@ -71,7 +73,11 @@ export class SettingsEditor { } await this.code.dispatchKeybinding('Delete'); await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => !results || (results?.length === 1 && !results[0].textContent)); - await this.code.waitForTypeInEditor('.settings-editor .suggest-input-container .monaco-editor textarea', query); + await this.code.waitForTypeInEditor(this._editContextSelector(), query); await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => results?.length === 1 && results[0].textContent.includes('Found')); } + + private _editContextSelector() { + return this.code.quality === Quality.Stable ? SEARCH_BOX_TEXTAREA : SEARCH_BOX_NATIVE_EDIT_CONTEXT; + } } From 8c37a6903f3219995e6f763cc3cdc6ab8c775f4a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 7 Jan 2025 10:03:20 +0100 Subject: [PATCH 0373/3587] Preserve user selected language mode when renaming a file (fix #203648) (#237382) --- .../common/editor/textEditorModel.ts | 31 +++++++++--- .../common/textFileEditorModelManager.ts | 47 ++++++++++++++----- 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 482364fda505..53b900ec6ed0 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -75,13 +75,22 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel return true; } - private _hasLanguageSetExplicitly: boolean = false; - get hasLanguageSetExplicitly(): boolean { return this._hasLanguageSetExplicitly; } + private _blockLanguageChangeListener = false; + private _languageChangeSource: 'user' | 'api' | undefined = undefined; + get languageChangeSource() { return this._languageChangeSource; } + get hasLanguageSetExplicitly() { + // This is technically not 100% correct, because 'api' can also be + // set as source if a model is resolved as text first and then + // transitions into the resolved language. But to preserve the current + // behaviour, we do not change this property. Rather, `languageChangeSource` + // can be used to get more fine grained information. + return typeof this._languageChangeSource === 'string'; + } setLanguageId(languageId: string, source?: string): void { // Remember that an explicit language was set - this._hasLanguageSetExplicitly = true; + this._languageChangeSource = 'user'; this.setLanguageIdInternal(languageId, source); } @@ -95,18 +104,26 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel return; } - this.textEditorModel.setLanguage(this.languageService.createById(languageId), source); + this._blockLanguageChangeListener = true; + try { + this.textEditorModel.setLanguage(this.languageService.createById(languageId), source); + } finally { + this._blockLanguageChangeListener = false; + } } protected installModelListeners(model: ITextModel): void { // Setup listener for lower level language changes - const disposable = this._register(model.onDidChangeLanguage((e) => { - if (e.source === LanguageDetectionLanguageEventSource) { + const disposable = this._register(model.onDidChangeLanguage(e => { + if ( + e.source === LanguageDetectionLanguageEventSource || + this._blockLanguageChangeListener + ) { return; } - this._hasLanguageSetExplicitly = true; + this._languageChangeSource = 'api'; disposable.dispose(); })); } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 8be051a58211..e48add78adb7 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -26,6 +26,17 @@ import { PLAINTEXT_EXTENSION, PLAINTEXT_LANGUAGE_ID } from '../../../../editor/c import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; +interface ITextFileEditorModelToRestore { + readonly source: URI; + readonly target: URI; + readonly snapshot?: ITextSnapshot; + readonly language?: { + readonly id: string; + readonly explicit: boolean; + }; + readonly encoding?: string; +} + export class TextFileEditorModelManager extends Disposable implements ITextFileEditorModelManager { private readonly _onDidCreate = this._register(new Emitter({ leakWarningThreshold: 500 /* increased for users with hundreds of inputs opened */ })); @@ -171,13 +182,13 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE } } - private readonly mapCorrelationIdToModelsToRestore = new Map(); + private readonly mapCorrelationIdToModelsToRestore = new Map(); private onWillRunWorkingCopyFileOperation(e: WorkingCopyFileEvent): void { // Move / Copy: remember models to restore after the operation if (e.operation === FileOperation.MOVE || e.operation === FileOperation.COPY) { - const modelsToRestore: { source: URI; target: URI; snapshot?: ITextSnapshot; languageId?: string; encoding?: string }[] = []; + const modelsToRestore: ITextFileEditorModelToRestore[] = []; for (const { source, target } of e.files) { if (source) { @@ -210,10 +221,14 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE targetModelResource = joinPath(target, sourceModelResource.path.substr(source.path.length + 1)); } + const languageId = sourceModel.getLanguageId(); modelsToRestore.push({ source: sourceModelResource, target: targetModelResource, - languageId: sourceModel.getLanguageId(), + language: languageId ? { + id: languageId, + explicit: sourceModel.languageChangeSource === 'user' + } : undefined, encoding: sourceModel.getEncoding(), snapshot: sourceModel.isDirty() ? sourceModel.createSnapshot() : undefined }); @@ -286,16 +301,22 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE encoding: modelToRestore.encoding }); - // restore previous language only if the language is now unspecified and it was specified - // but not when the file was explicitly stored with the plain text extension - // (https://github.com/microsoft/vscode/issues/125795) - if ( - modelToRestore.languageId && - modelToRestore.languageId !== PLAINTEXT_LANGUAGE_ID && - restoredModel.getLanguageId() === PLAINTEXT_LANGUAGE_ID && - extname(target) !== PLAINTEXT_EXTENSION - ) { - restoredModel.updateTextEditorModel(undefined, modelToRestore.languageId); + // restore model language only if it is specific + if (modelToRestore.language?.id && modelToRestore.language.id !== PLAINTEXT_LANGUAGE_ID) { + + // an explicitly set language is restored via `setLanguageId` + // to preserve it as explicitly set by the user. + // (https://github.com/microsoft/vscode/issues/203648) + if (modelToRestore.language.explicit) { + restoredModel.setLanguageId(modelToRestore.language.id); + } + + // otherwise, a model language is applied via lower level + // APIs to not confuse it with an explicitly set language. + // (https://github.com/microsoft/vscode/issues/125795) + else if (restoredModel.getLanguageId() === PLAINTEXT_LANGUAGE_ID && extname(target) !== PLAINTEXT_EXTENSION) { + restoredModel.updateTextEditorModel(undefined, modelToRestore.language.id); + } } })); } From 12c1d4fb1753aeda4b55de73b8a8ee58c607d780 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 7 Jan 2025 10:08:23 +0100 Subject: [PATCH 0374/3587] milestone update (#237383) --- .vscode/notebooks/api.github-issues | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index 0423e9e3afc8..d29f2bc441d4 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.vscode/notebooks/api.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"November 2024\"" + "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"January 2025\"" }, { "kind": 1, From bfb4f5fc1c0add3766e73dfdf676c2e193a7ca7a Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 7 Jan 2025 12:18:17 +0100 Subject: [PATCH 0375/3587] Git - Revert "Git - escape shell-sensitive characters (#236849)" (#237388) Revert "Git - escape shell-sensitive characters (#236849)" This reverts commit fca210cd103a496f25c23786b861a67f4d1ee16b. --- extensions/git/src/git.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 0ca691fda643..dcb3b4eb65f6 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -349,15 +349,10 @@ function getGitErrorCode(stderr: string): string | undefined { return undefined; } +// https://github.com/microsoft/vscode/issues/89373 +// https://github.com/git-for-windows/git/issues/2478 function sanitizePath(path: string): string { - return path - // Drive letter - // https://github.com/microsoft/vscode/issues/89373 - // https://github.com/git-for-windows/git/issues/2478 - .replace(/^([a-z]):\\/i, (_, letter) => `${letter.toUpperCase()}:\\`) - // Shell-sensitive characters - // https://github.com/microsoft/vscode/issues/133566 - .replace(/(["'\\\$!><#()\[\]*&^| ;{}?`])/g, '\\$1'); + return path.replace(/^([a-z]):\\/i, (_, letter) => `${letter.toUpperCase()}:\\`); } const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%D%n%B'; From eab8316ec8bae70e336e9daaf7b1dea056abfc19 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 7 Jan 2025 12:48:35 +0100 Subject: [PATCH 0376/3587] Git - switch back to using options instead of subcomands (#237390) --- extensions/git/src/git.ts | 2 +- extensions/git/src/repository.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index dcb3b4eb65f6..327903aa9592 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1168,7 +1168,7 @@ export class Repository { } async config(command: string, scope: string, key: string, value: any = null, options: SpawnOptions = {}): Promise { - const args = ['config', command]; + const args = ['config', `--${command}`]; if (scope) { args.push(`--${scope}`); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index e1b71d87f0ad..28f9b70b3b37 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1088,7 +1088,7 @@ export class Repository implements Disposable { } setConfig(key: string, value: string): Promise { - return this.run(Operation.Config(false), () => this.repository.config('set', 'local', key, value)); + return this.run(Operation.Config(false), () => this.repository.config('add', 'local', key, value)); } log(options?: LogOptions & { silent?: boolean }): Promise { From 4fc21e1068e583a41efeb187e9e399a078edca3a Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 7 Jan 2025 12:59:05 +0100 Subject: [PATCH 0377/3587] chore - fix leaking disposable (#237391) --- .../chat/browser/chatContentParts/chatMarkdownContentPart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index aad084c2bb28..44c0c0dbb6b4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -287,7 +287,7 @@ class CollapsedCodeBlock extends Disposable { return this._uri; } - private readonly _progressStore = new DisposableStore(); + private readonly _progressStore = this._store.add(new DisposableStore()); constructor( sessionId: string, From 9289e2be508d232a6865cbe0eb7b666c05187d9a Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 7 Jan 2025 13:54:55 +0100 Subject: [PATCH 0378/3587] chat overlay tweaks (#237395) * don't disable accept/reject when having no changes (it's confusing) * add drop shadow --- src/vs/workbench/contrib/chat/browser/chatEditorActions.ts | 2 +- .../workbench/contrib/chat/browser/media/chatEditorOverlay.css | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts index a8060733fff3..731a921c0cad 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts @@ -126,7 +126,7 @@ abstract class AcceptDiscardAction extends Action2 { ? localize2('accept2', 'Accept') : localize2('discard2', 'Discard'), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(ctxHasRequestInProgress.negate(), hasUndecidedChatEditingResourceContextKey, ContextKeyExpr.or(ctxHasEditorModification, ctxNotebookHasEditorModification)), + precondition: ContextKeyExpr.and(ctxHasRequestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), icon: accept ? Codicon.check : Codicon.discard, diff --git a/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css index 26ece80c64ca..a52e4ba5979c 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css @@ -12,6 +12,7 @@ display: flex; align-items: center; z-index: 10; + box-shadow: 0 2px 8px var(--vscode-widget-shadow); } .chat-editor-overlay-widget .chat-editor-overlay-progress { From 2c9f25bcaa5043ee385db0695b608ff3728df010 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 7 Jan 2025 13:55:59 +0100 Subject: [PATCH 0379/3587] Setting `restoreWindows: none` still restores empty windows with backups (fix #237212) (#237396) --- .../electron-main/windowsMainService.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 8db5f04b7e3e..9999cd867769 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -160,6 +160,8 @@ interface IPathToOpen extends IPath { label?: string; } +const EMPTY_WINDOW: IPathToOpen = Object.create(null); + interface IWorkspacePathToOpen extends IPathToOpen { readonly workspace: IWorkspaceIdentifier; } @@ -304,7 +306,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const emptyWindowsWithBackupsToRestore: IEmptyWindowBackupInfo[] = []; let filesToOpen: IFilesToOpen | undefined; - let openOneEmptyWindow = false; + let maybeOpenEmptyWindow = false; // Identify things to open from open config const pathsToOpen = await this.getPathsToOpen(openConfig); @@ -332,7 +334,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } else if (path.backupPath) { emptyWindowsWithBackupsToRestore.push({ backupFolder: basename(path.backupPath), remoteAuthority: path.remoteAuthority }); } else { - openOneEmptyWindow = true; + maybeOpenEmptyWindow = true; // depends on other parameters such as `forceEmpty` and how many windows have opened already } } @@ -368,9 +370,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // Open based on config - const { windows: usedWindows, filesOpenedInWindow } = await this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyWindowsWithBackupsToRestore, openOneEmptyWindow, filesToOpen, foldersToAdd, foldersToRemove); + const { windows: usedWindows, filesOpenedInWindow } = await this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyWindowsWithBackupsToRestore, maybeOpenEmptyWindow, filesToOpen, foldersToAdd, foldersToRemove); - this.logService.trace(`windowsManager#open used window count ${usedWindows.length} (workspacesToOpen: ${workspacesToOpen.length}, foldersToOpen: ${foldersToOpen.length}, emptyToRestore: ${emptyWindowsWithBackupsToRestore.length}, openOneEmptyWindow: ${openOneEmptyWindow})`); + this.logService.trace(`windowsManager#open used window count ${usedWindows.length} (workspacesToOpen: ${workspacesToOpen.length}, foldersToOpen: ${foldersToOpen.length}, emptyToRestore: ${emptyWindowsWithBackupsToRestore.length}, maybeOpenEmptyWindow: ${maybeOpenEmptyWindow})`); // Make sure to pass focus to the most relevant of the windows if we open multiple if (usedWindows.length > 1) { @@ -469,7 +471,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic workspacesToOpen: IWorkspacePathToOpen[], foldersToOpen: ISingleFolderWorkspacePathToOpen[], emptyToRestore: IEmptyWindowBackupInfo[], - openOneEmptyWindow: boolean, + maybeOpenEmptyWindow: boolean, filesToOpen: IFilesToOpen | undefined, foldersToAdd: ISingleFolderWorkspacePathToOpen[], foldersToRemove: ISingleFolderWorkspacePathToOpen[] @@ -639,8 +641,11 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } } - // Open empty window either if enforced or when files still have to open - if (filesToOpen || openOneEmptyWindow) { + // Finally, open an empty window if + // - we still have files to open + // - user forces an empty window (e.g. via command line) + // - no window has opened yet + if (filesToOpen || (maybeOpenEmptyWindow && (openConfig.forceEmpty || usedWindows.length === 0))) { const remoteAuthority = filesToOpen ? filesToOpen.remoteAuthority : openConfig.remoteAuthority; addUsedWindow(await this.doOpenEmpty(openConfig, openFolderInNewWindow, remoteAuthority, filesToOpen), !!filesToOpen); @@ -749,14 +754,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Check for force empty else if (openConfig.forceEmpty) { - pathsToOpen = [Object.create(null)]; + pathsToOpen = [EMPTY_WINDOW]; } // Extract paths: from CLI else if (openConfig.cli._.length || openConfig.cli['folder-uri'] || openConfig.cli['file-uri']) { pathsToOpen = await this.doExtractPathsFromCLI(openConfig.cli); if (pathsToOpen.length === 0) { - pathsToOpen.push(Object.create(null)); // add an empty window if we did not have windows to open from command line + pathsToOpen.push(EMPTY_WINDOW); // add an empty window if we did not have windows to open from command line } isCommandLineOrAPICall = true; @@ -766,7 +771,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic else { pathsToOpen = await this.doGetPathsFromLastSession(); if (pathsToOpen.length === 0) { - pathsToOpen.push(Object.create(null)); // add an empty window if we did not have windows to restore + pathsToOpen.push(EMPTY_WINDOW); // add an empty window if we did not have windows to restore } isRestoringPaths = true; From e258958d355a68944fb4abea9172dfcee3e807db Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 7 Jan 2025 14:46:07 +0100 Subject: [PATCH 0380/3587] Git - expose unsetConfig through the extension API (#237393) --- extensions/git/src/api/api1.ts | 4 ++++ extensions/git/src/api/git.d.ts | 1 + extensions/git/src/repository.ts | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index e559c0cb8073..4090efa9551f 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -112,6 +112,10 @@ export class ApiRepository implements Repository { return this.#repository.setConfig(key, value); } + unsetConfig(key: string): Promise { + return this.#repository.unsetConfig(key); + } + getGlobalConfig(key: string): Promise { return this.#repository.getGlobalConfig(key); } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 851d5734977d..ea78ac4d99a7 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -204,6 +204,7 @@ export interface Repository { getConfigs(): Promise<{ key: string; value: string; }[]>; getConfig(key: string): Promise; setConfig(key: string, value: string): Promise; + unsetConfig(key: string): Promise; getGlobalConfig(key: string): Promise; getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }>; diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 28f9b70b3b37..2a722502b75c 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1091,6 +1091,10 @@ export class Repository implements Disposable { return this.run(Operation.Config(false), () => this.repository.config('add', 'local', key, value)); } + unsetConfig(key: string): Promise { + return this.run(Operation.Config(false), () => this.repository.config('unset', 'local', key)); + } + log(options?: LogOptions & { silent?: boolean }): Promise { const showProgress = !options || options.silent !== true; return this.run(Operation.Log(showProgress), () => this.repository.log(options)); From b26ee646a2e5140bdebfd2c8dcb8c59dc0f29d1b Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:22:36 +0100 Subject: [PATCH 0381/3587] Support web links as context (#237401) support web links as context --- src/vs/editor/contrib/links/browser/links.ts | 4 ++++ .../chatAttachmentsContentPart.ts | 7 +++++- .../contrib/chat/browser/chatInputPart.ts | 24 ++++++++++++++++++- .../contrib/chat/common/chatModel.ts | 12 +++++++++- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/links/browser/links.ts b/src/vs/editor/contrib/links/browser/links.ts index ef1b67addd1b..a07122f53b42 100644 --- a/src/vs/editor/contrib/links/browser/links.ts +++ b/src/vs/editor/contrib/links/browser/links.ts @@ -287,6 +287,10 @@ export class LinkDetector extends Disposable implements IEditorContribution { return null; } + public getAllLinkOccurrences(): LinkOccurrence[] { + return Object.values(this.currentOccurrences); + } + private isEnabled(mouseEvent: ClickLinkMouseEvent, withKey?: ClickLinkKeyboardEvent | null): boolean { return Boolean( (mouseEvent.target.type === MouseTargetType.CONTENT_TEXT) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index 9e745f6554f7..6bd5ad400231 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -7,6 +7,7 @@ import * as dom from '../../../../../base/browser/dom.js'; import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; import { IManagedHoverTooltipMarkdownString } from '../../../../../base/browser/ui/hover/hover.js'; import { createInstantHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; import { basename, dirname } from '../../../../../base/common/path.js'; @@ -38,7 +39,7 @@ import { fillEditorsDragData } from '../../../../browser/dnd.js'; import { ResourceLabels } from '../../../../browser/labels.js'; import { ResourceContextKey } from '../../../../common/contextkeys.js'; import { revealInSideBarCommand } from '../../../files/browser/fileActions.contribution.js'; -import { IChatRequestVariableEntry, isPasteVariableEntry } from '../../common/chatModel.js'; +import { IChatRequestVariableEntry, isLinkVariableEntry, isPasteVariableEntry } from '../../common/chatModel.js'; import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../common/chatService.js'; export const chatAttachmentResourceContextKey = new RawContextKey('chatAttachmentResource', undefined, { type: 'URI', description: localize('resource', "The full value of the chat attachment resource, including scheme and path") }); @@ -185,6 +186,10 @@ export class ChatAttachmentsContentPart extends Disposable { this.attachedContextDisposables.add(this.instantiationService.invokeFunction(accessor => hookUpResourceAttachmentDragAndContextMenu(accessor, widget, resource))); } } + } else if (isLinkVariableEntry(attachment)) { + ariaLabel = localize('chat.attachment.link', "Attached link, {0}", attachment.name); + + label.setResource({ resource: attachment.value, name: attachment.name }, { icon: Codicon.link, title: attachment.value.toString() }); } else { const attachmentLabel = attachment.fullName ?? attachment.name; const withIcon = attachment.icon?.id ? `$(${attachment.icon.id}) ${attachmentLabel}` : attachmentLabel; diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 39bb44149b6e..cc9ebf1b7baf 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -46,6 +46,7 @@ import { CopyPasteController } from '../../../../editor/contrib/dropOrPasteInto/ import { DropIntoEditorController } from '../../../../editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.js'; import { ContentHoverController } from '../../../../editor/contrib/hover/browser/contentHoverController.js'; import { GlyphHoverController } from '../../../../editor/contrib/hover/browser/glyphHoverController.js'; +import { LinkDetector } from '../../../../editor/contrib/links/browser/links.js'; import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js'; import { localize } from '../../../../nls.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; @@ -158,6 +159,27 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge contextArr.push(this.implicitContext.toBaseEntry()); } + // retrieve links from the input editor + const linkOccurrences = this.inputEditor.getContribution(LinkDetector.ID)?.getAllLinkOccurrences() ?? []; + const linksSeen = new Set(); + for (const linkOccurrence of linkOccurrences) { + const link = linkOccurrence.link; + const uri = URI.isUri(link.url) ? link.url : link.url ? URI.parse(link.url) : undefined; + if (!uri || linksSeen.has(uri.toString())) { + continue; + } + + linksSeen.add(uri.toString()); + contextArr.push({ + kind: 'link', + id: uri.toString(), + name: uri.fsPath, + value: uri, + isFile: false, + isDynamic: true, + }); + } + // factor in nested file references into the implicit context const variables = this.variableService.getDynamicVariables(sessionId); for (const variable of variables) { @@ -649,7 +671,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._inputEditorElement = dom.append(editorContainer!, $(chatInputEditorContainerSelector)); const editorOptions = getSimpleCodeEditorWidgetOptions(); - editorOptions.contributions?.push(...EditorExtensionsRegistry.getSomeEditorContributions([ContentHoverController.ID, GlyphHoverController.ID, CopyPasteController.ID])); + editorOptions.contributions?.push(...EditorExtensionsRegistry.getSomeEditorContributions([ContentHoverController.ID, GlyphHoverController.ID, CopyPasteController.ID, LinkDetector.ID])); this._inputEditor = this._register(scopedInstantiationService.createInstance(CodeEditorWidget, this._inputEditorElement, options, editorOptions)); SuggestController.get(this._inputEditor)?.forceRenderingAbove(); diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index e8c17f2848f1..8849cd0314ca 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -85,7 +85,13 @@ export interface ICommandResultVariableEntry extends Omit { + readonly kind: 'link'; + readonly isDynamic: true; + readonly value: URI; +} + +export type IChatRequestVariableEntry = IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry | ISymbolVariableEntry | ICommandResultVariableEntry | ILinkVariableEntry | IBaseChatRequestVariableEntry; export function isImplicitVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestImplicitVariableEntry { return obj.kind === 'implicit'; @@ -95,6 +101,10 @@ export function isPasteVariableEntry(obj: IChatRequestVariableEntry): obj is ICh return obj.kind === 'paste'; } +export function isLinkVariableEntry(obj: IChatRequestVariableEntry): obj is ILinkVariableEntry { + return obj.kind === 'link'; +} + export function isChatRequestVariableEntry(obj: unknown): obj is IChatRequestVariableEntry { const entry = obj as IChatRequestVariableEntry; return typeof entry === 'object' && From 7b7c63c8d5834e12ab1ed820edba6521880d4bd9 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 7 Jan 2025 15:34:50 +0100 Subject: [PATCH 0382/3587] retain the extensions scanning order since they are cached (#237402) --- .../common/extensionsScannerService.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 0800126329da..c613e0669b4e 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -406,7 +406,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem private async scanDefaultSystemExtensions(language: string | undefined): Promise { this.logService.trace('Started scanning system extensions'); const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, language, true, undefined, this.getProductVersion()); - const extensionsScanner = !extensionsScannerInput.devMode ? this.systemExtensionsCachedScanner : this.extensionsScanner; + const extensionsScanner = extensionsScannerInput.devMode ? this.extensionsScanner : this.systemExtensionsCachedScanner; const result = await extensionsScanner.scanExtensions(extensionsScannerInput); this.logService.trace('Scanned system extensions:', result.length); return result; @@ -609,16 +609,15 @@ class ExtensionsScanner extends Disposable { if (!scannedProfileExtensions.length) { return []; } - const extensions: IRelaxedScannedExtension[] = []; - await Promise.all(scannedProfileExtensions.map(async extensionInfo => { - if (!filter(extensionInfo)) { - return; - } - const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); - const extension = await this.scanExtension(extensionScannerInput, extensionInfo); - extensions.push(extension); - })); - return extensions; + const extensions = await Promise.all( + scannedProfileExtensions.map(async extensionInfo => { + if (filter(extensionInfo)) { + const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); + return this.scanExtension(extensionScannerInput, extensionInfo); + } + return null; + })); + return coalesce(extensions); } async scanOneOrMultipleExtensions(input: ExtensionScannerInput): Promise { From 3aaf01d58c0edc747087dfe599c2e4ecd961f1bb Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 7 Jan 2025 16:06:16 +0100 Subject: [PATCH 0383/3587] Tree sitter improvements (#237392) * Tree sitter improvements * Fix test --- .../common/model/tokenizationTextModelPart.ts | 6 +-- .../editor/common/model/treeSitterTokens.ts | 6 +-- .../inspectEditorTokens.ts | 2 +- .../browser/treeSitterTokenizationFeature.ts | 52 +++++++++++++------ 4 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/vs/editor/common/model/tokenizationTextModelPart.ts b/src/vs/editor/common/model/tokenizationTextModelPart.ts index f51bf1b98bcb..704c128b9928 100644 --- a/src/vs/editor/common/model/tokenizationTextModelPart.ts +++ b/src/vs/editor/common/model/tokenizationTextModelPart.ts @@ -23,7 +23,6 @@ import { TextModelPart } from './textModelPart.js'; import { DefaultBackgroundTokenizer, TokenizerWithStateStoreAndTextModel, TrackingTokenizationStateStore } from './textModelTokens.js'; import { AbstractTokens, AttachedViewHandler, AttachedViews } from './tokens.js'; import { TreeSitterTokens } from './treeSitterTokens.js'; -import { ITreeSitterParserService } from '../services/treeSitterParserService.js'; import { IModelContentChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelTokensChangedEvent } from '../textModelEvents.js'; import { BackgroundTokenizationState, ITokenizationTextModelPart } from '../tokenizationTextModelPart.js'; import { ContiguousMultilineTokens } from '../tokens/contiguousMultilineTokens.js'; @@ -32,6 +31,7 @@ import { ContiguousTokensStore } from '../tokens/contiguousTokensStore.js'; import { LineTokens } from '../tokens/lineTokens.js'; import { SparseMultilineTokens } from '../tokens/sparseMultilineTokens.js'; import { SparseTokensStore } from '../tokens/sparseTokensStore.js'; +import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; export class TokenizationTextModelPart extends TextModelPart implements ITokenizationTextModelPart { private readonly _semanticTokens: SparseTokensStore = new SparseTokensStore(this._languageService.languageIdCodec); @@ -55,7 +55,7 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz private readonly _attachedViews: AttachedViews, @ILanguageService private readonly _languageService: ILanguageService, @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, - @ITreeSitterParserService private readonly _treeSitterService: ITreeSitterParserService, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); @@ -73,7 +73,7 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz } private createTreeSitterTokens(): AbstractTokens { - return this._register(new TreeSitterTokens(this._treeSitterService, this._languageService.languageIdCodec, this._textModel, () => this._languageId)); + return this._register(this._instantiationService.createInstance(TreeSitterTokens, this._languageService.languageIdCodec, this._textModel, () => this._languageId)); } private createTokens(useTreeSitter: boolean): void { diff --git a/src/vs/editor/common/model/treeSitterTokens.ts b/src/vs/editor/common/model/treeSitterTokens.ts index f4077388ef08..7f8f91bb2762 100644 --- a/src/vs/editor/common/model/treeSitterTokens.ts +++ b/src/vs/editor/common/model/treeSitterTokens.ts @@ -17,10 +17,10 @@ export class TreeSitterTokens extends AbstractTokens { private _lastLanguageId: string | undefined; private readonly _tokensChangedListener: MutableDisposable = this._register(new MutableDisposable()); - constructor(private readonly _treeSitterService: ITreeSitterParserService, - languageIdCodec: ILanguageIdCodec, + constructor(languageIdCodec: ILanguageIdCodec, textModel: TextModel, - languageId: () => string) { + languageId: () => string, + @ITreeSitterParserService private readonly _treeSitterService: ITreeSitterParserService) { super(languageIdCodec, textModel, languageId); this._initialize(); diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index 2b0f345b3c58..4da76c311442 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -405,7 +405,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { const tbody = dom.append(table, $('tbody')); dom.append(tbody, $('tr', undefined, - $('td.tiw-metadata-key', undefined, 'tree-sitter token' as string), + $('td.tiw-metadata-key', undefined, `tree-sitter token ${treeSitterTokenInfo.id}` as string), $('td.tiw-metadata-value', undefined, `${treeSitterTokenInfo.text}`) )); const scopes = new Array(); diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts index 0fa967c6b26f..24c4a1698c5c 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts @@ -140,12 +140,12 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok captureAtPosition(lineNumber: number, column: number, textModel: ITextModel): Parser.QueryCapture[] { const tree = this._getTree(textModel); - const captures = this._captureAtRange(lineNumber, new ColumnRange(column, column), tree?.tree); + const captures = this._captureAtRange(lineNumber, new ColumnRange(column, column + 1), tree?.tree); return captures; } captureAtPositionTree(lineNumber: number, column: number, tree: Parser.Tree): Parser.QueryCapture[] { - const captures = this._captureAtRange(lineNumber, new ColumnRange(column, column), tree); + const captures = this._captureAtRange(lineNumber, new ColumnRange(column, column + 1), tree); return captures; } @@ -156,7 +156,7 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok return []; } // Tree sitter row is 0 based, column is 0 based - return query.captures(tree.rootNode, { startPosition: { row: lineNumber - 1, column: columnRange.startColumn - 1 }, endPosition: { row: lineNumber - 1, column: columnRange.endColumnExclusive } }); + return query.captures(tree.rootNode, { startPosition: { row: lineNumber - 1, column: columnRange.startColumn - 1 }, endPosition: { row: lineNumber - 1, column: columnRange.endColumnExclusive - 1 } }); } /** @@ -179,8 +179,16 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok const lineLength = textModel.getLineMaxColumn(lineNumber); const tree = this._getTree(textModel); const captures = this._captureAtRange(lineNumber, new ColumnRange(1, lineLength), tree?.tree); + const encodedLanguageId = this._languageIdCodec.encodeLanguageId(this._languageId); if (captures.length === 0) { + if (tree) { + stopwatch.stop(); + const result = new Uint32Array(2); + result[0] = lineLength; + result[1] = findMetadata(this._colorThemeData, [], encodedLanguageId); + return { result, captureTime: stopwatch.elapsed(), metadataTime: 0 }; + } return undefined; } @@ -193,7 +201,6 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok endOffsetsAndScopes.push({ endOffset: 0, scopes: [] }); }; - const encodedLanguageId = this._languageIdCodec.encodeLanguageId(this._languageId); for (let captureIndex = 0; captureIndex < captures.length; captureIndex++) { const capture = captures[captureIndex]; @@ -225,23 +232,36 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok }; if (previousTokenEnd >= lineRelativeOffset) { - const previousTokenStartOffset = ((tokenIndex >= 2) ? endOffsetsAndScopes[tokenIndex - 2].endOffset : 0); const originalPreviousTokenEndOffset = endOffsetsAndScopes[tokenIndex - 1].endOffset; + const previousTokenStartOffset = ((tokenIndex >= 2) ? endOffsetsAndScopes[tokenIndex - 2].endOffset : 0); + const loopOriginalPreviousTokenEndOffset = endOffsetsAndScopes[tokenIndex - 1].endOffset; + const previousPreviousTokenEndOffset = (tokenIndex >= 2) ? endOffsetsAndScopes[tokenIndex - 2].endOffset : 0; + // Check that the current token doesn't just replace the last token - if ((previousTokenStartOffset + currentTokenLength) === originalPreviousTokenEndOffset) { + if ((previousTokenStartOffset + currentTokenLength) === loopOriginalPreviousTokenEndOffset) { // Current token and previous token span the exact same characters, replace the last scope endOffsetsAndScopes[tokenIndex - 1].scopes[endOffsetsAndScopes[tokenIndex - 1].scopes.length - 1] = capture.name; - } else { - // The current token is within the previous token. Adjust the end of the previous token. - endOffsetsAndScopes[tokenIndex - 1].endOffset = intermediateTokenOffset; + } else if (previousPreviousTokenEndOffset <= intermediateTokenOffset) { + let originalPreviousTokenScopes; + // The current token is within the previous token. Adjust the end of the previous token + if (previousPreviousTokenEndOffset !== intermediateTokenOffset) { + endOffsetsAndScopes[tokenIndex - 1] = { endOffset: intermediateTokenOffset, scopes: endOffsetsAndScopes[tokenIndex - 1].scopes }; + addCurrentTokenToArray(); + originalPreviousTokenScopes = endOffsetsAndScopes[tokenIndex - 2].scopes; + } else { + originalPreviousTokenScopes = endOffsetsAndScopes[tokenIndex - 1].scopes; + endOffsetsAndScopes[tokenIndex - 1] = { endOffset: lineRelativeOffset, scopes: [capture.name] }; + } - addCurrentTokenToArray(); // Add the rest of the previous token after the current token - increaseSizeOfTokensByOneToken(); - endOffsetsAndScopes[tokenIndex].endOffset = originalPreviousTokenEndOffset; - endOffsetsAndScopes[tokenIndex].scopes = endOffsetsAndScopes[tokenIndex - 2].scopes; - tokenIndex++; + if (originalPreviousTokenEndOffset !== lineRelativeOffset) { + increaseSizeOfTokensByOneToken(); + endOffsetsAndScopes[tokenIndex] = { endOffset: originalPreviousTokenEndOffset, scopes: originalPreviousTokenScopes }; + tokenIndex++; + } else { + endOffsetsAndScopes[tokenIndex - 1].scopes.unshift(...originalPreviousTokenScopes); + } } } else { // Just add the token to the array @@ -250,9 +270,9 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok } // Account for uncaptured characters at the end of the line - if (captures[captures.length - 1].node.endPosition.column + 1 < lineLength) { + if (endOffsetsAndScopes[tokenIndex - 1].endOffset < lineLength - 1) { increaseSizeOfTokensByOneToken(); - endOffsetsAndScopes[tokenIndex].endOffset = lineLength - 1; + endOffsetsAndScopes[tokenIndex] = { endOffset: lineLength - 1, scopes: endOffsetsAndScopes[tokenIndex].scopes }; tokenIndex++; } const captureTime = stopwatch.elapsed(); From c7bd5a698ae5c13b99e5728afdcf986d70ee65b5 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 7 Jan 2025 16:18:14 +0100 Subject: [PATCH 0384/3587] support malicious extensions by publisher id (#237403) --- .../common/abstractExtensionManagementService.ts | 4 ++-- .../common/extensionGalleryService.ts | 10 +++++++--- .../common/extensionManagement.ts | 2 +- .../common/extensionManagementUtil.ts | 12 +++++++++++- .../contrib/extensions/browser/extensionsViewlet.ts | 5 ++--- .../extensions/browser/extensionsWorkbenchService.ts | 4 ++-- .../browser/webExtensionsScannerService.ts | 4 ++-- 7 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 6b854b0bd87e..4bef58907b79 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -24,7 +24,7 @@ import { ExtensionSignatureVerificationCode, IAllowedExtensionsService } from './extensionManagement.js'; -import { areSameExtensions, ExtensionKey, getGalleryExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from './extensionManagementUtil.js'; +import { areSameExtensions, ExtensionKey, getGalleryExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, isMalicious } from './extensionManagementUtil.js'; import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from '../../extensions/common/extensions.js'; import { areApiProposalsCompatible } from '../../extensions/common/extensionValidator.js'; import { ILogService } from '../../log/common/log.js'; @@ -639,7 +639,7 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio let compatibleExtension: IGalleryExtension | null; const extensionsControlManifest = await this.getExtensionsControlManifest(); - if (extensionsControlManifest.malicious.some(identifier => areSameExtensions(extension.identifier, identifier))) { + if (isMalicious(extension.identifier, extensionsControlManifest)) { throw new ExtensionManagementError(nls.localize('malicious extension', "Can't install '{0}' extension since it was reported to be problematic.", extension.identifier.id), ExtensionManagementErrorCode.Malicious); } diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 9a9157545cee..4a8c3b3222b7 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -15,7 +15,7 @@ import { URI } from '../../../base/common/uri.js'; import { IHeaders, IRequestContext, IRequestOptions, isOfflineError } from '../../../base/parts/request/common/request.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; -import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion, UseUnpkgResourceApiConfigKey, IAllowedExtensionsService } from './extensionManagement.js'; +import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion, UseUnpkgResourceApiConfigKey, IAllowedExtensionsService, EXTENSION_IDENTIFIER_REGEX } from './extensionManagement.js'; import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from './extensionManagementUtil.js'; import { IExtensionManifest, TargetPlatform } from '../../extensions/common/extensions.js'; import { areApiProposalsCompatible, isEngineValid } from '../../extensions/common/extensionValidator.js'; @@ -1527,13 +1527,17 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } const result = await asJson(context); - const malicious: IExtensionIdentifier[] = []; + const malicious: Array = []; const deprecated: IStringDictionary = {}; const search: ISearchPrefferedResults[] = []; const extensionsEnabledWithPreRelease: string[] = []; if (result) { for (const id of result.malicious) { - malicious.push({ id }); + if (EXTENSION_IDENTIFIER_REGEX.test(id)) { + malicious.push({ id }); + } else { + malicious.push(id); + } } if (result.migrateToPreRelease) { for (const [unsupportedPreReleaseExtensionId, preReleaseExtensionInfo] of Object.entries(result.migrateToPreRelease)) { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index f08b46ae65b3..dd719f089e99 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -328,7 +328,7 @@ export interface ISearchPrefferedResults { } export interface IExtensionsControlManifest { - readonly malicious: IExtensionIdentifier[]; + readonly malicious: ReadonlyArray; readonly deprecated: IStringDictionary; readonly search: ISearchPrefferedResults[]; readonly extensionsEnabledWithPreRelease?: string[]; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index 39d008ba8be8..7a2cd54e8654 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { compareIgnoreCase } from '../../../base/common/strings.js'; -import { IExtensionIdentifier, IGalleryExtension, ILocalExtension, getTargetPlatform } from './extensionManagement.js'; +import { IExtensionIdentifier, IExtensionsControlManifest, IGalleryExtension, ILocalExtension, getTargetPlatform } from './extensionManagement.js'; import { ExtensionIdentifier, IExtension, TargetPlatform, UNDEFINED_PUBLISHER } from '../../extensions/common/extensions.js'; import { IFileService } from '../../files/common/files.js'; import { isLinux, platform } from '../../../base/common/platform.js'; @@ -13,6 +13,7 @@ import { getErrorMessage } from '../../../base/common/errors.js'; import { ILogService } from '../../log/common/log.js'; import { arch } from '../../../base/common/process.js'; import { TelemetryTrustedValue } from '../../telemetry/common/telemetryUtils.js'; +import { isString } from '../../../base/common/types.js'; export function areSameExtensions(a: IExtensionIdentifier, b: IExtensionIdentifier): boolean { if (a.uuid && b.uuid) { @@ -196,3 +197,12 @@ export async function computeTargetPlatform(fileService: IFileService, logServic logService.debug('ComputeTargetPlatform:', targetPlatform); return targetPlatform; } + +export function isMalicious(identifier: IExtensionIdentifier, controlManifest: IExtensionsControlManifest): boolean { + return controlManifest.malicious.some(publisherOrIdentifier => { + if (isString(publisherOrIdentifier)) { + return compareIgnoreCase(identifier.id.split('.')[0], publisherOrIdentifier) === 0; + } + return areSameExtensions(identifier, publisherOrIdentifier); + }); +} diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 7ae9f77168d0..ee75bba1ed6f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -59,7 +59,7 @@ import { IPaneCompositePartService } from '../../../services/panecomposite/brows import { coalesce } from '../../../../base/common/arrays.js'; import { extractEditorsAndFilesDropData } from '../../../../platform/dnd/browser/dnd.js'; import { extname } from '../../../../base/common/resources.js'; -import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; +import { isMalicious } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; import { ILocalizedString } from '../../../../platform/action/common/action.js'; import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js'; import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; @@ -992,8 +992,7 @@ export class MaliciousExtensionChecker implements IWorkbenchContribution { return this.extensionsManagementService.getExtensionsControlManifest().then(extensionsControlManifest => { return this.extensionsManagementService.getInstalled(ExtensionType.User).then(installed => { - const maliciousExtensions = installed - .filter(e => extensionsControlManifest.malicious.some(identifier => areSameExtensions(e.identifier, identifier))); + const maliciousExtensions = installed.filter(e => isMalicious(e.identifier, extensionsControlManifest)); if (maliciousExtensions.length) { return Promises.settled(maliciousExtensions.map(e => this.extensionsManagementService.uninstall(e).then(() => { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 33692adf1485..56e0f1638ca7 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -23,7 +23,7 @@ import { AllowedExtensionsConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath, IResourceExtension } from '../../../services/extensionManagement/common/extensionManagement.js'; -import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; +import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId, isMalicious } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IHostService } from '../../../services/host/browser/host.js'; @@ -550,7 +550,7 @@ ${this.description} } setExtensionsControlManifest(extensionsControlManifest: IExtensionsControlManifest): void { - this.isMalicious = extensionsControlManifest.malicious.some(identifier => areSameExtensions(this.identifier, identifier)); + this.isMalicious = isMalicious(this.identifier, extensionsControlManifest); this.deprecationInfo = extensionsControlManifest.deprecated ? extensionsControlManifest.deprecated[this.identifier.id.toLowerCase()] : undefined; this._extensionEnabledWithPreRelease = extensionsControlManifest?.extensionsEnabledWithPreRelease?.includes(this.identifier.id.toLowerCase()); } diff --git a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index 529aa4e6dbdd..976d3aa1c14d 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -16,7 +16,7 @@ import { VSBuffer } from '../../../../base/common/buffer.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IExtensionGalleryService, IExtensionInfo, IGalleryExtension, IGalleryMetadata, Metadata } from '../../../../platform/extensionManagement/common/extensionManagement.js'; -import { areSameExtensions, getGalleryExtensionId, getExtensionId } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; +import { areSameExtensions, getGalleryExtensionId, getExtensionId, isMalicious } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { ITranslations, localizeManifest } from '../../../../platform/extensionManagement/common/extensionNls.js'; import { localize, localize2 } from '../../../../nls.js'; @@ -175,7 +175,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten const extensionsControlManifest = await this.galleryService.getExtensionsControlManifest(); const result: ExtensionInfo[] = []; for (const extension of extensions) { - if (extensionsControlManifest.malicious.some(e => areSameExtensions(e, { id: extension.id }))) { + if (isMalicious({ id: extension.id }, extensionsControlManifest)) { this.logService.info(`Checking additional builtin extensions: Ignoring '${extension.id}' because it is reported to be malicious.`); continue; } From 1c10a0e5519c20dd63eaaf1a1a346f336ddf4520 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 7 Jan 2025 17:00:02 +0100 Subject: [PATCH 0385/3587] Git - remove proposed api typings from git/github extensions (#237409) --- extensions/git/src/api/api1.ts | 2 +- extensions/git/src/commands.ts | 2 +- extensions/git/src/git-base.ts | 2 +- extensions/git/src/remoteSource.ts | 2 +- .../git/src/{api => typings}/git-base.d.ts | 0 .../vscode.proposed.canonicalUriProvider.d.ts | 47 ------------ ....proposed.editSessionIdentityProvider.d.ts | 71 ------------------- extensions/git/tsconfig.json | 2 + .../vscode.proposed.canonicalUriProvider.d.ts | 47 ------------ .../vscode.proposed.shareProvider.d.ts | 29 -------- extensions/github/tsconfig.json | 4 +- 11 files changed, 9 insertions(+), 199 deletions(-) rename extensions/git/src/{api => typings}/git-base.d.ts (100%) delete mode 100644 extensions/git/src/typings/vscode.proposed.canonicalUriProvider.d.ts delete mode 100644 extensions/git/src/typings/vscode.proposed.editSessionIdentityProvider.d.ts delete mode 100644 extensions/github/src/typings/vscode.proposed.canonicalUriProvider.d.ts delete mode 100644 extensions/github/src/typings/vscode.proposed.shareProvider.d.ts diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 4090efa9551f..518269c41628 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -13,7 +13,7 @@ import { combinedDisposable, filterEvent, mapEvent } from '../util'; import { toGitUri } from '../uri'; import { GitExtensionImpl } from './extension'; import { GitBaseApi } from '../git-base'; -import { PickRemoteSourceOptions } from './git-base'; +import { PickRemoteSourceOptions } from '../typings/git-base'; import { OperationKind, OperationResult } from '../operation'; class ApiInputBox implements InputBox { diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index b8fa9009e1f6..d3e852ceab86 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -18,7 +18,7 @@ import { dispose, getCommitShortHash, grep, isDefined, isDescendant, pathEquals, import { GitTimelineItem } from './timelineProvider'; import { ApiRepository } from './api/api1'; import { getRemoteSourceActions, pickRemoteSource } from './remoteSource'; -import { RemoteSourceAction } from './api/git-base'; +import { RemoteSourceAction } from './typings/git-base'; abstract class CheckoutCommandItem implements QuickPickItem { abstract get label(): string; diff --git a/extensions/git/src/git-base.ts b/extensions/git/src/git-base.ts index 6cef535cfa54..437a126c9f3c 100644 --- a/extensions/git/src/git-base.ts +++ b/extensions/git/src/git-base.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { extensions } from 'vscode'; -import { API as GitBaseAPI, GitBaseExtension } from './api/git-base'; +import { API as GitBaseAPI, GitBaseExtension } from './typings/git-base'; export class GitBaseApi { diff --git a/extensions/git/src/remoteSource.ts b/extensions/git/src/remoteSource.ts index 4fdd6f06c1d1..eb63e5db81f6 100644 --- a/extensions/git/src/remoteSource.ts +++ b/extensions/git/src/remoteSource.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { PickRemoteSourceOptions, PickRemoteSourceResult } from './api/git-base'; +import { PickRemoteSourceOptions, PickRemoteSourceResult } from './typings/git-base'; import { GitBaseApi } from './git-base'; export async function pickRemoteSource(options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise; diff --git a/extensions/git/src/api/git-base.d.ts b/extensions/git/src/typings/git-base.d.ts similarity index 100% rename from extensions/git/src/api/git-base.d.ts rename to extensions/git/src/typings/git-base.d.ts diff --git a/extensions/git/src/typings/vscode.proposed.canonicalUriProvider.d.ts b/extensions/git/src/typings/vscode.proposed.canonicalUriProvider.d.ts deleted file mode 100644 index 84ee599797d9..000000000000 --- a/extensions/git/src/typings/vscode.proposed.canonicalUriProvider.d.ts +++ /dev/null @@ -1,47 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // https://github.com/microsoft/vscode/issues/180582 - - export namespace workspace { - /** - * - * @param scheme The URI scheme that this provider can provide canonical URIs for. - * A canonical URI represents the conversion of a resource's alias into a source of truth URI. - * Multiple aliases may convert to the same source of truth URI. - * @param provider A provider which can convert URIs of scheme @param scheme to - * a canonical URI which is stable across machines. - */ - export function registerCanonicalUriProvider(scheme: string, provider: CanonicalUriProvider): Disposable; - - /** - * - * @param uri The URI to provide a canonical URI for. - * @param token A cancellation token for the request. - */ - export function getCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, token: CancellationToken): ProviderResult; - } - - export interface CanonicalUriProvider { - /** - * - * @param uri The URI to provide a canonical URI for. - * @param options Options that the provider should honor in the URI it returns. - * @param token A cancellation token for the request. - * @returns The canonical URI for the requested URI or undefined if no canonical URI can be provided. - */ - provideCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, token: CancellationToken): ProviderResult; - } - - export interface CanonicalUriRequestOptions { - /** - * - * The desired scheme of the canonical URI. - */ - targetScheme: string; - } -} diff --git a/extensions/git/src/typings/vscode.proposed.editSessionIdentityProvider.d.ts b/extensions/git/src/typings/vscode.proposed.editSessionIdentityProvider.d.ts deleted file mode 100644 index e09d0e142b48..000000000000 --- a/extensions/git/src/typings/vscode.proposed.editSessionIdentityProvider.d.ts +++ /dev/null @@ -1,71 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // https://github.com/microsoft/vscode/issues/157734 - - export namespace workspace { - /** - * An event that is emitted when an edit session identity is about to be requested. - */ - export const onWillCreateEditSessionIdentity: Event; - - /** - * - * @param scheme The URI scheme that this provider can provide edit session identities for. - * @param provider A provider which can convert URIs for workspace folders of scheme @param scheme to - * an edit session identifier which is stable across machines. This enables edit sessions to be resolved. - */ - export function registerEditSessionIdentityProvider(scheme: string, provider: EditSessionIdentityProvider): Disposable; - } - - export interface EditSessionIdentityProvider { - /** - * - * @param workspaceFolder The workspace folder to provide an edit session identity for. - * @param token A cancellation token for the request. - * @returns A string representing the edit session identity for the requested workspace folder. - */ - provideEditSessionIdentity(workspaceFolder: WorkspaceFolder, token: CancellationToken): ProviderResult; - - /** - * - * @param identity1 An edit session identity. - * @param identity2 A second edit session identity to compare to @param identity1. - * @param token A cancellation token for the request. - * @returns An {@link EditSessionIdentityMatch} representing the edit session identity match confidence for the provided identities. - */ - provideEditSessionIdentityMatch(identity1: string, identity2: string, token: CancellationToken): ProviderResult; - } - - export enum EditSessionIdentityMatch { - Complete = 100, - Partial = 50, - None = 0 - } - - export interface EditSessionIdentityWillCreateEvent { - - /** - * A cancellation token. - */ - readonly token: CancellationToken; - - /** - * The workspace folder to create an edit session identity for. - */ - readonly workspaceFolder: WorkspaceFolder; - - /** - * Allows to pause the event until the provided thenable resolves. - * - * *Note:* This function can only be called during event dispatch. - * - * @param thenable A thenable that delays saving. - */ - waitUntil(thenable: Thenable): void; - } -} diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index ccf029e2df37..5a65f5c82ae2 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -10,7 +10,9 @@ "include": [ "src/**/*", "../../src/vscode-dts/vscode.d.ts", + "../../src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts", "../../src/vscode-dts/vscode.proposed.diffCommand.d.ts", + "../../src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts", "../../src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts", "../../src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts", "../../src/vscode-dts/vscode.proposed.scmActionButton.d.ts", diff --git a/extensions/github/src/typings/vscode.proposed.canonicalUriProvider.d.ts b/extensions/github/src/typings/vscode.proposed.canonicalUriProvider.d.ts deleted file mode 100644 index 84ee599797d9..000000000000 --- a/extensions/github/src/typings/vscode.proposed.canonicalUriProvider.d.ts +++ /dev/null @@ -1,47 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // https://github.com/microsoft/vscode/issues/180582 - - export namespace workspace { - /** - * - * @param scheme The URI scheme that this provider can provide canonical URIs for. - * A canonical URI represents the conversion of a resource's alias into a source of truth URI. - * Multiple aliases may convert to the same source of truth URI. - * @param provider A provider which can convert URIs of scheme @param scheme to - * a canonical URI which is stable across machines. - */ - export function registerCanonicalUriProvider(scheme: string, provider: CanonicalUriProvider): Disposable; - - /** - * - * @param uri The URI to provide a canonical URI for. - * @param token A cancellation token for the request. - */ - export function getCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, token: CancellationToken): ProviderResult; - } - - export interface CanonicalUriProvider { - /** - * - * @param uri The URI to provide a canonical URI for. - * @param options Options that the provider should honor in the URI it returns. - * @param token A cancellation token for the request. - * @returns The canonical URI for the requested URI or undefined if no canonical URI can be provided. - */ - provideCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, token: CancellationToken): ProviderResult; - } - - export interface CanonicalUriRequestOptions { - /** - * - * The desired scheme of the canonical URI. - */ - targetScheme: string; - } -} diff --git a/extensions/github/src/typings/vscode.proposed.shareProvider.d.ts b/extensions/github/src/typings/vscode.proposed.shareProvider.d.ts deleted file mode 100644 index 6470557cac1c..000000000000 --- a/extensions/github/src/typings/vscode.proposed.shareProvider.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// https://github.com/microsoft/vscode/issues/176316 - -declare module 'vscode' { - export interface TreeItem { - shareableItem?: ShareableItem; - } - - export interface ShareableItem { - resourceUri: Uri; - selection?: Range; - } - - export interface ShareProvider { - readonly id: string; - readonly label: string; - readonly priority: number; - - provideShare(item: ShareableItem, token: CancellationToken): ProviderResult; - } - - export namespace window { - export function registerShareProvider(selector: DocumentSelector, provider: ShareProvider): Disposable; - } -} diff --git a/extensions/github/tsconfig.json b/extensions/github/tsconfig.json index d7aed1836eed..452c74b8bc40 100644 --- a/extensions/github/tsconfig.json +++ b/extensions/github/tsconfig.json @@ -9,6 +9,8 @@ }, "include": [ "src/**/*", - "../../src/vscode-dts/vscode.d.ts" + "../../src/vscode-dts/vscode.d.ts", + "../../src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts", + "../../src/vscode-dts/vscode.proposed.shareProvider.d.ts" ] } From 972686cb00c49ba91105dafc0571c2bcf4d14b7a Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Tue, 7 Jan 2025 17:48:12 +0100 Subject: [PATCH 0386/3587] Allow running behind proxy (#237411) * Serve from '/' and basePath * Honor X-Forwarded-Prefix header This allows determining the base path automatically if set correctly by the proxy. * Honor X-Forwarded-Port header * use cleaner names, make sure the web server uses the original URL path * add logging --------- Co-authored-by: maleo --- src/vs/base/common/network.ts | 6 +- .../node/remoteExtensionHostAgentServer.ts | 26 +++-- src/vs/server/node/webClientServer.ts | 95 ++++++++++++------- 3 files changed, 81 insertions(+), 46 deletions(-) diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index f220611a7220..e77c500097a6 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -173,7 +173,7 @@ class RemoteAuthoritiesImpl { } setServerRootPath(product: { quality?: string; commit?: string }, serverBasePath: string | undefined): void { - this._serverRootPath = getServerRootPath(product, serverBasePath); + this._serverRootPath = paths.posix.join(serverBasePath ?? '/', getServerProductSegment(product)); } getServerRootPath(): string { @@ -228,8 +228,8 @@ class RemoteAuthoritiesImpl { export const RemoteAuthorities = new RemoteAuthoritiesImpl(); -export function getServerRootPath(product: { quality?: string; commit?: string }, basePath: string | undefined): string { - return paths.posix.join(basePath ?? '/', `${product.quality ?? 'oss'}-${product.commit ?? 'dev'}`); +export function getServerProductSegment(product: { quality?: string; commit?: string }) { + return `${product.quality ?? 'oss'}-${product.commit ?? 'dev'}`; } /** diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index 77b124f97000..8323d0f13d0a 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -14,7 +14,7 @@ import { CharCode } from '../../base/common/charCode.js'; import { isSigPipeError, onUnexpectedError, setUnexpectedErrorHandler } from '../../base/common/errors.js'; import { isEqualOrParent } from '../../base/common/extpath.js'; import { Disposable, DisposableStore } from '../../base/common/lifecycle.js'; -import { connectionTokenQueryName, FileAccess, getServerRootPath, Schemas } from '../../base/common/network.js'; +import { connectionTokenQueryName, FileAccess, getServerProductSegment, Schemas } from '../../base/common/network.js'; import { dirname, join } from '../../base/common/path.js'; import * as perf from '../../base/common/performance.js'; import * as platform from '../../base/common/platform.js'; @@ -66,7 +66,8 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { private readonly _webClientServer: WebClientServer | null; private readonly _webEndpointOriginChecker = WebEndpointOriginChecker.create(this._productService); - private readonly _serverRootPath: string; + private readonly _serverBasePath: string | undefined; + private readonly _serverProductPath: string; private shutdownTimer: NodeJS.Timeout | undefined; @@ -83,13 +84,18 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { ) { super(); - this._serverRootPath = getServerRootPath(_productService, serverBasePath); + if (serverBasePath !== undefined && serverBasePath.charCodeAt(serverBasePath.length - 1) === CharCode.Slash) { + // Remove trailing slash from base path + serverBasePath = serverBasePath.substring(0, serverBasePath.length - 1); + } + this._serverBasePath = serverBasePath; // undefined or starts with a slash + this._serverProductPath = `/${getServerProductSegment(_productService)}`; // starts with a slash this._extHostConnections = Object.create(null); this._managementConnections = Object.create(null); this._allReconnectionTokens = new Set(); this._webClientServer = ( hasWebClient - ? this._instantiationService.createInstance(WebClientServer, this._connectionToken, serverBasePath ?? '/', this._serverRootPath) + ? this._instantiationService.createInstance(WebClientServer, this._connectionToken, serverBasePath ?? '/', this._serverProductPath) : null ); this._logService.info(`Extension host agent started.`); @@ -114,9 +120,13 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { return serveError(req, res, 400, `Bad request.`); } - // for now accept all paths, with or without server root path - if (pathname.startsWith(this._serverRootPath) && pathname.charCodeAt(this._serverRootPath.length) === CharCode.Slash) { - pathname = pathname.substring(this._serverRootPath.length); + // Serve from both '/' and serverBasePath + if (this._serverBasePath !== undefined && pathname.startsWith(this._serverBasePath)) { + pathname = pathname.substring(this._serverBasePath.length) || '/'; + } + // for now accept all paths, with or without server product path + if (pathname.startsWith(this._serverProductPath) && pathname.charCodeAt(this._serverProductPath.length) === CharCode.Slash) { + pathname = pathname.substring(this._serverProductPath.length); } // Version @@ -172,7 +182,7 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { // workbench web UI if (this._webClientServer) { - this._webClientServer.handle(req, res, parsedUrl); + this._webClientServer.handle(req, res, parsedUrl, pathname); return; } diff --git a/src/vs/server/node/webClientServer.ts b/src/vs/server/node/webClientServer.ts index 7c6430e23806..5ce490dc1394 100644 --- a/src/vs/server/node/webClientServer.ts +++ b/src/vs/server/node/webClientServer.ts @@ -12,9 +12,9 @@ import * as crypto from 'crypto'; import { isEqualOrParent } from '../../base/common/extpath.js'; import { getMediaMime } from '../../base/common/mime.js'; import { isLinux } from '../../base/common/platform.js'; -import { ILogService } from '../../platform/log/common/log.js'; +import { ILogService, LogLevel } from '../../platform/log/common/log.js'; import { IServerEnvironmentService } from './serverEnvironmentService.js'; -import { extname, dirname, join, normalize } from '../../base/common/path.js'; +import { extname, dirname, join, normalize, posix } from '../../base/common/path.js'; import { FileAccess, connectionTokenCookieName, connectionTokenQueryName, Schemas, builtinExtensionsPath } from '../../base/common/network.js'; import { generateUuid } from '../../base/common/uuid.js'; import { IProductService } from '../../platform/product/common/productService.js'; @@ -93,18 +93,18 @@ export async function serveFile(filePath: string, cacheControl: CacheControl, lo const APP_ROOT = dirname(FileAccess.asFileUri('').fsPath); +const STATIC_PATH = `/static`; +const CALLBACK_PATH = `/callback`; +const WEB_EXTENSION_PATH = `/web-extension-resource`; + export class WebClientServer { private readonly _webExtensionResourceUrlTemplate: URI | undefined; - private readonly _staticRoute: string; - private readonly _callbackRoute: string; - private readonly _webExtensionRoute: string; - constructor( private readonly _connectionToken: ServerConnectionToken, private readonly _basePath: string, - readonly serverRootPath: string, + private readonly _productPath: string, @IServerEnvironmentService private readonly _environmentService: IServerEnvironmentService, @ILogService private readonly _logService: ILogService, @IRequestService private readonly _requestService: IRequestService, @@ -112,34 +112,30 @@ export class WebClientServer { @ICSSDevelopmentService private readonly _cssDevService: ICSSDevelopmentService ) { this._webExtensionResourceUrlTemplate = this._productService.extensionsGallery?.resourceUrlTemplate ? URI.parse(this._productService.extensionsGallery.resourceUrlTemplate) : undefined; - - this._staticRoute = `${serverRootPath}/static`; - this._callbackRoute = `${serverRootPath}/callback`; - this._webExtensionRoute = `${serverRootPath}/web-extension-resource`; } /** * Handle web resources (i.e. only needed by the web client). * **NOTE**: This method is only invoked when the server has web bits. * **NOTE**: This method is only invoked after the connection token has been validated. + * @param parsedUrl The URL to handle, including base and product path + * @param pathname The pathname of the URL, without base and product path */ - async handle(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise { + async handle(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery, pathname: string): Promise { try { - const pathname = parsedUrl.pathname!; - - if (pathname.startsWith(this._staticRoute) && pathname.charCodeAt(this._staticRoute.length) === CharCode.Slash) { - return this._handleStatic(req, res, parsedUrl); + if (pathname.startsWith(STATIC_PATH) && pathname.charCodeAt(STATIC_PATH.length) === CharCode.Slash) { + return this._handleStatic(req, res, pathname.substring(STATIC_PATH.length)); } - if (pathname === this._basePath) { + if (pathname === '/') { return this._handleRoot(req, res, parsedUrl); } - if (pathname === this._callbackRoute) { + if (pathname === CALLBACK_PATH) { // callback support return this._handleCallback(res); } - if (pathname.startsWith(this._webExtensionRoute) && pathname.charCodeAt(this._webExtensionRoute.length) === CharCode.Slash) { + if (pathname.startsWith(WEB_EXTENSION_PATH) && pathname.charCodeAt(WEB_EXTENSION_PATH.length) === CharCode.Slash) { // extension resource support - return this._handleWebExtensionResource(req, res, parsedUrl); + return this._handleWebExtensionResource(req, res, pathname.substring(WEB_EXTENSION_PATH.length)); } return serveError(req, res, 404, 'Not found.'); @@ -152,15 +148,15 @@ export class WebClientServer { } /** * Handle HTTP requests for /static/* + * @param resourcePath The path after /static/ */ - private async _handleStatic(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise { + private async _handleStatic(req: http.IncomingMessage, res: http.ServerResponse, resourcePath: string): Promise { const headers: Record = Object.create(null); // Strip the this._staticRoute from the path - const normalizedPathname = decodeURIComponent(parsedUrl.pathname!); // support paths that are uri-encoded (e.g. spaces => %20) - const relativeFilePath = normalizedPathname.substring(this._staticRoute.length + 1); + const normalizedPathname = decodeURIComponent(resourcePath); // support paths that are uri-encoded (e.g. spaces => %20) - const filePath = join(APP_ROOT, relativeFilePath); // join also normalizes the path + const filePath = join(APP_ROOT, normalizedPathname); // join also normalizes the path if (!isEqualOrParent(filePath, APP_ROOT, !isLinux)) { return serveError(req, res, 400, `Bad request.`); } @@ -175,15 +171,15 @@ export class WebClientServer { /** * Handle extension resources + * @param resourcePath The path after /web-extension-resource/ */ - private async _handleWebExtensionResource(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise { + private async _handleWebExtensionResource(req: http.IncomingMessage, res: http.ServerResponse, resourcePath: string): Promise { if (!this._webExtensionResourceUrlTemplate) { return serveError(req, res, 500, 'No extension gallery service configured.'); } - // Strip `/web-extension-resource/` from the path - const normalizedPathname = decodeURIComponent(parsedUrl.pathname!); // support paths that are uri-encoded (e.g. spaces => %20) - const path = normalize(normalizedPathname.substring(this._webExtensionRoute.length + 1)); + const normalizedPathname = decodeURIComponent(resourcePath); // support paths that are uri-encoded (e.g. spaces => %20) + const path = normalize(normalizedPathname); const uri = URI.parse(path).with({ scheme: this._webExtensionResourceUrlTemplate.scheme, authority: path.substring(0, path.indexOf('/')), @@ -243,7 +239,6 @@ export class WebClientServer { * Handle HTTP requests for / */ private async _handleRoot(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise { - const queryConnectionToken = parsedUrl.query[connectionTokenQueryName]; if (typeof queryConnectionToken === 'string') { // We got a connection token as a query parameter. @@ -276,8 +271,17 @@ export class WebClientServer { return Array.isArray(val) ? val[0] : val; }; + const replacePort = (host: string, port: string) => { + const index = host?.indexOf(':'); + if (index !== -1) { + host = host?.substring(0, index); + } + host += `:${port}`; + return host; + }; + const useTestResolver = (!this._environmentService.isBuilt && this._environmentService.args['use-test-resolver']); - const remoteAuthority = ( + let remoteAuthority = ( useTestResolver ? 'test+test' : (getFirstHeader('x-original-host') || getFirstHeader('x-forwarded-host') || req.headers.host) @@ -285,6 +289,10 @@ export class WebClientServer { if (!remoteAuthority) { return serveError(req, res, 400, `Bad request.`); } + const forwardedPort = getFirstHeader('x-forwarded-port'); + if (forwardedPort) { + remoteAuthority = replacePort(remoteAuthority, forwardedPort); + } function asJSON(value: unknown): string { return JSON.stringify(value).replace(/"/g, '"'); @@ -297,6 +305,23 @@ export class WebClientServer { _wrapWebWorkerExtHostInIframe = false; } + // Prefix routes with basePath for clients + const basePath = getFirstHeader('x-forwarded-prefix') || this._basePath; + + if (this._logService.getLevel() === LogLevel.Trace) { + ['x-original-host', 'x-forwarded-host', 'x-forwarded-port', 'host'].forEach(header => { + const value = getFirstHeader(header); + if (value) { + this._logService.trace(`[WebClientServer] ${header}: ${value}`); + } + }); + this._logService.trace(`[WebClientServer] Request URL: ${req.url}, basePath: ${basePath}, remoteAuthority: ${remoteAuthority}`); + } + + const staticRoute = posix.join(basePath, this._productPath, STATIC_PATH); + const callbackRoute = posix.join(basePath, this._productPath, CALLBACK_PATH); + const webExtensionRoute = posix.join(basePath, this._productPath, WEB_EXTENSION_PATH); + const resolveWorkspaceURI = (defaultLocation?: string) => defaultLocation && URI.file(path.resolve(defaultLocation)).with({ scheme: Schemas.vscodeRemote, authority: remoteAuthority }); const filePath = FileAccess.asFileUri(`vs/code/browser/workbench/workbench${this._environmentService.isBuilt ? '' : '-dev'}.html`).fsPath; @@ -314,7 +339,7 @@ export class WebClientServer { resourceUrlTemplate: this._webExtensionResourceUrlTemplate.with({ scheme: 'http', authority: remoteAuthority, - path: `${this._webExtensionRoute}/${this._webExtensionResourceUrlTemplate.authority}${this._webExtensionResourceUrlTemplate.path}` + path: `${webExtensionRoute}/${this._webExtensionResourceUrlTemplate.authority}${this._webExtensionResourceUrlTemplate.path}` }).toString(true) } : undefined } satisfies Partial; @@ -328,7 +353,7 @@ export class WebClientServer { const workbenchWebConfiguration = { remoteAuthority, - serverBasePath: this._basePath, + serverBasePath: basePath, _wrapWebWorkerExtHostInIframe, developmentOptions: { enableSmokeTestDriver: this._environmentService.args['enable-smoke-test-driver'] ? true : undefined, logLevel: this._logService.getLevel() }, settingsSyncOptions: !this._environmentService.isBuilt && this._environmentService.args['enable-sync'] ? { enabled: true } : undefined, @@ -336,7 +361,7 @@ export class WebClientServer { folderUri: resolveWorkspaceURI(this._environmentService.args['default-folder']), workspaceUri: resolveWorkspaceURI(this._environmentService.args['default-workspace']), productConfiguration, - callbackRoute: this._callbackRoute + callbackRoute: callbackRoute }; const cookies = cookie.parse(req.headers.cookie || ''); @@ -353,9 +378,9 @@ export class WebClientServer { const values: { [key: string]: string } = { WORKBENCH_WEB_CONFIGURATION: asJSON(workbenchWebConfiguration), WORKBENCH_AUTH_SESSION: authSessionInfo ? asJSON(authSessionInfo) : '', - WORKBENCH_WEB_BASE_URL: this._staticRoute, + WORKBENCH_WEB_BASE_URL: staticRoute, WORKBENCH_NLS_URL, - WORKBENCH_NLS_FALLBACK_URL: `${this._staticRoute}/out/nls.messages.js` + WORKBENCH_NLS_FALLBACK_URL: `${staticRoute}/out/nls.messages.js` }; // DEV --------------------------------------------------------------------------------------- From e2e9a7306bf484236a3fb3a4a9cb928cb7692227 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 7 Jan 2025 09:37:48 -0800 Subject: [PATCH 0387/3587] cli: update url to 2.5.4 for dependabot (#237417) --- cli/Cargo.lock | 296 +++++++++++++++++++++++++++++++++++++++++++------ cli/Cargo.toml | 2 +- 2 files changed, 266 insertions(+), 32 deletions(-) diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 27fe79896a2a..5da9906e1acd 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -649,6 +649,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -1148,14 +1159,143 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "icu_normalizer", + "icu_properties", ] [[package]] @@ -1336,6 +1476,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "lock_api" version = "0.4.12" @@ -2358,6 +2504,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2404,6 +2556,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + [[package]] name = "sysinfo" version = "0.29.11" @@ -2502,20 +2665,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] -name = "tinyvec" -version = "1.6.0" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ - "tinyvec_macros", + "displaydoc", + "zerovec", ] -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.37.0" @@ -2721,27 +2879,12 @@ dependencies = [ "winapi", ] -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-normalization" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-width" version = "0.1.12" @@ -2756,9 +2899,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "url" -version = "2.5.0" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -2777,6 +2920,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.1" @@ -3119,6 +3274,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "xattr" version = "1.3.1" @@ -3150,6 +3317,30 @@ dependencies = [ "num-bigint", ] +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", + "synstructure", +] + [[package]] name = "zbus" version = "3.15.2" @@ -3211,12 +3402,55 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", + "synstructure", +] + [[package]] name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.65", +] + [[package]] name = "zip" version = "0.6.6" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 2907ff3d7e70..2c87d662e073 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -41,7 +41,7 @@ hyper = { version = "0.14.26", features = ["server", "http1", "runtime"] } indicatif = "0.17.4" tempfile = "3.5.0" clap_lex = "0.7.0" -url = "2.3.1" +url = "2.5.4" async-trait = "0.1.68" log = "0.4.18" const_format = "0.2.31" From cbb7f99ba8e15177d7c5cc26339707ef8c086d66 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 7 Jan 2025 20:07:07 +0100 Subject: [PATCH 0388/3587] Git - remove hard coded remote name when detecting the default branch (#237423) --- extensions/git/src/git.ts | 4 ++-- extensions/git/src/repository.ts | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 327903aa9592..b000245a0f2e 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -2779,8 +2779,8 @@ export class Repository { return Promise.reject(new Error(`No such branch: ${name}.`)); } - async getDefaultBranch(): Promise { - const result = await this.exec(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']); + async getDefaultBranch(remoteName: string): Promise { + const result = await this.exec(['symbolic-ref', '--short', `refs/remotes/${remoteName}/HEAD`]); if (!result.stdout || result.stderr) { throw new Error('No default branch'); } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 2a722502b75c..00419ef85fc2 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1593,7 +1593,12 @@ export class Repository implements Disposable { private async getDefaultBranch(): Promise { try { - const defaultBranch = await this.repository.getDefaultBranch(); + if (this.remotes.length === 0) { + return undefined; + } + + const remote = this.remotes.find(r => r.name === 'origin') ?? this.remotes[0]; + const defaultBranch = await this.repository.getDefaultBranch(remote.name); return defaultBranch; } catch (err) { From 07f8b09e2fc8309f3c333223efff1901e33c472d Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Tue, 7 Jan 2025 12:06:44 -0800 Subject: [PATCH 0389/3587] fix: don't validate URIs for edits in response stream (#237430) --- .../browser/chatEditing/chatEditingSession.ts | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 8552946891ff..134c3de87edb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -18,7 +18,6 @@ import { ITextModel } from '../../../../../editor/common/model.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { localize } from '../../../../../nls.js'; -import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { EditorActivation } from '../../../../../platform/editor/common/editor.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -29,12 +28,10 @@ import { IEditorGroupsService } from '../../../../services/editor/common/editorG import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js'; import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; -import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IModifiedFileEntry, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; import { ChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; -import { Schemas } from '../../../../../base/common/network.js'; import { isEqual, joinPath } from '../../../../../base/common/resources.js'; import { StringSHA1 } from '../../../../../base/common/hash.js'; import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; @@ -94,7 +91,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio * Contains the contents of a file when the AI first began doing edits to it. */ private readonly _initialFileContents = new ResourceMap(); - private readonly _filesToSkipCreating = new ResourceSet(); private readonly _entriesObs = observableValue(this, []); public get entries(): IObservable { @@ -174,10 +170,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio @IBulkEditService public readonly _bulkEditService: IBulkEditService, @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, @IEditorService private readonly _editorService: IEditorService, - @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, - @IFileService private readonly _fileService: IFileService, - @IFileDialogService private readonly _dialogService: IFileDialogService, - @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IChatService private readonly _chatService: IChatService, @INotebookService private readonly _notebookService: INotebookService, ) { @@ -187,9 +179,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio public async init(): Promise { const restoredSessionState = await this._instantiationService.createInstance(ChatEditingSessionStorage, this.chatSessionId).restoreState(); if (restoredSessionState) { - for (const uri of restoredSessionState.filesToSkipCreating) { - this._filesToSkipCreating.add(uri); - } for (const [uri, content] of restoredSessionState.initialFileContents) { this._initialFileContents.set(uri, content); } @@ -225,7 +214,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio public storeState(): Promise { const storage = this._instantiationService.createInstance(ChatEditingSessionStorage, this.chatSessionId); const state: StoredSessionState = { - filesToSkipCreating: [...this._filesToSkipCreating], initialFileContents: this._initialFileContents, pendingSnapshot: this._pendingSnapshot, recentSnapshot: this._createSnapshot(undefined), @@ -625,26 +613,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } private async _acceptTextEdits(resource: URI, textEdits: TextEdit[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise { - if (this._filesToSkipCreating.has(resource)) { - return; - } - if (!this._entriesObs.get().find(e => isEqual(e.modifiedURI, resource)) && this._entriesObs.get().length >= (await this.editingSessionFileLimitPromise)) { // Do not create files in a single editing session that would be in excess of our limit return; } - if (resource.scheme !== Schemas.untitled && !this._workspaceContextService.getWorkspaceFolder(resource) && !(await this._fileService.exists(resource))) { - // if the file doesn't exist yet and is outside the workspace, prompt the user for a location to save it to - const saveLocation = await this._dialogService.showSaveDialog({ title: localize('chatEditing.fileSave', '{0} wants to create a file. Choose where it should be saved.', this._chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession)?.fullName ?? 'Chat') }); - if (!saveLocation) { - // don't ask the user to create the file again when the next text edit for this same resource streams in - this._filesToSkipCreating.add(resource); - return; - } - resource = saveLocation; - } - // Make these getters because the response result is not available when the file first starts to be edited const telemetryInfo = new class { get agentId() { return responseModel.agent?.id; } @@ -730,7 +703,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } interface StoredSessionState { - readonly filesToSkipCreating: URI[]; readonly initialFileContents: ResourceMap; readonly pendingSnapshot?: IChatEditingSessionSnapshot; readonly recentSnapshot: IChatEditingSessionSnapshot; @@ -801,7 +773,6 @@ class ChatEditingSessionStorage { } const linearHistory = await Promise.all(data.linearHistory.map(deserializeChatEditingSessionSnapshot)); - const filesToSkipCreating = data.filesToSkipCreating.map((uriStr: string) => URI.parse(uriStr)); const initialFileContents = new ResourceMap(); for (const fileContentDTO of data.initialFileContents) { @@ -811,7 +782,6 @@ class ChatEditingSessionStorage { const recentSnapshot = await deserializeChatEditingSessionSnapshot(data.recentSnapshot); return { - filesToSkipCreating, initialFileContents, pendingSnapshot, recentSnapshot, @@ -889,7 +859,6 @@ class ChatEditingSessionStorage { initialFileContents: serializeResourceMap(state.initialFileContents, value => addFileContent(value)), pendingSnapshot: state.pendingSnapshot ? serializeChatEditingSessionSnapshot(state.pendingSnapshot) : undefined, recentSnapshot: serializeChatEditingSessionSnapshot(state.recentSnapshot), - filesToSkipCreating: state.filesToSkipCreating.map(uri => uri.toString()), } satisfies IChatEditingSessionDTO; this._logService.debug(`chatEditingSession: Storing editing session at ${storageFolder.toString()}: ${fileContents.size} files`); @@ -959,5 +928,4 @@ interface IChatEditingSessionDTO { readonly linearHistoryIndex: number; readonly pendingSnapshot: IChatEditingSessionSnapshotDTO | undefined; readonly initialFileContents: ResourceMapDTO; - readonly filesToSkipCreating: string[]; } From 4daea74ba9cf57292cbc3ef04cfe3a50c234de8a Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 7 Jan 2025 14:57:16 -0800 Subject: [PATCH 0390/3587] Finalize paste api and drop api tweaks Fixes #30066 This finalizes the documentPaste api. This api allows extensions to attach metadata on text copy and change how content is pasted. Some examples: - Updating imports for pasted code - Inserting an image as an attachment in notebooks - Pasting files converts them to relative text paths --- extensions/css-language-features/package.json | 3 - extensions/ipynb/package.json | 1 - .../markdown-language-features/package.json | 3 - .../typescript-language-features/package.json | 1 - .../src/languageFeatures/copyPaste.ts | 2 +- extensions/vscode-api-tests/package.json | 1 - .../common/extensionsApiProposals.ts | 3 - .../workbench/api/common/extHost.api.impl.ts | 3 +- .../api/common/extHostLanguageFeatures.ts | 2 +- src/vs/workbench/api/common/extHostTypes.ts | 2 + src/vscode-dts/vscode.d.ts | 306 ++++++++++++++++- .../vscode.proposed.documentPaste.d.ts | 316 ------------------ 12 files changed, 307 insertions(+), 336 deletions(-) delete mode 100644 src/vscode-dts/vscode.proposed.documentPaste.d.ts diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index af75de5386b6..0533881380c1 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -23,9 +23,6 @@ "supported": true } }, - "enabledApiProposals": [ - "documentPaste" - ], "scripts": { "compile": "npx gulp compile-extension:css-language-features-client compile-extension:css-language-features-server", "watch": "npx gulp watch-extension:css-language-features-client watch-extension:css-language-features-server", diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index 4209ddc130ac..d9a9dd7a5143 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -10,7 +10,6 @@ "vscode": "^1.57.0" }, "enabledApiProposals": [ - "documentPaste", "diffContentOptions" ], "activationEvents": [ diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index d7eddbf31bd1..6d097be5470a 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -15,9 +15,6 @@ "categories": [ "Programming Languages" ], - "enabledApiProposals": [ - "documentPaste" - ], "activationEvents": [ "onLanguage:markdown", "onCommand:markdown.api.render", diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 51ac7bd20c94..ac6d0487a4c3 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -13,7 +13,6 @@ "mappedEditsProvider", "codeActionAI", "codeActionRanges", - "documentPaste", "editorHoverVerbosityLevel" ], "capabilities": { diff --git a/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts index 71b6797f585c..30a73d0adf7f 100644 --- a/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts +++ b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts @@ -43,7 +43,7 @@ const enabledSettingId = 'updateImportsOnPaste.enabled'; class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { - static readonly kind = vscode.DocumentDropOrPasteEditKind.Text.append('updateImports', 'jsts'); + static readonly kind = vscode.DocumentDropOrPasteEditKind.TextUpdateImports.append('jsts'); static readonly metadataMimeType = 'application/vnd.code.jsts.metadata'; constructor( diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 5f7681bf9d61..60b35b4dd9e1 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -17,7 +17,6 @@ "defaultChatParticipant", "diffCommand", "documentFiltersExclusive", - "documentPaste", "editorInsets", "embeddings", "extensionRuntime", diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index bd35500287e2..cced7c27af0d 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -165,9 +165,6 @@ const _allApiProposals = { documentFiltersExclusive: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.d.ts', }, - documentPaste: { - proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentPaste.d.ts', - }, editSessionIdentityProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts', }, diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 868906908ea7..0ca018013ca6 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -545,7 +545,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostLanguageFeatures.registerCodeActionProvider(extension, checkSelector(selector), provider, metadata); }, registerDocumentPasteEditProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentPasteEditProvider, metadata: vscode.DocumentPasteProviderMetadata): vscode.Disposable { - checkProposedApiEnabled(extension, 'documentPaste'); return extHostLanguageFeatures.registerDocumentPasteEditProvider(extension, checkSelector(selector), provider, metadata); }, registerCodeLensProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable { @@ -669,7 +668,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostLanguages.createLanguageStatusItem(extension, id, selector); }, registerDocumentDropEditProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentDropEditProvider, metadata?: vscode.DocumentDropEditProviderMetadata): vscode.Disposable { - return extHostLanguageFeatures.registerDocumentOnDropEditProvider(extension, selector, provider, isProposedApiEnabled(extension, 'documentPaste') ? metadata : undefined); + return extHostLanguageFeatures.registerDocumentOnDropEditProvider(extension, selector, provider, metadata); } }; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 9783b16e8801..53a5c89264e9 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -2914,7 +2914,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF const handle = this._nextHandle(); this._adapter.set(handle, new AdapterData(new DocumentDropEditAdapter(this._proxy, this._documents, provider, handle, extension), extension)); - this._proxy.$registerDocumentOnDropEditProvider(handle, this._transformDocumentSelector(selector, extension), isProposedApiEnabled(extension, 'documentPaste') && metadata ? { + this._proxy.$registerDocumentOnDropEditProvider(handle, this._transformDocumentSelector(selector, extension), metadata ? { supportsResolve: !!provider.resolveDocumentDropEdit, dropMimeTypes: metadata.dropMimeTypes, providedDropKinds: metadata.providedDropEditKinds?.map(x => x.value), diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index d388236e8379..667b018aa332 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2950,6 +2950,7 @@ export enum DocumentPasteTriggerKind { export class DocumentDropOrPasteEditKind { static Empty: DocumentDropOrPasteEditKind; static Text: DocumentDropOrPasteEditKind; + static TextUpdateImports: DocumentDropOrPasteEditKind; private static sep = '.'; @@ -2971,6 +2972,7 @@ export class DocumentDropOrPasteEditKind { } DocumentDropOrPasteEditKind.Empty = new DocumentDropOrPasteEditKind(''); DocumentDropOrPasteEditKind.Text = new DocumentDropOrPasteEditKind('text'); +DocumentDropOrPasteEditKind.TextUpdateImports = DocumentDropOrPasteEditKind.Text.append('updateImports'); export class DocumentPasteEdit { diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index cfe9d1226095..580acdeb53b4 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -6073,10 +6073,88 @@ declare module 'vscode' { provideLinkedEditingRanges(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } + /** + * Identifies a {@linkcode DocumentDropEdit} or {@linkcode DocumentPasteEdit} + */ + class DocumentDropOrPasteEditKind { + static readonly Empty: DocumentDropOrPasteEditKind; + + /** + * The root kind for basic text edits. + * + * This kind should be used for edits that insert basic text into the document. A good example of this is + * an edit that pastes the clipboard text while also updating imports in the file based on the pasted text. + * For this we could use a kind such as `text.updateImports.someLanguageId`. + * + * Even though most drop/paste edits ultimately insert text, you should not use {@linkcode Text} as the base kind + * for every edit as this is redundant. Instead a more specific kind that describes the type of content being + * inserted should be used instead. For example, if the edit adds a Markdown link, use `markdown.link` since even + * though the content being inserted is text, it's more important to know that the edit inserts Markdown syntax. + */ + static readonly Text: DocumentDropOrPasteEditKind; + + /** + * Root kind for edits that update imports in a document in addition to inserting text. + */ + static readonly TextUpdateImports: DocumentDropOrPasteEditKind; + + /** + * Use {@linkcode DocumentDropOrPasteEditKind.Empty} instead. + */ + private constructor(value: string); + + /** + * The raw string value of the kind. + */ + readonly value: string; + + /** + * Create a new kind by appending additional scopes to the current kind. + * + * Does not modify the current kind. + */ + append(...parts: string[]): DocumentDropOrPasteEditKind; + + /** + * Checks if this kind intersects `other`. + * + * The kind `"text.plain"` for example intersects `text`, `"text.plain"` and `"text.plain.list"`, + * but not `"unicorn"`, or `"textUnicorn.plain"`. + * + * @param other Kind to check. + */ + intersects(other: DocumentDropOrPasteEditKind): boolean; + + /** + * Checks if `other` is a sub-kind of this `DocumentDropOrPasteEditKind`. + * + * The kind `"text.plain"` for example contains `"text.plain"` and `"text.plain.list"`, + * but not `"text"` or `"unicorn.text.plain"`. + * + * @param other Kind to check. + */ + contains(other: DocumentDropOrPasteEditKind): boolean; + } + /** * An edit operation applied {@link DocumentDropEditProvider on drop}. */ export class DocumentDropEdit { + /** + * Human readable label that describes the edit. + */ + title?: string; + + /** + * {@link DocumentDropOrPasteEditKind Kind} of the edit. + */ + kind?: DocumentDropOrPasteEditKind; + + /** + * Controls the ordering or multiple edits. If this provider yield to edits, it will be shown lower in the list. + */ + yieldTo?: readonly DocumentDropOrPasteEditKind[]; + /** * The text or snippet to insert at the drop location. */ @@ -6089,8 +6167,10 @@ declare module 'vscode' { /** * @param insertText The text or snippet to insert at the drop location. + * @param title Human readable label that describes the edit. + * @param kind {@link DocumentDropOrPasteEditKind Kind} of the edit. */ - constructor(insertText: string | SnippetString); + constructor(insertText: string | SnippetString, title?: string, kind?: DocumentDropOrPasteEditKind); } /** @@ -6100,7 +6180,7 @@ declare module 'vscode' { * and dropping files, users can hold down `shift` to drop the file into the editor instead of opening it. * Requires `editor.dropIntoEditor.enabled` to be on. */ - export interface DocumentDropEditProvider { + export interface DocumentDropEditProvider { /** * Provide edits which inserts the content being dragged and dropped into the document. * @@ -6112,7 +6192,212 @@ declare module 'vscode' { * @returns A {@link DocumentDropEdit} or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ - provideDocumentDropEdits(document: TextDocument, position: Position, dataTransfer: DataTransfer, token: CancellationToken): ProviderResult; + provideDocumentDropEdits(document: TextDocument, position: Position, dataTransfer: DataTransfer, token: CancellationToken): ProviderResult; + + /** + * Optional method which fills in the {@linkcode DocumentDropEdit.additionalEdit} before the edit is applied. + * + * This is called once per edit and should be used if generating the complete edit may take a long time. + * Resolve can only be used to change {@link DocumentDropEdit.additionalEdit}. + * + * @param edit The {@linkcode DocumentDropEdit} to resolve. + * @param token A cancellation token. + * + * @returns The resolved edit or a thenable that resolves to such. It is OK to return the given + * `edit`. If no result is returned, the given `edit` is used. + */ + resolveDocumentDropEdit?(edit: T, token: CancellationToken): ProviderResult; + } + + /** + * Provides additional metadata about how a {@linkcode DocumentDropEditProvider} works. + */ + export interface DocumentDropEditProviderMetadata { + /** + * List of {@link DocumentDropOrPasteEditKind kinds} that the provider may return in {@linkcode DocumentDropEditProvider.provideDocumentDropEdits provideDocumentDropEdits}. + * + * This is used to filter out providers when a specific {@link DocumentDropOrPasteEditKind kind} of edit is requested. + */ + readonly providedDropEditKinds?: readonly DocumentDropOrPasteEditKind[]; + + /** + * List of {@link DataTransfer} mime types that the provider can handle. + * + * This can either be an exact mime type such as `image/png`, or a wildcard pattern such as `image/*`. + * + * Use `text/uri-list` for resources dropped from the explorer or other tree views in the workbench. + * + * Use `files` to indicate that the provider should be invoked if any {@link DataTransferFile files} are present in the {@link DataTransfer}. + * Note that {@link DataTransferFile} entries are only created when dropping content from outside the editor, such as + * from the operating system. + */ + readonly dropMimeTypes: readonly string[]; + } + + + /** + * The reason why paste edits were requested. + */ + export enum DocumentPasteTriggerKind { + /** + * Pasting was requested as part of a normal paste operation. + */ + Automatic = 0, + + /** + * Pasting was requested by the user with the `paste as` command. + */ + PasteAs = 1, + } + + /** + * Additional information about the paste operation. + */ + export interface DocumentPasteEditContext { + + /** + * Requested kind of paste edits to return. + * + * When a explicit kind if requested by {@linkcode DocumentPasteTriggerKind.PasteAs PasteAs}, providers are + * encourage to be more flexible when generating an edit of the requested kind. + */ + readonly only: DocumentDropOrPasteEditKind | undefined; + + /** + * The reason why paste edits were requested. + */ + readonly triggerKind: DocumentPasteTriggerKind; + } + + /** + * Provider invoked when the user copies or pastes in a {@linkcode TextDocument}. + */ + interface DocumentPasteEditProvider { + + /** + * Optional method invoked after the user copies from a {@link TextEditor text editor}. + * + * This allows the provider to attach metadata about the copied text to the {@link DataTransfer}. This data + * transfer is then passed back to providers in {@linkcode provideDocumentPasteEdits}. + * + * Note that currently any changes to the {@linkcode DataTransfer} are isolated to the current editor window. + * This means that any added metadata cannot be seen by other editor windows or by other applications. + * + * @param document Text document where the copy took place. + * @param ranges Ranges being copied in {@linkcode document}. + * @param dataTransfer The data transfer associated with the copy. You can store additional values on this for + * later use in {@linkcode provideDocumentPasteEdits}. This object is only valid for the duration of this method. + * @param token A cancellation token. + * + * @return Optional thenable that resolves when all changes to the `dataTransfer` are complete. + */ + prepareDocumentPaste?(document: TextDocument, ranges: readonly Range[], dataTransfer: DataTransfer, token: CancellationToken): void | Thenable; + + /** + * Invoked before the user pastes into a {@link TextEditor text editor}. + * + * Returned edits can replace the standard pasting behavior. + * + * @param document Document being pasted into + * @param ranges Range in the {@linkcode document} to paste into. + * @param dataTransfer The {@link DataTransfer data transfer} associated with the paste. This object is only + * valid for the duration of the paste operation. + * @param context Additional context for the paste. + * @param token A cancellation token. + * + * @return Set of potential {@link DocumentPasteEdit edits} that can apply the paste. Only a single returned + * {@linkcode DocumentPasteEdit} is applied at a time. If multiple edits are returned from all providers, then + * the first is automatically applied and a widget is shown that lets the user switch to the other edits. + */ + provideDocumentPasteEdits?(document: TextDocument, ranges: readonly Range[], dataTransfer: DataTransfer, context: DocumentPasteEditContext, token: CancellationToken): ProviderResult; + + /** + * Optional method which fills in the {@linkcode DocumentPasteEdit.additionalEdit} before the edit is applied. + * + * This is called once per edit and should be used if generating the complete edit may take a long time. + * Resolve can only be used to change {@linkcode DocumentPasteEdit.additionalEdit}. + * + * @param pasteEdit The {@linkcode DocumentPasteEdit} to resolve. + * @param token A cancellation token. + * + * @returns The resolved paste edit or a thenable that resolves to such. It is OK to return the given + * `pasteEdit`. If no result is returned, the given `pasteEdit` is used. + */ + resolveDocumentPasteEdit?(pasteEdit: T, token: CancellationToken): ProviderResult; + } + + /** + * An edit the applies a paste operation. + */ + class DocumentPasteEdit { + + /** + * Human readable label that describes the edit. + */ + title: string; + + /** + * {@link DocumentDropOrPasteEditKind Kind} of the edit. + */ + kind: DocumentDropOrPasteEditKind; + + /** + * The text or snippet to insert at the pasted locations. + * + * If your edit requires more advanced insertion logic, set this to an empty string and provide an {@link DocumentPasteEdit.additionalEdit additional edit} instead. + */ + insertText: string | SnippetString; + + /** + * An optional additional edit to apply on paste. + */ + additionalEdit?: WorkspaceEdit; + + /** + * Controls ordering when multiple paste edits can potentially be applied. + * + * If this edit yields to another, it will be shown lower in the list of possible paste edits shown to the user. + */ + yieldTo?: readonly DocumentDropOrPasteEditKind[]; + + /** + * Create a new paste edit. + * + * @param insertText The text or snippet to insert at the pasted locations. + * @param title Human readable label that describes the edit. + * @param kind {@link DocumentDropOrPasteEditKind Kind} of the edit. + */ + constructor(insertText: string | SnippetString, title: string, kind: DocumentDropOrPasteEditKind); + } + + /** + * Provides additional metadata about how a {@linkcode DocumentPasteEditProvider} works. + */ + interface DocumentPasteProviderMetadata { + /** + * List of {@link DocumentDropOrPasteEditKind kinds} that the provider may return in {@linkcode DocumentPasteEditProvider.provideDocumentPasteEdits provideDocumentPasteEdits}. + * + * This is used to filter out providers when a specific {@link DocumentDropOrPasteEditKind kind} of edit is requested. + */ + readonly providedPasteEditKinds: readonly DocumentDropOrPasteEditKind[]; + + /** + * Mime types that {@linkcode DocumentPasteEditProvider.prepareDocumentPaste prepareDocumentPaste} may add on copy. + */ + readonly copyMimeTypes?: readonly string[]; + + /** + * Mime types that {@linkcode DocumentPasteEditProvider.provideDocumentPasteEdits provideDocumentPasteEdits} should be invoked for. + * + * This can either be an exact mime type such as `image/png`, or a wildcard pattern such as `image/*`. + * + * Use `text/uri-list` for resources dropped from the explorer or other tree views in the workbench. + * + * Use `files` to indicate that the provider should be invoked if any {@link DataTransferFile files} are present in the {@linkcode DataTransfer}. + * Note that {@linkcode DataTransferFile} entries are only created when pasting content from outside the editor, such as + * from the operating system. + */ + readonly pasteMimeTypes?: readonly string[]; } /** @@ -14586,10 +14871,23 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider applies to. * @param provider A drop provider. + * @param metadata Additional metadata about the provider. + * + * @returns A {@link Disposable} that unregisters this provider when disposed of. + */ + export function registerDocumentDropEditProvider(selector: DocumentSelector, provider: DocumentDropEditProvider, metadata?: DocumentDropEditProviderMetadata): Disposable; + + /** + * Registers a new {@linkcode DocumentPasteEditProvider}. + * + * @param selector A selector that defines the documents this provider applies to. + * @param provider A paste editor provider. + * @param metadata Additional metadata about the provider. * * @returns A {@link Disposable} that unregisters this provider when disposed of. */ - export function registerDocumentDropEditProvider(selector: DocumentSelector, provider: DocumentDropEditProvider): Disposable; + export function registerDocumentPasteEditProvider(selector: DocumentSelector, provider: DocumentPasteEditProvider, metadata: DocumentPasteProviderMetadata): Disposable; + /** * Set a {@link LanguageConfiguration language configuration} for a language. diff --git a/src/vscode-dts/vscode.proposed.documentPaste.d.ts b/src/vscode-dts/vscode.proposed.documentPaste.d.ts deleted file mode 100644 index 2318a15212b8..000000000000 --- a/src/vscode-dts/vscode.proposed.documentPaste.d.ts +++ /dev/null @@ -1,316 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // https://github.com/microsoft/vscode/issues/30066/ - - /** - * Identifies a {@linkcode DocumentDropEdit} or {@linkcode DocumentPasteEdit} - */ - class DocumentDropOrPasteEditKind { - static readonly Empty: DocumentDropOrPasteEditKind; - - /** - * The root kind for basic text edits. - * - * This kind should be used for edits that insert basic text into the document. A good example of this is - * an edit that pastes the clipboard text while also updating imports in the file based on the pasted text. - * For this we could use a kind such as `text.updateImports.someLanguageId`. - * - * Even though most drop/paste edits ultimately insert text, you should not use {@linkcode Text} as the base kind - * for every edit as this is redundant. Instead a more specific kind that describes the type of content being - * inserted should be used instead For example, if the edit adds a Markdown link, use `markdown.link` since even - * though the content being inserted is text, it's more important to know that the edit inserts Markdown syntax. - */ - static readonly Text: DocumentDropOrPasteEditKind; - - private constructor(value: string); - - /** - * The raw string value of the kind. - */ - readonly value: string; - - /** - * Create a new kind by appending additional scopes to the current kind. - * - * Does not modify the current kind. - */ - append(...parts: string[]): DocumentDropOrPasteEditKind; - - /** - * Checks if this kind intersects `other`. - * - * The kind `"text.plain"` for example intersects `text`, `"text.plain"` and `"text.plain.list"`, - * but not `"unicorn"`, or `"textUnicorn.plain"`. - * - * @param other Kind to check. - */ - intersects(other: DocumentDropOrPasteEditKind): boolean; - - /** - * Checks if `other` is a sub-kind of this `DocumentDropOrPasteEditKind`. - * - * The kind `"text.plain"` for example contains `"text.plain"` and `"text.plain.list"`, - * but not `"text"` or `"unicorn.text.plain"`. - * - * @param other Kind to check. - */ - contains(other: DocumentDropOrPasteEditKind): boolean; - } - - /** - * The reason why paste edits were requested. - */ - export enum DocumentPasteTriggerKind { - /** - * Pasting was requested as part of a normal paste operation. - */ - Automatic = 0, - - /** - * Pasting was requested by the user with the `paste as` command. - */ - PasteAs = 1, - } - - /** - * Additional information about the paste operation. - */ - // TODO: Should we also have this for drop? - export interface DocumentPasteEditContext { - /** - * Requested kind of paste edits to return. - * - * When a explicit kind if requested by {@linkcode DocumentPasteTriggerKind.PasteAs PasteAs}, providers are - * encourage to be more flexible when generating an edit of the requested kind. - */ - readonly only: DocumentDropOrPasteEditKind | undefined; - - // TODO: should we also expose preferences? - - /** - * The reason why paste edits were requested. - */ - readonly triggerKind: DocumentPasteTriggerKind; - } - - /** - * Provider invoked when the user copies or pastes in a {@linkcode TextDocument}. - */ - interface DocumentPasteEditProvider { - - /** - * Optional method invoked after the user copies from a {@link TextEditor text editor}. - * - * This allows the provider to attach metadata about the copied text to the {@link DataTransfer}. This data - * transfer is then passed back to providers in {@linkcode provideDocumentPasteEdits}. - * - * Note that currently any changes to the {@linkcode DataTransfer} are isolated to the current editor window. - * This means that any added metadata cannot be seen by other editor windows or by other applications. - * - * @param document Text document where the copy took place. - * @param ranges Ranges being copied in {@linkcode document}. - * @param dataTransfer The data transfer associated with the copy. You can store additional values on this for - * later use in {@linkcode provideDocumentPasteEdits}. This object is only valid for the duration of this method. - * @param token A cancellation token. - * - * @return Optional thenable that resolves when all changes to the `dataTransfer` are complete. - */ - prepareDocumentPaste?(document: TextDocument, ranges: readonly Range[], dataTransfer: DataTransfer, token: CancellationToken): void | Thenable; - - /** - * Invoked before the user pastes into a {@link TextEditor text editor}. - * - * Returned edits can replace the standard pasting behavior. - * - * @param document Document being pasted into - * @param ranges Range in the {@linkcode document} to paste into. - * @param dataTransfer The {@link DataTransfer data transfer} associated with the paste. This object is only - * valid for the duration of the paste operation. - * @param context Additional context for the paste. - * @param token A cancellation token. - * - * @return Set of potential {@link DocumentPasteEdit edits} that can apply the paste. Only a single returned - * {@linkcode DocumentPasteEdit} is applied at a time. If multiple edits are returned from all providers, then - * the first is automatically applied and a widget is shown that lets the user switch to the other edits. - */ - provideDocumentPasteEdits?(document: TextDocument, ranges: readonly Range[], dataTransfer: DataTransfer, context: DocumentPasteEditContext, token: CancellationToken): ProviderResult; - - /** - * Optional method which fills in the {@linkcode DocumentPasteEdit.additionalEdit} before the edit is applied. - * - * This is called once per edit and should be used if generating the complete edit may take a long time. - * Resolve can only be used to change {@linkcode DocumentPasteEdit.additionalEdit}. - * - * @param pasteEdit The {@linkcode DocumentPasteEdit} to resolve. - * @param token A cancellation token. - * - * @returns The resolved paste edit or a thenable that resolves to such. It is OK to return the given - * `pasteEdit`. If no result is returned, the given `pasteEdit` is used. - */ - resolveDocumentPasteEdit?(pasteEdit: T, token: CancellationToken): ProviderResult; - } - - /** - * An edit the applies a paste operation. - */ - class DocumentPasteEdit { - - /** - * Human readable label that describes the edit. - */ - title: string; - - /** - * {@link DocumentDropOrPasteEditKind Kind} of the edit. - */ - kind: DocumentDropOrPasteEditKind; - - /** - * The text or snippet to insert at the pasted locations. - * - * If your edit requires more advanced insertion logic, set this to an empty string and provide an {@link DocumentPasteEdit.additionalEdit additional edit} instead. - */ - insertText: string | SnippetString; - - /** - * An optional additional edit to apply on paste. - */ - additionalEdit?: WorkspaceEdit; - - /** - * Controls ordering when multiple paste edits can potentially be applied. - * - * If this edit yields to another, it will be shown lower in the list of possible paste edits shown to the user. - */ - yieldTo?: readonly DocumentDropOrPasteEditKind[]; - - /** - * Create a new paste edit. - * - * @param insertText The text or snippet to insert at the pasted locations. - * @param title Human readable label that describes the edit. - * @param kind {@link DocumentDropOrPasteEditKind Kind} of the edit. - */ - constructor(insertText: string | SnippetString, title: string, kind: DocumentDropOrPasteEditKind); - } - - /** - * Provides additional metadata about how a {@linkcode DocumentPasteEditProvider} works. - */ - interface DocumentPasteProviderMetadata { - /** - * List of {@link DocumentDropOrPasteEditKind kinds} that the provider may return in {@linkcode DocumentPasteEditProvider.provideDocumentPasteEdits provideDocumentPasteEdits}. - * - * This is used to filter out providers when a specific {@link DocumentDropOrPasteEditKind kind} of edit is requested. - */ - readonly providedPasteEditKinds: readonly DocumentDropOrPasteEditKind[]; - - /** - * Mime types that {@linkcode DocumentPasteEditProvider.prepareDocumentPaste prepareDocumentPaste} may add on copy. - */ - readonly copyMimeTypes?: readonly string[]; - - /** - * Mime types that {@linkcode DocumentPasteEditProvider.provideDocumentPasteEdits provideDocumentPasteEdits} should be invoked for. - * - * This can either be an exact mime type such as `image/png`, or a wildcard pattern such as `image/*`. - * - * Use `text/uri-list` for resources dropped from the explorer or other tree views in the workbench. - * - * Use `files` to indicate that the provider should be invoked if any {@link DataTransferFile files} are present in the {@linkcode DataTransfer}. - * Note that {@linkcode DataTransferFile} entries are only created when pasting content from outside the editor, such as - * from the operating system. - */ - readonly pasteMimeTypes?: readonly string[]; - } - - /** - * TODO on finalization: - * - Add ctor(insertText: string | SnippetString, title?: string, kind?: DocumentDropOrPasteEditKind); Can't be done as this is an extension to an existing class - */ - - export interface DocumentDropEdit { - /** - * Human readable label that describes the edit. - */ - title?: string; - - /** - * {@link DocumentDropOrPasteEditKind Kind} of the edit. - */ - kind: DocumentDropOrPasteEditKind; - - /** - * Controls the ordering or multiple edits. If this provider yield to edits, it will be shown lower in the list. - */ - yieldTo?: readonly DocumentDropOrPasteEditKind[]; - } - - export interface DocumentDropEditProvider { - // Overload that allows returning multiple edits - // Will be merged in on finalization - provideDocumentDropEdits(document: TextDocument, position: Position, dataTransfer: DataTransfer, token: CancellationToken): ProviderResult; - - /** - * Optional method which fills in the {@linkcode DocumentDropEdit.additionalEdit} before the edit is applied. - * - * This is called once per edit and should be used if generating the complete edit may take a long time. - * Resolve can only be used to change {@link DocumentDropEdit.additionalEdit}. - * - * @param pasteEdit The {@linkcode DocumentDropEdit} to resolve. - * @param token A cancellation token. - * - * @returns The resolved edit or a thenable that resolves to such. It is OK to return the given - * `edit`. If no result is returned, the given `edit` is used. - */ - resolveDocumentDropEdit?(edit: T, token: CancellationToken): ProviderResult; - } - - /** - * Provides additional metadata about how a {@linkcode DocumentDropEditProvider} works. - */ - export interface DocumentDropEditProviderMetadata { - /** - * List of {@link DocumentDropOrPasteEditKind kinds} that the provider may return in {@linkcode DocumentDropEditProvider.provideDocumentDropEdits provideDocumentDropEdits}. - * - * This is used to filter out providers when a specific {@link DocumentDropOrPasteEditKind kind} of edit is requested. - */ - readonly providedDropEditKinds?: readonly DocumentDropOrPasteEditKind[]; - - /** - * List of {@link DataTransfer} mime types that the provider can handle. - * - * This can either be an exact mime type such as `image/png`, or a wildcard pattern such as `image/*`. - * - * Use `text/uri-list` for resources dropped from the explorer or other tree views in the workbench. - * - * Use `files` to indicate that the provider should be invoked if any {@link DataTransferFile files} are present in the {@link DataTransfer}. - * Note that {@link DataTransferFile} entries are only created when dropping content from outside the editor, such as - * from the operating system. - */ - readonly dropMimeTypes: readonly string[]; - } - - namespace languages { - /** - * Registers a new {@linkcode DocumentPasteEditProvider}. - * - * @param selector A selector that defines the documents this provider applies to. - * @param provider A paste editor provider. - * @param metadata Additional metadata about the provider. - * - * @returns A {@link Disposable} that unregisters this provider when disposed of. - */ - export function registerDocumentPasteEditProvider(selector: DocumentSelector, provider: DocumentPasteEditProvider, metadata: DocumentPasteProviderMetadata): Disposable; - - /** - * Overload which adds extra metadata. Will be removed on finalization. - */ - export function registerDocumentDropEditProvider(selector: DocumentSelector, provider: DocumentDropEditProvider, metadata?: DocumentDropEditProviderMetadata): Disposable; - } -} From 4f3043630dcc080b8205ba2e38555ff71f656876 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 7 Jan 2025 18:12:56 -0600 Subject: [PATCH 0391/3587] add chat edits a11y help (#237439) --- .../browser/actions/chatAccessibilityHelp.ts | 39 +++++++++++++++++-- .../contrib/chat/browser/chat.contribution.ts | 3 +- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index b1fcadf5d507..e935fda978c8 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -12,6 +12,7 @@ import { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType import { IAccessibleViewImplentation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; +import { ActiveAuxiliaryContext } from '../../../../common/contextkeys.js'; import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { INLINE_CHAT_ID } from '../../../inlineChat/common/inlineChat.js'; import { ChatAgentLocation } from '../../common/chatAgents.js'; @@ -22,7 +23,7 @@ export class PanelChatAccessibilityHelp implements IAccessibleViewImplentation { readonly priority = 107; readonly name = 'panelChat'; readonly type = AccessibleViewType.Help; - readonly when = ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ChatContextKeys.inQuickChat.negate(), ContextKeyExpr.or(ChatContextKeys.inChatSession, ChatContextKeys.isResponse, ChatContextKeys.isRequest)); + readonly when = ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ChatContextKeys.inQuickChat.negate(), ActiveAuxiliaryContext.isEqualTo('workbench.panel.chat'), ContextKeyExpr.or(ChatContextKeys.inChatSession, ChatContextKeys.isResponse, ChatContextKeys.isRequest)); getProvider(accessor: ServicesAccessor) { const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'panelChat'); @@ -40,7 +41,18 @@ export class QuickChatAccessibilityHelp implements IAccessibleViewImplentation { } } -export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'quickChat', keybindingService: IKeybindingService): string { +export class EditsChatAccessibilityHelp implements IAccessibleViewImplentation { + readonly priority = 119; + readonly name = 'editsView'; + readonly type = AccessibleViewType.Help; + readonly when = ActiveAuxiliaryContext.isEqualTo('workbench.panel.chatEditing'); + getProvider(accessor: ServicesAccessor) { + const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); + return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'editsView'); + } +} + +export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'quickChat' | 'editsView', keybindingService: IKeybindingService): string { const content = []; if (type === 'panelChat' || type === 'quickChat') { if (type === 'quickChat') { @@ -61,6 +73,25 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'qui content.push(localize('workbench.action.chat.newChat', 'To create a new chat session, invoke the New Chat command{0}.', '')); } } + if (type === 'editsView') { + content.push(localize('chatEditing.overview', 'The chat editing view is used to apply edits across files.')); + content.push(localize('chatEditing.format', 'It is comprised of an input box and a file working set (Shift+Tab).')); + content.push(localize('chatEditing.expectation', 'When a request is made, a progress indicator will play while the edits are being applied.')); + content.push(localize('chatEditing.review', 'Once the edits are applied, focus the editor(s) to review, accept, and discard changes.')); + content.push(localize('chatEditing.sections', 'Navigate between edits in the editor with navigate previous{0} and next{1}', '', '')); + content.push(localize('chatEditing.acceptHunk', 'In the editor, Accept{0} and Reject the current Change{1}.', '', '')); + content.push(localize('chatEditing.helpfulCommands', 'When in the edits view, some helpful commands include:')); + content.push(localize('workbench.action.chat.undoEdits', '- Undo Edits{0}.', '')); + content.push(localize('workbench.action.chat.editing.attachFiles', '- Attach Files{0}.', '')); + content.push(localize('chatEditing.removeFileFromWorkingSet', '- Remove File from Working Set{0}.', '')); + content.push(localize('chatEditing.acceptFile', '- Accept{0} and Discard File{1}.', '', '')); + content.push(localize('chatEditing.saveAllFiles', '- Save All Files{0}.', '')); + content.push(localize('chatEditing.acceptAllFiles', '- Accept All Edits{0}.', '')); + content.push(localize('chatEditing.discardAllFiles', '- Discard All Edits{0}.', '')); + content.push(localize('chatEditing.openFileInDiff', '- Open File in Diff{0}.', '')); + content.push(localize('chatEditing.addFileToWorkingSet', '- Add File to Working Set{0}.', '')); + content.push(localize('chatEditing.viewChanges', '- View Changes{0}.', '')); + } else { content.push(localize('inlineChat.overview', "Inline chat occurs within a code editor and takes into account the current selection. It is useful for making changes to the current editor. For example, fixing diagnostics, documenting or refactoring code. Keep in mind that AI generated code may be incorrect.")); content.push(localize('inlineChat.access', "It can be activated via code actions or directly using the command: Inline Chat: Start Inline Chat{0}.", '')); @@ -75,10 +106,10 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'qui return content.join('\n'); } -export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, editor: ICodeEditor | undefined, type: 'panelChat' | 'inlineChat' | 'quickChat') { +export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, editor: ICodeEditor | undefined, type: 'panelChat' | 'inlineChat' | 'quickChat' | 'editsView') { const widgetService = accessor.get(IChatWidgetService); const keybindingService = accessor.get(IKeybindingService); - const inputEditor: ICodeEditor | undefined = type === 'panelChat' || type === 'quickChat' ? widgetService.lastFocusedWidget?.inputEditor : editor; + const inputEditor: ICodeEditor | undefined = type === 'panelChat' || type === 'editsView' || type === 'quickChat' ? widgetService.lastFocusedWidget?.inputEditor : editor; if (!inputEditor) { return; diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 86b780b350c8..a43f6b50cacf 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -37,7 +37,7 @@ import { ILanguageModelStatsService, LanguageModelStatsService } from '../common import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; import { LanguageModelToolsExtensionPointHandler } from '../common/tools/languageModelToolsContribution.js'; import { IVoiceChatService, VoiceChatService } from '../common/voiceChatService.js'; -import { PanelChatAccessibilityHelp, QuickChatAccessibilityHelp } from './actions/chatAccessibilityHelp.js'; +import { EditsChatAccessibilityHelp, PanelChatAccessibilityHelp, QuickChatAccessibilityHelp } from './actions/chatAccessibilityHelp.js'; import { ChatCommandCenterRendering, registerChatActions } from './actions/chatActions.js'; import { ACTION_ID_NEW_CHAT, registerNewChatActions } from './actions/chatClearActions.js'; import { registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from './actions/chatCodeblockActions.js'; @@ -207,6 +207,7 @@ class ChatResolverContribution extends Disposable { AccessibleViewRegistry.register(new ChatResponseAccessibleView()); AccessibleViewRegistry.register(new PanelChatAccessibilityHelp()); AccessibleViewRegistry.register(new QuickChatAccessibilityHelp()); +AccessibleViewRegistry.register(new EditsChatAccessibilityHelp()); registerEditorFeature(ChatInputBoxContentProvider); From ade73d22b7b9595f69854a98b9dae450d03102b6 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 7 Jan 2025 18:56:42 -0800 Subject: [PATCH 0392/3587] Fix php date method signatures (#237451) Fix #237172 --- .../src/features/phpGlobalFunctions.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/extensions/php-language-features/src/features/phpGlobalFunctions.ts b/extensions/php-language-features/src/features/phpGlobalFunctions.ts index caf9eb11b711..ab1c5487ae86 100644 --- a/extensions/php-language-features/src/features/phpGlobalFunctions.ts +++ b/extensions/php-language-features/src/features/phpGlobalFunctions.ts @@ -1360,7 +1360,7 @@ export const globalfunctions: IEntries = { }, date_add: { description: 'Adds an amount of days, months, years, hours, minutes and seconds to a DateTime object', - signature: '( DateInterval $interval , DateTime $object ): DateTime' + signature: '( DateTime $object , DateInterval $interval ): DateTime' }, date_create: { description: 'Returns new DateTime object', @@ -1376,31 +1376,31 @@ export const globalfunctions: IEntries = { }, date_modify: { description: 'Alters the timestamp', - signature: '( string $modify , DateTime $object ): DateTime' + signature: '( DateTime $object , string $modify ): DateTime' }, date_date_set: { description: 'Sets the date', - signature: '( int $year , int $month , int $day , DateTime $object ): DateTime' + signature: '( DateTime $object , int $year , int $month , int $day ): DateTime' }, date_isodate_set: { description: 'Sets the ISO date', - signature: '( int $year , int $week [, int $day = 1 , DateTime $object ]): DateTime' + signature: '( DateTime $object , int $year , int $week [, int $day = 1 ]): DateTime' }, date_time_set: { description: 'Sets the time', - signature: '( int $hour , int $minute [, int $second = 0 [, int $microseconds = 0 , DateTime $object ]]): DateTime' + signature: '( DateTime $object , int $hour , int $minute [, int $second = 0 [, int $microseconds = 0 ]]): DateTime' }, date_timestamp_set: { description: 'Sets the date and time based on an Unix timestamp', - signature: '( int $unixtimestamp , DateTime $object ): DateTime' + signature: '( DateTime $object , int $unixtimestamp ): DateTime' }, date_timezone_set: { description: 'Sets the time zone for the DateTime object', - signature: '( DateTimeZone $timezone , DateTime $object ): object' + signature: '( DateTime $object , DateTimeZone $timezone ): object' }, date_sub: { description: 'Subtracts an amount of days, months, years, hours, minutes and seconds from a DateTime object', - signature: '( DateInterval $interval , DateTime $object ): DateTime' + signature: '( DateTime $object , DateInterval $interval ): DateTime' }, date_create_immutable: { description: 'Returns new DateTimeImmutable object', From cee47068303e4b9fdc07334f9a19f4fad67a1adf Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 7 Jan 2025 19:05:34 -0800 Subject: [PATCH 0393/3587] Make 'enter' a no-op while chat loads instead of sending it to the editor (#237453) Fix microsoft/vscode-copilot#10718 --- .../contrib/chat/browser/actions/chatExecuteActions.ts | 4 ++-- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 6557d40e8c78..caf69f26262a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -52,7 +52,7 @@ export class ChatSubmitAction extends SubmitAction { f1: false, category: CHAT_CATEGORY, icon: Codicon.send, - precondition: ContextKeyExpr.and(ChatContextKeys.inputHasText, ChatContextKeys.requestInProgress.negate(), ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession)), + precondition: ContextKeyExpr.and(ChatContextKeys.inputHasText, ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession)), keybinding: { when: ChatContextKeys.inChatInput, primary: KeyCode.Enter, @@ -131,7 +131,7 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { f1: false, category: CHAT_CATEGORY, icon: Codicon.send, - precondition: ContextKeyExpr.and(ChatContextKeys.inputHasText, ChatContextKeys.requestInProgress.negate(), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey.toNegated()), + precondition: ContextKeyExpr.and(ChatContextKeys.inputHasText, ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey.toNegated()), keybinding: { when: ChatContextKeys.inChatInput, primary: KeyCode.Enter, diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 3bb7d3b2285a..dc8397439704 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -985,6 +985,10 @@ export class ChatWidget extends Disposable implements IChatWidget { } private async _acceptInput(query: { query: string } | { prefix: string } | undefined, options?: IChatAcceptInputOptions): Promise { + if (this.viewModel?.requestInProgress) { + return; + } + if (this.viewModel) { this._onDidAcceptInput.fire(); if (!this.viewOptions.autoScroll) { From 9b0b13d9bfe21c3dfd227bfaa8ed5693e309a2e0 Mon Sep 17 00:00:00 2001 From: g122622 <51020363+g122622@users.noreply.github.com> Date: Wed, 8 Jan 2025 12:40:26 +0800 Subject: [PATCH 0394/3587] Scrollbar for File menu is displaying over Open Recent (#236998) --- src/vs/base/browser/ui/menu/menu.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index eefff7f8b484..8aec45d8facd 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -902,8 +902,6 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { // This allows the menu constructor to calculate the proper max height const computedStyles = getWindow(this.parentData.parent.domNode).getComputedStyle(this.parentData.parent.domNode); const paddingTop = parseFloat(computedStyles.paddingTop || '0') || 0; - // this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset - paddingTop}px`; - this.submenuContainer.style.zIndex = '1'; this.submenuContainer.style.position = 'fixed'; this.submenuContainer.style.top = '0'; this.submenuContainer.style.left = '0'; @@ -1371,6 +1369,10 @@ ${formatRule(Codicon.menuSubmenu)} height: 3px; width: 3px; } + /* Fix for https://github.com/microsoft/vscode/issues/103170 */ + .monaco-menu .action-item .monaco-submenu { + z-index: 1; + } `; // Scrollbars From b0d6d34fbbdd73e273715a6f68e5dfbc7bf6147b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 8 Jan 2025 08:04:48 +0100 Subject: [PATCH 0395/3587] Nonresponsive editor with large project when `typescript.tsserver.watchOptions: vscode` (fix #237351) (#237459) --- .../node/watcher/nodejs/nodejsWatcher.ts | 35 ++++++------------ .../node/watcher/parcel/parcelWatcher.ts | 36 +++++++------------ .../files/node/watcher/watcherStats.ts | 10 +++--- .../files/test/node/parcelWatcher.test.ts | 3 +- 4 files changed, 29 insertions(+), 55 deletions(-) diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts index 0c60edd91a97..6b40b9d4ba38 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts @@ -9,7 +9,6 @@ import { BaseWatcher } from '../baseWatcher.js'; import { isLinux } from '../../../../../base/common/platform.js'; import { INonRecursiveWatchRequest, INonRecursiveWatcher, IRecursiveWatcherWithSubscribe } from '../../../common/watcher.js'; import { NodeJSFileWatcherLibrary } from './nodejsWatcherLib.js'; -import { isEqual } from '../../../../../base/common/extpath.js'; export interface INodeJSWatcherInstance { @@ -28,7 +27,8 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher { readonly onDidError = Event.None; - readonly watchers = new Set(); + private readonly _watchers = new Map(); + get watchers() { return this._watchers.values(); } constructor(protected readonly recursiveWatcher: IRecursiveWatcherWithSubscribe | undefined) { super(); @@ -43,7 +43,7 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher { const requestsToStart: INonRecursiveWatchRequest[] = []; const watchersToStop = new Set(Array.from(this.watchers)); for (const request of requests) { - const watcher = this.findWatcher(request); + const watcher = this._watchers.get(this.requestToWatcherKey(request)); if (watcher && patternsEquals(watcher.request.excludes, request.excludes) && patternsEquals(watcher.request.includes, request.includes)) { watchersToStop.delete(watcher); // keep watcher } else { @@ -72,25 +72,12 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher { } } - private findWatcher(request: INonRecursiveWatchRequest): INodeJSWatcherInstance | undefined { - for (const watcher of this.watchers) { - - // Requests or watchers with correlation always match on that - if (typeof request.correlationId === 'number' || typeof watcher.request.correlationId === 'number') { - if (watcher.request.correlationId === request.correlationId) { - return watcher; - } - } - - // Non-correlated requests or watchers match on path - else { - if (isEqual(watcher.request.path, request.path, !isLinux /* ignorecase */)) { - return watcher; - } - } - } + private requestToWatcherKey(request: INonRecursiveWatchRequest): string | number { + return typeof request.correlationId === 'number' ? request.correlationId : this.pathToWatcherKey(request.path); + } - return undefined; + private pathToWatcherKey(path: string): string { + return isLinux ? path : path.toLowerCase() /* ignore path casing */; } private startWatching(request: INonRecursiveWatchRequest): void { @@ -100,7 +87,7 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher { // Remember as watcher instance const watcher: INodeJSWatcherInstance = { request, instance }; - this.watchers.add(watcher); + this._watchers.set(this.requestToWatcherKey(request), watcher); } override async stop(): Promise { @@ -114,7 +101,7 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher { private stopWatching(watcher: INodeJSWatcherInstance): void { this.trace(`stopping file watcher`, watcher); - this.watchers.delete(watcher); + this._watchers.delete(this.requestToWatcherKey(watcher.request)); watcher.instance.dispose(); } @@ -124,7 +111,6 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher { // Ignore requests for the same paths that have the same correlation for (const request of requests) { - const path = isLinux ? request.path : request.path.toLowerCase(); // adjust for case sensitivity let requestsForCorrelation = mapCorrelationtoRequests.get(request.correlationId); if (!requestsForCorrelation) { @@ -132,6 +118,7 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher { mapCorrelationtoRequests.set(request.correlationId, requestsForCorrelation); } + const path = this.pathToWatcherKey(request.path); if (requestsForCorrelation.has(path)) { this.trace(`ignoring a request for watching who's path is already watched: ${this.requestToString(request)}`); } diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index 3b47a55d6915..fbc46fb9f8c9 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -157,7 +157,8 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS private readonly _onDidError = this._register(new Emitter()); readonly onDidError = this._onDidError.event; - readonly watchers = new Set(); + private readonly _watchers = new Map(); + get watchers() { return this._watchers.values(); } // A delay for collecting file changes from Parcel // before collecting them for coalescing and emitting. @@ -206,7 +207,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS const requestsToStart: IRecursiveWatchRequest[] = []; const watchersToStop = new Set(Array.from(this.watchers)); for (const request of requests) { - const watcher = this.findWatcher(request); + const watcher = this._watchers.get(this.requestToWatcherKey(request)); if (watcher && patternsEquals(watcher.request.excludes, request.excludes) && patternsEquals(watcher.request.includes, request.includes) && watcher.request.pollingInterval === request.pollingInterval) { watchersToStop.delete(watcher); // keep watcher } else { @@ -238,25 +239,12 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS } } - private findWatcher(request: IRecursiveWatchRequest): ParcelWatcherInstance | undefined { - for (const watcher of this.watchers) { - - // Requests or watchers with correlation always match on that - if (this.isCorrelated(request) || this.isCorrelated(watcher.request)) { - if (watcher.request.correlationId === request.correlationId) { - return watcher; - } - } - - // Non-correlated requests or watchers match on path - else { - if (isEqual(watcher.request.path, request.path, !isLinux /* ignorecase */)) { - return watcher; - } - } - } + private requestToWatcherKey(request: IRecursiveWatchRequest): string | number { + return typeof request.correlationId === 'number' ? request.correlationId : this.pathToWatcherKey(request.path); + } - return undefined; + private pathToWatcherKey(path: string): string { + return isLinux ? path : path.toLowerCase() /* ignore path casing */; } private startPolling(request: IRecursiveWatchRequest, pollingInterval: number, restarts = 0): void { @@ -283,7 +271,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS unlinkSync(snapshotFile); } ); - this.watchers.add(watcher); + this._watchers.set(this.requestToWatcherKey(request), watcher); // Path checks for symbolic links / wrong casing const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); @@ -352,7 +340,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS await watcherInstance?.unsubscribe(); } ); - this.watchers.add(watcher); + this._watchers.set(this.requestToWatcherKey(request), watcher); // Path checks for symbolic links / wrong casing const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); @@ -643,7 +631,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS private async stopWatching(watcher: ParcelWatcherInstance, joinRestart?: Promise): Promise { this.trace(`stopping file watcher`, watcher); - this.watchers.delete(watcher); + this._watchers.delete(this.requestToWatcherKey(watcher.request)); try { await watcher.stop(joinRestart); @@ -666,7 +654,6 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS continue; // path is ignored entirely (via `**` glob exclude) } - const path = isLinux ? request.path : request.path.toLowerCase(); // adjust for case sensitivity let requestsForCorrelation = mapCorrelationtoRequests.get(request.correlationId); if (!requestsForCorrelation) { @@ -674,6 +661,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS mapCorrelationtoRequests.set(request.correlationId, requestsForCorrelation); } + const path = this.pathToWatcherKey(request.path); if (requestsForCorrelation.has(path)) { this.trace(`ignoring a request for watching who's path is already watched: ${this.requestToString(request)}`); } diff --git a/src/vs/platform/files/node/watcher/watcherStats.ts b/src/vs/platform/files/node/watcher/watcherStats.ts index b13a2187fb4c..eca99eddef90 100644 --- a/src/vs/platform/files/node/watcher/watcherStats.ts +++ b/src/vs/platform/files/node/watcher/watcherStats.ts @@ -34,8 +34,8 @@ export function computeStats( lines.push('[Summary]'); lines.push(`- Recursive Requests: total: ${allRecursiveRequests.length}, suspended: ${recursiveRequestsStatus.suspended}, polling: ${recursiveRequestsStatus.polling}, failed: ${failedRecursiveRequests}`); lines.push(`- Non-Recursive Requests: total: ${allNonRecursiveRequests.length}, suspended: ${nonRecursiveRequestsStatus.suspended}, polling: ${nonRecursiveRequestsStatus.polling}`); - lines.push(`- Recursive Watchers: total: ${recursiveWatcher.watchers.size}, active: ${recursiveWatcherStatus.active}, failed: ${recursiveWatcherStatus.failed}, stopped: ${recursiveWatcherStatus.stopped}`); - lines.push(`- Non-Recursive Watchers: total: ${nonRecursiveWatcher.watchers.size}, active: ${nonRecursiveWatcherStatus.active}, failed: ${nonRecursiveWatcherStatus.failed}, reusing: ${nonRecursiveWatcherStatus.reusing}`); + lines.push(`- Recursive Watchers: total: ${Array.from(recursiveWatcher.watchers).length}, active: ${recursiveWatcherStatus.active}, failed: ${recursiveWatcherStatus.failed}, stopped: ${recursiveWatcherStatus.stopped}`); + lines.push(`- Non-Recursive Watchers: total: ${Array.from(nonRecursiveWatcher.watchers).length}, active: ${nonRecursiveWatcherStatus.active}, failed: ${nonRecursiveWatcherStatus.failed}, reusing: ${nonRecursiveWatcherStatus.reusing}`); lines.push(`- I/O Handles Impact: total: ${recursiveRequestsStatus.polling + nonRecursiveRequestsStatus.polling + recursiveWatcherStatus.active + nonRecursiveWatcherStatus.active}`); lines.push(`\n[Recursive Requests (${allRecursiveRequests.length}, suspended: ${recursiveRequestsStatus.suspended}, polling: ${recursiveRequestsStatus.polling})]:`); @@ -106,7 +106,7 @@ function computeRecursiveWatchStatus(recursiveWatcher: ParcelWatcher): { active: let failed = 0; let stopped = 0; - for (const watcher of recursiveWatcher.watchers.values()) { + for (const watcher of recursiveWatcher.watchers) { if (!watcher.failed && !watcher.stopped) { active++; } @@ -189,7 +189,7 @@ function requestDetailsToString(request: IUniversalWatchRequest): string { } function fillRecursiveWatcherStats(lines: string[], recursiveWatcher: ParcelWatcher): void { - const watchers = sortByPathPrefix(Array.from(recursiveWatcher.watchers.values())); + const watchers = sortByPathPrefix(Array.from(recursiveWatcher.watchers)); const { active, failed, stopped } = computeRecursiveWatchStatus(recursiveWatcher); lines.push(`\n[Recursive Watchers (${watchers.length}, active: ${active}, failed: ${failed}, stopped: ${stopped})]:`); @@ -213,7 +213,7 @@ function fillRecursiveWatcherStats(lines: string[], recursiveWatcher: ParcelWatc } function fillNonRecursiveWatcherStats(lines: string[], nonRecursiveWatcher: NodeJSWatcher): void { - const allWatchers = sortByPathPrefix(Array.from(nonRecursiveWatcher.watchers.values())); + const allWatchers = sortByPathPrefix(Array.from(nonRecursiveWatcher.watchers)); const activeWatchers = allWatchers.filter(watcher => !watcher.instance.failed && !watcher.instance.isReusingRecursiveWatcher); const failedWatchers = allWatchers.filter(watcher => watcher.instance.failed); const reusingWatchers = allWatchers.filter(watcher => watcher.instance.isReusingRecursiveWatcher); diff --git a/src/vs/platform/files/test/node/parcelWatcher.test.ts b/src/vs/platform/files/test/node/parcelWatcher.test.ts index 558b81e19a9a..8b1824af3bd8 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.test.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.test.ts @@ -105,7 +105,7 @@ suite.skip('File Watcher (parcel)', function () { }); teardown(async () => { - const watchers = watcher.watchers.size; + const watchers = Array.from(watcher.watchers).length; let stoppedInstances = 0; for (const instance of watcher.watchers) { Event.once(instance.onDidStop)(() => { @@ -190,7 +190,6 @@ suite.skip('File Watcher (parcel)', function () { test('basics', async function () { const request = { path: testDir, excludes: [], recursive: true }; await watcher.watch([request]); - assert.strictEqual(watcher.watchers.size, watcher.watchers.size); const instance = Array.from(watcher.watchers)[0]; assert.strictEqual(request, instance.request); From 6b0c4399e0e2a0002d1cd31292e8ec3c4074e083 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 8 Jan 2025 10:01:54 +0100 Subject: [PATCH 0396/3587] fix `hash.ts` typings (#237464) --- src/vs/base/common/hash.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/vs/base/common/hash.ts b/src/vs/base/common/hash.ts index 5ab5846682b8..edaaf2b4f0ee 100644 --- a/src/vs/base/common/hash.ts +++ b/src/vs/base/common/hash.ts @@ -69,6 +69,13 @@ function objectHash(obj: any, initialHashVal: number): number { }, initialHashVal); } +// this is shared global between browsers and nodejs +declare const crypto: { + subtle: { + digest(a: string, b: ArrayBufferView): Promise; + }; +}; + /** Hashes the input as SHA-1, returning a hex-encoded string. */ export const hashAsync = (input: string | ArrayBufferView | VSBuffer) => { // Note: I would very much like to expose a streaming interface for hashing From 8ad5ac54b317867b6ff75b30f1392fff79104042 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 8 Jan 2025 10:32:33 +0100 Subject: [PATCH 0397/3587] debt - add `crypto.d.ts` file defining the crypto-global that's shared between nodejs and browsers (#237465) --- src/typings/crypto.d.ts | 83 ++++++++++++++++++++++++++++++++++++++ src/vs/base/common/hash.ts | 7 +--- src/vs/base/common/uuid.ts | 70 +------------------------------- 3 files changed, 85 insertions(+), 75 deletions(-) create mode 100644 src/typings/crypto.d.ts diff --git a/src/typings/crypto.d.ts b/src/typings/crypto.d.ts new file mode 100644 index 000000000000..378904595fb8 --- /dev/null +++ b/src/typings/crypto.d.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// NOTE that this is a partial copy from lib.dom.d.ts which is NEEDED because these utils are used in the /common/ +// layer which has no dependency on the DOM/browser-context. However, `crypto` is available as global in all browsers and +// in nodejs. Therefore it's OK to spell out its typings here + +declare global { + + /** + * This Web Crypto API interface provides a number of low-level cryptographic functions. It is accessed via the Crypto.subtle properties available in a window context (via Window.crypto). + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto) + */ + interface SubtleCrypto { + // /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/decrypt) */ + // decrypt(algorithm: AlgorithmIdentifier | RsaOaepParams | AesCtrParams | AesCbcParams | AesGcmParams, key: CryptoKey, data: BufferSource): Promise; + // /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveBits) */ + // deriveBits(algorithm: AlgorithmIdentifier | EcdhKeyDeriveParams | HkdfParams | Pbkdf2Params, baseKey: CryptoKey, length?: number | null): Promise; + // /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveKey) */ + // deriveKey(algorithm: AlgorithmIdentifier | EcdhKeyDeriveParams | HkdfParams | Pbkdf2Params, baseKey: CryptoKey, derivedKeyType: AlgorithmIdentifier | AesDerivedKeyParams | HmacImportParams | HkdfParams | Pbkdf2Params, extractable: boolean, keyUsages: KeyUsage[]): Promise; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) */ + digest(algorithm: { name: string } | string, data: ArrayBufferView | ArrayBuffer): Promise; + // /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/encrypt) */ + // encrypt(algorithm: AlgorithmIdentifier | RsaOaepParams | AesCtrParams | AesCbcParams | AesGcmParams, key: CryptoKey, data: BufferSource): Promise; + // /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/exportKey) */ + // exportKey(format: "jwk", key: CryptoKey): Promise; + // exportKey(format: Exclude, key: CryptoKey): Promise; + // exportKey(format: KeyFormat, key: CryptoKey): Promise; + // /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/generateKey) */ + // generateKey(algorithm: "Ed25519", extractable: boolean, keyUsages: ReadonlyArray<"sign" | "verify">): Promise; + // generateKey(algorithm: RsaHashedKeyGenParams | EcKeyGenParams, extractable: boolean, keyUsages: ReadonlyArray): Promise; + // generateKey(algorithm: AesKeyGenParams | HmacKeyGenParams | Pbkdf2Params, extractable: boolean, keyUsages: ReadonlyArray): Promise; + // generateKey(algorithm: AlgorithmIdentifier, extractable: boolean, keyUsages: KeyUsage[]): Promise; + // /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/importKey) */ + // importKey(format: "jwk", keyData: JsonWebKey, algorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm, extractable: boolean, keyUsages: ReadonlyArray): Promise; + // importKey(format: Exclude, keyData: BufferSource, algorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise; + // /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/sign) */ + // sign(algorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams, key: CryptoKey, data: BufferSource): Promise; + // /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/unwrapKey) */ + // unwrapKey(format: KeyFormat, wrappedKey: BufferSource, unwrappingKey: CryptoKey, unwrapAlgorithm: AlgorithmIdentifier | RsaOaepParams | AesCtrParams | AesCbcParams | AesGcmParams, unwrappedKeyAlgorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise; + // /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/verify) */ + // verify(algorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams, key: CryptoKey, signature: BufferSource, data: BufferSource): Promise; + // /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/wrapKey) */ + // wrapKey(format: KeyFormat, key: CryptoKey, wrappingKey: CryptoKey, wrapAlgorithm: AlgorithmIdentifier | RsaOaepParams | AesCtrParams | AesCbcParams | AesGcmParams): Promise; + } + + /** + * Basic cryptography features available in the current context. It allows access to a cryptographically strong random number generator and to cryptographic primitives. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto) + */ + interface Crypto { + /** + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/subtle) + */ + readonly subtle: SubtleCrypto; + /** + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/getRandomValues) + */ + getRandomValues(array: T): T; + /** + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/randomUUID) + */ + randomUUID(): `${string}-${string}-${string}-${string}-${string}`; + } + + var Crypto: { + prototype: Crypto; + new(): Crypto; + }; + + var crypto: Crypto; + +} +export { } diff --git a/src/vs/base/common/hash.ts b/src/vs/base/common/hash.ts index edaaf2b4f0ee..78697a888d74 100644 --- a/src/vs/base/common/hash.ts +++ b/src/vs/base/common/hash.ts @@ -69,12 +69,7 @@ function objectHash(obj: any, initialHashVal: number): number { }, initialHashVal); } -// this is shared global between browsers and nodejs -declare const crypto: { - subtle: { - digest(a: string, b: ArrayBufferView): Promise; - }; -}; + /** Hashes the input as SHA-1, returning a hex-encoded string. */ export const hashAsync = (input: string | ArrayBufferView | VSBuffer) => { diff --git a/src/vs/base/common/uuid.ts b/src/vs/base/common/uuid.ts index 0bd0c937caed..d1d08064f5af 100644 --- a/src/vs/base/common/uuid.ts +++ b/src/vs/base/common/uuid.ts @@ -10,72 +10,4 @@ export function isUUID(value: string): boolean { return _UUIDPattern.test(value); } -declare const crypto: undefined | { - //https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#browser_compatibility - getRandomValues?(data: Uint8Array): Uint8Array; - //https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID#browser_compatibility - randomUUID?(): string; -}; - -export const generateUuid = (function (): () => string { - - // use `randomUUID` if possible - if (typeof crypto === 'object' && typeof crypto.randomUUID === 'function') { - return crypto.randomUUID.bind(crypto); - } - - // use `randomValues` if possible - let getRandomValues: (bucket: Uint8Array) => Uint8Array; - if (typeof crypto === 'object' && typeof crypto.getRandomValues === 'function') { - getRandomValues = crypto.getRandomValues.bind(crypto); - - } else { - getRandomValues = function (bucket: Uint8Array): Uint8Array { - for (let i = 0; i < bucket.length; i++) { - bucket[i] = Math.floor(Math.random() * 256); - } - return bucket; - }; - } - - // prep-work - const _data = new Uint8Array(16); - const _hex: string[] = []; - for (let i = 0; i < 256; i++) { - _hex.push(i.toString(16).padStart(2, '0')); - } - - return function generateUuid(): string { - // get data - getRandomValues(_data); - - // set version bits - _data[6] = (_data[6] & 0x0f) | 0x40; - _data[8] = (_data[8] & 0x3f) | 0x80; - - // print as string - let i = 0; - let result = ''; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += '-'; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += '-'; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += '-'; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += '-'; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - return result; - }; -})(); +export const generateUuid: () => string = crypto.randomUUID.bind(crypto); From 46fdeafcce703b28f9eb3ba1083bedbca1fc6f2d Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 8 Jan 2025 11:24:27 +0100 Subject: [PATCH 0398/3587] Update tree-sitter-wasm (#237466) --- package-lock.json | 9 +++++---- package.json | 2 +- remote/package-lock.json | 9 +++++---- remote/package.json | 2 +- remote/web/package-lock.json | 9 +++++---- remote/web/package.json | 2 +- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7750736d8398..7f69a0019637 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", "@vscode/sudo-prompt": "9.3.1", - "@vscode/tree-sitter-wasm": "^0.0.4", + "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", @@ -3173,9 +3173,10 @@ } }, "node_modules/@vscode/tree-sitter-wasm": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.4.tgz", - "integrity": "sha512-vOONG3Zxsh1I4JOA48WdQ5KiXjJAdfMvYTuHbW7b27tGtRqsPLY5WZyTwLXc5uujKHyhG3LJXE9poxRZSxTIiA==" + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.5.tgz", + "integrity": "sha512-qA+BkB2UgkfXMQVGsqPeG3vR3pXv0inP6WQ/dq6BALy7dIX9KQvGXvDCiqehdFvZZO4tDFt4qb5DdSsvwR4Y9Q==", + "license": "MIT" }, "node_modules/@vscode/v8-heap-parser": { "version": "0.1.0", diff --git a/package.json b/package.json index 66df5510d218..50b2f8c444e0 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", "@vscode/sudo-prompt": "9.3.1", - "@vscode/tree-sitter-wasm": "^0.0.4", + "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", diff --git a/remote/package-lock.json b/remote/package-lock.json index 53a1dddc2924..cc8a96de9adc 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -16,7 +16,7 @@ "@vscode/proxy-agent": "^0.28.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", - "@vscode/tree-sitter-wasm": "^0.0.4", + "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", @@ -468,9 +468,10 @@ } }, "node_modules/@vscode/tree-sitter-wasm": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.4.tgz", - "integrity": "sha512-vOONG3Zxsh1I4JOA48WdQ5KiXjJAdfMvYTuHbW7b27tGtRqsPLY5WZyTwLXc5uujKHyhG3LJXE9poxRZSxTIiA==" + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.5.tgz", + "integrity": "sha512-qA+BkB2UgkfXMQVGsqPeG3vR3pXv0inP6WQ/dq6BALy7dIX9KQvGXvDCiqehdFvZZO4tDFt4qb5DdSsvwR4Y9Q==", + "license": "MIT" }, "node_modules/@vscode/vscode-languagedetection": { "version": "1.0.21", diff --git a/remote/package.json b/remote/package.json index 09dc79f4b0ae..5e92099c7e93 100644 --- a/remote/package.json +++ b/remote/package.json @@ -11,7 +11,7 @@ "@vscode/proxy-agent": "^0.28.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", - "@vscode/tree-sitter-wasm": "^0.0.4", + "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 6712d40c6282..8b10d83600df 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -11,7 +11,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/tree-sitter-wasm": "^0.0.4", + "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", "@xterm/addon-clipboard": "^0.2.0-beta.57", "@xterm/addon-image": "^0.9.0-beta.74", @@ -75,9 +75,10 @@ "integrity": "sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg==" }, "node_modules/@vscode/tree-sitter-wasm": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.4.tgz", - "integrity": "sha512-vOONG3Zxsh1I4JOA48WdQ5KiXjJAdfMvYTuHbW7b27tGtRqsPLY5WZyTwLXc5uujKHyhG3LJXE9poxRZSxTIiA==" + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.5.tgz", + "integrity": "sha512-qA+BkB2UgkfXMQVGsqPeG3vR3pXv0inP6WQ/dq6BALy7dIX9KQvGXvDCiqehdFvZZO4tDFt4qb5DdSsvwR4Y9Q==", + "license": "MIT" }, "node_modules/@vscode/vscode-languagedetection": { "version": "1.0.21", diff --git a/remote/web/package.json b/remote/web/package.json index 31f552e7f5c9..9b342d347e69 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -6,7 +6,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/tree-sitter-wasm": "^0.0.4", + "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", "@xterm/addon-clipboard": "^0.2.0-beta.57", "@xterm/addon-image": "^0.9.0-beta.74", From be5c64a3df273ba6556ff76f41524d922931c54c Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 8 Jan 2025 11:43:30 +0100 Subject: [PATCH 0399/3587] Revert "Enable edit context" (#237468) Revert "Enable edit context (#235386)" This reverts commit 3958e26f650620710d7bc02e70a580cc18c0cbc5. --- src/typings/editContext.d.ts | 4 +- src/vs/editor/common/config/editorOptions.ts | 3 +- .../services/driver/browser/driver.ts | 44 ++++++------------- test/automation/src/code.ts | 9 ++-- test/automation/src/debug.ts | 9 ++-- test/automation/src/editor.ts | 15 +++---- test/automation/src/editors.ts | 3 +- test/automation/src/extensions.ts | 3 +- test/automation/src/notebook.ts | 7 ++- test/automation/src/scm.ts | 16 +++---- test/automation/src/settings.ts | 14 ++---- 11 files changed, 41 insertions(+), 86 deletions(-) diff --git a/src/typings/editContext.d.ts b/src/typings/editContext.d.ts index 095858486671..5b5da0ac7e95 100644 --- a/src/typings/editContext.d.ts +++ b/src/typings/editContext.d.ts @@ -58,8 +58,8 @@ interface EditContextEventHandlersEventMap { type EventHandler = (event: TEvent) => void; -declare class TextUpdateEvent extends Event { - constructor(type: DOMString, options?: TextUpdateEventInit); +interface TextUpdateEvent extends Event { + new(type: DOMString, options?: TextUpdateEventInit): TextUpdateEvent; readonly updateRangeStart: number; readonly updateRangeEnd: number; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 1847c2ff3456..5fe2028e3666 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -16,7 +16,6 @@ import { USUAL_WORD_SEPARATORS } from '../core/wordHelper.js'; import * as nls from '../../../nls.js'; import { AccessibilitySupport } from '../../../platform/accessibility/common/accessibility.js'; import { IConfigurationPropertySchema } from '../../../platform/configuration/common/configurationRegistry.js'; -import product from '../../../platform/product/common/product.js'; //#region typed options @@ -5823,7 +5822,7 @@ export const EditorOptions = { emptySelectionClipboard: register(new EditorEmptySelectionClipboard()), dropIntoEditor: register(new EditorDropIntoEditor()), experimentalEditContextEnabled: register(new EditorBooleanOption( - EditorOption.experimentalEditContextEnabled, 'experimentalEditContextEnabled', product.quality !== 'stable', + EditorOption.experimentalEditContextEnabled, 'experimentalEditContextEnabled', false, { description: nls.localize('experimentalEditContextEnabled', "Sets whether the new experimental edit context should be used instead of the text area."), included: platform.isChrome || platform.isEdge || platform.isNative diff --git a/src/vs/workbench/services/driver/browser/driver.ts b/src/vs/workbench/services/driver/browser/driver.ts index ad689b9db6f3..d78e55aa9737 100644 --- a/src/vs/workbench/services/driver/browser/driver.ts +++ b/src/vs/workbench/services/driver/browser/driver.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getClientArea, getTopLeftOffset, isHTMLDivElement, isHTMLTextAreaElement } from '../../../../base/browser/dom.js'; +import { getClientArea, getTopLeftOffset } from '../../../../base/browser/dom.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { language, locale } from '../../../../base/common/platform.js'; @@ -133,36 +133,18 @@ export class BrowserWindowDriver implements IWindowDriver { if (!element) { throw new Error(`Editor not found: ${selector}`); } - if (isHTMLDivElement(element)) { - // Edit context is enabled - const editContext = element.editContext; - if (!editContext) { - throw new Error(`Edit context not found: ${selector}`); - } - const selectionStart = editContext.selectionStart; - const selectionEnd = editContext.selectionEnd; - const event = new TextUpdateEvent('textupdate', { - updateRangeStart: selectionStart, - updateRangeEnd: selectionEnd, - text, - selectionStart: selectionStart + text.length, - selectionEnd: selectionStart + text.length, - compositionStart: 0, - compositionEnd: 0 - }); - editContext.dispatchEvent(event); - } else if (isHTMLTextAreaElement(element)) { - const start = element.selectionStart; - const newStart = start + text.length; - const value = element.value; - const newValue = value.substr(0, start) + text + value.substr(start); - - element.value = newValue; - element.setSelectionRange(newStart, newStart); - - const event = new Event('input', { 'bubbles': true, 'cancelable': true }); - element.dispatchEvent(event); - } + + const textarea = element as HTMLTextAreaElement; + const start = textarea.selectionStart; + const newStart = start + text.length; + const value = textarea.value; + const newValue = value.substr(0, start) + text + value.substr(start); + + textarea.value = newValue; + textarea.setSelectionRange(newStart, newStart); + + const event = new Event('input', { 'bubbles': true, 'cancelable': true }); + textarea.dispatchEvent(event); } async getTerminalBuffer(selector: string): Promise { diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index fd64b7cdeb1a..a925cdd65bc9 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -12,7 +12,6 @@ import { launch as launchPlaywrightBrowser } from './playwrightBrowser'; import { PlaywrightDriver } from './playwrightDriver'; import { launch as launchPlaywrightElectron } from './playwrightElectron'; import { teardown } from './processes'; -import { Quality } from './application'; export interface LaunchOptions { codePath?: string; @@ -29,7 +28,6 @@ export interface LaunchOptions { readonly tracing?: boolean; readonly headless?: boolean; readonly browser?: 'chromium' | 'webkit' | 'firefox'; - readonly quality: Quality; } interface ICodeInstance { @@ -79,7 +77,7 @@ export async function launch(options: LaunchOptions): Promise { const { serverProcess, driver } = await measureAndLog(() => launchPlaywrightBrowser(options), 'launch playwright (browser)', options.logger); registerInstance(serverProcess, options.logger, 'server'); - return new Code(driver, options.logger, serverProcess, options.quality); + return new Code(driver, options.logger, serverProcess); } // Electron smoke tests (playwright) @@ -87,7 +85,7 @@ export async function launch(options: LaunchOptions): Promise { const { electronProcess, driver } = await measureAndLog(() => launchPlaywrightElectron(options), 'launch playwright (electron)', options.logger); registerInstance(electronProcess, options.logger, 'electron'); - return new Code(driver, options.logger, electronProcess, options.quality); + return new Code(driver, options.logger, electronProcess); } } @@ -98,8 +96,7 @@ export class Code { constructor( driver: PlaywrightDriver, readonly logger: Logger, - private readonly mainProcess: cp.ChildProcess, - readonly quality: Quality + private readonly mainProcess: cp.ChildProcess ) { this.driver = new Proxy(driver, { get(target, prop) { diff --git a/test/automation/src/debug.ts b/test/automation/src/debug.ts index e2e227fc35e1..b7b7d427f4b0 100644 --- a/test/automation/src/debug.ts +++ b/test/automation/src/debug.ts @@ -9,7 +9,6 @@ import { Code, findElement } from './code'; import { Editors } from './editors'; import { Editor } from './editor'; import { IElement } from './driver'; -import { Quality } from './application'; const VIEWLET = 'div[id="workbench.view.debug"]'; const DEBUG_VIEW = `${VIEWLET}`; @@ -32,8 +31,7 @@ const CONSOLE_OUTPUT = `.repl .output.expression .value`; const CONSOLE_EVALUATION_RESULT = `.repl .evaluation-result.expression .value`; const CONSOLE_LINK = `.repl .value a.link`; -const REPL_FOCUSED_NATIVE_EDIT_CONTEXT = '.repl-input-wrapper .monaco-editor .native-edit-context'; -const REPL_FOCUSED_TEXTAREA = '.repl-input-wrapper .monaco-editor textarea'; +const REPL_FOCUSED = '.repl-input-wrapper .monaco-editor textarea'; export interface IStackFrame { name: string; @@ -129,9 +127,8 @@ export class Debug extends Viewlet { async waitForReplCommand(text: string, accept: (result: string) => boolean): Promise { await this.commands.runCommand('Debug: Focus on Debug Console View'); - const selector = this.code.quality === Quality.Stable ? REPL_FOCUSED_TEXTAREA : REPL_FOCUSED_NATIVE_EDIT_CONTEXT; - await this.code.waitForActiveElement(selector); - await this.code.waitForSetValue(selector, text); + await this.code.waitForActiveElement(REPL_FOCUSED); + await this.code.waitForSetValue(REPL_FOCUSED, text); // Wait for the keys to be picked up by the editor model such that repl evaluates what just got typed await this.editor.waitForEditorContents('debug:replinput', s => s.indexOf(text) >= 0); diff --git a/test/automation/src/editor.ts b/test/automation/src/editor.ts index dd6160795650..538866bfc060 100644 --- a/test/automation/src/editor.ts +++ b/test/automation/src/editor.ts @@ -6,7 +6,6 @@ import { References } from './peek'; import { Commands } from './workbench'; import { Code } from './code'; -import { Quality } from './application'; const RENAME_BOX = '.monaco-editor .monaco-editor.rename-box'; const RENAME_INPUT = `${RENAME_BOX} .rename-input`; @@ -79,10 +78,10 @@ export class Editor { async waitForEditorFocus(filename: string, lineNumber: number, selectorPrefix = ''): Promise { const editor = [selectorPrefix || '', EDITOR(filename)].join(' '); const line = `${editor} .view-lines > .view-line:nth-child(${lineNumber})`; - const editContext = `${editor} ${this._editContextSelector()}`; + const textarea = `${editor} textarea`; await this.code.waitAndClick(line, 1, 1); - await this.code.waitForActiveElement(editContext); + await this.code.waitForActiveElement(textarea); } async waitForTypeInEditor(filename: string, text: string, selectorPrefix = ''): Promise { @@ -93,18 +92,14 @@ export class Editor { await this.code.waitForElement(editor); - const editContext = `${editor} ${this._editContextSelector()}`; - await this.code.waitForActiveElement(editContext); + const textarea = `${editor} textarea`; + await this.code.waitForActiveElement(textarea); - await this.code.waitForTypeInEditor(editContext, text); + await this.code.waitForTypeInEditor(textarea, text); await this.waitForEditorContents(filename, c => c.indexOf(text) > -1, selectorPrefix); } - private _editContextSelector() { - return this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'; - } - async waitForEditorContents(filename: string, accept: (contents: string) => boolean, selectorPrefix = ''): Promise { const selector = [selectorPrefix || '', `${EDITOR(filename)} .view-lines`].join(' '); return this.code.waitForTextContent(selector, undefined, c => accept(c.replace(/\u00a0/g, ' '))); diff --git a/test/automation/src/editors.ts b/test/automation/src/editors.ts index 472385c8534d..b3a914ffff02 100644 --- a/test/automation/src/editors.ts +++ b/test/automation/src/editors.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Quality } from './application'; import { Code } from './code'; export class Editors { @@ -54,7 +53,7 @@ export class Editors { } async waitForActiveEditor(fileName: string, retryCount?: number): Promise { - const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`; + const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] textarea`; return this.code.waitForActiveElement(selector, retryCount); } diff --git a/test/automation/src/extensions.ts b/test/automation/src/extensions.ts index c881e4fd8dc6..2a481f9fe766 100644 --- a/test/automation/src/extensions.ts +++ b/test/automation/src/extensions.ts @@ -8,7 +8,6 @@ import { Code } from './code'; import { ncp } from 'ncp'; import { promisify } from 'util'; import { Commands } from './workbench'; -import { Quality } from './application'; import path = require('path'); import fs = require('fs'); @@ -21,7 +20,7 @@ export class Extensions extends Viewlet { async searchForExtension(id: string): Promise { await this.commands.runCommand('Extensions: Focus on Extensions View', { exactLabelMatch: true }); - await this.code.waitForTypeInEditor(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`, `@id:${id}`); + await this.code.waitForTypeInEditor('div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor textarea', `@id:${id}`); await this.code.waitForTextContent(`div.part.sidebar div.composite.title h2`, 'Extensions: Marketplace'); let retrials = 1; diff --git a/test/automation/src/notebook.ts b/test/automation/src/notebook.ts index cd46cbdb0dd4..dff250027db7 100644 --- a/test/automation/src/notebook.ts +++ b/test/automation/src/notebook.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Quality } from './application'; import { Code } from './code'; import { QuickAccess } from './quickaccess'; import { QuickInput } from './quickinput'; @@ -47,10 +46,10 @@ export class Notebook { await this.code.waitForElement(editor); - const editContext = `${editor} ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`; - await this.code.waitForActiveElement(editContext); + const textarea = `${editor} textarea`; + await this.code.waitForActiveElement(textarea); - await this.code.waitForTypeInEditor(editContext, text); + await this.code.waitForTypeInEditor(textarea, text); await this._waitForActiveCellEditorContents(c => c.indexOf(text) > -1); } diff --git a/test/automation/src/scm.ts b/test/automation/src/scm.ts index 6489badbe8a4..9f950f2b16a7 100644 --- a/test/automation/src/scm.ts +++ b/test/automation/src/scm.ts @@ -6,11 +6,9 @@ import { Viewlet } from './viewlet'; import { IElement } from './driver'; import { findElement, findElements, Code } from './code'; -import { Quality } from './application'; const VIEWLET = 'div[id="workbench.view.scm"]'; -const SCM_INPUT_NATIVE_EDIT_CONTEXT = `${VIEWLET} .scm-editor .native-edit-context`; -const SCM_INPUT_TEXTAREA = `${VIEWLET} .scm-editor textarea`; +const SCM_INPUT = `${VIEWLET} .scm-editor textarea`; const SCM_RESOURCE = `${VIEWLET} .monaco-list-row .resource`; const REFRESH_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Refresh"]`; const COMMIT_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Commit"]`; @@ -46,7 +44,7 @@ export class SCM extends Viewlet { async openSCMViewlet(): Promise { await this.code.dispatchKeybinding('ctrl+shift+g'); - await this.code.waitForElement(this._editContextSelector()); + await this.code.waitForElement(SCM_INPUT); } async waitForChange(name: string, type?: string): Promise { @@ -73,13 +71,9 @@ export class SCM extends Viewlet { } async commit(message: string): Promise { - await this.code.waitAndClick(this._editContextSelector()); - await this.code.waitForActiveElement(this._editContextSelector()); - await this.code.waitForSetValue(this._editContextSelector(), message); + await this.code.waitAndClick(SCM_INPUT); + await this.code.waitForActiveElement(SCM_INPUT); + await this.code.waitForSetValue(SCM_INPUT, message); await this.code.waitAndClick(COMMIT_COMMAND); } - - private _editContextSelector(): string { - return this.code.quality === Quality.Stable ? SCM_INPUT_TEXTAREA : SCM_INPUT_NATIVE_EDIT_CONTEXT; - } } diff --git a/test/automation/src/settings.ts b/test/automation/src/settings.ts index 8cf221b1487b..68401eb0edaa 100644 --- a/test/automation/src/settings.ts +++ b/test/automation/src/settings.ts @@ -7,10 +7,8 @@ import { Editor } from './editor'; import { Editors } from './editors'; import { Code } from './code'; import { QuickAccess } from './quickaccess'; -import { Quality } from './application'; -const SEARCH_BOX_NATIVE_EDIT_CONTEXT = '.settings-editor .suggest-input-container .monaco-editor .native-edit-context'; -const SEARCH_BOX_TEXTAREA = '.settings-editor .suggest-input-container .monaco-editor textarea'; +const SEARCH_BOX = '.settings-editor .suggest-input-container .monaco-editor textarea'; export class SettingsEditor { constructor(private code: Code, private editors: Editors, private editor: Editor, private quickaccess: QuickAccess) { } @@ -59,13 +57,13 @@ export class SettingsEditor { async openUserSettingsUI(): Promise { await this.quickaccess.runCommand('workbench.action.openSettings2'); - await this.code.waitForActiveElement(this._editContextSelector()); + await this.code.waitForActiveElement(SEARCH_BOX); } async searchSettingsUI(query: string): Promise { await this.openUserSettingsUI(); - await this.code.waitAndClick(this._editContextSelector()); + await this.code.waitAndClick(SEARCH_BOX); if (process.platform === 'darwin') { await this.code.dispatchKeybinding('cmd+a'); } else { @@ -73,11 +71,7 @@ export class SettingsEditor { } await this.code.dispatchKeybinding('Delete'); await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => !results || (results?.length === 1 && !results[0].textContent)); - await this.code.waitForTypeInEditor(this._editContextSelector(), query); + await this.code.waitForTypeInEditor('.settings-editor .suggest-input-container .monaco-editor textarea', query); await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => results?.length === 1 && results[0].textContent.includes('Found')); } - - private _editContextSelector() { - return this.code.quality === Quality.Stable ? SEARCH_BOX_TEXTAREA : SEARCH_BOX_NATIVE_EDIT_CONTEXT; - } } From 6ac92b9bda306ffb418f3e3d50dc7aefcf597877 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 8 Jan 2025 12:11:35 +0100 Subject: [PATCH 0400/3587] watcher - drop expensive `realcase` use for non recursive watching (#237472) In testing with many thousand individual files, this operation is very expensive. Refs: https://github.com/microsoft/vscode/issues/237351 --- .../node/watcher/nodejs/nodejsWatcherLib.ts | 26 +++++++++---------- .../node/watcher/parcel/parcelWatcher.ts | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts index a7c89ceb00e9..35ea5eea71f6 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts @@ -6,14 +6,14 @@ import { watch, promises } from 'fs'; import { RunOnceWorker, ThrottledWorker } from '../../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; -import { isEqualOrParent } from '../../../../../base/common/extpath.js'; +import { isEqual, isEqualOrParent } from '../../../../../base/common/extpath.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { normalizeNFC } from '../../../../../base/common/normalization.js'; import { basename, dirname, join } from '../../../../../base/common/path.js'; import { isLinux, isMacintosh } from '../../../../../base/common/platform.js'; import { joinPath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; -import { realcase } from '../../../../../base/node/extpath.js'; +import { realpath } from '../../../../../base/node/extpath.js'; import { Promises } from '../../../../../base/node/pfs.js'; import { FileChangeType, IFileChange } from '../../../common/files.js'; import { ILogMessage, coalesceEvents, INonRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, isWatchRequestWithCorrelation } from '../../../common/watcher.js'; @@ -111,18 +111,18 @@ export class NodeJSFileWatcherLibrary extends Disposable { try { - // First check for symbolic link - realPath = await Promises.realpath(request.path); + // Check for symbolic link + realPath = await realpath(request.path); - // Second check for casing difference - // Note: this will be a no-op on Linux platforms - if (request.path === realPath) { - realPath = await realcase(request.path, this.cts.token) ?? request.path; - } + // Note: we used to also call `realcase()` here, but + // that operation is very expensive for large amounts + // of files and is actually not needed for single + // file/folder watching where we report on the original + // path anyway. + // (https://github.com/microsoft/vscode/issues/237351) - // Correct watch path as needed if (request.path !== realPath) { - this.trace(`correcting a path to watch that seems to be a symbolic link or wrong casing (original: ${request.path}, real: ${realPath})`); + this.trace(`correcting a path to watch that seems to be a symbolic link (original: ${request.path}, real: ${realPath})`); } } catch (error) { // ignore @@ -311,7 +311,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { // file watching specifically we want to handle // the atomic-write cases where the file is being // deleted and recreated with different contents. - if (changedFileName === pathBasename && !await Promises.exists(realPath)) { + if (isEqual(changedFileName, pathBasename, !isLinux) && !await Promises.exists(realPath)) { this.onWatchedPathDeleted(requestResource); return; @@ -374,7 +374,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { else { // File added/deleted - if (type === 'rename' || changedFileName !== pathBasename) { + if (type === 'rename' || !isEqual(changedFileName, pathBasename, !isLinux)) { // Depending on the OS the watcher runs on, there // is different behaviour for when the watched diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index fbc46fb9f8c9..f6eea7fb0e52 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as parcelWatcher from '@parcel/watcher'; +import parcelWatcher from '@parcel/watcher'; import { statSync, unlinkSync } from 'fs'; import { tmpdir, homedir } from 'os'; import { URI } from '../../../../../base/common/uri.js'; From a46a23d6fccc5a95ac04a3d7c11c004725b254dd Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 8 Jan 2025 12:32:07 +0100 Subject: [PATCH 0401/3587] chat setup - log sign up error code (#237475) --- .../contrib/chat/browser/chatSetup.ts | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 52bb5bea1b0f..596f67188405 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -591,7 +591,7 @@ class ChatSetupRequests extends Disposable { return this.resolveEntitlement(session, CancellationToken.None); } - async signUpLimited(session: AuthenticationSession): Promise { + async signUpLimited(session: AuthenticationSession): Promise { const body = { restricted_telemetry: this.telemetryService.telemetryLevel === TelemetryLevel.NONE ? 'disabled' : 'enabled', public_code_suggestions: 'enabled' @@ -600,7 +600,7 @@ class ChatSetupRequests extends Disposable { const response = await this.request(defaultChat.entitlementSignupLimitedUrl, 'POST', body, session, CancellationToken.None); if (!response) { this.onUnknownSignUpError('[chat setup] sign-up: no response'); - return undefined; + return { errorCode: 1 }; } if (response.res.statusCode && response.res.statusCode !== 200) { @@ -611,7 +611,7 @@ class ChatSetupRequests extends Disposable { const responseError: { message: string } = JSON.parse(responseText); if (typeof responseError.message === 'string' && responseError.message) { this.onUnprocessableSignUpError(`[chat setup] sign-up: unprocessable entity (${responseError.message})`, responseError.message); - return undefined; + return { errorCode: response.res.statusCode }; } } } catch (error) { @@ -619,7 +619,7 @@ class ChatSetupRequests extends Disposable { } } this.onUnknownSignUpError(`[chat setup] sign-up: unexpected status code ${response.res.statusCode}`); - return undefined; + return { errorCode: response.res.statusCode }; } let responseText: string | null = null; @@ -631,7 +631,7 @@ class ChatSetupRequests extends Disposable { if (!responseText) { this.onUnknownSignUpError('[chat setup] sign-up: response has no content'); - return undefined; + return { errorCode: 2 }; } let parsedResult: { subscribed: boolean } | undefined = undefined; @@ -640,7 +640,7 @@ class ChatSetupRequests extends Disposable { this.logService.trace(`[chat setup] sign-up: response is ${responseText}`); } catch (err) { this.onUnknownSignUpError(`[chat setup] sign-up: error parsing response (${err})`); - return undefined; + return { errorCode: 3 }; } // We have made it this far, so the user either did sign-up or was signed-up already. @@ -690,10 +690,12 @@ type InstallChatClassification = { comment: 'Provides insight into chat installation.'; installResult: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the extension was installed successfully, cancelled or failed to install.' }; signedIn: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user did sign in prior to installing the extension.' }; + signUpErrorCode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The error code in case of an error signing up.' }; }; type InstallChatEvent = { installResult: 'installed' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn' | 'failedSignUp'; signedIn: boolean; + signUpErrorCode: number | undefined; }; enum ChatSetupStep { @@ -824,7 +826,7 @@ class ChatSetupController extends Disposable { } if (!session) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signedIn: false }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signedIn: false, signUpErrorCode: undefined }); } return { session, entitlement }; @@ -835,15 +837,15 @@ class ChatSetupController extends Disposable { let installResult: 'installed' | 'cancelled' | 'failedInstall' | undefined = undefined; const wasInstalled = this.context.state.installed; - let didSignUp: boolean | undefined = undefined; + let didSignUp: boolean | { errorCode: number } | undefined = undefined; try { showCopilotView(this.viewsService, this.layoutService); if (entitlement !== ChatEntitlement.Limited && entitlement !== ChatEntitlement.Pro && entitlement !== ChatEntitlement.Unavailable) { didSignUp = await this.requests.signUpLimited(session); - if (typeof didSignUp === 'undefined' /* error */) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', signedIn }); + if (typeof didSignUp !== 'boolean' /* error */) { + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', signedIn, signUpErrorCode: didSignUp.errorCode }); } } @@ -873,7 +875,7 @@ class ChatSetupController extends Disposable { } } - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult, signedIn }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult, signedIn, signUpErrorCode: undefined }); } } From 22578788ad73001613f4c0d4d43ebc3c4788c782 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 8 Jan 2025 12:33:34 +0100 Subject: [PATCH 0402/3587] more shadow tweaks (#237470) --- .../chat/browser/media/chatEditorController.css | 1 + .../chat/browser/media/chatEditorOverlay.css | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/media/chatEditorController.css b/src/vs/workbench/contrib/chat/browser/media/chatEditorController.css index 03d8b6e61716..ccef0cfb1916 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatEditorController.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatEditorController.css @@ -7,6 +7,7 @@ opacity: 0; transition: opacity 0.2s ease-in-out; display: flex; + box-shadow: 0 2px 8px var(--vscode-widget-shadow); } .chat-diff-change-content-widget.hover { diff --git a/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css index a52e4ba5979c..b7a54ada1a2a 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css @@ -15,6 +15,22 @@ box-shadow: 0 2px 8px var(--vscode-widget-shadow); } +@keyframes pulse { + 0% { + box-shadow: 0 2px 8px 0 var(--vscode-widget-shadow); + } + 50% { + box-shadow: 0 2px 8px 4px var(--vscode-widget-shadow); + } + 100% { + box-shadow: 0 2px 8px 0 var(--vscode-widget-shadow); + } +} + +.chat-editor-overlay-widget.busy { + animation: pulse ease-in 2.3s infinite; +} + .chat-editor-overlay-widget .chat-editor-overlay-progress { align-items: center; display: none; From afbb64ba5c1b28000b0e9b3f3d4b9f74573a66d0 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 8 Jan 2025 12:47:40 +0100 Subject: [PATCH 0403/3587] watcher - disable failing test (windows) (#237476) --- src/vs/platform/files/test/node/nodejsWatcher.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/files/test/node/nodejsWatcher.test.ts b/src/vs/platform/files/test/node/nodejsWatcher.test.ts index a46279aaf50d..9a234b80cf57 100644 --- a/src/vs/platform/files/test/node/nodejsWatcher.test.ts +++ b/src/vs/platform/files/test/node/nodejsWatcher.test.ts @@ -630,7 +630,7 @@ suite.skip('File Watcher (node.js)', function () { await basicCrudTest(filePath, undefined, null, undefined, true); }); - test('watch requests support suspend/resume (folder, does not exist in beginning)', async function () { + (isWindows /* Windows: does not seem to report this */ ? test.skip : test)('watch requests support suspend/resume (folder, does not exist in beginning)', async function () { let onDidWatchFail = Event.toPromise(watcher.onWatchFail); const folderPath = join(testDir, 'not-found'); From 33f3b654c2b63a06bfa50fff6b671fcfbd3bd16c Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 8 Jan 2025 14:16:22 +0100 Subject: [PATCH 0404/3587] recovery fix (#237474) * recovery fix for #236429 * fix tests * fix tests --- .../test/common/testConfigurationService.ts | 8 ++++---- .../common/allowedExtensionsService.ts | 3 ++- .../extensionsWorkbenchService.test.ts | 3 +++ .../configurationResolverService.test.ts | 11 ++++++++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/vs/platform/configuration/test/common/testConfigurationService.ts b/src/vs/platform/configuration/test/common/testConfigurationService.ts index c2e63fa4dfb9..13d6c0b622aa 100644 --- a/src/vs/platform/configuration/test/common/testConfigurationService.ts +++ b/src/vs/platform/configuration/test/common/testConfigurationService.ts @@ -64,12 +64,12 @@ export class TestConfigurationService implements IConfigurationService { } public inspect(key: string, overrides?: IConfigurationOverrides): IConfigurationValue { - const config = this.getValue(undefined, overrides); + const value = this.getValue(key, overrides); return { - value: getConfigurationValue(config, key), - defaultValue: getConfigurationValue(config, key), - userValue: getConfigurationValue(config, key), + value, + defaultValue: undefined, + userValue: value, overrideIdentifiers: this.overrideIdentifiers.get(key) }; } diff --git a/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts b/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts index 0d00bc2bfeb3..7a2892fd098f 100644 --- a/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts +++ b/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts @@ -54,7 +54,8 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte } private getAllowedExtensionsValue(): AllowedExtensionsConfigValueType | undefined { - const value = this.configurationService.getValue(AllowedExtensionsConfigKey); + const inspectValue = this.configurationService.inspect(AllowedExtensionsConfigKey); + const value = inspectValue.policyValue ?? inspectValue.userValue ?? inspectValue.defaultValue; if (!isObject(value) || Array.isArray(value)) { return undefined; } diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index 03fd27f96b8f..c49ca4dfa186 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -1640,6 +1640,9 @@ suite('ExtensionsWorkbenchServiceTest', () => { return true; }, }); + }, + inspect: (key: string) => { + return {}; } }); } diff --git a/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts index 9140cb41d9e3..804966dd7ec9 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts @@ -16,7 +16,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/tes import { Selection } from '../../../../../editor/common/core/selection.js'; import { EditorType } from '../../../../../editor/common/editorCommon.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IConfigurationOverrides, IConfigurationService, IConfigurationValue } from '../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { IExtensionDescription } from '../../../../../platform/extensions/common/extensions.js'; import { IFormatterChangeEvent, ILabelService, ResourceLabelFormatter, Verbosity } from '../../../../../platform/label/common/label.js'; @@ -763,4 +763,13 @@ class MockInputsConfigurationService extends TestConfigurationService { } return configuration; } + + public override inspect(key: string, overrides?: IConfigurationOverrides): IConfigurationValue { + return { + value: undefined, + defaultValue: undefined, + userValue: undefined, + overrideIdentifiers: [] + }; + } } From 2569d71b0491afddb23e173ee6cc2eb284f1b0b9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 8 Jan 2025 14:23:59 +0100 Subject: [PATCH 0405/3587] Chat welcome view never dismisses when in an untrusted workspace (fix microsoft/vscode-copilot#11517) (#237477) --- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 596f67188405..ad8f7f717f9a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -61,6 +61,7 @@ import Severity from '../../../../base/common/severity.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { isWeb } from '../../../../base/common/platform.js'; import { ExtensionUrlHandlerOverrideRegistry } from '../../../services/extensions/browser/extensionUrlHandler.js'; +import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -174,8 +175,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr ensureSideBarChatViewSize(400, viewDescriptorService, layoutService); if (startSetup === true) { - const controller = that.controller.value; - controller.setup(); + that.controller.value.setup(); } configurationService.updateValue('chat.commandCenter.enabled', true); @@ -728,7 +728,8 @@ class ChatSetupController extends Disposable { @IChatAgentService private readonly chatAgentService: IChatAgentService, @IActivityService private readonly activityService: IActivityService, @ICommandService private readonly commandService: ICommandService, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, + @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService ) { super(); @@ -792,6 +793,13 @@ class ChatSetupController extends Disposable { } } + const trusted = await this.workspaceTrustRequestService.requestWorkspaceTrust({ + message: localize('copilotWorkspaceTrust', "Copilot is currently only supported in trusted workspaces.") + }); + if (!trusted) { + return; + } + const activeElement = getActiveElement(); // Install From cfe07a3592693a6307eddeb77c20b556616b137a Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 8 Jan 2025 14:25:07 +0100 Subject: [PATCH 0406/3587] save files on accept when `alwaysSaveWithGeneratedChanges` in on (#237482) * chore - cleanup, use observable-config * save files on accept when `alwaysSaveWithGeneratedChanges` in on https://github.com/microsoft/vscode-copilot/issues/11498 --- .../contrib/chat/browser/chatEditorSaving.ts | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts b/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts index 0970f7ae916e..378f23630857 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts @@ -26,7 +26,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IEditorIdentifier, SaveReason } from '../../../common/editor.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; +import { AutoSaveMode, IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; @@ -116,6 +116,7 @@ export class ChatEditorSaving extends Disposable implements IWorkbenchContributi @IConfigurationService configService: IConfigurationService, @IChatEditingService chatEditingService: IChatEditingService, @IChatAgentService chatAgentService: IChatAgentService, + @IFilesConfigurationService fileConfigService: IFilesConfigurationService, @ITextFileService textFileService: ITextFileService, @ILabelService labelService: ILabelService, @IDialogService dialogService: IDialogService, @@ -141,13 +142,11 @@ export class ChatEditorSaving extends Disposable implements IWorkbenchContributi })); })); - const store = this._store.add(new DisposableStore()); - - const update = () => { + const alwaysSaveConfig = observableConfigValue(ChatEditorSaving._config, false, configService); + this._store.add(autorunWithStore((r, store) => { - store.clear(); + const alwaysSave = alwaysSaveConfig.read(r); - const alwaysSave = configService.getValue(ChatEditorSaving._config); if (alwaysSave) { return; } @@ -235,14 +234,27 @@ export class ChatEditorSaving extends Disposable implements IWorkbenchContributi return saveJobs.add(entry.modifiedURI); } })); - }; + })); - configService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(ChatEditorSaving._config)) { - update(); + // autosave: OFF & alwaysSaveWithAIChanges - save files after accept + this._store.add(autorun(r => { + const saveConfig = fileConfigService.getAutoSaveMode(undefined); + if (saveConfig.mode !== AutoSaveMode.OFF) { + return; } - }); - update(); + if (!alwaysSaveConfig.read(r)) { + return; + } + const session = chatEditingService.currentEditingSessionObs.read(r); + if (!session) { + return; + } + for (const entry of session.entries.read(r)) { + if (entry.state.read(r) === WorkingSetEntryState.Accepted) { + textFileService.save(entry.modifiedURI); + } + } + })); } private _reportSaved(entry: IModifiedFileEntry) { @@ -277,13 +289,11 @@ export class ChatEditorSaving extends Disposable implements IWorkbenchContributi export class ChatEditingSaveAllAction extends Action2 { static readonly ID = 'chatEditing.saveAllFiles'; - static readonly LABEL = localize('save.allFiles', 'Save All'); constructor() { super({ id: ChatEditingSaveAllAction.ID, - title: ChatEditingSaveAllAction.LABEL, - tooltip: ChatEditingSaveAllAction.LABEL, + title: localize('save.allFiles', 'Save All'), precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), icon: Codicon.saveAll, menu: [ From 912d749ae0c808940cdf902d0eb6897c51473b4f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 8 Jan 2025 05:56:13 -0800 Subject: [PATCH 0407/3587] Disable pwsh script suggest on Windows 10 Fixes #236994 --- .../terminal/browser/terminalProcessManager.ts | 3 +++ src/vs/workbench/contrib/terminal/common/terminal.ts | 2 ++ .../suggest/browser/terminal.suggest.contribution.ts | 11 ++++++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index ab4dab9a7ed9..e8c35b335a60 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -94,6 +94,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce private _processListeners?: IDisposable[]; private _isDisconnected: boolean = false; + private _processTraits: IProcessReadyEvent | undefined; private _shellLaunchConfig?: IShellLaunchConfig; private _dimensions: ITerminalDimensions = { cols: 0, rows: 0 }; @@ -128,6 +129,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce get hasChildProcesses(): boolean { return this._hasChildProcesses; } get reconnectionProperties(): IReconnectionProperties | undefined { return this._shellLaunchConfig?.attachPersistentProcess?.reconnectionProperties || this._shellLaunchConfig?.reconnectionProperties || undefined; } get extEnvironmentVariableCollection(): IMergedEnvironmentVariableCollection | undefined { return this._extEnvironmentVariableCollection; } + get processTraits(): IProcessReadyEvent | undefined { return this._processTraits; } constructor( private readonly _instanceId: number, @@ -357,6 +359,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce } this._processListeners = [ newProcess.onProcessReady((e: IProcessReadyEvent) => { + this._processTraits = e; this.shellProcessId = e.pid; this._initialCwd = e.cwd; this._onDidChangeProperty.fire({ type: ProcessPropertyType.InitialCwd, value: this._initialCwd }); diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 6fbd41d8637f..992fdf516fad 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -274,6 +274,8 @@ export interface ITerminalProcessInfo { export const isTerminalProcessManager = (t: ITerminalProcessInfo | ITerminalProcessManager): t is ITerminalProcessManager => typeof (t as ITerminalProcessManager).write === 'function'; export interface ITerminalProcessManager extends IDisposable, ITerminalProcessInfo { + readonly processTraits: IProcessReadyEvent | undefined; + readonly onPtyDisconnect: Event; readonly onPtyReconnect: Event; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 35a845c1f7b8..f4d6285a61ee 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -102,10 +102,19 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo })); } - private _loadPwshCompletionAddon(xterm: RawXtermTerminal): void { + private async _loadPwshCompletionAddon(xterm: RawXtermTerminal): Promise { + // Disable when shell type is not powershell if (this._ctx.instance.shellType !== GeneralShellType.PowerShell) { return; } + + // Disable the addon on old backends (not conpty or Windows 11) + await this._ctx.instance.processReady; + const processTraits = this._ctx.processManager.processTraits; + if (processTraits?.windowsPty && (processTraits.windowsPty.backend !== 'conpty' || processTraits?.windowsPty.buildNumber <= 19045)) { + return; + } + const pwshCompletionProviderAddon = this._pwshAddon.value = this._instantiationService.createInstance(PwshCompletionProviderAddon, undefined, this._ctx.instance.capabilities); xterm.loadAddon(pwshCompletionProviderAddon); this.add(pwshCompletionProviderAddon); From d4a589ef8fdbd3797e49493e779efd954019d6a3 Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Wed, 8 Jan 2025 15:47:16 +0100 Subject: [PATCH 0408/3587] Turn on new renderings (#237491) --- src/vs/editor/common/config/editorOptions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 5fe2028e3666..2bd9dfd9e2ed 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -4233,10 +4233,10 @@ class InlineEditorSuggest extends BaseEditorOption Date: Wed, 8 Jan 2025 16:06:32 +0100 Subject: [PATCH 0409/3587] Enable custom titlebar on Linux by default as experiment (fix microsoft/vscode-internalbacklog#4857) (#237490) --- src/vs/code/electron-main/app.ts | 2 +- src/vs/platform/native/common/native.ts | 2 +- .../electron-main/nativeHostMainService.ts | 7 +- src/vs/platform/window/common/window.ts | 20 ++--- .../electron-main/windowsMainService.ts | 3 +- .../electron-sandbox/desktop.contribution.ts | 6 +- .../electron-sandbox/desktop.main.ts | 10 ++- .../parts/titlebar/titlebarPart.ts | 83 ++++++++++++------- 8 files changed, 79 insertions(+), 54 deletions(-) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 7c7fc636e21d..47481c0de3f5 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -596,7 +596,7 @@ export class CodeApplication extends Disposable { // Linux (stable only): custom title default style override if (isLinux && this.productService.quality === 'stable') { const titleBarDefaultStyleOverride = this.stateService.getItem('window.titleBarStyleOverride'); - if (titleBarDefaultStyleOverride === TitlebarStyle.CUSTOM || titleBarDefaultStyleOverride === TitlebarStyle.NATIVE) { + if (titleBarDefaultStyleOverride === TitlebarStyle.CUSTOM) { overrideDefaultTitlebarStyle(titleBarDefaultStyleOverride); } } diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 1f8d0adabe7b..13141d890634 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -114,7 +114,7 @@ export interface ICommonNativeHostService { focusWindow(options?: INativeHostOptions & { force?: boolean }): Promise; // Titlebar default style override - overrideDefaultTitlebarStyle(style: 'native' | 'custom' | undefined): Promise; + overrideDefaultTitlebarStyle(style: 'custom' | undefined): Promise; // Dialogs showMessageBox(options: MessageBoxOptions & INativeHostOptions): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 8e5c129ea4f2..030b6a275061 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -33,7 +33,7 @@ import { IProductService } from '../../product/common/productService.js'; import { IPartsSplash } from '../../theme/common/themeService.js'; import { IThemeMainService } from '../../theme/electron-main/themeMainService.js'; import { defaultWindowState, ICodeWindow } from '../../window/electron-main/window.js'; -import { IColorScheme, IOpenedAuxiliaryWindow, IOpenedMainWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IPoint, IRectangle, IWindowOpenable, overrideDefaultTitlebarStyle } from '../../window/common/window.js'; +import { IColorScheme, IOpenedAuxiliaryWindow, IOpenedMainWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IPoint, IRectangle, IWindowOpenable } from '../../window/common/window.js'; import { defaultBrowserWindowOptions, IWindowsMainService, OpenContext } from '../../windows/electron-main/windows.js'; import { isWorkspaceIdentifier, toWorkspaceIdentifier } from '../../workspace/common/workspace.js'; import { IWorkspacesManagementMainService } from '../../workspaces/electron-main/workspacesManagementMainService.js'; @@ -326,13 +326,12 @@ export class NativeHostMainService extends Disposable implements INativeHostMain this.themeMainService.saveWindowSplash(windowId, splash); } - async overrideDefaultTitlebarStyle(windowId: number | undefined, style: 'native' | 'custom' | undefined): Promise { - if (typeof style === 'string') { + async overrideDefaultTitlebarStyle(windowId: number | undefined, style: 'custom' | undefined): Promise { + if (style === 'custom') { this.stateService.setItem('window.titleBarStyleOverride', style); } else { this.stateService.removeItem('window.titleBarStyleOverride'); } - overrideDefaultTitlebarStyle(style); } //#endregion diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 02e4152376da..cec025d66324 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -187,18 +187,9 @@ export const enum CustomTitleBarVisibility { NEVER = 'never', } -export let titlebarStyleDefaultOverride: TitlebarStyle | undefined = undefined; -export function overrideDefaultTitlebarStyle(style: 'native' | 'custom' | undefined): void { - switch (style) { - case 'native': - titlebarStyleDefaultOverride = TitlebarStyle.NATIVE; - break; - case 'custom': - titlebarStyleDefaultOverride = TitlebarStyle.CUSTOM; - break; - default: - titlebarStyleDefaultOverride = undefined; - } +export let titlebarStyleDefaultOverride: 'custom' | undefined = undefined; +export function overrideDefaultTitlebarStyle(style: 'custom'): void { + titlebarStyleDefaultOverride = style; } export function hasCustomTitlebar(configurationService: IConfigurationService, titleBarStyle?: TitlebarStyle): boolean { @@ -238,8 +229,8 @@ export function getTitleBarStyle(configurationService: IConfigurationService): T } } - if (titlebarStyleDefaultOverride) { - return titlebarStyleDefaultOverride; + if (titlebarStyleDefaultOverride === 'custom') { + return TitlebarStyle.CUSTOM; } return isLinux && product.quality === 'stable' ? TitlebarStyle.NATIVE : TitlebarStyle.CUSTOM; // default to custom on all OS except Linux stable (for now) @@ -413,6 +404,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native autoDetectHighContrast?: boolean; autoDetectColorScheme?: boolean; isCustomZoomLevel?: boolean; + overrideDefaultTitlebarStyle?: 'custom'; perfMarks: PerformanceMark[]; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 9999cd867769..b47f1a17cb4f 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -37,7 +37,7 @@ import product from '../../product/common/product.js'; import { IProtocolMainService } from '../../protocol/electron-main/protocol.js'; import { getRemoteAuthority } from '../../remote/common/remoteHosts.js'; import { IStateService } from '../../state/node/state.js'; -import { IAddRemoveFoldersRequest, INativeOpenFileRequest, INativeWindowConfiguration, IOpenEmptyWindowOptions, IPath, IPathsToWaitFor, isFileToOpen, isFolderToOpen, isWorkspaceToOpen, IWindowOpenable, IWindowSettings } from '../../window/common/window.js'; +import { IAddRemoveFoldersRequest, INativeOpenFileRequest, INativeWindowConfiguration, IOpenEmptyWindowOptions, IPath, IPathsToWaitFor, isFileToOpen, isFolderToOpen, isWorkspaceToOpen, IWindowOpenable, IWindowSettings, titlebarStyleDefaultOverride } from '../../window/common/window.js'; import { CodeWindow } from './windowImpl.js'; import { IOpenConfiguration, IOpenEmptyConfiguration, IWindowsCountChangedEvent, IWindowsMainService, OpenContext, getLastFocused } from './windows.js'; import { findWindowOnExtensionDevelopmentPath, findWindowOnFile, findWindowOnWorkspaceOrFolder } from './windowsFinder.js'; @@ -1507,6 +1507,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic autoDetectHighContrast: windowConfig?.autoDetectHighContrast ?? true, autoDetectColorScheme: windowConfig?.autoDetectColorScheme ?? false, + overrideDefaultTitlebarStyle: titlebarStyleDefaultOverride, accessibilitySupport: app.accessibilitySupportEnabled, colorScheme: this.themeMainService.getColorScheme(), policiesData: this.policyService.serialize(), diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index da72519dee35..8681fcd2c7ec 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -29,6 +29,8 @@ import { ModifierKeyEmitter } from '../../base/browser/dom.js'; import { applicationConfigurationNodeBase, securityConfigurationNodeBase } from '../common/configuration.js'; import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '../../platform/window/electron-sandbox/window.js'; import product from '../../platform/product/common/product.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../common/contributions.js'; +import { LinuxCustomTitlebarExperiment } from './parts/titlebar/titlebarPart.js'; // Actions (function registerActions(): void { @@ -234,7 +236,6 @@ import product from '../../platform/product/common/product.js'; 'type': 'string', 'enum': ['native', 'custom'], 'default': isLinux && product.quality === 'stable' ? 'native' : 'custom', - 'tags': isLinux && product.quality === 'stable' ? ['onExP'] : undefined, 'scope': ConfigurationScope.APPLICATION, 'description': localize('titleBarStyle', "Adjust the appearance of the window title bar to be native by the OS or custom. On Linux and Windows, this setting also affects the application and context menu appearances. Changes require a full restart to apply."), }, @@ -424,3 +425,6 @@ import product from '../../platform/product/common/product.js'; jsonRegistry.registerSchema(argvDefinitionFileSchemaId, schema); })(); + +// Workbench Contributions +registerWorkbenchContribution2(LinuxCustomTitlebarExperiment.ID, LinuxCustomTitlebarExperiment, WorkbenchPhase.Eventually); diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index 40cfd9c7ec3a..105275414263 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -45,7 +45,7 @@ import { WorkspaceTrustEnablementService, WorkspaceTrustManagementService } from import { IWorkspaceTrustEnablementService, IWorkspaceTrustManagementService } from '../../platform/workspace/common/workspaceTrust.js'; import { safeStringify } from '../../base/common/objects.js'; import { IUtilityProcessWorkerWorkbenchService, UtilityProcessWorkerWorkbenchService } from '../services/utilityProcess/electron-sandbox/utilityProcessWorkerWorkbenchService.js'; -import { isBigSurOrNewer, isCI, isMacintosh } from '../../base/common/platform.js'; +import { isBigSurOrNewer, isCI, isLinux, isMacintosh } from '../../base/common/platform.js'; import { Schemas } from '../../base/common/network.js'; import { DiskFileSystemProvider } from '../services/files/electron-sandbox/diskFileSystemProvider.js'; import { FileUserDataProvider } from '../../platform/userData/common/fileUserDataProvider.js'; @@ -61,6 +61,8 @@ import { ElectronRemoteResourceLoader } from '../../platform/remote/electron-san import { IConfigurationService } from '../../platform/configuration/common/configuration.js'; import { applyZoom } from '../../platform/window/electron-sandbox/window.js'; import { mainWindow } from '../../base/browser/window.js'; +import { Registry } from '../../platform/registry/common/platform.js'; +import { IConfigurationRegistry, Extensions } from '../../platform/configuration/common/configurationRegistry.js'; export class DesktopMain extends Disposable { @@ -79,6 +81,12 @@ export class DesktopMain extends Disposable { // Apply fullscreen early if configured setFullscreen(!!this.configuration.fullscreen, mainWindow); + + // Apply custom title override to defaults if any + if (isLinux && product.quality === 'stable' && this.configuration.overrideDefaultTitlebarStyle === 'custom') { + const configurationRegistry = Registry.as(Extensions.Configuration); + configurationRegistry.registerDefaultConfigurations([{ overrides: { 'window.titleBarStyle': 'custom' } }]); + } } private reviveUris() { diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index 0d7a06de173d..fa989d363bd4 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts @@ -28,6 +28,9 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { CodeWindow, mainWindow } from '../../../../base/browser/window.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; export class NativeTitlebarPart extends BrowserTitlebarPart { @@ -71,7 +74,7 @@ export class NativeTitlebarPart extends BrowserTitlebarPart { @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IContextKeyService contextKeyService: IContextKeyService, @IHostService hostService: IHostService, - @INativeHostService protected readonly nativeHostService: INativeHostService, + @INativeHostService private readonly nativeHostService: INativeHostService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @IEditorService editorService: IEditorService, @IMenuService menuService: IMenuService, @@ -288,38 +291,9 @@ export class MainNativeTitlebarPart extends NativeTitlebarPart { @IEditorGroupsService editorGroupService: IEditorGroupsService, @IEditorService editorService: IEditorService, @IMenuService menuService: IMenuService, - @IKeybindingService keybindingService: IKeybindingService, - @IProductService productService: IProductService + @IKeybindingService keybindingService: IKeybindingService ) { super(Parts.TITLEBAR_PART, mainWindow, 'main', contextMenuService, configurationService, environmentService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, nativeHostService, editorGroupService, editorService, menuService, keybindingService); - - if (isLinux && productService.quality === 'stable') { - this.handleDefaultTitlebarStyle(); // TODO@bpasero remove me eventually once settled - } - } - - private handleDefaultTitlebarStyle(): void { - this.updateDefaultTitlebarStyle(); - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('window.titleBarStyle')) { - this.updateDefaultTitlebarStyle(); - } - })); - } - - private updateDefaultTitlebarStyle(): void { - const titleBarStyle = this.configurationService.inspect('window.titleBarStyle'); - - let titleBarStyleOverride: 'custom' | undefined; - if (titleBarStyle.applicationValue || titleBarStyle.userValue || titleBarStyle.userLocalValue) { - // configured by user or application: clear override - titleBarStyleOverride = undefined; - } else { - // not configured: set override if experiment is active - titleBarStyleOverride = titleBarStyle.defaultValue === 'native' ? undefined : 'custom'; - } - - this.nativeHostService.overrideDefaultTitlebarStyle(titleBarStyleOverride); } } @@ -374,3 +348,50 @@ export class NativeTitleService extends BrowserTitleService { return this.instantiationService.createInstance(AuxiliaryNativeTitlebarPart, container, editorGroupsContainer, this.mainPart); } } + +export class LinuxCustomTitlebarExperiment extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.linuxCustomTitlebarExperiment'; + + private readonly treatment = this.assignmentService.getTreatment('config.window.experimentalTitleBarStyle'); + + constructor( + @IProductService productService: IProductService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @INativeHostService private readonly nativeHostService: INativeHostService, + @IWorkbenchAssignmentService private readonly assignmentService: IWorkbenchAssignmentService + ) { + super(); + + if (isLinux && productService.quality === 'stable') { + this.handleDefaultTitlebarStyle(); // TODO@bpasero remove me eventually once settled + } + } + + private handleDefaultTitlebarStyle(): void { + this.updateDefaultTitlebarStyle(); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('window.titleBarStyle')) { + this.updateDefaultTitlebarStyle(); + } + })); + } + + private async updateDefaultTitlebarStyle(): Promise { + const titleBarStyle = this.configurationService.inspect('window.titleBarStyle'); + + let titleBarStyleOverride: 'custom' | undefined; + if (titleBarStyle.applicationValue || titleBarStyle.userValue) { + // configured by user or application: clear override + titleBarStyleOverride = undefined; + } else { + // not configured: set override if experiment is active + const value = await this.treatment; + if (value === 'custom') { + titleBarStyleOverride = 'custom'; + } + } + + await this.nativeHostService.overrideDefaultTitlebarStyle(titleBarStyleOverride); + } +} From 7297b69dc71ae51779448974a91d47c9279e2053 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 8 Jan 2025 07:49:13 -0800 Subject: [PATCH 0410/3587] Update xterm.js --- package-lock.json | 86 ++++++++++++++++++------------------ package.json | 18 ++++---- remote/package-lock.json | 86 ++++++++++++++++++------------------ remote/package.json | 18 ++++---- remote/web/package-lock.json | 78 ++++++++++++++++---------------- remote/web/package.json | 16 +++---- 6 files changed, 151 insertions(+), 151 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7f69a0019637..9a706f0ae8aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,15 +27,15 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.57", - "@xterm/addon-image": "^0.9.0-beta.74", - "@xterm/addon-ligatures": "^0.10.0-beta.74", - "@xterm/addon-search": "^0.16.0-beta.74", - "@xterm/addon-serialize": "^0.14.0-beta.74", - "@xterm/addon-unicode11": "^0.9.0-beta.74", - "@xterm/addon-webgl": "^0.19.0-beta.74", - "@xterm/headless": "^5.6.0-beta.74", - "@xterm/xterm": "^5.6.0-beta.74", + "@xterm/addon-clipboard": "^0.2.0-beta.68", + "@xterm/addon-image": "^0.9.0-beta.85", + "@xterm/addon-ligatures": "^0.10.0-beta.85", + "@xterm/addon-search": "^0.16.0-beta.85", + "@xterm/addon-serialize": "^0.14.0-beta.85", + "@xterm/addon-unicode11": "^0.9.0-beta.85", + "@xterm/addon-webgl": "^0.19.0-beta.85", + "@xterm/headless": "^5.6.0-beta.85", + "@xterm/xterm": "^5.6.0-beta.85", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -3458,30 +3458,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.57", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.57.tgz", - "integrity": "sha512-/GSI8Fkmb8s/V1t2EGc2U2PUfSqge6f9gAeob65EwarsfBf66cmCxMG0ZSPE8+nti1pGIsrJA8XfeEaJt4clcA==", + "version": "0.2.0-beta.68", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.68.tgz", + "integrity": "sha512-z/4urYG3dySjmnfwig2eH3rJNLVFIk3IGQ+Ibadu4GZwCAVkX7eV/uGMssIeVGMg/ZD3uVdocFvcaILPOz01Pg==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.74.tgz", - "integrity": "sha512-mJPWNPov2mqrUkYZCs6UCn5p6DBLeN6xjpLu5mLh8cmXr544VWfqEVNAYPQ9+8uNgXdzSsKInBv9ZGtbXV0SfA==", + "version": "0.9.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.85.tgz", + "integrity": "sha512-XyIG+v6eVXBKkW6rT5GLF8VBVvNdsdcCNBOlw1kWPiK31/hzxcnoPXXRDp6bqxxFOtcB8tlHe2mk/5lQG4JtPA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.74.tgz", - "integrity": "sha512-/X3OemVqPSgdely8OgdQb0cJqv9HqiMaBLeLe2QHfTWdXDBOLG/O4g8n/lChqR9rulEMwPCt2LqvtyIMA3iZsw==", + "version": "0.10.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.85.tgz", + "integrity": "sha512-fJKsmqjRIBr8TphyOefYnhH1nw1+HvtBmO1f6DX893a0qyLZ0cPIowuAABTBbu/j5mhwJveKw7pYIXScT42cBA==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -3491,55 +3491,55 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.74.tgz", - "integrity": "sha512-KMOeOu3EvtDWcH/HCs6fe4KyaMMdjoUjP1C7R3AtZwJVdm5GaFxASxdol+9evfGhUUei4qt+zVsaFraNKyFvsA==", + "version": "0.16.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.85.tgz", + "integrity": "sha512-Z1IZlBIfqyB4weBffIwHKFGRVVgxBz105RdXOk+Jt5iIXeocg/sjCM7iFNwwQL0vLGOfzIQBWrS8oqjWeYvDEg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.74.tgz", - "integrity": "sha512-zBhQAoXagBMhKFGYQ6n52sKapY61Jt3hKT8awhG/49f04JkaX1Jhc6xohD+aZs6J2ABHI7EWW/y0xTN9BDLLBw==", + "version": "0.14.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.85.tgz", + "integrity": "sha512-uMjrxcyF4ZCCKGI/4XLfq0/xEbCZkUsj6rORN0kem7GXKenNA1ggxk4z0Z8rFxwEyjV3wfjOaVPmxHJM3xh+gA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.74.tgz", - "integrity": "sha512-1AyuDST77Xg33ed+3neNrQfsfZVwa+C16uWP9eTdJ1rO48ylqPcFTDnahCfIZGPHGjPqLl90ZKZ8tt1zWSefmA==", + "version": "0.9.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.85.tgz", + "integrity": "sha512-mpsRneCyY9Jmu05KYISOmDqkWS4WCV4D0UxCHxGmdmjeHDsNItauGG7u2Qnct7K+RBhfJPDp0j2yCTMTWuv0KQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.74.tgz", - "integrity": "sha512-1wKiuv6WHMqOcSlLeIRU7UF8zkU4KU35rnPvLw2G45aAJSr9B3f7EVIaJ4IjXGeY/C+WCM3wWuybPN5VdB8qAQ==", + "version": "0.19.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.85.tgz", + "integrity": "sha512-qenYMn7XwBxujNkhialgGYvoKyGxbpGYiUmgQIdQPiV5yMQsaqz5S/o+iPKJM8sSRcI+ghxYS6KhiUOkzg5C3Q==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.74.tgz", - "integrity": "sha512-6PSNk1/CaLGNZLhXULswjfbf/rJrG1EomT9hR2nNbX6Osm9LVbhgIzg/mYHPu9wX5Pz7Gpd1VWp4/1NcKpN53w==", + "version": "5.6.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.85.tgz", + "integrity": "sha512-GzIULvPPz+I9tvdpMM5k5GeURz7bHBQyVQyZE6d6UUSyVVVd0NWgJXwF25t62y9cZQt1Bfp8i+1eCJ7p2+8ZSQ==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.74.tgz", - "integrity": "sha512-gVu7+4Cfd7O/6cQ/UK0sZM+TJBaI/VgQ/JFAhuAnNFj29wts2MzxSH3fIp3KUG1kqlJBWEUShCTwB8nKwtbCjQ==", + "version": "5.6.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.85.tgz", + "integrity": "sha512-A2HpImW8FIlUOtkWm2FPnUdhhFa+ejshv5RJbejGCihGOnizsCeG8vBLt7uEJ37msvcJJSgFJ/VSmcFnh9Y9vA==", "license": "MIT" }, "node_modules/@xtuc/ieee754": { diff --git a/package.json b/package.json index 50b2f8c444e0..58b368af92e6 100644 --- a/package.json +++ b/package.json @@ -85,15 +85,15 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.57", - "@xterm/addon-image": "^0.9.0-beta.74", - "@xterm/addon-ligatures": "^0.10.0-beta.74", - "@xterm/addon-search": "^0.16.0-beta.74", - "@xterm/addon-serialize": "^0.14.0-beta.74", - "@xterm/addon-unicode11": "^0.9.0-beta.74", - "@xterm/addon-webgl": "^0.19.0-beta.74", - "@xterm/headless": "^5.6.0-beta.74", - "@xterm/xterm": "^5.6.0-beta.74", + "@xterm/addon-clipboard": "^0.2.0-beta.68", + "@xterm/addon-image": "^0.9.0-beta.85", + "@xterm/addon-ligatures": "^0.10.0-beta.85", + "@xterm/addon-search": "^0.16.0-beta.85", + "@xterm/addon-serialize": "^0.14.0-beta.85", + "@xterm/addon-unicode11": "^0.9.0-beta.85", + "@xterm/addon-webgl": "^0.19.0-beta.85", + "@xterm/headless": "^5.6.0-beta.85", + "@xterm/xterm": "^5.6.0-beta.85", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", diff --git a/remote/package-lock.json b/remote/package-lock.json index cc8a96de9adc..26c6690ae30d 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -20,15 +20,15 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.57", - "@xterm/addon-image": "^0.9.0-beta.74", - "@xterm/addon-ligatures": "^0.10.0-beta.74", - "@xterm/addon-search": "^0.16.0-beta.74", - "@xterm/addon-serialize": "^0.14.0-beta.74", - "@xterm/addon-unicode11": "^0.9.0-beta.74", - "@xterm/addon-webgl": "^0.19.0-beta.74", - "@xterm/headless": "^5.6.0-beta.74", - "@xterm/xterm": "^5.6.0-beta.74", + "@xterm/addon-clipboard": "^0.2.0-beta.68", + "@xterm/addon-image": "^0.9.0-beta.85", + "@xterm/addon-ligatures": "^0.10.0-beta.85", + "@xterm/addon-search": "^0.16.0-beta.85", + "@xterm/addon-serialize": "^0.14.0-beta.85", + "@xterm/addon-unicode11": "^0.9.0-beta.85", + "@xterm/addon-webgl": "^0.19.0-beta.85", + "@xterm/headless": "^5.6.0-beta.85", + "@xterm/xterm": "^5.6.0-beta.85", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -521,30 +521,30 @@ "hasInstallScript": true }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.57", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.57.tgz", - "integrity": "sha512-/GSI8Fkmb8s/V1t2EGc2U2PUfSqge6f9gAeob65EwarsfBf66cmCxMG0ZSPE8+nti1pGIsrJA8XfeEaJt4clcA==", + "version": "0.2.0-beta.68", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.68.tgz", + "integrity": "sha512-z/4urYG3dySjmnfwig2eH3rJNLVFIk3IGQ+Ibadu4GZwCAVkX7eV/uGMssIeVGMg/ZD3uVdocFvcaILPOz01Pg==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.74.tgz", - "integrity": "sha512-mJPWNPov2mqrUkYZCs6UCn5p6DBLeN6xjpLu5mLh8cmXr544VWfqEVNAYPQ9+8uNgXdzSsKInBv9ZGtbXV0SfA==", + "version": "0.9.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.85.tgz", + "integrity": "sha512-XyIG+v6eVXBKkW6rT5GLF8VBVvNdsdcCNBOlw1kWPiK31/hzxcnoPXXRDp6bqxxFOtcB8tlHe2mk/5lQG4JtPA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.74.tgz", - "integrity": "sha512-/X3OemVqPSgdely8OgdQb0cJqv9HqiMaBLeLe2QHfTWdXDBOLG/O4g8n/lChqR9rulEMwPCt2LqvtyIMA3iZsw==", + "version": "0.10.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.85.tgz", + "integrity": "sha512-fJKsmqjRIBr8TphyOefYnhH1nw1+HvtBmO1f6DX893a0qyLZ0cPIowuAABTBbu/j5mhwJveKw7pYIXScT42cBA==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -554,55 +554,55 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.74.tgz", - "integrity": "sha512-KMOeOu3EvtDWcH/HCs6fe4KyaMMdjoUjP1C7R3AtZwJVdm5GaFxASxdol+9evfGhUUei4qt+zVsaFraNKyFvsA==", + "version": "0.16.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.85.tgz", + "integrity": "sha512-Z1IZlBIfqyB4weBffIwHKFGRVVgxBz105RdXOk+Jt5iIXeocg/sjCM7iFNwwQL0vLGOfzIQBWrS8oqjWeYvDEg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.74.tgz", - "integrity": "sha512-zBhQAoXagBMhKFGYQ6n52sKapY61Jt3hKT8awhG/49f04JkaX1Jhc6xohD+aZs6J2ABHI7EWW/y0xTN9BDLLBw==", + "version": "0.14.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.85.tgz", + "integrity": "sha512-uMjrxcyF4ZCCKGI/4XLfq0/xEbCZkUsj6rORN0kem7GXKenNA1ggxk4z0Z8rFxwEyjV3wfjOaVPmxHJM3xh+gA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.74.tgz", - "integrity": "sha512-1AyuDST77Xg33ed+3neNrQfsfZVwa+C16uWP9eTdJ1rO48ylqPcFTDnahCfIZGPHGjPqLl90ZKZ8tt1zWSefmA==", + "version": "0.9.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.85.tgz", + "integrity": "sha512-mpsRneCyY9Jmu05KYISOmDqkWS4WCV4D0UxCHxGmdmjeHDsNItauGG7u2Qnct7K+RBhfJPDp0j2yCTMTWuv0KQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.74.tgz", - "integrity": "sha512-1wKiuv6WHMqOcSlLeIRU7UF8zkU4KU35rnPvLw2G45aAJSr9B3f7EVIaJ4IjXGeY/C+WCM3wWuybPN5VdB8qAQ==", + "version": "0.19.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.85.tgz", + "integrity": "sha512-qenYMn7XwBxujNkhialgGYvoKyGxbpGYiUmgQIdQPiV5yMQsaqz5S/o+iPKJM8sSRcI+ghxYS6KhiUOkzg5C3Q==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.74.tgz", - "integrity": "sha512-6PSNk1/CaLGNZLhXULswjfbf/rJrG1EomT9hR2nNbX6Osm9LVbhgIzg/mYHPu9wX5Pz7Gpd1VWp4/1NcKpN53w==", + "version": "5.6.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.85.tgz", + "integrity": "sha512-GzIULvPPz+I9tvdpMM5k5GeURz7bHBQyVQyZE6d6UUSyVVVd0NWgJXwF25t62y9cZQt1Bfp8i+1eCJ7p2+8ZSQ==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.74.tgz", - "integrity": "sha512-gVu7+4Cfd7O/6cQ/UK0sZM+TJBaI/VgQ/JFAhuAnNFj29wts2MzxSH3fIp3KUG1kqlJBWEUShCTwB8nKwtbCjQ==", + "version": "5.6.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.85.tgz", + "integrity": "sha512-A2HpImW8FIlUOtkWm2FPnUdhhFa+ejshv5RJbejGCihGOnizsCeG8vBLt7uEJ37msvcJJSgFJ/VSmcFnh9Y9vA==", "license": "MIT" }, "node_modules/agent-base": { diff --git a/remote/package.json b/remote/package.json index 5e92099c7e93..44281195658d 100644 --- a/remote/package.json +++ b/remote/package.json @@ -15,15 +15,15 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.57", - "@xterm/addon-image": "^0.9.0-beta.74", - "@xterm/addon-ligatures": "^0.10.0-beta.74", - "@xterm/addon-search": "^0.16.0-beta.74", - "@xterm/addon-serialize": "^0.14.0-beta.74", - "@xterm/addon-unicode11": "^0.9.0-beta.74", - "@xterm/addon-webgl": "^0.19.0-beta.74", - "@xterm/headless": "^5.6.0-beta.74", - "@xterm/xterm": "^5.6.0-beta.74", + "@xterm/addon-clipboard": "^0.2.0-beta.68", + "@xterm/addon-image": "^0.9.0-beta.85", + "@xterm/addon-ligatures": "^0.10.0-beta.85", + "@xterm/addon-search": "^0.16.0-beta.85", + "@xterm/addon-serialize": "^0.14.0-beta.85", + "@xterm/addon-unicode11": "^0.9.0-beta.85", + "@xterm/addon-webgl": "^0.19.0-beta.85", + "@xterm/headless": "^5.6.0-beta.85", + "@xterm/xterm": "^5.6.0-beta.85", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 8b10d83600df..628d03e23cb9 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -13,14 +13,14 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.57", - "@xterm/addon-image": "^0.9.0-beta.74", - "@xterm/addon-ligatures": "^0.10.0-beta.74", - "@xterm/addon-search": "^0.16.0-beta.74", - "@xterm/addon-serialize": "^0.14.0-beta.74", - "@xterm/addon-unicode11": "^0.9.0-beta.74", - "@xterm/addon-webgl": "^0.19.0-beta.74", - "@xterm/xterm": "^5.6.0-beta.74", + "@xterm/addon-clipboard": "^0.2.0-beta.68", + "@xterm/addon-image": "^0.9.0-beta.85", + "@xterm/addon-ligatures": "^0.10.0-beta.85", + "@xterm/addon-search": "^0.16.0-beta.85", + "@xterm/addon-serialize": "^0.14.0-beta.85", + "@xterm/addon-unicode11": "^0.9.0-beta.85", + "@xterm/addon-webgl": "^0.19.0-beta.85", + "@xterm/xterm": "^5.6.0-beta.85", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", @@ -89,30 +89,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.57", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.57.tgz", - "integrity": "sha512-/GSI8Fkmb8s/V1t2EGc2U2PUfSqge6f9gAeob65EwarsfBf66cmCxMG0ZSPE8+nti1pGIsrJA8XfeEaJt4clcA==", + "version": "0.2.0-beta.68", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.68.tgz", + "integrity": "sha512-z/4urYG3dySjmnfwig2eH3rJNLVFIk3IGQ+Ibadu4GZwCAVkX7eV/uGMssIeVGMg/ZD3uVdocFvcaILPOz01Pg==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.74.tgz", - "integrity": "sha512-mJPWNPov2mqrUkYZCs6UCn5p6DBLeN6xjpLu5mLh8cmXr544VWfqEVNAYPQ9+8uNgXdzSsKInBv9ZGtbXV0SfA==", + "version": "0.9.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.85.tgz", + "integrity": "sha512-XyIG+v6eVXBKkW6rT5GLF8VBVvNdsdcCNBOlw1kWPiK31/hzxcnoPXXRDp6bqxxFOtcB8tlHe2mk/5lQG4JtPA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.74.tgz", - "integrity": "sha512-/X3OemVqPSgdely8OgdQb0cJqv9HqiMaBLeLe2QHfTWdXDBOLG/O4g8n/lChqR9rulEMwPCt2LqvtyIMA3iZsw==", + "version": "0.10.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.85.tgz", + "integrity": "sha512-fJKsmqjRIBr8TphyOefYnhH1nw1+HvtBmO1f6DX893a0qyLZ0cPIowuAABTBbu/j5mhwJveKw7pYIXScT42cBA==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -122,49 +122,49 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.74.tgz", - "integrity": "sha512-KMOeOu3EvtDWcH/HCs6fe4KyaMMdjoUjP1C7R3AtZwJVdm5GaFxASxdol+9evfGhUUei4qt+zVsaFraNKyFvsA==", + "version": "0.16.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.85.tgz", + "integrity": "sha512-Z1IZlBIfqyB4weBffIwHKFGRVVgxBz105RdXOk+Jt5iIXeocg/sjCM7iFNwwQL0vLGOfzIQBWrS8oqjWeYvDEg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.74.tgz", - "integrity": "sha512-zBhQAoXagBMhKFGYQ6n52sKapY61Jt3hKT8awhG/49f04JkaX1Jhc6xohD+aZs6J2ABHI7EWW/y0xTN9BDLLBw==", + "version": "0.14.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.85.tgz", + "integrity": "sha512-uMjrxcyF4ZCCKGI/4XLfq0/xEbCZkUsj6rORN0kem7GXKenNA1ggxk4z0Z8rFxwEyjV3wfjOaVPmxHJM3xh+gA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.74.tgz", - "integrity": "sha512-1AyuDST77Xg33ed+3neNrQfsfZVwa+C16uWP9eTdJ1rO48ylqPcFTDnahCfIZGPHGjPqLl90ZKZ8tt1zWSefmA==", + "version": "0.9.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.85.tgz", + "integrity": "sha512-mpsRneCyY9Jmu05KYISOmDqkWS4WCV4D0UxCHxGmdmjeHDsNItauGG7u2Qnct7K+RBhfJPDp0j2yCTMTWuv0KQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.74.tgz", - "integrity": "sha512-1wKiuv6WHMqOcSlLeIRU7UF8zkU4KU35rnPvLw2G45aAJSr9B3f7EVIaJ4IjXGeY/C+WCM3wWuybPN5VdB8qAQ==", + "version": "0.19.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.85.tgz", + "integrity": "sha512-qenYMn7XwBxujNkhialgGYvoKyGxbpGYiUmgQIdQPiV5yMQsaqz5S/o+iPKJM8sSRcI+ghxYS6KhiUOkzg5C3Q==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.74" + "@xterm/xterm": "^5.6.0-beta.85" } }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.74", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.74.tgz", - "integrity": "sha512-gVu7+4Cfd7O/6cQ/UK0sZM+TJBaI/VgQ/JFAhuAnNFj29wts2MzxSH3fIp3KUG1kqlJBWEUShCTwB8nKwtbCjQ==", + "version": "5.6.0-beta.85", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.85.tgz", + "integrity": "sha512-A2HpImW8FIlUOtkWm2FPnUdhhFa+ejshv5RJbejGCihGOnizsCeG8vBLt7uEJ37msvcJJSgFJ/VSmcFnh9Y9vA==", "license": "MIT" }, "node_modules/font-finder": { diff --git a/remote/web/package.json b/remote/web/package.json index 9b342d347e69..5b0a3a2a72cb 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -8,14 +8,14 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.57", - "@xterm/addon-image": "^0.9.0-beta.74", - "@xterm/addon-ligatures": "^0.10.0-beta.74", - "@xterm/addon-search": "^0.16.0-beta.74", - "@xterm/addon-serialize": "^0.14.0-beta.74", - "@xterm/addon-unicode11": "^0.9.0-beta.74", - "@xterm/addon-webgl": "^0.19.0-beta.74", - "@xterm/xterm": "^5.6.0-beta.74", + "@xterm/addon-clipboard": "^0.2.0-beta.68", + "@xterm/addon-image": "^0.9.0-beta.85", + "@xterm/addon-ligatures": "^0.10.0-beta.85", + "@xterm/addon-search": "^0.16.0-beta.85", + "@xterm/addon-serialize": "^0.14.0-beta.85", + "@xterm/addon-unicode11": "^0.9.0-beta.85", + "@xterm/addon-webgl": "^0.19.0-beta.85", + "@xterm/xterm": "^5.6.0-beta.85", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", From eb3dd0a4eed548baa1d14f0c5677d28bac5400b9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 8 Jan 2025 07:49:31 -0800 Subject: [PATCH 0411/3587] Create expanded terminal ligatures config options --- src/vs/platform/terminal/common/terminal.ts | 4 ++- .../terminal/browser/xterm/xtermTerminal.ts | 4 ++- .../terminal/common/terminalConfiguration.ts | 28 +++++++++++++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 97fc8e80462c..2accdc4b6121 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -117,7 +117,9 @@ export const enum TerminalSettingId { SmoothScrolling = 'terminal.integrated.smoothScrolling', IgnoreBracketedPasteMode = 'terminal.integrated.ignoreBracketedPasteMode', FocusAfterRun = 'terminal.integrated.focusAfterRun', - FontLigatures = 'terminal.integrated.fontLigatures', + FontLigaturesEnabled = 'terminal.integrated.fontLigatures.enabled', + FontLigaturesFeatureSettings = 'terminal.integrated.fontLigatures.featureSettings', + FontLigaturesFallbackLigatures = 'terminal.integrated.fontLigatures.fallbackLigatures', // Debug settings that are hidden from user diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 4e860821f7e9..b328523302a2 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -706,7 +706,9 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach if (this._store.isDisposed) { return; } - this._ligaturesAddon.value = this._instantiationService.createInstance(LigaturesAddon); + this._ligaturesAddon.value = this._instantiationService.createInstance(LigaturesAddon, { + + }); this.raw.loadAddon(this._ligaturesAddon.value); }); } diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 6743ad25d3c9..931c73f5575f 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -174,12 +174,36 @@ const terminalConfiguration: IConfigurationNode = { markdownDescription: localize('terminal.integrated.fontFamily', "Controls the font family of the terminal. Defaults to {0}'s value.", '`#editor.fontFamily#`'), type: 'string' }, - [TerminalSettingId.FontLigatures]: { - markdownDescription: localize('terminal.integrated.fontLigatures', "Controls whether font ligatures are enabled in the terminal. Ligatures will only work if the configured {0} supports them.", `\`#${TerminalSettingId.FontFamily}#\``), + [TerminalSettingId.FontLigaturesEnabled]: { + markdownDescription: localize('terminal.integrated.fontLigatures.enabled', "Controls whether font ligatures are enabled in the terminal. Ligatures will only work if the configured {0} supports them.", `\`#${TerminalSettingId.FontFamily}#\``), type: 'boolean', tags: ['preview'], default: false }, + [TerminalSettingId.FontLigaturesFeatureSettings]: { + markdownDescription: localize('terminal.integrated.fontLigatures.fontFeatureSettings', "Controls what font feature settings are used when ligatures are enabled, in the format of the `font-feature-settings` CSS property. Some examples which may be valid depending on the font:") + '\n\n- ' + [ + `\`"calt" off, "ss03"\``, + `\`"liga" on"\``, + `\`"calt" off, "dlig" on\`` + ].join('\n- '), + type: 'string', + tags: ['preview'], + default: '"calt" on' + }, + [TerminalSettingId.FontLigaturesFallbackLigatures]: { + markdownDescription: localize('terminal.integrated.fontLigatures.fallbackLigatures', "When {0} is enabled and the particular {1} cannot be parsed, this is the set of character sequences that will always be drawn together. This lets you leverage ligatures even when the font isn't supported.", `\`#${TerminalSettingId.GpuAcceleration}#\``, `\`#${TerminalSettingId.FontFamily}#\``), + type: 'array', + items: [{ type: 'string' }], + tags: ['preview'], + default: [ + '<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->', + '<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=', + '<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '::', ':::', + '<~~', '', '/>', '~~>', '==', '!=', '/=', '~=', '<>', '===', '!==', '!===', + '<:', ':=', '*=', '*+', '<*', '<*>', '*>', '<|', '<|>', '|>', '+*', '=*', '=:', ':>', + '/*', '*/', '+++', '', '--->', '<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=', From 2bcd6c467e6700b9876e07bcd7baf33115bf73e5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 8 Jan 2025 09:16:51 -0800 Subject: [PATCH 0414/3587] Retry executeCommand integration test on failure It's unclear how this happened and seems like a very rare flake. --- .../singlefolder-tests/terminal.shellIntegration.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts index 521a41ea865a..db48c2593e07 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts @@ -212,7 +212,12 @@ import { assertNoRpc } from '../utils'; await closeTerminalAsync(terminal); }); - test('executeCommand(executable, args)', async () => { + test('executeCommand(executable, args)', async function () { + // HACK: This test has flaked before where the `value` was `e`, not `echo hello`. After an + // investigation it's not clear how this happened, so in order to keep some of the value + // that the test adds, it will retry after a failure. + this.retries(3); + const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration(); const { execution, endEvent } = executeCommandAsync(shellIntegration, 'echo', ['hello']); const executionSync = await execution; From 4f2c166752c938b98b14244acb4003126536278f Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 8 Jan 2025 18:36:12 +0100 Subject: [PATCH 0415/3587] Use ICodeMapperService for 'Apply In Editor' --- .../api/browser/mainThreadChatCodeMapper.ts | 6 +- .../workbench/api/common/extHost.protocol.ts | 2 +- .../workbench/api/common/extHostCodeMapper.ts | 19 +- .../browser/actions/codeBlockOperations.ts | 198 +++++------------- .../contrib/chat/browser/tools/tools.ts | 3 +- .../chat/common/chatCodeMapperService.ts | 152 +------------- .../vscode.proposed.mappedEditsProvider.d.ts | 1 - 7 files changed, 72 insertions(+), 309 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts index abacb824c24b..eea3e819b976 100644 --- a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts +++ b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts @@ -25,15 +25,15 @@ export class MainThreadChatCodemapper extends Disposable implements MainThreadCo this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostCodeMapper); } - $registerCodeMapperProvider(handle: number): void { + $registerCodeMapperProvider(handle: number, displayName: string): void { const impl: ICodeMapperProvider = { + displayName, mapCode: async (uiRequest: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken) => { const requestId = String(MainThreadChatCodemapper._requestHandlePool++); this._responseMap.set(requestId, response); const extHostRequest: ICodeMapperRequestDto = { requestId, - codeBlocks: uiRequest.codeBlocks, - conversation: uiRequest.conversation + codeBlocks: uiRequest.codeBlocks }; try { return await this._proxy.$mapCode(handle, extHostRequest, token).then((result) => result ?? undefined); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 7bccc00f5b9c..9b7b0424e7a8 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1299,7 +1299,7 @@ export interface ICodeMapperTextEdit { export type ICodeMapperProgressDto = Dto; export interface MainThreadCodeMapperShape extends IDisposable { - $registerCodeMapperProvider(handle: number): void; + $registerCodeMapperProvider(handle: number, displayName: string): void; $unregisterCodeMapperProvider(handle: number): void; $handleProgress(requestId: string, data: ICodeMapperProgressDto): Promise; } diff --git a/src/vs/workbench/api/common/extHostCodeMapper.ts b/src/vs/workbench/api/common/extHostCodeMapper.ts index 0f6b9d129223..4a421e6f23d7 100644 --- a/src/vs/workbench/api/common/extHostCodeMapper.ts +++ b/src/vs/workbench/api/common/extHostCodeMapper.ts @@ -8,7 +8,7 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js'; import * as extHostProtocol from './extHost.protocol.js'; -import { ChatAgentResult, DocumentContextItem, TextEdit } from './extHostTypeConverters.js'; +import { TextEdit } from './extHostTypeConverters.js'; import { URI } from '../../../base/common/uri.js'; export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape { @@ -48,21 +48,6 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape resource: URI.revive(block.resource), markdownBeforeBlock: block.markdownBeforeBlock }; - }), - conversation: internalRequest.conversation.map(item => { - if (item.type === 'request') { - return { - type: 'request', - message: item.message - } satisfies vscode.ConversationRequest; - } else { - return { - type: 'response', - message: item.message, - result: item.result ? ChatAgentResult.to(item.result) : undefined, - references: item.references?.map(DocumentContextItem.to) - } satisfies vscode.ConversationResponse; - } }) }; @@ -72,7 +57,7 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape registerMappedEditsProvider(extension: IExtensionDescription, provider: vscode.MappedEditsProvider2): vscode.Disposable { const handle = ExtHostCodeMapper._providerHandlePool++; - this._proxy.$registerCodeMapperProvider(handle); + this._proxy.$registerCodeMapperProvider(handle, extension.displayName ?? extension.name); this.providers.set(handle, provider); return { dispose: () => { diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index 5462565f4ab3..c43362c46504 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce } from '../../../../../base/common/arrays.js'; import { AsyncIterableObject } from '../../../../../base/common/async.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; @@ -10,28 +9,27 @@ import { CharCode } from '../../../../../base/common/charCode.js'; import { isCancellationError } from '../../../../../base/common/errors.js'; import { isEqual } from '../../../../../base/common/resources.js'; import * as strings from '../../../../../base/common/strings.js'; +import { URI } from '../../../../../base/common/uri.js'; import { IActiveCodeEditor, isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js'; import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; import { Range } from '../../../../../editor/common/core/range.js'; -import { ConversationRequest, ConversationResponse, DocumentContextItem, IWorkspaceFileEdit, IWorkspaceTextEdit } from '../../../../../editor/common/languages.js'; +import { TextEdit } from '../../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { ITextModel } from '../../../../../editor/common/model.js'; -import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; import { localize } from '../../../../../nls.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; -import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js'; import { insertCell } from '../../../notebook/browser/controller/cellOperations.js'; import { IActiveNotebookEditor, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; import { CellKind, NOTEBOOK_EDITOR_ID } from '../../../notebook/common/notebookCommon.js'; -import { getReferencesAsDocumentContext } from '../../common/chatCodeMapperService.js'; +import { ICodeMapperRequest, ICodeMapperResponse, ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { ChatUserAction, IChatService } from '../../common/chatService.js'; -import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; +import { isResponseVM } from '../../common/chatViewModel.js'; import { ICodeBlockActionContext } from '../codeBlockPart.js'; export class InsertCodeBlockOperation { @@ -98,7 +96,7 @@ export class InsertCodeBlockOperation { } } -type IComputeEditsResult = { readonly edits?: Array; readonly codeMapper?: string }; +type IComputeEditsResult = { readonly editsProposed: boolean; readonly codeMapper?: string }; export class ApplyCodeBlockOperation { @@ -107,15 +105,13 @@ export class ApplyCodeBlockOperation { constructor( @IEditorService private readonly editorService: IEditorService, @ITextFileService private readonly textFileService: ITextFileService, - @IBulkEditService private readonly bulkEditService: IBulkEditService, @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @IChatService private readonly chatService: IChatService, - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IProgressService private readonly progressService: IProgressService, @ILanguageService private readonly languageService: ILanguageService, @IFileService private readonly fileService: IFileService, @IDialogService private readonly dialogService: IDialogService, @ILogService private readonly logService: ILogService, + @ICodeMapperService private readonly codeMapperService: ICodeMapperService ) { } @@ -166,7 +162,7 @@ export class ApplyCodeBlockOperation { codeBlockIndex: context.codeBlockIndex, totalCharacters: context.code.length, codeMapper: result?.codeMapper, - editsProposed: !!result?.edits, + editsProposed: !!result?.editsProposed }); } @@ -187,101 +183,60 @@ export class ApplyCodeBlockOperation { return undefined; } - const result = await this.computeEdits(codeEditor, codeBlockContext); - if (result.edits) { - const showWithPreview = await this.applyWithInlinePreview(result.edits, codeEditor); - if (!showWithPreview) { - await this.bulkEditService.apply(result.edits, { showPreview: true }); - const activeModel = codeEditor.getModel(); - this.codeEditorService.listCodeEditors().find(editor => editor.getModel()?.uri.toString() === activeModel.uri.toString())?.focus(); - } - } - return result; - } + const cancellationTokenSource = new CancellationTokenSource(); + try { + const activeModel = codeEditor.getModel(); + const resource = codeBlockContext.codemapperUri ?? activeModel.uri; + let codeMapper: string | undefined; - private async computeEdits(codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { - const activeModel = codeEditor.getModel(); - - const mappedEditsProviders = this.languageFeaturesService.mappedEditsProvider.ordered(activeModel); - if (mappedEditsProviders.length > 0) { - - // 0th sub-array - editor selections array if there are any selections - // 1st sub-array - array with documents used to get the chat reply - const docRefs: DocumentContextItem[][] = []; - collectDocumentContextFromSelections(codeEditor, docRefs); - collectDocumentContextFromContext(codeBlockActionContext, docRefs); - - const cancellationTokenSource = new CancellationTokenSource(); - let codeMapper; // the last used code mapper - try { - const result = await this.progressService.withProgress( - { location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true }, - async progress => { - for (const provider of mappedEditsProviders) { - codeMapper = provider.displayName; - progress.report({ message: localize('applyCodeBlock.progress', "Applying code block using {0}...", codeMapper) }); - const mappedEdits = await provider.provideMappedEdits( - activeModel, - [codeBlockActionContext.code], - { - documents: docRefs, - conversation: getChatConversation(codeBlockActionContext), - }, - cancellationTokenSource.token - ); - if (mappedEdits) { - return { edits: mappedEdits.edits, codeMapper }; - } - } - return undefined; - }, - () => cancellationTokenSource.cancel() - ); - if (result) { - return result; - } - } catch (e) { - if (!isCancellationError(e)) { - this.notify(localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message)); + const iterable = new AsyncIterableObject(async executor => { + const request: ICodeMapperRequest = { + codeBlocks: [{ code: codeBlockContext.code, resource, markdownBeforeBlock: undefined }] + }; + const response: ICodeMapperResponse = { + textEdit: (target: URI, edit: TextEdit[]) => { + executor.emitOne(edit); + } + }; + const result = await this.codeMapperService.mapCode(request, response, cancellationTokenSource.token); + codeMapper = result?.providerName; + if (result?.errorMessage) { + executor.reject(new Error(result.errorMessage)); } - } finally { - cancellationTokenSource.dispose(); + }); + + const editorToApply = await this.codeEditorService.openCodeEditor({ resource }, codeEditor); + let result = false; + if (editorToApply && editorToApply.hasModel()) { + result = await this.applyWithInlinePreview(iterable, editorToApply, cancellationTokenSource); } - return { edits: [], codeMapper }; + return { editsProposed: result, codeMapper }; + } catch (e) { + if (!isCancellationError(e)) { + this.notify(localize('applyCodeBlock.error', "An error occurred while applying the code block.")); + } + } finally { + cancellationTokenSource.dispose(); } - return { edits: [], codeMapper: undefined }; + return undefined; + } + private async applyWithInlinePreview(edits: AsyncIterable, codeEditor: IActiveCodeEditor, tokenSource: CancellationTokenSource): Promise { + const inlineChatController = InlineChatController.get(codeEditor); + if (inlineChatController) { + let isOpen = true; + const promise = inlineChatController.reviewEdits(codeEditor.getSelection(), edits, tokenSource.token); + promise.finally(() => { + isOpen = false; + tokenSource.dispose(); + }); + this.inlineChatPreview = { + promise, + isOpen: () => isOpen, + cancel: () => tokenSource.cancel(), + }; + return true; - private async applyWithInlinePreview(edits: Array, codeEditor: IActiveCodeEditor): Promise { - const firstEdit = edits[0]; - if (!ResourceTextEdit.is(firstEdit)) { - return false; - } - const resource = firstEdit.resource; - const textEdits = coalesce(edits.map(edit => ResourceTextEdit.is(edit) && isEqual(resource, edit.resource) ? edit.textEdit : undefined)); - if (textEdits.length !== edits.length) { // more than one file has changed, fall back to bulk edit preview - return false; - } - const editorToApply = await this.codeEditorService.openCodeEditor({ resource }, codeEditor); - if (editorToApply) { - const inlineChatController = InlineChatController.get(editorToApply); - if (inlineChatController) { - const tokenSource = new CancellationTokenSource(); - let isOpen = true; - const firstEdit = textEdits[0]; - editorToApply.revealLineInCenterIfOutsideViewport(firstEdit.range.startLineNumber); - const promise = inlineChatController.reviewEdits(textEdits[0].range, AsyncIterableObject.fromArray([textEdits]), tokenSource.token); - promise.finally(() => { - isOpen = false; - tokenSource.dispose(); - }); - this.inlineChatPreview = { - promise, - isOpen: () => isOpen, - cancel: () => tokenSource.cancel(), - }; - return true; - } } return false; } @@ -360,49 +315,6 @@ function isReadOnly(model: ITextModel, textFileService: ITextFileService): boole return !!activeTextModel?.isReadonly(); } -function collectDocumentContextFromSelections(codeEditor: IActiveCodeEditor, result: DocumentContextItem[][]): void { - const activeModel = codeEditor.getModel(); - const currentDocUri = activeModel.uri; - const currentDocVersion = activeModel.getVersionId(); - const selections = codeEditor.getSelections(); - if (selections.length > 0) { - result.push([ - { - uri: currentDocUri, - version: currentDocVersion, - ranges: selections, - } - ]); - } -} - - -function collectDocumentContextFromContext(context: ICodeBlockActionContext, result: DocumentContextItem[][]): void { - if (isResponseVM(context.element) && context.element.usedContext?.documents) { - result.push(context.element.usedContext.documents); - } -} - -function getChatConversation(context: ICodeBlockActionContext): (ConversationRequest | ConversationResponse)[] { - // TODO@aeschli for now create a conversation with just the current element - // this will be expanded in the future to include the request and any other responses - - if (isResponseVM(context.element)) { - return [{ - type: 'response', - message: context.element.response.getMarkdown(), - references: getReferencesAsDocumentContext(context.element.contentReferences) - }]; - } else if (isRequestVM(context.element)) { - return [{ - type: 'request', - message: context.element.messageText, - }]; - } else { - return []; - } -} - function reindent(codeBlockContent: string, model: ITextModel, seletionStartLine: number): string { const newContent = strings.splitLines(codeBlockContent); if (newContent.length === 0) { diff --git a/src/vs/workbench/contrib/chat/browser/tools/tools.ts b/src/vs/workbench/contrib/chat/browser/tools/tools.ts index 359b47eb4548..53320370c5a0 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/tools.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/tools.ts @@ -125,8 +125,7 @@ class EditTool implements IToolData, IToolImpl { } const result = await this.codeMapperService.mapCode({ - codeBlocks: [{ code: parameters.code, resource: uri, markdownBeforeBlock: parameters.explanation }], - conversation: [] + codeBlocks: [{ code: parameters.code, resource: uri, markdownBeforeBlock: parameters.explanation }] }, { textEdit: (target, edits) => { model.acceptResponseProgress(request, { kind: 'textEdit', uri: target, edits }); diff --git a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts index 097ca6d89157..f8aab6da4b34 100644 --- a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts +++ b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts @@ -4,18 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { CharCode } from '../../../../base/common/charCode.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; -import { ResourceMap } from '../../../../base/common/map.js'; -import { splitLinesIncludeSeparators } from '../../../../base/common/strings.js'; -import { isString } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; -import { DocumentContextItem, isLocation, TextEdit } from '../../../../editor/common/languages.js'; +import { TextEdit } from '../../../../editor/common/languages.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IChatAgentResult } from './chatAgents.js'; -import { IChatResponseModel } from './chatModel.js'; -import { IChatContentReference } from './chatService.js'; - export interface ICodeMapperResponse { textEdit: (resource: URI, textEdit: TextEdit[]) => void; @@ -27,21 +19,8 @@ export interface ICodeMapperCodeBlock { readonly markdownBeforeBlock?: string; } -export interface ConversationRequest { - readonly type: 'request'; - readonly message: string; -} - -export interface ConversationResponse { - readonly type: 'response'; - readonly message: string; - readonly result?: IChatAgentResult; - readonly references?: DocumentContextItem[]; -} - export interface ICodeMapperRequest { readonly codeBlocks: ICodeMapperCodeBlock[]; - readonly conversation: (ConversationResponse | ConversationRequest)[]; } export interface ICodeMapperResult { @@ -49,16 +28,20 @@ export interface ICodeMapperResult { } export interface ICodeMapperProvider { + readonly displayName: string; mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise; } export const ICodeMapperService = createDecorator('codeMapperService'); +export interface ICodeMapperServiceResult extends ICodeMapperResult { + readonly providerName: string; +} + export interface ICodeMapperService { readonly _serviceBrand: undefined; registerCodeMapperProvider(handle: number, provider: ICodeMapperProvider): IDisposable; - mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise; - mapCodeFromResponse(responseModel: IChatResponseModel, response: ICodeMapperResponse, token: CancellationToken): Promise; + mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise; } export class CodeMapperService implements ICodeMapperService { @@ -82,127 +65,12 @@ export class CodeMapperService implements ICodeMapperService { for (const provider of this.providers) { const result = await provider.mapCode(request, response, token); if (result) { - return result; + return { providerName: provider.displayName, ...result }; + } else if (token.isCancellationRequested) { + return undefined; } } return undefined; } - - async mapCodeFromResponse(responseModel: IChatResponseModel, response: ICodeMapperResponse, token: CancellationToken) { - const fenceLanguageRegex = /^`{3,}/; - const codeBlocks: ICodeMapperCodeBlock[] = []; - - const currentBlock = []; - const markdownBeforeBlock = []; - let currentBlockUri = undefined; - - let fence = undefined; // if set, we are in a block - - for (const lineOrUri of iterateLinesOrUris(responseModel)) { - if (isString(lineOrUri)) { - const fenceLanguageIdMatch = lineOrUri.match(fenceLanguageRegex); - if (fenceLanguageIdMatch) { - // we found a line that starts with a fence - if (fence !== undefined && fenceLanguageIdMatch[0] === fence) { - // we are in a code block and the fence matches the opening fence: Close the code block - fence = undefined; - if (currentBlockUri) { - // report the code block if we have a URI - codeBlocks.push({ code: currentBlock.join(''), resource: currentBlockUri, markdownBeforeBlock: markdownBeforeBlock.join('') }); - currentBlock.length = 0; - markdownBeforeBlock.length = 0; - currentBlockUri = undefined; - } - } else { - // we are not in a code block. Open the block - fence = fenceLanguageIdMatch[0]; - } - } else { - if (fence !== undefined) { - currentBlock.push(lineOrUri); - } else { - markdownBeforeBlock.push(lineOrUri); - } - } - } else { - currentBlockUri = lineOrUri; - } - } - const conversation: (ConversationRequest | ConversationResponse)[] = []; - for (const request of responseModel.session.getRequests()) { - const response = request.response; - if (!response || response === responseModel) { - break; - } - conversation.push({ - type: 'request', - message: request.message.text - }); - conversation.push({ - type: 'response', - message: response.response.getMarkdown(), - result: response.result, - references: getReferencesAsDocumentContext(response.contentReferences) - }); - } - return this.mapCode({ codeBlocks, conversation }, response, token); - } -} - -function iterateLinesOrUris(responseModel: IChatResponseModel): Iterable { - return { - *[Symbol.iterator](): Iterator { - let lastIncompleteLine = undefined; - for (const part of responseModel.response.value) { - if (part.kind === 'markdownContent' || part.kind === 'markdownVuln') { - const lines = splitLinesIncludeSeparators(part.content.value); - if (lines.length > 0) { - if (lastIncompleteLine !== undefined) { - lines[0] = lastIncompleteLine + lines[0]; // merge the last incomplete line with the first markdown line - } - lastIncompleteLine = isLineIncomplete(lines[lines.length - 1]) ? lines.pop() : undefined; - for (const line of lines) { - yield line; - } - } - } else if (part.kind === 'codeblockUri') { - yield part.uri; - } - } - if (lastIncompleteLine !== undefined) { - yield lastIncompleteLine; - } - } - }; -} - -function isLineIncomplete(line: string) { - const lastChar = line.charCodeAt(line.length - 1); - return lastChar !== CharCode.LineFeed && lastChar !== CharCode.CarriageReturn; } - -export function getReferencesAsDocumentContext(res: readonly IChatContentReference[]): DocumentContextItem[] { - const map = new ResourceMap(); - for (const r of res) { - let uri; - let range; - if (URI.isUri(r.reference)) { - uri = r.reference; - } else if (isLocation(r.reference)) { - uri = r.reference.uri; - range = r.reference.range; - } - if (uri) { - const item = map.get(uri); - if (item) { - if (range) { - item.ranges.push(range); - } - } else { - map.set(uri, { uri, version: -1, ranges: range ? [range] : [] }); - } - } - } - return [...map.values()]; -} diff --git a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts index 04d95579f4d2..fb10b6a3385c 100644 --- a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts @@ -57,7 +57,6 @@ declare module 'vscode' { export interface MappedEditsRequest { readonly codeBlocks: { code: string; resource: Uri; markdownBeforeBlock?: string }[]; - readonly conversation: Array; // for every prior response that contains codeblocks, make sure we pass the code as well as the resources based on the reported codemapper URIs } export interface MappedEditsResponseStream { From 4468259a6a71add0bd00ecb5c3ef09c8c98bd16e Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 8 Jan 2025 12:02:44 -0600 Subject: [PATCH 0416/3587] add info to a11y help dialog about chat edits (#237510) --- src/vs/editor/common/standaloneStrings.ts | 3 +++ .../browser/editorAccessibilityHelp.ts | 17 +++++++++++++++++ .../browser/actions/chatAccessibilityHelp.ts | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/common/standaloneStrings.ts b/src/vs/editor/common/standaloneStrings.ts index a90add968659..f2edfd33f60a 100644 --- a/src/vs/editor/common/standaloneStrings.ts +++ b/src/vs/editor/common/standaloneStrings.ts @@ -34,6 +34,9 @@ export namespace AccessibilityHelpNLS { export const setBreakpoint = nls.localize('debugConsole.setBreakpoint', "The Debug: Inline Breakpoint command{0} will set or unset a breakpoint at the current cursor position in the active editor.", ''); export const addToWatch = nls.localize('debugConsole.addToWatch', "The Debug: Add to Watch command{0} will add the selected text to the watch view.", ''); export const debugExecuteSelection = nls.localize('debugConsole.executeSelection', "The Debug: Execute Selection command{0} will execute the selected text in the debug console.", ''); + export const chatEditorModification = nls.localize('chatEditorModification', "The editor contains pending modifications that have been made by chat."); + export const chatEditorRequestInProgress = nls.localize('chatEditorRequestInProgress', "The editor is currently waiting for modifications to be made by chat."); + export const chatEditActions = nls.localize('chatEditing.navigation', 'Navigate between edits in the editor with navigate previous{0} and next{1} and accept{3} and reject the current change{4}.', '', '', '', ''); } export namespace InspectTokensNLS { diff --git a/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts b/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts index 9f0b4190a226..9a0043120880 100644 --- a/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts @@ -19,6 +19,7 @@ import { CommentContextKeys } from '../../comments/common/commentContextKeys.js' import { NEW_UNTITLED_FILE_COMMAND_ID } from '../../files/browser/fileConstants.js'; import { IAccessibleViewService, IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType } from '../../../../platform/accessibility/browser/accessibleView.js'; import { AccessibilityVerbositySettingId } from './accessibilityConfiguration.js'; +import { ctxHasEditorModification, ctxHasRequestInProgress } from '../../chat/browser/chatEditorController.js'; export class EditorAccessibilityHelpContribution extends Disposable { static ID: 'editorAccessibilityHelpContribution'; @@ -72,9 +73,15 @@ class EditorAccessibilityHelpProvider extends Disposable implements IAccessibleV } } + const chatEditInfo = getChatEditInfo(this._keybindingService, this._contextKeyService, this._editor); + if (chatEditInfo) { + content.push(chatEditInfo); + } + content.push(AccessibilityHelpNLS.listSignalSounds); content.push(AccessibilityHelpNLS.listAlerts); + const chatCommandInfo = getChatCommandInfo(this._keybindingService, this._contextKeyService); if (chatCommandInfo) { content.push(chatCommandInfo); @@ -120,3 +127,13 @@ export function getChatCommandInfo(keybindingService: IKeybindingService, contex } return; } + +export function getChatEditInfo(keybindingService: IKeybindingService, contextKeyService: IContextKeyService, editor: ICodeEditor): string | undefined { + const editorContext = contextKeyService.getContext(editor.getDomNode()!); + if (editorContext.getValue(ctxHasEditorModification.key)) { + return AccessibilityHelpNLS.chatEditorModification + '\n' + AccessibilityHelpNLS.chatEditActions; + } else if (editorContext.getValue(ctxHasRequestInProgress.key)) { + return AccessibilityHelpNLS.chatEditorRequestInProgress; + } + return; +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index e935fda978c8..098054522798 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -45,7 +45,7 @@ export class EditsChatAccessibilityHelp implements IAccessibleViewImplentation { readonly priority = 119; readonly name = 'editsView'; readonly type = AccessibleViewType.Help; - readonly when = ActiveAuxiliaryContext.isEqualTo('workbench.panel.chatEditing'); + readonly when = ContextKeyExpr.and(ActiveAuxiliaryContext.isEqualTo('workbench.panel.chatEditing'), ChatContextKeys.inChatInput); getProvider(accessor: ServicesAccessor) { const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'editsView'); From 691eaea3bdc320ca911a8dd32fb23c25717461bb Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 8 Jan 2025 10:11:01 -0800 Subject: [PATCH 0417/3587] Remove old code, simplify properties (#237512) --- .../src/common/async.ts | 137 +----------------- .../src/node/cachedPublicClientApplication.ts | 79 +++------- 2 files changed, 25 insertions(+), 191 deletions(-) diff --git a/extensions/microsoft-authentication/src/common/async.ts b/extensions/microsoft-authentication/src/common/async.ts index 9eebbb24f65a..3555bb095026 100644 --- a/extensions/microsoft-authentication/src/common/async.ts +++ b/extensions/microsoft-authentication/src/common/async.ts @@ -5,11 +5,6 @@ import { CancellationError, CancellationToken, Disposable, Event } from 'vscode'; -/** - * Can be passed into the Delayed to defer using a microtask - */ -export const MicrotaskDelay = Symbol('MicrotaskDelay'); - export class SequencerByKey { private promiseMap = new Map>(); @@ -57,7 +52,7 @@ export class IntervalTimer extends Disposable { * Returns a promise that rejects with an {@CancellationError} as soon as the passed token is cancelled. * @see {@link raceCancellation} */ -export function raceCancellationError(promise: Promise, token: CancellationToken): Promise { +function raceCancellationError(promise: Promise, token: CancellationToken): Promise { return new Promise((resolve, reject) => { const ref = token.onCancellationRequested(() => { ref.dispose(); @@ -67,7 +62,7 @@ export function raceCancellationError(promise: Promise, token: Cancellatio }); } -export function raceTimeoutError(promise: Promise, timeout: number): Promise { +function raceTimeoutError(promise: Promise, timeout: number): Promise { return new Promise((resolve, reject) => { const ref = setTimeout(() => { reject(new CancellationError()); @@ -80,138 +75,12 @@ export function raceCancellationAndTimeoutError(promise: Promise, token: C return raceCancellationError(raceTimeoutError(promise, timeout), token); } -interface IScheduledLater extends Disposable { - isTriggered(): boolean; -} - -const timeoutDeferred = (timeout: number, fn: () => void): IScheduledLater => { - let scheduled = true; - const handle = setTimeout(() => { - scheduled = false; - fn(); - }, timeout); - return { - isTriggered: () => scheduled, - dispose: () => { - clearTimeout(handle); - scheduled = false; - }, - }; -}; - -const microtaskDeferred = (fn: () => void): IScheduledLater => { - let scheduled = true; - queueMicrotask(() => { - if (scheduled) { - scheduled = false; - fn(); - } - }); - - return { - isTriggered: () => scheduled, - dispose: () => { scheduled = false; }, - }; -}; - -/** - * A helper to delay (debounce) execution of a task that is being requested often. - * - * Following the throttler, now imagine the mail man wants to optimize the number of - * trips proactively. The trip itself can be long, so he decides not to make the trip - * as soon as a letter is submitted. Instead he waits a while, in case more - * letters are submitted. After said waiting period, if no letters were submitted, he - * decides to make the trip. Imagine that N more letters were submitted after the first - * one, all within a short period of time between each other. Even though N+1 - * submissions occurred, only 1 delivery was made. - * - * The delayer offers this behavior via the trigger() method, into which both the task - * to be executed and the waiting period (delay) must be passed in as arguments. Following - * the example: - * - * const delayer = new Delayer(WAITING_PERIOD); - * const letters = []; - * - * function letterReceived(l) { - * letters.push(l); - * delayer.trigger(() => { return makeTheTrip(); }); - * } - */ -export class Delayer implements Disposable { - - private deferred: IScheduledLater | null; - private completionPromise: Promise | null; - private doResolve: ((value?: any | Promise) => void) | null; - private doReject: ((err: any) => void) | null; - private task: (() => T | Promise) | null; - - constructor(public defaultDelay: number | typeof MicrotaskDelay) { - this.deferred = null; - this.completionPromise = null; - this.doResolve = null; - this.doReject = null; - this.task = null; - } - - trigger(task: () => T | Promise, delay = this.defaultDelay): Promise { - this.task = task; - this.cancelTimeout(); - - if (!this.completionPromise) { - this.completionPromise = new Promise((resolve, reject) => { - this.doResolve = resolve; - this.doReject = reject; - }).then(() => { - this.completionPromise = null; - this.doResolve = null; - if (this.task) { - const task = this.task; - this.task = null; - return task(); - } - return undefined; - }); - } - - const fn = () => { - this.deferred = null; - this.doResolve?.(null); - }; - - this.deferred = delay === MicrotaskDelay ? microtaskDeferred(fn) : timeoutDeferred(delay, fn); - - return this.completionPromise; - } - - isTriggered(): boolean { - return !!this.deferred?.isTriggered(); - } - - cancel(): void { - this.cancelTimeout(); - - if (this.completionPromise) { - this.doReject?.(new CancellationError()); - this.completionPromise = null; - } - } - - private cancelTimeout(): void { - this.deferred?.dispose(); - this.deferred = null; - } - - dispose(): void { - this.cancel(); - } -} - /** * Given an event, returns another event which only fires once. * * @param event The event source for the new event. */ -export function once(event: Event): Event { +function once(event: Event): Event { return (listener, thisArgs = null, disposables?) => { // we need this, in case the event fires during the listener call let didFire = false; diff --git a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts index 616b3e1120d5..7ebf7a1630cb 100644 --- a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts +++ b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts @@ -6,44 +6,29 @@ import { PublicClientApplication, AccountInfo, Configuration, SilentFlowRequest, AuthenticationResult, InteractiveRequest, LogLevel, RefreshTokenRequest } from '@azure/msal-node'; import { NativeBrokerPlugin } from '@azure/msal-node-extensions'; import { Disposable, Memento, SecretStorage, LogOutputChannel, window, ProgressLocation, l10n, EventEmitter } from 'vscode'; -import { Delayer, raceCancellationAndTimeoutError } from '../common/async'; +import { raceCancellationAndTimeoutError } from '../common/async'; import { SecretStorageCachePlugin } from '../common/cachePlugin'; import { MsalLoggerOptions } from '../common/loggerOptions'; import { ICachedPublicClientApplication } from '../common/publicClientCache'; import { ScopedAccountAccess } from '../common/accountAccess'; export class CachedPublicClientApplication implements ICachedPublicClientApplication { + // Core properties private _pca: PublicClientApplication; - private _sequencer = new Sequencer(); - // private readonly _refreshDelayer = new DelayerByKey(); - private _accounts: AccountInfo[] = []; + private _sequencer = new Sequencer(); private readonly _disposable: Disposable; - private readonly _loggerOptions = new MsalLoggerOptions(this._logger); + // Cache properties private readonly _secretStorageCachePlugin = new SecretStorageCachePlugin( this._secretStorage, // Include the prefix as a differentiator to other secrets `pca:${JSON.stringify({ clientId: this._clientId, authority: this._authority })}` ); + + // Broker properties private readonly _accountAccess = new ScopedAccountAccess(this._secretStorage, this._cloudName, this._clientId, this._authority); - private readonly _config: Configuration = { - auth: { clientId: this._clientId, authority: this._authority }, - system: { - loggerOptions: { - correlationId: `${this._clientId}] [${this._authority}`, - loggerCallback: (level, message, containsPii) => this._loggerOptions.loggerCallback(level, message, containsPii), - logLevel: LogLevel.Trace - } - }, - broker: { - nativeBrokerPlugin: new NativeBrokerPlugin() - }, - cache: { - cachePlugin: this._secretStorageCachePlugin - } - }; - private readonly _isBrokerAvailable = this._config.broker?.nativeBrokerPlugin?.isBrokerAvailable ?? false; + private readonly _isBrokerAvailable: boolean; //#region Events @@ -65,7 +50,21 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica ) { // TODO:@TylerLeonhardt clean up old use of memento. Remove this in an iteration this._globalMemento.update(`lastRemoval:${this._clientId}:${this._authority}`, undefined); - this._pca = new PublicClientApplication(this._config); + const loggerOptions = new MsalLoggerOptions(_logger); + const nativeBrokerPlugin = new NativeBrokerPlugin(); + this._isBrokerAvailable = nativeBrokerPlugin.isBrokerAvailable ?? false; + this._pca = new PublicClientApplication({ + auth: { clientId: _clientId, authority: _authority }, + system: { + loggerOptions: { + correlationId: `${_clientId}] [${_authority}`, + loggerCallback: (level, message, containsPii) => loggerOptions.loggerCallback(level, message, containsPii), + logLevel: LogLevel.Trace + } + }, + broker: { nativeBrokerPlugin }, + cache: { cachePlugin: this._secretStorageCachePlugin } + }); this._disposable = Disposable.from( this._registerOnSecretStorageChanged(), this._onDidAccountsChangeEmitter, @@ -117,7 +116,6 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica } } - // this._setupRefresh(result); if (result.account && !result.fromCache && this._verifyIfUsingBroker(result)) { this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] firing event due to change`); this._onDidAccountsChangeEmitter.fire({ added: [], changed: [result.account], deleted: [] }); @@ -139,7 +137,6 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica 1000 * 60 * 5 )) ); - // this._setupRefresh(result); if (this._isBrokerAvailable) { await this._accountAccess.setAllowedAccess(result.account!, true); } @@ -226,24 +223,6 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica } this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update complete`); } - - // private _setupRefresh(result: AuthenticationResult) { - // const on = result.refreshOn || result.expiresOn; - // if (!result.account || !on) { - // return; - // } - - // const account = result.account; - // const scopes = result.scopes; - // const timeToRefresh = on.getTime() - Date.now() - 5 * 60 * 1000; // 5 minutes before expiry - // const key = JSON.stringify({ accountId: account.homeAccountId, scopes }); - // this._logger.debug(`[_setupRefresh] [${this._clientId}] [${this._authority}] [${scopes.join(' ')}] [${account.username}] timeToRefresh: ${timeToRefresh}`); - // this._refreshDelayer.trigger( - // key, - // () => this.acquireTokenSilent({ account, scopes, redirectUri: 'https://vscode.dev/redirect', forceRefresh: true }), - // timeToRefresh > 0 ? timeToRefresh : 0 - // ); - // } } export class Sequencer { @@ -254,17 +233,3 @@ export class Sequencer { return this.current = this.current.then(() => promiseTask(), () => promiseTask()); } } - -// class DelayerByKey { -// private _delayers = new Map>(); - -// trigger(key: string, fn: () => Promise, delay: number): Promise { -// let delayer = this._delayers.get(key); -// if (!delayer) { -// delayer = new Delayer(delay); -// this._delayers.set(key, delayer); -// } - -// return delayer.trigger(fn, delay); -// } -// } From dca80ea667ad45137e53e0e7d2622ea815c1b6de Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 8 Jan 2025 20:34:54 +0100 Subject: [PATCH 0418/3587] GitHub - add "Open on GitHub" to blame hover (#237514) * WIP - saving my work * Refactor hover rendering code --- extensions/git-base/src/api/api1.ts | 8 +- extensions/git-base/src/api/git-base.d.ts | 5 +- extensions/git-base/src/remoteSource.ts | 16 ++- extensions/git/src/blame.ts | 104 +++++++++++------- extensions/git/src/remoteSource.ts | 4 + extensions/git/src/repository.ts | 20 +++- extensions/git/src/typings/git-base.d.ts | 8 +- extensions/github/src/commands.ts | 7 +- extensions/github/src/links.ts | 9 ++ extensions/github/src/remoteSourceProvider.ts | 16 ++- extensions/github/src/typings/git-base.d.ts | 5 +- 11 files changed, 146 insertions(+), 56 deletions(-) diff --git a/extensions/git-base/src/api/api1.ts b/extensions/git-base/src/api/api1.ts index 005a79303563..74edc7f4452c 100644 --- a/extensions/git-base/src/api/api1.ts +++ b/extensions/git-base/src/api/api1.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, commands } from 'vscode'; +import { Command, Disposable, commands } from 'vscode'; import { Model } from '../model'; -import { getRemoteSourceActions, pickRemoteSource } from '../remoteSource'; +import { getRemoteSourceActions, getRemoteSourceControlHistoryItemCommands, pickRemoteSource } from '../remoteSource'; import { GitBaseExtensionImpl } from './extension'; import { API, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction, RemoteSourceProvider } from './git-base'; @@ -21,6 +21,10 @@ export class ApiImpl implements API { return getRemoteSourceActions(this._model, url); } + getRemoteSourceControlHistoryItemCommands(url: string): Promise { + return getRemoteSourceControlHistoryItemCommands(this._model, url); + } + registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable { return this._model.registerRemoteSourceProvider(provider); } diff --git a/extensions/git-base/src/api/git-base.d.ts b/extensions/git-base/src/api/git-base.d.ts index 53cac4d5c70f..37dd2c4229c4 100644 --- a/extensions/git-base/src/api/git-base.d.ts +++ b/extensions/git-base/src/api/git-base.d.ts @@ -3,11 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, Event, ProviderResult, Uri } from 'vscode'; +import { Command, Disposable, Event, ProviderResult } from 'vscode'; export { ProviderResult } from 'vscode'; export interface API { registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; + getRemoteSourceActions(url: string): Promise; + getRemoteSourceControlHistoryItemCommands(url: string): Promise; pickRemoteSource(options: PickRemoteSourceOptions): Promise; } @@ -80,6 +82,7 @@ export interface RemoteSourceProvider { getBranches?(url: string): ProviderResult; getRemoteSourceActions?(url: string): ProviderResult; + getRemoteSourceControlHistoryItemCommands?(url: string): ProviderResult; getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; } diff --git a/extensions/git-base/src/remoteSource.ts b/extensions/git-base/src/remoteSource.ts index eb86b27367aa..8d8d4ab102f5 100644 --- a/extensions/git-base/src/remoteSource.ts +++ b/extensions/git-base/src/remoteSource.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { QuickPickItem, window, QuickPick, QuickPickItemKind, l10n, Disposable } from 'vscode'; +import { QuickPickItem, window, QuickPick, QuickPickItemKind, l10n, Disposable, Command } from 'vscode'; import { RemoteSourceProvider, RemoteSource, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction } from './api/git-base'; import { Model } from './model'; import { throttle, debounce } from './decorators'; @@ -123,6 +123,20 @@ export async function getRemoteSourceActions(model: Model, url: string): Promise return remoteSourceActions; } +export async function getRemoteSourceControlHistoryItemCommands(model: Model, url: string): Promise { + const providers = model.getRemoteProviders(); + + const remoteSourceCommands = []; + for (const provider of providers) { + const providerCommands = await provider.getRemoteSourceControlHistoryItemCommands?.(url); + if (providerCommands?.length) { + remoteSourceCommands.push(...providerCommands); + } + } + + return remoteSourceCommands; +} + export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise; export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch: true }): Promise; export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise { diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index c8f15ffdf947..110b4601b15f 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -12,6 +12,7 @@ import { BlameInformation, Commit } from './git'; import { fromGitUri, isGitUri } from './uri'; import { emojify, ensureEmojis } from './emoji'; import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation } from './staging'; +import { getRemoteSourceControlHistoryItemCommands } from './remoteSource'; function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean { return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive); @@ -60,10 +61,6 @@ function getEditorDecorationRange(lineNumber: number): Range { return new Range(position, position); } -function isBlameInformation(object: any): object is BlameInformation { - return Array.isArray((object as BlameInformation).ranges); -} - function isResourceSchemeSupported(uri: Uri): boolean { return uri.scheme === 'file' || isGitUri(uri); } @@ -206,68 +203,95 @@ export class GitBlameController { }); } - async getBlameInformationDetailedHover(documentUri: Uri, blameInformation: BlameInformation): Promise { + async getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation, includeCommitDetails = false): Promise { + let commitInformation: Commit | undefined; + const remoteSourceCommands: Command[] = []; + const repository = this._model.getRepository(documentUri); - if (!repository) { - return this.getBlameInformationHover(documentUri, blameInformation); - } + if (repository) { + // Commit details + if (includeCommitDetails) { + try { + commitInformation = await repository.getCommit(blameInformation.hash); + } catch { } + } - try { - const commit = await repository.getCommit(blameInformation.hash); - return this.getBlameInformationHover(documentUri, commit); - } catch { - return this.getBlameInformationHover(documentUri, blameInformation); + // Remote commands + const defaultRemote = repository.getDefaultRemote(); + if (defaultRemote?.fetchUrl) { + remoteSourceCommands.push(...await getRemoteSourceControlHistoryItemCommands(defaultRemote.fetchUrl)); + } } - } - getBlameInformationHover(documentUri: Uri, blameInformationOrCommit: BlameInformation | Commit): MarkdownString { const markdownString = new MarkdownString(); markdownString.isTrusted = true; markdownString.supportHtml = true; markdownString.supportThemeIcons = true; - if (blameInformationOrCommit.authorName) { - if (blameInformationOrCommit.authorEmail) { + // Author, date + const authorName = commitInformation?.authorName ?? blameInformation.authorName; + const authorEmail = commitInformation?.authorEmail ?? blameInformation.authorEmail; + const authorDate = commitInformation?.authorDate ?? blameInformation.authorDate; + + if (authorName) { + if (authorEmail) { const emailTitle = l10n.t('Email'); - markdownString.appendMarkdown(`$(account) [**${blameInformationOrCommit.authorName}**](mailto:${blameInformationOrCommit.authorEmail} "${emailTitle} ${blameInformationOrCommit.authorName}")`); + markdownString.appendMarkdown(`$(account) [**${authorName}**](mailto:${authorEmail} "${emailTitle} ${authorName}")`); } else { - markdownString.appendMarkdown(`$(account) **${blameInformationOrCommit.authorName}**`); + markdownString.appendMarkdown(`$(account) **${authorName}**`); } - if (blameInformationOrCommit.authorDate) { - const dateString = new Date(blameInformationOrCommit.authorDate).toLocaleString(undefined, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); - markdownString.appendMarkdown(`, $(history) ${fromNow(blameInformationOrCommit.authorDate, true, true)} (${dateString})`); + if (authorDate) { + const dateString = new Date(authorDate).toLocaleString(undefined, { + year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' + }); + markdownString.appendMarkdown(`, $(history) ${fromNow(authorDate, true, true)} (${dateString})`); } markdownString.appendMarkdown('\n\n'); } - markdownString.appendMarkdown(`${emojify(isBlameInformation(blameInformationOrCommit) ? blameInformationOrCommit.subject ?? '' : blameInformationOrCommit.message)}\n\n`); + // Subject | Message + markdownString.appendMarkdown(`${emojify(commitInformation?.message ?? blameInformation.subject ?? '')}\n\n`); markdownString.appendMarkdown(`---\n\n`); - if (!isBlameInformation(blameInformationOrCommit) && blameInformationOrCommit.shortStat) { - markdownString.appendMarkdown(`${blameInformationOrCommit.shortStat.files === 1 ? - l10n.t('{0} file changed', blameInformationOrCommit.shortStat.files) : - l10n.t('{0} files changed', blameInformationOrCommit.shortStat.files)}`); + // Short stats + if (commitInformation?.shortStat) { + markdownString.appendMarkdown(`${commitInformation.shortStat.files === 1 ? + l10n.t('{0} file changed', commitInformation.shortStat.files) : + l10n.t('{0} files changed', commitInformation.shortStat.files)}`); - if (blameInformationOrCommit.shortStat.insertions) { - markdownString.appendMarkdown(`, ${blameInformationOrCommit.shortStat.insertions === 1 ? - l10n.t('{0} insertion{1}', blameInformationOrCommit.shortStat.insertions, '(+)') : - l10n.t('{0} insertions{1}', blameInformationOrCommit.shortStat.insertions, '(+)')}`); + if (commitInformation.shortStat.insertions) { + markdownString.appendMarkdown(`, ${commitInformation.shortStat.insertions === 1 ? + l10n.t('{0} insertion{1}', commitInformation.shortStat.insertions, '(+)') : + l10n.t('{0} insertions{1}', commitInformation.shortStat.insertions, '(+)')}`); } - if (blameInformationOrCommit.shortStat.deletions) { - markdownString.appendMarkdown(`, ${blameInformationOrCommit.shortStat.deletions === 1 ? - l10n.t('{0} deletion{1}', blameInformationOrCommit.shortStat.deletions, '(-)') : - l10n.t('{0} deletions{1}', blameInformationOrCommit.shortStat.deletions, '(-)')}`); + if (commitInformation.shortStat.deletions) { + markdownString.appendMarkdown(`, ${commitInformation.shortStat.deletions === 1 ? + l10n.t('{0} deletion{1}', commitInformation.shortStat.deletions, '(-)') : + l10n.t('{0} deletions{1}', commitInformation.shortStat.deletions, '(-)')}`); } markdownString.appendMarkdown(`\n\n---\n\n`); } - markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, blameInformationOrCommit.hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformationOrCommit.hash]))} "${l10n.t('View Commit')}")`); + // Commands + const hash = commitInformation?.hash ?? blameInformation.hash; + + markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, hash]))} "${l10n.t('View Commit')}")`); markdownString.appendMarkdown(' '); - markdownString.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(blameInformationOrCommit.hash))} "${l10n.t('Copy Commit Hash')}")`); + markdownString.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`); + + // Remote commands + if (remoteSourceCommands.length > 0) { + markdownString.appendMarkdown('  |  '); + + const remoteCommandsMarkdown = remoteSourceCommands + .map(command => `[${command.title}](command:${command.command}?${encodeURIComponent(JSON.stringify([...command.arguments ?? [], hash]))} "${command.tooltip}")`); + markdownString.appendMarkdown(remoteCommandsMarkdown.join(' ')); + } + markdownString.appendMarkdown('  |  '); markdownString.appendMarkdown(`[$(gear)](command:workbench.action.openSettings?%5B%22git.blame%22%5D "${l10n.t('Open Settings')}")`); @@ -566,7 +590,7 @@ class GitBlameEditorDecoration implements HoverProvider { return undefined; } - const contents = await this._controller.getBlameInformationDetailedHover(textEditor.document.uri, lineBlameInformation.blameInformation); + const contents = await this._controller.getBlameInformationHover(textEditor.document.uri, lineBlameInformation.blameInformation, true); if (!contents || token.isCancellationRequested) { return undefined; @@ -678,7 +702,7 @@ class GitBlameStatusBarItem { this._onDidChangeBlameInformation(); } - private _onDidChangeBlameInformation(): void { + private async _onDidChangeBlameInformation(): Promise { if (!window.activeTextEditor) { this._statusBarItem.hide(); return; @@ -699,7 +723,7 @@ class GitBlameStatusBarItem { const template = config.get('blame.statusBarItem.template', '${authorName} (${authorDateAgo})'); this._statusBarItem.text = `$(git-commit) ${this._controller.formatBlameInformationMessage(window.activeTextEditor.document.uri, template, blameInformation[0].blameInformation)}`; - this._statusBarItem.tooltip = this._controller.getBlameInformationHover(window.activeTextEditor.document.uri, blameInformation[0].blameInformation); + this._statusBarItem.tooltip = await this._controller.getBlameInformationHover(window.activeTextEditor.document.uri, blameInformation[0].blameInformation); this._statusBarItem.command = { title: l10n.t('View Commit'), command: 'git.viewCommit', diff --git a/extensions/git/src/remoteSource.ts b/extensions/git/src/remoteSource.ts index eb63e5db81f6..dfdb36fc11f5 100644 --- a/extensions/git/src/remoteSource.ts +++ b/extensions/git/src/remoteSource.ts @@ -15,3 +15,7 @@ export async function pickRemoteSource(options: PickRemoteSourceOptions = {}): P export async function getRemoteSourceActions(url: string) { return GitBaseApi.getAPI().getRemoteSourceActions(url); } + +export async function getRemoteSourceControlHistoryItemCommands(url: string) { + return GitBaseApi.getAPI().getRemoteSourceControlHistoryItemCommands(url); +} diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 00419ef85fc2..c576bc790b08 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1592,13 +1592,13 @@ export class Repository implements Disposable { } private async getDefaultBranch(): Promise { - try { - if (this.remotes.length === 0) { - return undefined; - } + const defaultRemote = this.getDefaultRemote(); + if (!defaultRemote) { + return undefined; + } - const remote = this.remotes.find(r => r.name === 'origin') ?? this.remotes[0]; - const defaultBranch = await this.repository.getDefaultBranch(remote.name); + try { + const defaultBranch = await this.repository.getDefaultBranch(defaultRemote.name); return defaultBranch; } catch (err) { @@ -1713,6 +1713,14 @@ export class Repository implements Disposable { await this.run(Operation.DeleteRef, () => this.repository.deleteRef(ref)); } + getDefaultRemote(): Remote | undefined { + if (this.remotes.length === 0) { + return undefined; + } + + return this.remotes.find(r => r.name === 'origin') ?? this.remotes[0]; + } + async addRemote(name: string, url: string): Promise { await this.run(Operation.Remote, () => this.repository.addRemote(name, url)); } diff --git a/extensions/git/src/typings/git-base.d.ts b/extensions/git/src/typings/git-base.d.ts index 1eeb17399010..37dd2c4229c4 100644 --- a/extensions/git/src/typings/git-base.d.ts +++ b/extensions/git/src/typings/git-base.d.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, Event, ProviderResult, Uri } from 'vscode'; +import { Command, Disposable, Event, ProviderResult } from 'vscode'; export { ProviderResult } from 'vscode'; export interface API { - pickRemoteSource(options: PickRemoteSourceOptions): Promise; - getRemoteSourceActions(url: string): Promise; registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; + getRemoteSourceActions(url: string): Promise; + getRemoteSourceControlHistoryItemCommands(url: string): Promise; + pickRemoteSource(options: PickRemoteSourceOptions): Promise; } export interface GitBaseExtension { @@ -81,6 +82,7 @@ export interface RemoteSourceProvider { getBranches?(url: string): ProviderResult; getRemoteSourceActions?(url: string): ProviderResult; + getRemoteSourceControlHistoryItemCommands?(url: string): ProviderResult; getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; } diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 1f1504521f83..2fc47e096f68 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import { API as GitAPI } from './typings/git'; import { publishRepository } from './publish'; import { DisposableStore } from './util'; -import { LinkContext, getLink, getVscodeDevHost } from './links'; +import { LinkContext, getCommitLink, getLink, getVscodeDevHost } from './links'; async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean, context: LinkContext, includeRange = true) { try { @@ -57,6 +57,11 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { return copyVscodeDevLink(gitAPI, true, context, false); })); + disposables.add(vscode.commands.registerCommand('github.openOnGitHub', async (url: string, historyItemId: string) => { + const link = getCommitLink(url, historyItemId); + vscode.env.openExternal(vscode.Uri.parse(link)); + })); + disposables.add(vscode.commands.registerCommand('github.openOnVscodeDev', async () => { return openVscodeDevLink(gitAPI); })); diff --git a/extensions/github/src/links.ts b/extensions/github/src/links.ts index 911f0e5376bf..fe97d1722496 100644 --- a/extensions/github/src/links.ts +++ b/extensions/github/src/links.ts @@ -186,6 +186,15 @@ export function getBranchLink(url: string, branch: string, hostPrefix: string = return `${hostPrefix}/${repo.owner}/${repo.repo}/tree/${branch}`; } +export function getCommitLink(url: string, hash: string, hostPrefix: string = 'https://github.com') { + const repo = getRepositoryFromUrl(url); + if (!repo) { + throw new Error('Invalid repository URL provided'); + } + + return `${hostPrefix}/${repo.owner}/${repo.repo}/commit/${hash}`; +} + export function getVscodeDevHost(): string { return `https://${vscode.env.appName.toLowerCase().includes('insiders') ? 'insiders.' : ''}vscode.dev/github`; } diff --git a/extensions/github/src/remoteSourceProvider.ts b/extensions/github/src/remoteSourceProvider.ts index 0d8b93406953..0c2ef1668327 100644 --- a/extensions/github/src/remoteSourceProvider.ts +++ b/extensions/github/src/remoteSourceProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Uri, env, l10n, workspace } from 'vscode'; +import { Command, Uri, env, l10n, workspace } from 'vscode'; import { RemoteSourceProvider, RemoteSource, RemoteSourceAction } from './typings/git-base'; import { getOctokit } from './auth'; import { Octokit } from '@octokit/rest'; @@ -136,4 +136,18 @@ export class GithubRemoteSourceProvider implements RemoteSourceProvider { } }]; } + + async getRemoteSourceControlHistoryItemCommands(url: string): Promise { + const repository = getRepositoryFromUrl(url); + if (!repository) { + return []; + } + + return [{ + title: l10n.t('{0} Open on GitHub', '$(github)'), + tooltip: l10n.t('Open on GitHub'), + command: 'github.openOnGitHub', + arguments: [url] + }]; + } } diff --git a/extensions/github/src/typings/git-base.d.ts b/extensions/github/src/typings/git-base.d.ts index 53cac4d5c70f..37dd2c4229c4 100644 --- a/extensions/github/src/typings/git-base.d.ts +++ b/extensions/github/src/typings/git-base.d.ts @@ -3,11 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, Event, ProviderResult, Uri } from 'vscode'; +import { Command, Disposable, Event, ProviderResult } from 'vscode'; export { ProviderResult } from 'vscode'; export interface API { registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; + getRemoteSourceActions(url: string): Promise; + getRemoteSourceControlHistoryItemCommands(url: string): Promise; pickRemoteSource(options: PickRemoteSourceOptions): Promise; } @@ -80,6 +82,7 @@ export interface RemoteSourceProvider { getBranches?(url: string): ProviderResult; getRemoteSourceActions?(url: string): ProviderResult; + getRemoteSourceControlHistoryItemCommands?(url: string): ProviderResult; getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; } From 68ec60529e7b44d518940a4e63be0d48ea2e0a53 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 8 Jan 2025 11:40:36 -0800 Subject: [PATCH 0419/3587] Require `export` on all vscode public api types Adds new eslint rule which requires `export` be used for public api types The `export` is not actually needed, but our existing typing files largely use it. This makes it consistent --- .eslint-plugin-local/vscode-dts-use-export.ts | 32 +++++++++++++++++ eslint.config.js | 1 + src/vscode-dts/vscode.d.ts | 34 +++++++++---------- .../vscode.proposed.aiRelatedInformation.d.ts | 2 +- .../vscode.proposed.externalUriOpener.d.ts | 6 ++-- ...ode.proposed.notebookVariableProvider.d.ts | 6 ++-- .../vscode.proposed.quickDiffProvider.d.ts | 2 +- src/vscode-dts/vscode.proposed.resolvers.d.ts | 6 ++-- ...ode.proposed.terminalQuickFixProvider.d.ts | 4 +-- 9 files changed, 63 insertions(+), 30 deletions(-) create mode 100644 .eslint-plugin-local/vscode-dts-use-export.ts diff --git a/.eslint-plugin-local/vscode-dts-use-export.ts b/.eslint-plugin-local/vscode-dts-use-export.ts new file mode 100644 index 000000000000..904feaeec36b --- /dev/null +++ b/.eslint-plugin-local/vscode-dts-use-export.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TSESTree } from '@typescript-eslint/utils'; +import * as eslint from 'eslint'; + +export = new class VscodeDtsUseExport implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + useExport: `Public api types must use 'export'`, + }, + schema: false, + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + return { + ['TSModuleDeclaration :matches(TSInterfaceDeclaration, ClassDeclaration, VariableDeclaration, TSEnumDeclaration, TSTypeAliasDeclaration)']: (node: any) => { + const parent = (node).parent; + if (parent && parent.type !== TSESTree.AST_NODE_TYPES.ExportNamedDeclaration) { + context.report({ + node, + messageId: 'useExport' + }); + } + } + }; + } +}; + diff --git a/eslint.config.js b/eslint.config.js index db83d096edde..f9f120acd41b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -267,6 +267,7 @@ export default tseslint.config( 'local/vscode-dts-string-type-literals': 'warn', 'local/vscode-dts-interface-naming': 'warn', 'local/vscode-dts-cancellation': 'warn', + 'local/vscode-dts-use-export': 'warn', 'local/vscode-dts-use-thenable': 'warn', 'local/vscode-dts-region-comments': 'warn', 'local/vscode-dts-vscode-in-comments': 'warn', diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 580acdeb53b4..f306830ba121 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -6076,7 +6076,7 @@ declare module 'vscode' { /** * Identifies a {@linkcode DocumentDropEdit} or {@linkcode DocumentPasteEdit} */ - class DocumentDropOrPasteEditKind { + export class DocumentDropOrPasteEditKind { static readonly Empty: DocumentDropOrPasteEditKind; /** @@ -6272,7 +6272,7 @@ declare module 'vscode' { /** * Provider invoked when the user copies or pastes in a {@linkcode TextDocument}. */ - interface DocumentPasteEditProvider { + export interface DocumentPasteEditProvider { /** * Optional method invoked after the user copies from a {@link TextEditor text editor}. @@ -6329,7 +6329,7 @@ declare module 'vscode' { /** * An edit the applies a paste operation. */ - class DocumentPasteEdit { + export class DocumentPasteEdit { /** * Human readable label that describes the edit. @@ -6373,7 +6373,7 @@ declare module 'vscode' { /** * Provides additional metadata about how a {@linkcode DocumentPasteEditProvider} works. */ - interface DocumentPasteProviderMetadata { + export interface DocumentPasteProviderMetadata { /** * List of {@link DocumentDropOrPasteEditKind kinds} that the provider may return in {@linkcode DocumentPasteEditProvider.provideDocumentPasteEdits provideDocumentPasteEdits}. * @@ -7924,7 +7924,7 @@ declare module 'vscode' { /** * The confidence of a {@link TerminalShellExecutionCommandLine} value. */ - enum TerminalShellExecutionCommandLineConfidence { + export enum TerminalShellExecutionCommandLineConfidence { /** * The command line value confidence is low. This means that the value was read from the * terminal buffer using markers reported by the shell integration script. Additionally one @@ -9167,7 +9167,7 @@ declare module 'vscode' { * * This interface is not intended to be implemented. */ - interface TaskStartEvent { + export interface TaskStartEvent { /** * The task item representing the task that got started. */ @@ -9179,7 +9179,7 @@ declare module 'vscode' { * * This interface is not intended to be implemented. */ - interface TaskEndEvent { + export interface TaskEndEvent { /** * The task item representing the task that finished. */ @@ -9971,7 +9971,7 @@ declare module 'vscode' { /** * A panel that contains a webview. */ - interface WebviewPanel { + export interface WebviewPanel { /** * Identifies the type of the webview panel, such as `'markdown.preview'`. */ @@ -10101,7 +10101,7 @@ declare module 'vscode' { * * @param T Type of the webview's state. */ - interface WebviewPanelSerializer { + export interface WebviewPanelSerializer { /** * Restore a webview panel from its serialized `state`. * @@ -10192,7 +10192,7 @@ declare module 'vscode' { * * @param T Type of the webview's state. */ - interface WebviewViewResolveContext { + export interface WebviewViewResolveContext { /** * Persisted state from the webview content. * @@ -10282,7 +10282,7 @@ declare module 'vscode' { * Custom documents are only used within a given `CustomEditorProvider`. The lifecycle of a `CustomDocument` is * managed by the editor. When no more references remain to a `CustomDocument`, it is disposed of. */ - interface CustomDocument { + export interface CustomDocument { /** * The associated uri for this document. */ @@ -10302,7 +10302,7 @@ declare module 'vscode' { * * @see {@linkcode CustomEditorProvider.onDidChangeCustomDocument}. */ - interface CustomDocumentEditEvent { + export interface CustomDocumentEditEvent { /** * The document that the edit is for. @@ -10341,7 +10341,7 @@ declare module 'vscode' { * * @see {@linkcode CustomEditorProvider.onDidChangeCustomDocument}. */ - interface CustomDocumentContentChangeEvent { + export interface CustomDocumentContentChangeEvent { /** * The document that the change is for. */ @@ -10351,7 +10351,7 @@ declare module 'vscode' { /** * A backup for an {@linkcode CustomDocument}. */ - interface CustomDocumentBackup { + export interface CustomDocumentBackup { /** * Unique identifier for the backup. * @@ -10371,7 +10371,7 @@ declare module 'vscode' { /** * Additional information used to implement {@linkcode CustomDocumentBackup}. */ - interface CustomDocumentBackupContext { + export interface CustomDocumentBackupContext { /** * Suggested file location to write the new backup. * @@ -10387,7 +10387,7 @@ declare module 'vscode' { /** * Additional information about the opening custom document. */ - interface CustomDocumentOpenContext { + export interface CustomDocumentOpenContext { /** * The id of the backup to restore the document from or `undefined` if there is no backup. * @@ -12462,7 +12462,7 @@ declare module 'vscode' { /** * Defines the interface of a terminal pty, enabling extensions to control a terminal. */ - interface Pseudoterminal { + export interface Pseudoterminal { /** * An event that when fired will write data to the terminal. Unlike * {@link Terminal.sendText} which sends text to the underlying child diff --git a/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts b/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts index e5e28653cec6..344a1fd9a841 100644 --- a/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts +++ b/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts @@ -14,7 +14,7 @@ declare module 'vscode' { SettingInformation = 4 } - interface RelatedInformationBaseResult { + export interface RelatedInformationBaseResult { type: RelatedInformationType; weight: number; } diff --git a/src/vscode-dts/vscode.proposed.externalUriOpener.d.ts b/src/vscode-dts/vscode.proposed.externalUriOpener.d.ts index 757844d2ecd3..ceb28ac33ee5 100644 --- a/src/vscode-dts/vscode.proposed.externalUriOpener.d.ts +++ b/src/vscode-dts/vscode.proposed.externalUriOpener.d.ts @@ -96,7 +96,7 @@ declare module 'vscode' { /** * Additional information about the uri being opened. */ - interface OpenExternalUriContext { + export interface OpenExternalUriContext { /** * The uri that triggered the open. * @@ -109,7 +109,7 @@ declare module 'vscode' { /** * Additional metadata about a registered `ExternalUriOpener`. */ - interface ExternalUriOpenerMetadata { + export interface ExternalUriOpenerMetadata { /** * List of uri schemes the opener is triggered for. @@ -142,7 +142,7 @@ declare module 'vscode' { export function registerExternalUriOpener(id: string, opener: ExternalUriOpener, metadata: ExternalUriOpenerMetadata): Disposable; } - interface OpenExternalOptions { + export interface OpenExternalOptions { /** * Allows using openers contributed by extensions through `registerExternalUriOpener` * when opening the resource. diff --git a/src/vscode-dts/vscode.proposed.notebookVariableProvider.d.ts b/src/vscode-dts/vscode.proposed.notebookVariableProvider.d.ts index 4fac96c45f0a..8567a9d4e816 100644 --- a/src/vscode-dts/vscode.proposed.notebookVariableProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.notebookVariableProvider.d.ts @@ -14,20 +14,20 @@ declare module 'vscode' { Indexed = 2 } - interface VariablesResult { + export interface VariablesResult { variable: Variable; hasNamedChildren: boolean; indexedChildrenCount: number; } - interface NotebookVariableProvider { + export interface NotebookVariableProvider { onDidChangeVariables: Event; /** When parent is undefined, this is requesting global Variables. When a variable is passed, it's requesting child props of that Variable. */ provideVariables(notebook: NotebookDocument, parent: Variable | undefined, kind: NotebookVariablesRequestKind, start: number, token: CancellationToken): AsyncIterable; } - interface Variable { + export interface Variable { /** The variable's name. */ name: string; diff --git a/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts b/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts index 923a98917f6a..674c1ae279d2 100644 --- a/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts @@ -11,7 +11,7 @@ declare module 'vscode' { export function registerQuickDiffProvider(selector: DocumentSelector, quickDiffProvider: QuickDiffProvider, label: string, rootUri?: Uri): Disposable; } - interface QuickDiffProvider { + export interface QuickDiffProvider { label?: string; readonly visible?: boolean; } diff --git a/src/vscode-dts/vscode.proposed.resolvers.d.ts b/src/vscode-dts/vscode.proposed.resolvers.d.ts index 4f49cdf7d4af..68a2c0639279 100644 --- a/src/vscode-dts/vscode.proposed.resolvers.d.ts +++ b/src/vscode-dts/vscode.proposed.resolvers.d.ts @@ -74,7 +74,7 @@ declare module 'vscode' { export const appCommit: string | undefined; } - interface TunnelOptions { + export interface TunnelOptions { remoteAddress: { port: number; host: string }; // The desired local port. If this port can't be used, then another will be chosen. localAddressPort?: number; @@ -87,7 +87,7 @@ declare module 'vscode' { protocol?: string; } - interface TunnelDescription { + export interface TunnelDescription { remoteAddress: { port: number; host: string }; //The complete local address(ex. localhost:1234) localAddress: { port: number; host: string } | string; @@ -100,7 +100,7 @@ declare module 'vscode' { protocol?: string; } - interface Tunnel extends TunnelDescription { + export interface Tunnel extends TunnelDescription { // Implementers of Tunnel should fire onDidDispose when dispose is called. onDidDispose: Event; dispose(): void | Thenable; diff --git a/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts b/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts index ab06faacd7fc..b186d5bbe4f9 100644 --- a/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts @@ -59,7 +59,7 @@ declare module 'vscode' { /** * A matcher that runs on a sub-section of a terminal command's output */ - interface TerminalOutputMatcher { + export interface TerminalOutputMatcher { /** * A string or regex to match against the unwrapped line. If this is a regex with the multiline * flag, it will scan an amount of lines equal to `\n` instances in the regex + 1. @@ -80,7 +80,7 @@ declare module 'vscode' { length: number; } - enum TerminalOutputAnchor { + export enum TerminalOutputAnchor { Top = 0, Bottom = 1 } From 4fca8e55572bf7b0ce80e13e4ad5783df2ac599c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 8 Jan 2025 20:48:17 +0100 Subject: [PATCH 0420/3587] chat setup - log when workspace trust is not given (#237518) --- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index ad8f7f717f9a..0ad1f26e379b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -693,7 +693,7 @@ type InstallChatClassification = { signUpErrorCode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The error code in case of an error signing up.' }; }; type InstallChatEvent = { - installResult: 'installed' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn' | 'failedSignUp'; + installResult: 'installed' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn' | 'failedSignUp' | 'failedNotTrusted'; signedIn: boolean; signUpErrorCode: number | undefined; }; @@ -779,6 +779,7 @@ class ChatSetupController extends Disposable { this.setStep(ChatSetupStep.SigningIn); const result = await this.signIn(); if (!result.session) { + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signedIn: false, signUpErrorCode: undefined }); return; // user cancelled } @@ -789,6 +790,7 @@ class ChatSetupController extends Disposable { if (!session) { session = (await this.authenticationService.getSessions(defaultChat.providerId)).at(0); if (!session) { + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signedIn: false, signUpErrorCode: undefined }); return; // unexpected } } @@ -797,6 +799,7 @@ class ChatSetupController extends Disposable { message: localize('copilotWorkspaceTrust', "Copilot is currently only supported in trusted workspaces.") }); if (!trusted) { + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotTrusted', signedIn: true, signUpErrorCode: undefined }); return; } @@ -833,10 +836,6 @@ class ChatSetupController extends Disposable { this.logService.error(`[chat setup] signIn: error ${error}`); } - if (!session) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signedIn: false, signUpErrorCode: undefined }); - } - return { session, entitlement }; } From 5e45dbc9440149e4d6f061534f9ac28c3cf5007c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:54:01 -0800 Subject: [PATCH 0421/3587] Add missing experimental tab to conptydll setting --- .../workbench/contrib/terminal/common/terminalConfiguration.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 6e73f6260a5a..632311c17343 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -483,6 +483,7 @@ const terminalConfiguration: IConfigurationNode = { [TerminalSettingId.ExperimentalWindowsUseConptyDll]: { markdownDescription: localize('terminal.integrated.experimentalWindowsUseConptyDll', "Whether to use the experimental conpty.dll (v1.20.240626001) shipped with VS Code, instead of the one bundled with Windows."), type: 'boolean', + tags: ['experimental'], default: false }, [TerminalSettingId.SplitCwd]: { From 42ae0610a0650e7b8a042fd9bb9bc949e9dc26ba Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 8 Jan 2025 21:00:44 +0100 Subject: [PATCH 0422/3587] Copilot plan/settings links should be Proxima-aware (fix microsoft/vscode-copilot#11427) (#237520) --- .../contrib/chat/browser/actions/chatActions.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index f0ece5fa2b95..4b1b916b7511 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -20,7 +20,7 @@ import { ILocalizedString, localize, localize2 } from '../../../../../nls.js'; import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; import { DropdownWithPrimaryActionViewItem } from '../../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js'; import { Action2, MenuId, MenuItemAction, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IsLinuxContext, IsWindowsContext } from '../../../../../platform/contextkey/common/contextkeys.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; @@ -464,7 +464,7 @@ export function registerChatActions() { } }); - function registerOpenLinkAction(id: string, title: ILocalizedString, url: string, order: number): void { + function registerOpenLinkAction(id: string, title: ILocalizedString, url: string, order: number, contextKey: ContextKeyExpression = ChatContextKeys.enabled): void { registerAction2(class extends Action2 { constructor() { super({ @@ -472,11 +472,12 @@ export function registerChatActions() { title, category: CHAT_CATEGORY, f1: true, - precondition: ChatContextKeys.enabled, + precondition: contextKey, menu: { id: MenuId.ChatCommandCenter, group: 'y_manage', - order + order, + when: contextKey } }); } @@ -488,8 +489,9 @@ export function registerChatActions() { }); } - registerOpenLinkAction('workbench.action.chat.managePlan', localize2('managePlan', "Manage Copilot Plan"), defaultChat.managePlanUrl, 1); - registerOpenLinkAction('workbench.action.chat.manageSettings', localize2('manageSettings', "Manage Copilot Settings"), defaultChat.manageSettingsUrl, 2); + const nonEnterpriseCopilotUsers = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.notEquals('config.github.copilot.advanced.authProvider', 'github-enterprise')); + registerOpenLinkAction('workbench.action.chat.managePlan', localize2('managePlan', "Manage Copilot Plan"), defaultChat.managePlanUrl, 1, nonEnterpriseCopilotUsers); + registerOpenLinkAction('workbench.action.chat.manageSettings', localize2('manageSettings', "Manage Copilot Settings"), defaultChat.manageSettingsUrl, 2, nonEnterpriseCopilotUsers); registerOpenLinkAction('workbench.action.chat.learnMore', localize2('learnMore', "Learn More"), defaultChat.documentationUrl, 3); registerAction2(class ShowExtensionsUsingCopilit extends Action2 { From 47b27ab160e91675511afc9620ad41725a0e3f94 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 7 Jan 2025 12:40:06 -0600 Subject: [PATCH 0423/3587] wip #234672, force shell integration change for reconnected --- .../api/browser/mainThreadTerminalShellIntegration.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts index 6181cd4c6379..bc204e19a068 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts @@ -41,6 +41,11 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma }), () => instance ); })).event; + for (const terminal of this._terminalService.instances.filter(i => !!i.shellLaunchConfig.attachPersistentProcess)) { + if (terminal.capabilities.has(TerminalCapability.CommandDetection) || terminal.capabilities.has(TerminalCapability.CwdDetection)) { + this._proxy.$shellIntegrationChange(terminal.instanceId); + } + } this._store.add(onDidAddCommandDetection(e => this._proxy.$shellIntegrationChange(e.instanceId))); // onDidStartTerminalShellExecution From 2e1cc3848f414cd60cec3bc58e81ad9ff40e38b8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 8 Jan 2025 12:59:02 -0800 Subject: [PATCH 0424/3587] Initialize shellIntegration.cwd Fixes #234672 --- .../mainThreadTerminalShellIntegration.ts | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts index bc204e19a068..e2ee50cc3a46 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts @@ -33,7 +33,18 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma } })); - // onDidChangeTerminalShellIntegration + // onDidchangeTerminalShellIntegration initial state + for (const terminal of this._terminalService.instances) { + if (terminal.capabilities.has(TerminalCapability.CommandDetection)) { + this._proxy.$shellIntegrationChange(terminal.instanceId); + const cwdDetection = terminal.capabilities.get(TerminalCapability.CwdDetection); + if (cwdDetection) { + this._proxy.$cwdChange(terminal.instanceId, this._convertCwdToUri(cwdDetection.getCwd())); + } + } + } + + // onDidChangeTerminalShellIntegration via command detection const onDidAddCommandDetection = this._store.add(this._terminalService.createOnInstanceEvent(instance => { return Event.map( Event.filter(instance.capabilities.onDidAddCapabilityType, e => { @@ -41,13 +52,14 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma }), () => instance ); })).event; - for (const terminal of this._terminalService.instances.filter(i => !!i.shellLaunchConfig.attachPersistentProcess)) { - if (terminal.capabilities.has(TerminalCapability.CommandDetection) || terminal.capabilities.has(TerminalCapability.CwdDetection)) { - this._proxy.$shellIntegrationChange(terminal.instanceId); - } - } this._store.add(onDidAddCommandDetection(e => this._proxy.$shellIntegrationChange(e.instanceId))); + // onDidChangeTerminalShellIntegration via cwd + const cwdChangeEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CwdDetection, e => e.onDidChangeCwd)); + this._store.add(cwdChangeEvent.event(e => { + this._proxy.$cwdChange(e.instance.instanceId, this._convertCwdToUri(e.data)); + })); + // onDidStartTerminalShellExecution const commandDetectionStartEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CommandDetection, e => e.onCommandExecuted)); let currentCommand: ITerminalCommand | undefined; @@ -80,12 +92,6 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma }); })); - // onDidChangeTerminalShellIntegration via cwd - const cwdChangeEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CwdDetection, e => e.onDidChangeCwd)); - this._store.add(cwdChangeEvent.event(e => { - this._proxy.$cwdChange(e.instance.instanceId, this._convertCwdToUri(e.data)); - })); - // Clean up after dispose this._store.add(this._terminalService.onDidDisposeInstance(e => this._proxy.$closeTerminal(e.instanceId))); } From e03d4a0ea2e833d3db053b19670e0e44145f8212 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 8 Jan 2025 22:05:04 +0100 Subject: [PATCH 0425/3587] Git - Add "Open on GitHub" command to timeline hover (#237523) --- extensions/git/src/timelineProvider.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 2b3da08f82e1..fbf9b139a0a6 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken, ConfigurationChangeEvent, Disposable, env, Event, EventEmitter, MarkdownString, ThemeIcon, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider, Uri, workspace, l10n } from 'vscode'; +import { CancellationToken, ConfigurationChangeEvent, Disposable, env, Event, EventEmitter, MarkdownString, ThemeIcon, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider, Uri, workspace, l10n, Command } from 'vscode'; import { Model } from './model'; import { Repository, Resource } from './repository'; import { debounce } from './decorators'; @@ -12,6 +12,7 @@ import { CommandCenter } from './commands'; import { OperationKind, OperationResult } from './operation'; import { getCommitShortHash } from './util'; import { CommitShortStat } from './git'; +import { getRemoteSourceControlHistoryItemCommands } from './remoteSource'; export class GitTimelineItem extends TimelineItem { static is(item: TimelineItem): item is GitTimelineItem { @@ -50,7 +51,7 @@ export class GitTimelineItem extends TimelineItem { return this.shortenRef(this.previousRef); } - setItemDetails(uri: Uri, hash: string | undefined, author: string, email: string | undefined, date: string, message: string, shortStat?: CommitShortStat): void { + setItemDetails(uri: Uri, hash: string | undefined, author: string, email: string | undefined, date: string, message: string, shortStat?: CommitShortStat, remoteSourceCommands: Command[] = []): void { this.tooltip = new MarkdownString('', true); this.tooltip.isTrusted = true; this.tooltip.supportHtml = true; @@ -89,6 +90,15 @@ export class GitTimelineItem extends TimelineItem { this.tooltip.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(uri, hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([uri, hash]))} "${l10n.t('View Commit')}")`); this.tooltip.appendMarkdown(' '); this.tooltip.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`); + + // Remote commands + if (remoteSourceCommands.length > 0) { + this.tooltip.appendMarkdown('  |  '); + + const remoteCommandsMarkdown = remoteSourceCommands + .map(command => `[${command.title}](command:${command.command}?${encodeURIComponent(JSON.stringify([...command.arguments ?? [], hash]))} "${command.tooltip}")`); + this.tooltip.appendMarkdown(remoteCommandsMarkdown.join(' ')); + } } } @@ -204,6 +214,11 @@ export class GitTimelineProvider implements TimelineProvider { const openComparison = l10n.t('Open Comparison'); + const defaultRemote = repo.getDefaultRemote(); + const remoteSourceCommands: Command[] = defaultRemote?.fetchUrl + ? await getRemoteSourceControlHistoryItemCommands(defaultRemote.fetchUrl) + : []; + const items = commits.map((c, i) => { const date = dateType === 'authored' ? c.authorDate : c.commitDate; @@ -215,7 +230,7 @@ export class GitTimelineProvider implements TimelineProvider { item.description = c.authorName; } - item.setItemDetails(uri, c.hash, c.authorName!, c.authorEmail, dateFormatter.format(date), message, c.shortStat); + item.setItemDetails(uri, c.hash, c.authorName!, c.authorEmail, dateFormatter.format(date), message, c.shortStat, remoteSourceCommands); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { From 1b8c51694938575c794ac9754205a8fab04fe691 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:08:23 -0800 Subject: [PATCH 0426/3587] Fix terminal chat placeholder Fixes microsoft/vscode-copilot#10440 --- .../terminalContrib/chat/browser/terminalChatWidget.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index d1a310aae291..9077da3811fe 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -328,6 +328,12 @@ export class TerminalChatWidget extends Disposable { const model = this._model.value; if (model) { this._inlineChatWidget.setChatModel(model, this._loadViewState()); + model.waitForInitialization().then(() => { + if (token.isCancellationRequested) { + return; + } + this._resetPlaceholder(); + }); } if (!this._model.value) { throw new Error('Failed to start chat session'); From e6cba379a4041fd8705e7fec052a8ea6960efb54 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 8 Jan 2025 14:55:18 -0800 Subject: [PATCH 0427/3587] Try asking for different claims in another request (#237531) I'm hoping that this solution will be good enough until we stop depending on the id token for certain things. Fixes https://github.com/microsoft/vscode/issues/237370 --- .../src/node/cachedPublicClientApplication.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts index 7ebf7a1630cb..a986b217983e 100644 --- a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts +++ b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts @@ -93,6 +93,7 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] got result`); // Check expiration of id token and if it's 5min before expiration, force a refresh. // this is what MSAL does for access tokens already so we're just adding it for id tokens since we care about those. + // NOTE: Once we stop depending on id tokens for some things we can remove all of this. const idTokenExpirationInSecs = (result.idTokenClaims as { exp?: number }).exp; if (idTokenExpirationInSecs) { const fiveMinutesBefore = new Date( @@ -106,12 +107,35 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica ? { ...request, claims: '{ "id_token": {}}' } : { ...request, forceRefresh: true }; result = await this._sequencer.queue(() => this._pca.acquireTokenSilent(newRequest)); - this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] got refreshed result`); + this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] got forced result`); } const newIdTokenExpirationInSecs = (result.idTokenClaims as { exp?: number }).exp; if (newIdTokenExpirationInSecs) { - if (new Date(newIdTokenExpirationInSecs * 1000) < new Date()) { + const fiveMinutesBefore = new Date( + (newIdTokenExpirationInSecs - 5 * 60) // subtract 5 minutes + * 1000 // convert to milliseconds + ); + if (fiveMinutesBefore < new Date()) { this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] id token is still expired.`); + + // HACK: Only for the Broker we try one more time with different claims to force a refresh. Why? We've seen the Broker caching tokens by the claims requested, thus + // there has been a situation where both tokens are expired. + if (this._isBrokerAvailable) { + this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] forcing refresh with different claims...`); + const newRequest = { ...request, claims: '{ "access_token": {}}' }; + result = await this._sequencer.queue(() => this._pca.acquireTokenSilent(newRequest)); + this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] got forced result with different claims`); + const newIdTokenExpirationInSecs = (result.idTokenClaims as { exp?: number }).exp; + if (newIdTokenExpirationInSecs) { + const fiveMinutesBefore = new Date( + (newIdTokenExpirationInSecs - 5 * 60) // subtract 5 minutes + * 1000 // convert to milliseconds + ); + if (fiveMinutesBefore < new Date()) { + this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] id token is still expired.`); + } + } + } } } } From a016c0be118543e874170db95ca7401aec8ce185 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 9 Jan 2025 00:37:44 +0100 Subject: [PATCH 0428/3587] Support show range for inline edits (#237532) Support a show range around the edit --- src/vs/editor/common/config/editorOptions.ts | 8 -------- src/vs/editor/common/languages.ts | 3 +++ .../browser/model/inlineCompletionsModel.ts | 9 +++++---- .../browser/model/inlineEditsAdapter.ts | 1 + .../browser/model/provideInlineCompletions.ts | 3 +++ src/vs/monaco.d.ts | 3 ++- .../workbench/api/common/extHostLanguageFeatures.ts | 1 + src/vscode-dts/vscode.proposed.inlineEdit.d.ts | 12 +++++++++--- 8 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 2bd9dfd9e2ed..908dcac26bd6 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -4200,7 +4200,6 @@ export interface IInlineSuggestOptions { useWordInsertionView?: 'never' | 'whenPossible'; useWordReplacementView?: 'never' | 'whenPossible'; - onlyShowWhenCloseToCursor?: boolean; useGutterIndicator?: boolean; }; }; @@ -4235,7 +4234,6 @@ class InlineEditorSuggest extends BaseEditorOption { @@ -2396,6 +2398,7 @@ export interface MappedEditsProvider { export interface IInlineEdit { text: string; range: IRange; + showRange?: IRange; accepted?: Command; rejected?: Command; shown?: Command; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index b77a2c0df81d..efb2b8c4bc0e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -54,7 +54,6 @@ export class InlineCompletionsModel extends Disposable { private readonly _editorObs = observableCodeEditor(this._editor); - private readonly _onlyShowWhenCloseToCursor = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !!v.edits.experimental.onlyShowWhenCloseToCursor); private readonly _suggestPreviewEnabled = this._editorObs.getOption(EditorOption.suggest).map(v => v.preview); private readonly _suggestPreviewMode = this._editorObs.getOption(EditorOption.suggest).map(v => v.previewMode); private readonly _inlineSuggestMode = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => v.mode); @@ -358,12 +357,13 @@ export class InlineCompletionsModel extends Disposable { const cursorPos = this.primaryPosition.read(reader); const cursorAtInlineEdit = LineRange.fromRangeInclusive(edit.range).addMargin(1, 1).contains(cursorPos.lineNumber); + const cursorInsideShowRange = cursorAtInlineEdit || (item.inlineEdit.inlineCompletion.cursorShowRange?.containsPosition(cursorPos) ?? true); - const cursorDist = LineRange.fromRange(edit.range).distanceToLine(this.primaryPosition.read(reader).lineNumber); - - if (this._onlyShowWhenCloseToCursor.read(reader) && cursorDist > 3 && !item.inlineEdit.request.isExplicitRequest && !this._inAcceptFlow.read(reader)) { + if (!cursorInsideShowRange && !this._inAcceptFlow.read(reader)) { return undefined; } + + const cursorDist = LineRange.fromRange(edit.range).distanceToLine(this.primaryPosition.read(reader).lineNumber); const disableCollapsing = true; const currentItemIsCollapsed = !disableCollapsing && (cursorDist > 1 && this._collapsedInlineEditId.read(reader) === item.inlineEdit.semanticId); @@ -575,6 +575,7 @@ export class InlineCompletionsModel extends Disposable { editor.pushUndoStop(); if (completion.snippetInfo) { + // ... editor.executeEdits( 'inlineSuggestion.accept', [ diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts index 8d651a32a85f..c2ba76a74560 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts @@ -69,6 +69,7 @@ export class InlineEditsAdapter extends Disposable { items: definedEdits.map(e => { return { range: e.result.range, + showRange: e.result.showRange, insertText: e.result.text, command: e.result.accepted, shownCommand: e.result.shown, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index a240f67595dd..d6b357f0b142 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -330,6 +330,7 @@ export class InlineCompletionItem { range, insertText, snippetInfo, + Range.lift(inlineCompletion.showRange) ?? undefined, inlineCompletion.additionalTextEdits || getReadonlyEmptyArray(), inlineCompletion, source, @@ -345,6 +346,7 @@ export class InlineCompletionItem { readonly range: Range, readonly insertText: string, readonly snippetInfo: SnippetInfo | undefined, + readonly cursorShowRange: Range | undefined, readonly additionalTextEdits: readonly ISingleEditOperation[], @@ -380,6 +382,7 @@ export class InlineCompletionItem { updatedRange, this.insertText, this.snippetInfo, + this.cursorShowRange, this.additionalTextEdits, this.sourceInlineCompletion, this.source, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index c1df5f99ffe1..bd27d4799411 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4607,7 +4607,6 @@ declare namespace monaco.editor { useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; useWordInsertionView?: 'never' | 'whenPossible'; useWordReplacementView?: 'never' | 'whenPossible'; - onlyShowWhenCloseToCursor?: boolean; useGutterIndicator?: boolean; }; }; @@ -7277,6 +7276,7 @@ declare namespace monaco.languages { */ readonly completeBracketPairs?: boolean; readonly isInlineEdit?: boolean; + readonly showRange?: IRange; } export interface InlineCompletions { @@ -8164,6 +8164,7 @@ declare namespace monaco.languages { export interface IInlineEdit { text: string; range: IRange; + showRange?: IRange; accepted?: Command; rejected?: Command; shown?: Command; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 53a5c89264e9..d5ef857b79fe 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1577,6 +1577,7 @@ class InlineEditAdapter { pid, text: result.text, range: typeConvert.Range.from(result.range), + showRange: typeConvert.Range.from(result.showRange), accepted: acceptCommand, rejected: rejectCommand, shown: shownCommand, diff --git a/src/vscode-dts/vscode.proposed.inlineEdit.d.ts b/src/vscode-dts/vscode.proposed.inlineEdit.d.ts index 9a5eabb9f8b0..21fd5e34c01c 100644 --- a/src/vscode-dts/vscode.proposed.inlineEdit.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineEdit.d.ts @@ -13,11 +13,17 @@ declare module 'vscode' { readonly text: string; /** - * An range that will be replaced by the text of the inline edit. - * If change is only additive, this can be empty (same start and end position). + * A range that will be replaced by the text of the inline edit. + * If the change is only additive, this can be empty (same start and end position). */ readonly range: Range; + /** + * A range specifying when the edit can be shown based on the cursor position. + * If the cursor is within this range, the inline edit can be displayed. + */ + readonly showRange?: Range; + /** * An optional command that will be executed after applying the inline edit. */ @@ -36,7 +42,7 @@ declare module 'vscode' { * Creates a new inline edit. * * @param text The new text for this edit. - * @param replaceRange An range that will be replaced by the text of the inline edit. + * @param range A range that will be replaced by the text of the inline edit. */ constructor(text: string, range: Range); } From 232768306ad655e7beb318a04f358ca3056dff4b Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 8 Jan 2025 20:42:13 -0800 Subject: [PATCH 0429/3587] Allow the QuickInput widget to be dragged (#237534) * Allow the QuickInput widget to be dragged Thanks to @lszomoru for the initial prototype! That also had resizing but there were a couple edge cases that made it not quite ready. However, the drag-n-drop is really quite polished so I wanted to get this out for folks to try and I don't think it needs a setting. Next steps: * Add a snap to the top (aka the original location) * Resize... Fixes https://github.com/microsoft/vscode/issues/17268 * Add top snap * don't allow it to go off screen --- .../quickinput/browser/media/quickInput.css | 28 ++- .../browser/quickInputController.ts | 213 +++++++++++++++++- 2 files changed, 236 insertions(+), 5 deletions(-) diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 53f42dfa5ae2..24394ac42cbb 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -8,12 +8,12 @@ width: 600px; z-index: 2550; left: 50%; - margin-left: -300px; -webkit-app-region: no-drag; border-radius: 6px; } .quick-input-titlebar { + cursor: grab; display: flex; align-items: center; border-top-right-radius: 5px; @@ -37,6 +37,7 @@ } .quick-input-title { + cursor: grab; padding: 3px 0px; text-align: center; text-overflow: ellipsis; @@ -69,6 +70,7 @@ } .quick-input-header { + cursor: grab; display: flex; padding: 8px 6px 2px 6px; } @@ -362,3 +364,27 @@ .quick-input-list .monaco-tl-twistie { display: none !important; } + +/* Quick input snap lines visible while DnD */ +.quick-input-widget-snapline { + position: absolute; + z-index: 2549; +} + +.quick-input-widget-snapline.hidden { + display: none; +} + +.quick-input-widget-snapline.horizontal { + border-top: 1px dashed var(--vscode-editorRuler-foreground); + height: 0; + width: 100%; + left: 0; +} + +.quick-input-widget-snapline.vertical { + border-left: 1px dashed var(--vscode-editorRuler-foreground); + height: 100%; + width: 0; + top: 0; +} diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 7b77f077c03c..783dde2dd5bd 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -26,9 +26,19 @@ import { IInstantiationService } from '../../instantiation/common/instantiation. import { QuickInputTree } from './quickInputTree.js'; import { IContextKeyService } from '../../contextkey/common/contextkey.js'; import './quickInputActions.js'; +import { autorun, observableValue } from '../../../base/common/observable.js'; +import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; const $ = dom.$; +const VIEWSTATE_STORAGE_KEY = 'workbench.quickInput.viewState'; + +type QuickInputViewState = { + readonly top?: number; + readonly left?: number; +}; + export class QuickInputController extends Disposable { private static readonly MAX_WIDTH = 600; // Max total width of quick input widget @@ -58,6 +68,9 @@ export class QuickInputController extends Disposable { private previousFocusElement?: HTMLElement; + private viewState: QuickInputViewState | undefined; + private dndController: QuickInputDragAndDropController | undefined; + private readonly inQuickInputContext = InQuickInputContextKey.bindTo(this.contextKeyService); private readonly quickInputTypeContext = QuickInputTypeContextKey.bindTo(this.contextKeyService); private readonly endOfQuickInputBoxContext = EndOfQuickInputBoxContextKey.bindTo(this.contextKeyService); @@ -66,7 +79,8 @@ export class QuickInputController extends Disposable { private options: IQuickInputOptions, @ILayoutService private readonly layoutService: ILayoutService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IContextKeyService private readonly contextKeyService: IContextKeyService + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IStorageService private readonly storageService: IStorageService ) { super(); this.idPrefix = options.idPrefix; @@ -83,6 +97,7 @@ export class QuickInputController extends Disposable { this.layout(this.layoutService.mainContainerDimension, this.layoutService.mainContainerOffset.quickPickTop); } })); + this.viewState = this.loadViewState(); } private registerKeyModsListeners(window: Window, disposables: DisposableStore): void { @@ -314,6 +329,36 @@ export class QuickInputController extends Disposable { } })); + // Drag and Drop support + this.dndController = this._register(this.instantiationService.createInstance( + QuickInputDragAndDropController, this._container, container, [titleBar, title, headerContainer])); + + // DnD update layout + this._register(autorun(reader => { + const dndViewState = this.dndController?.dndViewState.read(reader); + if (!dndViewState) { + return; + } + + if (dndViewState.top !== undefined && dndViewState.left !== undefined) { + this.viewState = { + ...this.viewState, + top: dndViewState.top, + left: dndViewState.left + }; + } else { + // Reset position/size + this.viewState = undefined; + } + + this.updateLayout(); + + // Save position + if (dndViewState.done) { + this.saveViewState(this.viewState); + } + })); + this.ui = { container, styleSheet, @@ -360,6 +405,7 @@ export class QuickInputController extends Disposable { if (this.ui) { this._container = container; dom.append(this._container, this.ui.container); + this.dndController?.reparentUI(this._container); } } @@ -729,12 +775,13 @@ export class QuickInputController extends Disposable { private updateLayout() { if (this.ui && this.isVisible()) { - this.ui.container.style.top = `${this.titleBarOffset}px`; - const style = this.ui.container.style; const width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH); style.width = width + 'px'; - style.marginLeft = '-' + (width / 2) + 'px'; + + // Position + style.top = `${this.viewState?.top ? Math.round(this.dimension!.height * this.viewState.top) : this.titleBarOffset}px`; + style.left = `${Math.round((this.dimension!.width * (this.viewState?.left ?? 0.5 /* center */)) - (width / 2))}px`; this.ui.inputBox.layout(); this.ui.list.layout(this.dimension && this.dimension.height * 0.4); @@ -800,6 +847,164 @@ export class QuickInputController extends Disposable { } } } + + private loadViewState(): QuickInputViewState | undefined { + try { + const data = JSON.parse(this.storageService.get(VIEWSTATE_STORAGE_KEY, StorageScope.APPLICATION, '{}')); + if (data.top !== undefined || data.left !== undefined) { + return data; + } + } catch { } + + return undefined; + } + + private saveViewState(viewState: QuickInputViewState | undefined): void { + const isMainWindow = this.layoutService.activeContainer === this.layoutService.mainContainer; + if (!isMainWindow) { + return; + } + + if (viewState !== undefined) { + this.storageService.store(VIEWSTATE_STORAGE_KEY, JSON.stringify(viewState), StorageScope.APPLICATION, StorageTarget.MACHINE); + } else { + this.storageService.remove(VIEWSTATE_STORAGE_KEY, StorageScope.APPLICATION); + } + } } + export interface IQuickInputControllerHost extends ILayoutService { } +class QuickInputDragAndDropController extends Disposable { + readonly dndViewState = observableValue<{ top?: number; left?: number; done: boolean } | undefined>(this, undefined); + + private readonly _snapThreshold = 20; + private readonly _snapLineHorizontalRatio = 0.15; + private readonly _snapLineHorizontal: HTMLElement; + private readonly _snapLineVertical1: HTMLElement; + private readonly _snapLineVertical2: HTMLElement; + + constructor( + private _container: HTMLElement, + private readonly _quickInputContainer: HTMLElement, + private _quickInputDragAreas: HTMLElement[], + @ILayoutService private readonly _layoutService: ILayoutService + ) { + super(); + + this._snapLineHorizontal = dom.append(this._container, $('.quick-input-widget-snapline.horizontal.hidden')); + this._snapLineVertical1 = dom.append(this._container, $('.quick-input-widget-snapline.vertical.hidden')); + this._snapLineVertical2 = dom.append(this._container, $('.quick-input-widget-snapline.vertical.hidden')); + + this.registerMouseListeners(); + } + + reparentUI(container: HTMLElement): void { + this._container = container; + this._snapLineHorizontal.remove(); + this._snapLineVertical1.remove(); + this._snapLineVertical2.remove(); + dom.append(this._container, this._snapLineHorizontal); + dom.append(this._container, this._snapLineVertical1); + dom.append(this._container, this._snapLineVertical2); + } + + private registerMouseListeners(): void { + for (const dragArea of this._quickInputDragAreas) { + let top: number | undefined; + let left: number | undefined; + + // Double click + this._register(dom.addDisposableGenericMouseUpListener(dragArea, (event: MouseEvent) => { + const originEvent = new StandardMouseEvent(dom.getWindow(dragArea), event); + + // Ignore event if the target is not the drag area + if (originEvent.target !== dragArea) { + return; + } + + if (originEvent.detail === 2) { + top = undefined; + left = undefined; + + this.dndViewState.set({ top, left, done: true }, undefined); + } + })); + + // Mouse down + this._register(dom.addDisposableGenericMouseDownListener(dragArea, (e: MouseEvent) => { + const activeWindow = dom.getWindow(this._layoutService.activeContainer); + const originEvent = new StandardMouseEvent(activeWindow, e); + + // Ignore event if the target is not the drag area + if (originEvent.target !== dragArea) { + return; + } + + // Mouse position offset relative to dragArea + const dragAreaRect = this._quickInputContainer.getBoundingClientRect(); + const dragOffsetX = originEvent.browserEvent.clientX - dragAreaRect.left; + const dragOffsetY = originEvent.browserEvent.clientY - dragAreaRect.top; + + // Snap lines + let snapLinesVisible = false; + const snapCoordinateYTop = this._layoutService.activeContainerOffset.quickPickTop; + const snapCoordinateY = Math.round(this._container.clientHeight * this._snapLineHorizontalRatio); + const snapCoordinateX = Math.round(this._container.clientWidth / 2) - Math.round(this._quickInputContainer.clientWidth / 2); + + // Mouse move + const mouseMoveListener = dom.addDisposableGenericMouseMoveListener(activeWindow, (e: MouseEvent) => { + const mouseMoveEvent = new StandardMouseEvent(activeWindow, e); + mouseMoveEvent.preventDefault(); + + if (!snapLinesVisible) { + this._showSnapLines(snapCoordinateY, snapCoordinateX); + snapLinesVisible = true; + } + + let topCoordinate = e.clientY - dragOffsetY; + topCoordinate = Math.max(0, Math.min(topCoordinate, this._container.clientHeight - this._quickInputContainer.clientHeight)); + topCoordinate = Math.abs(topCoordinate - snapCoordinateYTop) < this._snapThreshold ? snapCoordinateYTop : topCoordinate; + topCoordinate = Math.abs(topCoordinate - snapCoordinateY) < this._snapThreshold ? snapCoordinateY : topCoordinate; + top = topCoordinate / this._container.clientHeight; + + let leftCoordinate = e.clientX - dragOffsetX; + leftCoordinate = Math.max(0, Math.min(leftCoordinate, this._container.clientWidth - this._quickInputContainer.clientWidth)); + leftCoordinate = Math.abs(leftCoordinate - snapCoordinateX) < this._snapThreshold ? snapCoordinateX : leftCoordinate; + left = (leftCoordinate + (this._quickInputContainer.clientWidth / 2)) / this._container.clientWidth; + + this.dndViewState.set({ top, left, done: false }, undefined); + }); + + // Mouse up + const mouseUpListener = dom.addDisposableGenericMouseUpListener(activeWindow, (e: MouseEvent) => { + // Hide snaplines + this._hideSnapLines(); + + // Save position + this.dndViewState.set({ top, left, done: true }, undefined); + + // Dispose listeners + mouseMoveListener.dispose(); + mouseUpListener.dispose(); + }); + })); + } + } + + private _showSnapLines(horizontal: number, vertical: number) { + this._snapLineHorizontal.style.top = `${horizontal}px`; + this._snapLineVertical1.style.left = `${vertical}px`; + this._snapLineVertical2.style.left = `${vertical + this._quickInputContainer.clientWidth}px`; + + this._snapLineHorizontal.classList.remove('hidden'); + this._snapLineVertical1.classList.remove('hidden'); + this._snapLineVertical2.classList.remove('hidden'); + } + + private _hideSnapLines() { + this._snapLineHorizontal.classList.add('hidden'); + this._snapLineVertical1.classList.add('hidden'); + this._snapLineVertical2.classList.add('hidden'); + } +} From 9cd58e41a4368fecc7dbd7479da66995919fa923 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 9 Jan 2025 09:49:43 +0100 Subject: [PATCH 0430/3587] Add requestUuid for inline edits (#237549) --- src/vs/editor/common/languages.ts | 6 ++++++ .../inlineCompletions/browser/model/inlineEditsAdapter.ts | 1 + src/vs/workbench/api/common/extHostLanguageFeatures.ts | 3 ++- src/vscode-dts/vscode.proposed.inlineEdit.d.ts | 2 ++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index d6946f0e6458..0a0545a891ab 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -2407,6 +2407,12 @@ export interface IInlineEdit { export interface IInlineEditContext { triggerKind: InlineEditTriggerKind; + + /** + * @experimental + * @internal + */ + requestUuid?: string; } export enum InlineEditTriggerKind { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts index c2ba76a74560..cf0346586475 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts @@ -58,6 +58,7 @@ export class InlineEditsAdapter extends Disposable { const inlineEdits = await Promise.all(allInlineEditProvider.map(async provider => { const result = await provider.provideInlineEdit(model, { triggerKind: InlineEditTriggerKind.Automatic, + requestUuid: context.requestUuid }, token); if (!result) { return undefined; } return { result, provider }; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index d5ef857b79fe..ac9576a3b22d 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1526,7 +1526,8 @@ class InlineEditAdapter { async provideInlineEdits(uri: URI, context: languages.IInlineEditContext, token: CancellationToken): Promise { const doc = this._documents.getDocument(uri); const result = await this._provider.provideInlineEdit(doc, { - triggerKind: this.languageTriggerKindToVSCodeTriggerKind[context.triggerKind] + triggerKind: this.languageTriggerKindToVSCodeTriggerKind[context.triggerKind], + requestUuid: context.requestUuid, }, token); if (!result) { diff --git a/src/vscode-dts/vscode.proposed.inlineEdit.d.ts b/src/vscode-dts/vscode.proposed.inlineEdit.d.ts index 21fd5e34c01c..41ae0bb557be 100644 --- a/src/vscode-dts/vscode.proposed.inlineEdit.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineEdit.d.ts @@ -52,6 +52,8 @@ declare module 'vscode' { * Describes how the inline edit was triggered. */ triggerKind: InlineEditTriggerKind; + + requestUuid?: string; } export enum InlineEditTriggerKind { From da8001297aa5a4f632b702451f21c386d21779e1 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 9 Jan 2025 11:20:20 +0100 Subject: [PATCH 0431/3587] fix #237551 (#237552) --- .../common/extensionGalleryService.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 4a8c3b3222b7..7ed28238492f 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -757,7 +757,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi version: this.productService.version, date: this.productService.date } - }); + }, false); if (extension) { result.push(extension); @@ -990,7 +990,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi if (hasAllVersions) { const extensions: IGalleryExtension[] = []; for (const rawGalleryExtension of rawGalleryExtensions) { - const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria, context); + const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria, true, context); if (extension) { extensions.push(extension); } @@ -1019,7 +1019,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi continue; } } - const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria, context); + const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria, false, context); if (!extension /** Need all versions if the extension is a pre-release version but * - the query is to look for a release version or @@ -1060,7 +1060,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return { extensions: result.sort((a, b) => a[0] - b[0]).map(([, extension]) => extension), total }; } - private async toGalleryExtensionWithCriteria(rawGalleryExtension: IRawGalleryExtension, criteria: IExtensionCriteria, queryContext?: IStringDictionary): Promise { + private async toGalleryExtensionWithCriteria(rawGalleryExtension: IRawGalleryExtension, criteria: IExtensionCriteria, hasAllVersions: boolean, queryContext?: IStringDictionary): Promise { const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId }; const version = criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version; @@ -1082,7 +1082,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi extensionIdentifier.id, rawGalleryExtensionVersion, rawGalleryExtension.publisher.displayName, - includePreRelease ? 'any' : 'release', + includePreRelease ? (hasAllVersions ? 'any' : 'prerelease') : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform, From 0c176cf4cf2661f1afb33d6aebf7b81bfb389c77 Mon Sep 17 00:00:00 2001 From: Parasaran <74203806+Parasaran-Python@users.noreply.github.com> Date: Thu, 9 Jan 2025 15:54:05 +0530 Subject: [PATCH 0432/3587] fix 227150: Added a recursive git clone button (#232497) * fix 227150: Added a recursive git clone button * Git - update command for "Clone Repository" action --------- Co-authored-by: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> --- extensions/git/package.nls.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index e7020f41890f..0fb33c69b48a 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -352,7 +352,7 @@ ] }, "view.workbench.scm.empty": { - "message": "In order to use Git features, you can open a folder containing a Git repository or clone from a URL.\n[Open Folder](command:vscode.openFolder)\n[Clone Repository](command:git.clone)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "message": "In order to use Git features, you can open a folder containing a Git repository or clone from a URL.\n[Open Folder](command:vscode.openFolder)\n[Clone Repository](command:git.cloneRecursive)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", "comment": [ "{Locked='](command:vscode.openFolder'}", "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", From c65a0aba7671f80d7ee69591ac207b548b2837a3 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 9 Jan 2025 12:29:01 +0100 Subject: [PATCH 0433/3587] Remove some chat widget debt (#237556) * chore - fix leaks editing service/session * more appropriate name for `isHidden`, move remove-on-send logic into chatService --- .../browser/chatEditing/chatEditingService.ts | 4 +-- .../browser/chatEditing/chatEditingSession.ts | 3 +- .../contrib/chat/browser/chatWidget.ts | 10 ++---- .../contrib/chat/common/chatModel.ts | 33 +++++++++++-------- .../contrib/chat/common/chatServiceImpl.ts | 8 +++++ .../contrib/chat/common/chatViewModel.ts | 20 +++++------ .../chat/test/common/chatModel.test.ts | 4 +-- 7 files changed, 44 insertions(+), 38 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts index 4d064636e7ad..f93b829cf148 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts @@ -97,8 +97,8 @@ export class ChatEditingService extends Disposable implements IChatEditingServic this._applyingChatEditsFailedContextKey.set(false); this._register(decorationsService.registerDecorationsProvider(_instantiationService.createInstance(ChatDecorationsProvider, this._currentSessionObs))); this._register(multiDiffSourceResolverService.registerResolver(_instantiationService.createInstance(ChatEditingMultiDiffSourceResolver, this._currentSessionObs))); - textModelService.registerTextModelContentProvider(ChatEditingTextModelContentProvider.scheme, _instantiationService.createInstance(ChatEditingTextModelContentProvider, this._currentSessionObs)); - textModelService.registerTextModelContentProvider(ChatEditingSnapshotTextModelContentProvider.scheme, _instantiationService.createInstance(ChatEditingSnapshotTextModelContentProvider, this._currentSessionObs)); + this._register(textModelService.registerTextModelContentProvider(ChatEditingTextModelContentProvider.scheme, _instantiationService.createInstance(ChatEditingTextModelContentProvider, this._currentSessionObs))); + this._register(textModelService.registerTextModelContentProvider(ChatEditingSnapshotTextModelContentProvider.scheme, _instantiationService.createInstance(ChatEditingSnapshotTextModelContentProvider, this._currentSessionObs))); this._register(bindContextKey(decidedChatEditingResourceContextKey, contextKeyService, (reader) => { const currentSession = this._currentSessionObs.read(reader); if (!currentSession) { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 134c3de87edb..1b5516e070e0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -143,7 +143,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return linearHistory.slice(linearHistoryIndex).map(s => s.requestId).filter((r): r is string => !!r); }); - private readonly _onDidChange = new Emitter(); + private readonly _onDidChange = this._register(new Emitter()); get onDidChange() { this._assertNotDisposed(); return this._onDidChange.event; @@ -517,6 +517,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio super.dispose(); this._state.set(ChatEditingSessionState.Disposed, undefined); this._onDidDispose.fire(); + this._onDidDispose.dispose(); } getVirtualModel(documentId: string): ITextModel | null { diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index dc8397439704..b4f126517730 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -538,7 +538,7 @@ export class ChatWidget extends Disposable implements IChatWidget { // Re-render once content references are loaded (isResponseVM(element) ? `_${element.contentReferences.length}` : '') + // Re-render if element becomes hidden due to undo/redo - `_${element.isHidden ? '1' : '0'}` + + `_${element.shouldBeRemovedOnSend ? '1' : '0'}` + // Rerender request if we got new content references in the response // since this may change how we render the corresponding attachments in the request (isRequestVM(element) && element.contentReferences ? `_${element.contentReferences?.length}` : ''); @@ -1002,13 +1002,7 @@ export class ChatWidget extends Disposable implements IChatWidget { `${query.prefix} ${editorValue}`; const isUserQuery = !query || 'prefix' in query; - const requests = this.viewModel.model.getRequests(); - for (let i = requests.length - 1; i >= 0; i -= 1) { - const request = requests[i]; - if (request.isHidden) { - this.chatService.removeRequest(this.viewModel.sessionId, request.id); - } - } + let attachedContext = this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId); let workingSet: URI[] | undefined; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 8849cd0314ca..2b23c26cffd6 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -132,7 +132,7 @@ export interface IChatRequestModel { readonly workingSet?: URI[]; readonly isCompleteAddedRequest: boolean; readonly response?: IChatResponseModel; - isHidden: boolean; + shouldBeRemovedOnSend: boolean; } export interface IChatTextEditGroupState { @@ -207,7 +207,7 @@ export interface IChatResponseModel { readonly response: IResponse; readonly isComplete: boolean; readonly isCanceled: boolean; - readonly isHidden: boolean; + readonly shouldBeRemovedOnSend: boolean; readonly isCompleteAddedRequest: boolean; /** A stale response is one that has been persisted and rehydrated, so e.g. Commands that have their arguments stored in the EH are gone. */ readonly isStale: boolean; @@ -230,7 +230,7 @@ export class ChatRequestModel implements IChatRequestModel { return this._session; } - public isHidden: boolean = false; + public shouldBeRemovedOnSend: boolean = false; public get username(): string { return this.session.requesterUsername; @@ -506,16 +506,16 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this._session; } - public get isHidden() { - return this._isHidden; + public get shouldBeRemovedOnSend() { + return this._shouldBeRemovedOnSend; } public get isComplete(): boolean { return this._isComplete; } - public set isHidden(hidden: boolean) { - this._isHidden = hidden; + public set shouldBeRemovedOnSend(hidden: boolean) { + this._shouldBeRemovedOnSend = hidden; this._onDidChange.fire(); } @@ -605,7 +605,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel private _result?: IChatAgentResult, followups?: ReadonlyArray, public readonly isCompleteAddedRequest = false, - private _isHidden: boolean = false, + private _shouldBeRemovedOnSend: boolean = false, restoredId?: string ) { super(); @@ -733,6 +733,8 @@ export interface ISerializableChatRequestData { /** Is really like "prompt data". This is the message in the format in which the agent gets it + variable values. */ variableData: IChatRequestVariableData; response: ReadonlyArray | undefined; + + /**Old, persisted name for shouldBeRemovedOnSend */ isHidden: boolean; responseId?: string; agent?: ISerializableChatAgentData; @@ -1084,7 +1086,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); - request.isHidden = !!raw.isHidden; + request.shouldBeRemovedOnSend = !!raw.isHidden; 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 reviveSerializedAgent(raw.agent) : undefined; @@ -1094,7 +1096,7 @@ export class ChatModel extends Disposable implements IChatModel { // eslint-disable-next-line local/code-no-dangerous-type-assertions { errorDetails: raw.responseErrorDetails } as IChatAgentResult : raw.result; request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, raw.slashCommand, request.id, true, raw.isCanceled, raw.vote, raw.voteDownReason, result, raw.followups, undefined, undefined, raw.responseId); - request.response.isHidden = !!raw.isHidden; + request.response.shouldBeRemovedOnSend = !!raw.isHidden; if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway? request.response.applyReference(revive(raw.usedContext)); } @@ -1193,11 +1195,14 @@ export class ChatModel extends Disposable implements IChatModel { } disableRequests(requestIds: ReadonlyArray) { + + const toHide = new Set(requestIds); + this._requests.forEach((request) => { - const isHidden = requestIds.includes(request.id); - request.isHidden = isHidden; + const shouldBeRemovedOnSend = toHide.has(request.id); + request.shouldBeRemovedOnSend = shouldBeRemovedOnSend; if (request.response) { - request.response.isHidden = isHidden; + request.response.shouldBeRemovedOnSend = shouldBeRemovedOnSend; } }); @@ -1364,7 +1369,7 @@ export class ChatModel extends Disposable implements IChatModel { }) : undefined, responseId: r.response?.id, - isHidden: r.isHidden, + isHidden: r.shouldBeRemovedOnSend, result: r.response?.result, followups: r.response?.followups, isCanceled: r.response?.isCanceled, diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 6cfb29ce0c95..aad0cd8dab14 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -498,6 +498,14 @@ export class ChatService extends Disposable implements IChatService { return; } + const requests = model.getRequests(); + for (let i = requests.length - 1; i >= 0; i -= 1) { + const request = requests[i]; + if (request.shouldBeRemovedOnSend) { + this.removeRequest(sessionId, request.id); + } + } + const location = options?.location ?? model.initialLocation; const attempt = options?.attempt ?? 0; const defaultAgent = this.chatAgentService.getDefaultAgent(location)!; diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index b078e519666b..5345af9ac65f 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, dispose } from '../../../../base/common/lifecycle.js'; import * as marked from '../../../../base/common/marked/marked.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; @@ -74,7 +74,7 @@ export interface IChatRequestViewModel { readonly contentReferences?: ReadonlyArray; readonly workingSet?: ReadonlyArray; readonly confirmation?: string; - readonly isHidden: boolean; + readonly shouldBeRemovedOnSend: boolean; readonly isComplete: boolean; readonly isCompleteAddedRequest: boolean; readonly slashCommand: IChatAgentCommand | undefined; @@ -180,7 +180,7 @@ export interface IChatResponseViewModel { readonly errorDetails?: IChatResponseErrorDetails; readonly result?: IChatAgentResult; readonly contentUpdateTimings?: IChatLiveUpdateData; - readonly isHidden: boolean; + readonly shouldBeRemovedOnSend: boolean; readonly isCompleteAddedRequest: boolean; renderData?: IChatResponseRenderData; currentRenderedHeight: number | undefined; @@ -299,14 +299,12 @@ export class ChatViewModel extends Disposable implements IChatViewModel { } getItems(): (IChatRequestViewModel | IChatResponseViewModel)[] { - return [...this._items].filter((item) => !item.isHidden); + return this._items.filter((item) => !item.shouldBeRemovedOnSend); } override dispose() { super.dispose(); - this._items - .filter((item): item is ChatResponseViewModel => item instanceof ChatResponseViewModel) - .forEach((item: ChatResponseViewModel) => item.dispose()); + dispose(this._items.filter((item): item is ChatResponseViewModel => item instanceof ChatResponseViewModel)); } updateCodeBlockTextModels(model: IChatRequestViewModel | IChatResponseViewModel) { @@ -385,8 +383,8 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this._model.isCompleteAddedRequest; } - get isHidden() { - return this._model.isHidden; + get shouldBeRemovedOnSend() { + return this._model.shouldBeRemovedOnSend; } get slashCommand(): IChatAgentCommand | undefined { @@ -486,8 +484,8 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi return this._model.isCanceled; } - get isHidden() { - return this._model.isHidden; + get shouldBeRemovedOnSend() { + return this._model.shouldBeRemovedOnSend; } get isCompleteAddedRequest() { diff --git a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index c9c2059b3071..78a712f1e450 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -163,8 +163,8 @@ suite('ChatModel', () => { assert.strictEqual(request1.isCompleteAddedRequest, true); assert.strictEqual(request1.response!.isCompleteAddedRequest, true); - assert.strictEqual(request1.isHidden, false); - assert.strictEqual(request1.response!.isHidden, false); + assert.strictEqual(request1.shouldBeRemovedOnSend, false); + assert.strictEqual(request1.response!.shouldBeRemovedOnSend, false); }); }); From 744eec95958adbdd360b0378b6451c50a9cdbdf9 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 9 Jan 2025 12:47:34 +0100 Subject: [PATCH 0434/3587] Git - update base branch revision when it changes (#237558) --- extensions/git/src/historyProvider.ts | 32 ++++++++++++++++++--------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index b18923d56b7b..9512facf774e 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -90,7 +90,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec readonly onDidChangeHistoryItemRefs: Event = this._onDidChangeHistoryItemRefs.event; private _HEAD: Branch | undefined; - private historyItemRefs: SourceControlHistoryItemRef[] = []; + private _historyItemRefs: SourceControlHistoryItemRef[] = []; private historyItemDecorations = new Map(); @@ -112,6 +112,14 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return; } + // Refs (alphabetically) + const historyItemRefs = this.repository.refs + .map(ref => toSourceControlHistoryItemRef(this.repository, ref)) + .sort((a, b) => a.id.localeCompare(b.id)); + + const delta = deltaHistoryItemRefs(this._historyItemRefs, historyItemRefs); + this._historyItemRefs = historyItemRefs; + let historyItemRefId = ''; let historyItemRefName = ''; @@ -130,8 +138,9 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec icon: new ThemeIcon('cloud') } : undefined; - // Base - compute only if the branch has changed + // Base if (this._HEAD?.name !== this.repository.HEAD.name) { + // Compute base if the branch has changed const mergeBase = await this.resolveHEADMergeBase(); this._currentHistoryItemBaseRef = mergeBase && @@ -142,6 +151,17 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec revision: mergeBase.commit, icon: new ThemeIcon('cloud') } : undefined; + } else { + // Update base revision if it has changed + const mergeBaseModified = delta.modified + .find(ref => ref.id === this._currentHistoryItemBaseRef?.id); + + if (this._currentHistoryItemBaseRef && mergeBaseModified) { + this._currentHistoryItemBaseRef = { + ...this._currentHistoryItemBaseRef, + revision: mergeBaseModified.revision + }; + } } } else { // Detached commit @@ -178,18 +198,10 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this.logger.trace(`[GitHistoryProvider][onDidRunWriteOperation] currentHistoryItemRemoteRef: ${JSON.stringify(this._currentHistoryItemRemoteRef)}`); this.logger.trace(`[GitHistoryProvider][onDidRunWriteOperation] currentHistoryItemBaseRef: ${JSON.stringify(this._currentHistoryItemBaseRef)}`); - // Refs (alphabetically) - const historyItemRefs = this.repository.refs - .map(ref => toSourceControlHistoryItemRef(this.repository, ref)) - .sort((a, b) => a.id.localeCompare(b.id)); - // Auto-fetch const silent = result.operation.kind === OperationKind.Fetch && result.operation.showProgress === false; - const delta = deltaHistoryItemRefs(this.historyItemRefs, historyItemRefs); this._onDidChangeHistoryItemRefs.fire({ ...delta, silent }); - this.historyItemRefs = historyItemRefs; - const deltaLog = { added: delta.added.map(ref => ref.id), modified: delta.modified.map(ref => ref.id), From cdfd702e2f021df12d8cd8c18f6ed1720a1a5c9a Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:04:34 +0100 Subject: [PATCH 0435/3587] api :lipstick: (#237559) --- src/vscode-dts/vscode.proposed.inlineEdit.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vscode-dts/vscode.proposed.inlineEdit.d.ts b/src/vscode-dts/vscode.proposed.inlineEdit.d.ts index 41ae0bb557be..12e87cab4cf2 100644 --- a/src/vscode-dts/vscode.proposed.inlineEdit.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineEdit.d.ts @@ -53,7 +53,7 @@ declare module 'vscode' { */ triggerKind: InlineEditTriggerKind; - requestUuid?: string; + readonly requestUuid?: string; } export enum InlineEditTriggerKind { From b7b6f26c6214e93095ab090b27cb069a85adc276 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:09:30 +0100 Subject: [PATCH 0436/3587] Engineering - disable binskim (#237562) --- build/azure-pipelines/product-build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 39075d822831..94da67c3ff82 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -178,6 +178,9 @@ extends: template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines parameters: sdl: + binskim: + enabled: false + justificationForDisabling: "BinSkim rebaselining is failing" tsa: enabled: true configFile: $(Build.SourcesDirectory)/build/azure-pipelines/config/tsaoptions.json From 23e6be7d99a82052fcaed8774da351b8823b23c8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 9 Jan 2025 06:04:51 -0800 Subject: [PATCH 0437/3587] Handle terminal progress Fixes #237564 --- package-lock.json | 96 ++++++++++--------- package.json | 19 ++-- remote/package-lock.json | 96 ++++++++++--------- remote/package.json | 19 ++-- remote/web/package-lock.json | 88 +++++++++-------- remote/web/package.json | 17 ++-- scripts/xterm-update.js | 1 + .../contrib/terminal/browser/terminal.ts | 2 + .../terminal/browser/terminalInstance.ts | 20 +++- .../browser/xterm/xtermAddonImporter.ts | 3 + .../terminal/browser/xterm/xtermTerminal.ts | 21 ++++ .../terminal/common/terminalConfiguration.ts | 1 + 12 files changed, 231 insertions(+), 152 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9a706f0ae8aa..b8b4e100fdd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,15 +27,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.68", - "@xterm/addon-image": "^0.9.0-beta.85", - "@xterm/addon-ligatures": "^0.10.0-beta.85", - "@xterm/addon-search": "^0.16.0-beta.85", - "@xterm/addon-serialize": "^0.14.0-beta.85", - "@xterm/addon-unicode11": "^0.9.0-beta.85", - "@xterm/addon-webgl": "^0.19.0-beta.85", - "@xterm/headless": "^5.6.0-beta.85", - "@xterm/xterm": "^5.6.0-beta.85", + "@xterm/addon-clipboard": "^0.2.0-beta.78", + "@xterm/addon-image": "^0.9.0-beta.95", + "@xterm/addon-ligatures": "^0.10.0-beta.95", + "@xterm/addon-progress": "^0.2.0-beta.1", + "@xterm/addon-search": "^0.16.0-beta.95", + "@xterm/addon-serialize": "^0.14.0-beta.95", + "@xterm/addon-unicode11": "^0.9.0-beta.95", + "@xterm/addon-webgl": "^0.19.0-beta.95", + "@xterm/headless": "^5.6.0-beta.95", + "@xterm/xterm": "^5.6.0-beta.95", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -3458,30 +3459,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.68", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.68.tgz", - "integrity": "sha512-z/4urYG3dySjmnfwig2eH3rJNLVFIk3IGQ+Ibadu4GZwCAVkX7eV/uGMssIeVGMg/ZD3uVdocFvcaILPOz01Pg==", + "version": "0.2.0-beta.78", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.78.tgz", + "integrity": "sha512-t9TG0WTkkSWZb9YL69aIUNXw9uHtozsMH5CNj5v+OfDgR0VMmJLjgVQEnOT7WyoFhQXOKgGzEZ1JfLQA8mbznA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.85.tgz", - "integrity": "sha512-XyIG+v6eVXBKkW6rT5GLF8VBVvNdsdcCNBOlw1kWPiK31/hzxcnoPXXRDp6bqxxFOtcB8tlHe2mk/5lQG4JtPA==", + "version": "0.9.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.95.tgz", + "integrity": "sha512-qbYmhFW9XxQ0N6sCZx91ilz74hB7mKhezGv/MnUkUsoo6HW4AuuWeohlTlpWJh025RulLndRMoVuacyVlEtpgQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.85.tgz", - "integrity": "sha512-fJKsmqjRIBr8TphyOefYnhH1nw1+HvtBmO1f6DX893a0qyLZ0cPIowuAABTBbu/j5mhwJveKw7pYIXScT42cBA==", + "version": "0.10.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.95.tgz", + "integrity": "sha512-JCsKuQUgnOcHGE1pbRXaRgLEt0aCgIPWW3Fo642YV1tmoOJxKyEM6IqqkXgmW0JRodRfy5xs2X3pUdqJvU4OzQ==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -3491,55 +3492,64 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" + } + }, + "node_modules/@xterm/addon-progress": { + "version": "0.2.0-beta.1", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.1.tgz", + "integrity": "sha512-lgaXmHvU3GYRiwUSet0SJ4DoofhwjcmNSsPkf4WWnHxpb7TakoJPWx9ItO2h9EswwBETjYGWut7GkQi2MwO5Rg==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.85.tgz", - "integrity": "sha512-Z1IZlBIfqyB4weBffIwHKFGRVVgxBz105RdXOk+Jt5iIXeocg/sjCM7iFNwwQL0vLGOfzIQBWrS8oqjWeYvDEg==", + "version": "0.16.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.95.tgz", + "integrity": "sha512-R7NFwRpV6sq6ELcJN9jkVZkSvo7LBDjqkZ3FmpCS2aumubHPNf1oHkejOYTbOKbHnABNM5Mp6Y32HVKgLRnPtQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.85.tgz", - "integrity": "sha512-uMjrxcyF4ZCCKGI/4XLfq0/xEbCZkUsj6rORN0kem7GXKenNA1ggxk4z0Z8rFxwEyjV3wfjOaVPmxHJM3xh+gA==", + "version": "0.14.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.95.tgz", + "integrity": "sha512-IiNo02PZLcRSN//+FlIKmm3zMwhjw9dLqViqths7YoADMLAl51VDfUEVz+jlu6pHuJ2msl0n8uWggS45aNTO9g==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.85.tgz", - "integrity": "sha512-mpsRneCyY9Jmu05KYISOmDqkWS4WCV4D0UxCHxGmdmjeHDsNItauGG7u2Qnct7K+RBhfJPDp0j2yCTMTWuv0KQ==", + "version": "0.9.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.95.tgz", + "integrity": "sha512-GR6sAg+UcLAzOXl7ewzchkEKt3eH0GRciEqk3kgPOi70RHAnEoKtKtXN/Jdz5eV32I17wU4rUwwGHGc+mMNHxw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.85.tgz", - "integrity": "sha512-qenYMn7XwBxujNkhialgGYvoKyGxbpGYiUmgQIdQPiV5yMQsaqz5S/o+iPKJM8sSRcI+ghxYS6KhiUOkzg5C3Q==", + "version": "0.19.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.95.tgz", + "integrity": "sha512-HUr5YTKLifhupP4cQGk7Oq4ydnRYks8060ueFDLAOGcI7gki17a+QTy5ZYzmeArrWwSrGxnO2TsyUZZu8ziJMg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.85.tgz", - "integrity": "sha512-GzIULvPPz+I9tvdpMM5k5GeURz7bHBQyVQyZE6d6UUSyVVVd0NWgJXwF25t62y9cZQt1Bfp8i+1eCJ7p2+8ZSQ==", + "version": "5.6.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.95.tgz", + "integrity": "sha512-drsSE8HVuzfyeemdWaSHlzBnkwW2tu4b2uB7NYF9Yu5gvcwbt60+Roih2LlaAO9mpUz5oU3L6PVP9SCjZXJhDg==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.85.tgz", - "integrity": "sha512-A2HpImW8FIlUOtkWm2FPnUdhhFa+ejshv5RJbejGCihGOnizsCeG8vBLt7uEJ37msvcJJSgFJ/VSmcFnh9Y9vA==", + "version": "5.6.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.95.tgz", + "integrity": "sha512-gG4ZjYrdob77QiWFultDhquYFYm2hldFN1jX0lA6WQp7H7utxW29VvCf/4hCW/qqFg56V3R76HiwXzKQMhzIiQ==", "license": "MIT" }, "node_modules/@xtuc/ieee754": { diff --git a/package.json b/package.json index 58b368af92e6..2a306c5eecc6 100644 --- a/package.json +++ b/package.json @@ -85,15 +85,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.68", - "@xterm/addon-image": "^0.9.0-beta.85", - "@xterm/addon-ligatures": "^0.10.0-beta.85", - "@xterm/addon-search": "^0.16.0-beta.85", - "@xterm/addon-serialize": "^0.14.0-beta.85", - "@xterm/addon-unicode11": "^0.9.0-beta.85", - "@xterm/addon-webgl": "^0.19.0-beta.85", - "@xterm/headless": "^5.6.0-beta.85", - "@xterm/xterm": "^5.6.0-beta.85", + "@xterm/addon-clipboard": "^0.2.0-beta.78", + "@xterm/addon-image": "^0.9.0-beta.95", + "@xterm/addon-ligatures": "^0.10.0-beta.95", + "@xterm/addon-progress": "^0.2.0-beta.1", + "@xterm/addon-search": "^0.16.0-beta.95", + "@xterm/addon-serialize": "^0.14.0-beta.95", + "@xterm/addon-unicode11": "^0.9.0-beta.95", + "@xterm/addon-webgl": "^0.19.0-beta.95", + "@xterm/headless": "^5.6.0-beta.95", + "@xterm/xterm": "^5.6.0-beta.95", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", diff --git a/remote/package-lock.json b/remote/package-lock.json index 26c6690ae30d..e15bd112fa4e 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -20,15 +20,16 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.68", - "@xterm/addon-image": "^0.9.0-beta.85", - "@xterm/addon-ligatures": "^0.10.0-beta.85", - "@xterm/addon-search": "^0.16.0-beta.85", - "@xterm/addon-serialize": "^0.14.0-beta.85", - "@xterm/addon-unicode11": "^0.9.0-beta.85", - "@xterm/addon-webgl": "^0.19.0-beta.85", - "@xterm/headless": "^5.6.0-beta.85", - "@xterm/xterm": "^5.6.0-beta.85", + "@xterm/addon-clipboard": "^0.2.0-beta.78", + "@xterm/addon-image": "^0.9.0-beta.95", + "@xterm/addon-ligatures": "^0.10.0-beta.95", + "@xterm/addon-progress": "^0.2.0-beta.1", + "@xterm/addon-search": "^0.16.0-beta.95", + "@xterm/addon-serialize": "^0.14.0-beta.95", + "@xterm/addon-unicode11": "^0.9.0-beta.95", + "@xterm/addon-webgl": "^0.19.0-beta.95", + "@xterm/headless": "^5.6.0-beta.95", + "@xterm/xterm": "^5.6.0-beta.95", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -521,30 +522,30 @@ "hasInstallScript": true }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.68", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.68.tgz", - "integrity": "sha512-z/4urYG3dySjmnfwig2eH3rJNLVFIk3IGQ+Ibadu4GZwCAVkX7eV/uGMssIeVGMg/ZD3uVdocFvcaILPOz01Pg==", + "version": "0.2.0-beta.78", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.78.tgz", + "integrity": "sha512-t9TG0WTkkSWZb9YL69aIUNXw9uHtozsMH5CNj5v+OfDgR0VMmJLjgVQEnOT7WyoFhQXOKgGzEZ1JfLQA8mbznA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.85.tgz", - "integrity": "sha512-XyIG+v6eVXBKkW6rT5GLF8VBVvNdsdcCNBOlw1kWPiK31/hzxcnoPXXRDp6bqxxFOtcB8tlHe2mk/5lQG4JtPA==", + "version": "0.9.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.95.tgz", + "integrity": "sha512-qbYmhFW9XxQ0N6sCZx91ilz74hB7mKhezGv/MnUkUsoo6HW4AuuWeohlTlpWJh025RulLndRMoVuacyVlEtpgQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.85.tgz", - "integrity": "sha512-fJKsmqjRIBr8TphyOefYnhH1nw1+HvtBmO1f6DX893a0qyLZ0cPIowuAABTBbu/j5mhwJveKw7pYIXScT42cBA==", + "version": "0.10.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.95.tgz", + "integrity": "sha512-JCsKuQUgnOcHGE1pbRXaRgLEt0aCgIPWW3Fo642YV1tmoOJxKyEM6IqqkXgmW0JRodRfy5xs2X3pUdqJvU4OzQ==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -554,55 +555,64 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" + } + }, + "node_modules/@xterm/addon-progress": { + "version": "0.2.0-beta.1", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.1.tgz", + "integrity": "sha512-lgaXmHvU3GYRiwUSet0SJ4DoofhwjcmNSsPkf4WWnHxpb7TakoJPWx9ItO2h9EswwBETjYGWut7GkQi2MwO5Rg==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.85.tgz", - "integrity": "sha512-Z1IZlBIfqyB4weBffIwHKFGRVVgxBz105RdXOk+Jt5iIXeocg/sjCM7iFNwwQL0vLGOfzIQBWrS8oqjWeYvDEg==", + "version": "0.16.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.95.tgz", + "integrity": "sha512-R7NFwRpV6sq6ELcJN9jkVZkSvo7LBDjqkZ3FmpCS2aumubHPNf1oHkejOYTbOKbHnABNM5Mp6Y32HVKgLRnPtQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.85.tgz", - "integrity": "sha512-uMjrxcyF4ZCCKGI/4XLfq0/xEbCZkUsj6rORN0kem7GXKenNA1ggxk4z0Z8rFxwEyjV3wfjOaVPmxHJM3xh+gA==", + "version": "0.14.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.95.tgz", + "integrity": "sha512-IiNo02PZLcRSN//+FlIKmm3zMwhjw9dLqViqths7YoADMLAl51VDfUEVz+jlu6pHuJ2msl0n8uWggS45aNTO9g==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.85.tgz", - "integrity": "sha512-mpsRneCyY9Jmu05KYISOmDqkWS4WCV4D0UxCHxGmdmjeHDsNItauGG7u2Qnct7K+RBhfJPDp0j2yCTMTWuv0KQ==", + "version": "0.9.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.95.tgz", + "integrity": "sha512-GR6sAg+UcLAzOXl7ewzchkEKt3eH0GRciEqk3kgPOi70RHAnEoKtKtXN/Jdz5eV32I17wU4rUwwGHGc+mMNHxw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.85.tgz", - "integrity": "sha512-qenYMn7XwBxujNkhialgGYvoKyGxbpGYiUmgQIdQPiV5yMQsaqz5S/o+iPKJM8sSRcI+ghxYS6KhiUOkzg5C3Q==", + "version": "0.19.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.95.tgz", + "integrity": "sha512-HUr5YTKLifhupP4cQGk7Oq4ydnRYks8060ueFDLAOGcI7gki17a+QTy5ZYzmeArrWwSrGxnO2TsyUZZu8ziJMg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.85.tgz", - "integrity": "sha512-GzIULvPPz+I9tvdpMM5k5GeURz7bHBQyVQyZE6d6UUSyVVVd0NWgJXwF25t62y9cZQt1Bfp8i+1eCJ7p2+8ZSQ==", + "version": "5.6.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.95.tgz", + "integrity": "sha512-drsSE8HVuzfyeemdWaSHlzBnkwW2tu4b2uB7NYF9Yu5gvcwbt60+Roih2LlaAO9mpUz5oU3L6PVP9SCjZXJhDg==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.85.tgz", - "integrity": "sha512-A2HpImW8FIlUOtkWm2FPnUdhhFa+ejshv5RJbejGCihGOnizsCeG8vBLt7uEJ37msvcJJSgFJ/VSmcFnh9Y9vA==", + "version": "5.6.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.95.tgz", + "integrity": "sha512-gG4ZjYrdob77QiWFultDhquYFYm2hldFN1jX0lA6WQp7H7utxW29VvCf/4hCW/qqFg56V3R76HiwXzKQMhzIiQ==", "license": "MIT" }, "node_modules/agent-base": { diff --git a/remote/package.json b/remote/package.json index 44281195658d..fc0e67e5c703 100644 --- a/remote/package.json +++ b/remote/package.json @@ -15,15 +15,16 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.68", - "@xterm/addon-image": "^0.9.0-beta.85", - "@xterm/addon-ligatures": "^0.10.0-beta.85", - "@xterm/addon-search": "^0.16.0-beta.85", - "@xterm/addon-serialize": "^0.14.0-beta.85", - "@xterm/addon-unicode11": "^0.9.0-beta.85", - "@xterm/addon-webgl": "^0.19.0-beta.85", - "@xterm/headless": "^5.6.0-beta.85", - "@xterm/xterm": "^5.6.0-beta.85", + "@xterm/addon-clipboard": "^0.2.0-beta.78", + "@xterm/addon-image": "^0.9.0-beta.95", + "@xterm/addon-ligatures": "^0.10.0-beta.95", + "@xterm/addon-progress": "^0.2.0-beta.1", + "@xterm/addon-search": "^0.16.0-beta.95", + "@xterm/addon-serialize": "^0.14.0-beta.95", + "@xterm/addon-unicode11": "^0.9.0-beta.95", + "@xterm/addon-webgl": "^0.19.0-beta.95", + "@xterm/headless": "^5.6.0-beta.95", + "@xterm/xterm": "^5.6.0-beta.95", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 628d03e23cb9..5bb8aa2115ef 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -13,14 +13,15 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.68", - "@xterm/addon-image": "^0.9.0-beta.85", - "@xterm/addon-ligatures": "^0.10.0-beta.85", - "@xterm/addon-search": "^0.16.0-beta.85", - "@xterm/addon-serialize": "^0.14.0-beta.85", - "@xterm/addon-unicode11": "^0.9.0-beta.85", - "@xterm/addon-webgl": "^0.19.0-beta.85", - "@xterm/xterm": "^5.6.0-beta.85", + "@xterm/addon-clipboard": "^0.2.0-beta.78", + "@xterm/addon-image": "^0.9.0-beta.95", + "@xterm/addon-ligatures": "^0.10.0-beta.95", + "@xterm/addon-progress": "^0.2.0-beta.1", + "@xterm/addon-search": "^0.16.0-beta.95", + "@xterm/addon-serialize": "^0.14.0-beta.95", + "@xterm/addon-unicode11": "^0.9.0-beta.95", + "@xterm/addon-webgl": "^0.19.0-beta.95", + "@xterm/xterm": "^5.6.0-beta.95", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", @@ -89,30 +90,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.68", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.68.tgz", - "integrity": "sha512-z/4urYG3dySjmnfwig2eH3rJNLVFIk3IGQ+Ibadu4GZwCAVkX7eV/uGMssIeVGMg/ZD3uVdocFvcaILPOz01Pg==", + "version": "0.2.0-beta.78", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.78.tgz", + "integrity": "sha512-t9TG0WTkkSWZb9YL69aIUNXw9uHtozsMH5CNj5v+OfDgR0VMmJLjgVQEnOT7WyoFhQXOKgGzEZ1JfLQA8mbznA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.85.tgz", - "integrity": "sha512-XyIG+v6eVXBKkW6rT5GLF8VBVvNdsdcCNBOlw1kWPiK31/hzxcnoPXXRDp6bqxxFOtcB8tlHe2mk/5lQG4JtPA==", + "version": "0.9.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.95.tgz", + "integrity": "sha512-qbYmhFW9XxQ0N6sCZx91ilz74hB7mKhezGv/MnUkUsoo6HW4AuuWeohlTlpWJh025RulLndRMoVuacyVlEtpgQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.85.tgz", - "integrity": "sha512-fJKsmqjRIBr8TphyOefYnhH1nw1+HvtBmO1f6DX893a0qyLZ0cPIowuAABTBbu/j5mhwJveKw7pYIXScT42cBA==", + "version": "0.10.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.95.tgz", + "integrity": "sha512-JCsKuQUgnOcHGE1pbRXaRgLEt0aCgIPWW3Fo642YV1tmoOJxKyEM6IqqkXgmW0JRodRfy5xs2X3pUdqJvU4OzQ==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -122,49 +123,58 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" + } + }, + "node_modules/@xterm/addon-progress": { + "version": "0.2.0-beta.1", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.1.tgz", + "integrity": "sha512-lgaXmHvU3GYRiwUSet0SJ4DoofhwjcmNSsPkf4WWnHxpb7TakoJPWx9ItO2h9EswwBETjYGWut7GkQi2MwO5Rg==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.85.tgz", - "integrity": "sha512-Z1IZlBIfqyB4weBffIwHKFGRVVgxBz105RdXOk+Jt5iIXeocg/sjCM7iFNwwQL0vLGOfzIQBWrS8oqjWeYvDEg==", + "version": "0.16.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.95.tgz", + "integrity": "sha512-R7NFwRpV6sq6ELcJN9jkVZkSvo7LBDjqkZ3FmpCS2aumubHPNf1oHkejOYTbOKbHnABNM5Mp6Y32HVKgLRnPtQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.85.tgz", - "integrity": "sha512-uMjrxcyF4ZCCKGI/4XLfq0/xEbCZkUsj6rORN0kem7GXKenNA1ggxk4z0Z8rFxwEyjV3wfjOaVPmxHJM3xh+gA==", + "version": "0.14.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.95.tgz", + "integrity": "sha512-IiNo02PZLcRSN//+FlIKmm3zMwhjw9dLqViqths7YoADMLAl51VDfUEVz+jlu6pHuJ2msl0n8uWggS45aNTO9g==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.85.tgz", - "integrity": "sha512-mpsRneCyY9Jmu05KYISOmDqkWS4WCV4D0UxCHxGmdmjeHDsNItauGG7u2Qnct7K+RBhfJPDp0j2yCTMTWuv0KQ==", + "version": "0.9.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.95.tgz", + "integrity": "sha512-GR6sAg+UcLAzOXl7ewzchkEKt3eH0GRciEqk3kgPOi70RHAnEoKtKtXN/Jdz5eV32I17wU4rUwwGHGc+mMNHxw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.85.tgz", - "integrity": "sha512-qenYMn7XwBxujNkhialgGYvoKyGxbpGYiUmgQIdQPiV5yMQsaqz5S/o+iPKJM8sSRcI+ghxYS6KhiUOkzg5C3Q==", + "version": "0.19.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.95.tgz", + "integrity": "sha512-HUr5YTKLifhupP4cQGk7Oq4ydnRYks8060ueFDLAOGcI7gki17a+QTy5ZYzmeArrWwSrGxnO2TsyUZZu8ziJMg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.85" + "@xterm/xterm": "^5.6.0-beta.95" } }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.85", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.85.tgz", - "integrity": "sha512-A2HpImW8FIlUOtkWm2FPnUdhhFa+ejshv5RJbejGCihGOnizsCeG8vBLt7uEJ37msvcJJSgFJ/VSmcFnh9Y9vA==", + "version": "5.6.0-beta.95", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.95.tgz", + "integrity": "sha512-gG4ZjYrdob77QiWFultDhquYFYm2hldFN1jX0lA6WQp7H7utxW29VvCf/4hCW/qqFg56V3R76HiwXzKQMhzIiQ==", "license": "MIT" }, "node_modules/font-finder": { diff --git a/remote/web/package.json b/remote/web/package.json index 5b0a3a2a72cb..5f1cd6bb0fdf 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -8,14 +8,15 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.68", - "@xterm/addon-image": "^0.9.0-beta.85", - "@xterm/addon-ligatures": "^0.10.0-beta.85", - "@xterm/addon-search": "^0.16.0-beta.85", - "@xterm/addon-serialize": "^0.14.0-beta.85", - "@xterm/addon-unicode11": "^0.9.0-beta.85", - "@xterm/addon-webgl": "^0.19.0-beta.85", - "@xterm/xterm": "^5.6.0-beta.85", + "@xterm/addon-clipboard": "^0.2.0-beta.78", + "@xterm/addon-image": "^0.9.0-beta.95", + "@xterm/addon-ligatures": "^0.10.0-beta.95", + "@xterm/addon-progress": "^0.2.0-beta.1", + "@xterm/addon-search": "^0.16.0-beta.95", + "@xterm/addon-serialize": "^0.14.0-beta.95", + "@xterm/addon-unicode11": "^0.9.0-beta.95", + "@xterm/addon-webgl": "^0.19.0-beta.95", + "@xterm/xterm": "^5.6.0-beta.95", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", diff --git a/scripts/xterm-update.js b/scripts/xterm-update.js index 5a7db71abac1..35c2084f7946 100644 --- a/scripts/xterm-update.js +++ b/scripts/xterm-update.js @@ -11,6 +11,7 @@ const moduleNames = [ '@xterm/addon-clipboard', '@xterm/addon-image', '@xterm/addon-ligatures', + '@xterm/addon-progress', '@xterm/addon-search', '@xterm/addon-serialize', '@xterm/addon-unicode11', diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index ead75598c0ec..a860d7ae9746 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -31,6 +31,7 @@ import type { ICurrentPartialCommand } from '../../../../platform/terminal/commo import type { IXtermCore } from './xterm-private.js'; import type { IMenu } from '../../../../platform/actions/common/actions.js'; import type { Barrier } from '../../../../base/common/async.js'; +import type { IProgressState } from '@xterm/addon-progress'; export const ITerminalService = createDecorator('terminalService'); export const ITerminalConfigurationService = createDecorator('terminalConfigurationService'); @@ -610,6 +611,7 @@ export interface ITerminalInstance extends IBaseTerminalInstance { readonly processName: string; readonly sequence?: string; readonly staticTitle?: string; + readonly progressState?: IProgressState; readonly workspaceFolder?: IWorkspaceFolder; readonly cwd?: string; readonly initialCwd?: string; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 589b1deaf9ca..15054267ff5a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -89,6 +89,7 @@ import { openContextMenu } from './terminalContextMenu.js'; import type { IMenu } from '../../../../platform/actions/common/actions.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { TerminalContribCommandId } from '../terminalContribExports.js'; +import type { IProgressState } from '@xterm/addon-progress'; const enum Constants { /** @@ -282,6 +283,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { get processName(): string { return this._processName; } get sequence(): string | undefined { return this._sequence; } get staticTitle(): string | undefined { return this._staticTitle; } + get progressState(): IProgressState | undefined { return this.xterm?.progressState; } get workspaceFolder(): IWorkspaceFolder | undefined { return this._workspaceFolder; } get cwd(): string | undefined { return this._cwd; } get initialCwd(): string | undefined { return this._initialCwd; } @@ -851,6 +853,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { xterm.refresh(); } })); + this._register(xterm.onDidChangeProgress(() => this._labelComputer?.refreshLabel(this))); // Set up updating of the process cwd on key press, this is only needed when the cwd // detection capability has not been registered @@ -2460,6 +2463,7 @@ interface ITerminalLabelTemplateProperties { local?: string | null | undefined; process?: string | null | undefined; sequence?: string | null | undefined; + progress?: string | null | undefined; task?: string | null | undefined; fixedDimensions?: string | null | undefined; separator?: string | ISeparator | null | undefined; @@ -2499,7 +2503,7 @@ export class TerminalLabelComputer extends Disposable { } computeLabel( - instance: Pick, + instance: Pick, labelTemplate: string, labelType: TerminalLabelType, reset?: boolean @@ -2530,6 +2534,7 @@ export class TerminalLabelComputer extends Disposable { shellPromptInput: commandDetection?.executingCommand && promptInputModel ? promptInputModel.getCombinedString(true) + nonTaskSpinner : promptInputModel?.getCombinedString(true), + progress: this._getProgressStateString(instance.progressState) }; templateProperties.workspaceFolderName = instance.workspaceFolder?.name ?? templateProperties.workspaceFolder; labelTemplate = labelTemplate.trim(); @@ -2567,6 +2572,19 @@ export class TerminalLabelComputer extends Disposable { const label = template(labelTemplate, (templateProperties as unknown) as { [key: string]: string | ISeparator | undefined | null }).replace(/[\n\r\t]/g, '').trim(); return label === '' && labelType === TerminalLabelType.Title ? (instance.processName || '') : label; } + + private _getProgressStateString(progressState?: IProgressState): string { + if (!progressState) { + return ''; + } + switch (progressState.state) { + case 0: return ''; + case 1: return `${Math.round(progressState.value)}%`; + case 2: return '$(error)'; + case 3: return '$(loading~spin)'; + case 4: return '$(alert)'; + } + } } export function parseExitResult( diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermAddonImporter.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermAddonImporter.ts index 753ba6f9e790..30e6bd1905be 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermAddonImporter.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermAddonImporter.ts @@ -6,6 +6,7 @@ import type { ClipboardAddon as ClipboardAddonType } from '@xterm/addon-clipboard'; import type { ImageAddon as ImageAddonType } from '@xterm/addon-image'; import type { LigaturesAddon as LigaturesAddonType } from '@xterm/addon-ligatures'; +import type { ProgressAddon as ProgressAddonType } from '@xterm/addon-progress'; import type { SearchAddon as SearchAddonType } from '@xterm/addon-search'; import type { SerializeAddon as SerializeAddonType } from '@xterm/addon-serialize'; import type { Unicode11Addon as Unicode11AddonType } from '@xterm/addon-unicode11'; @@ -16,6 +17,7 @@ export interface IXtermAddonNameToCtor { clipboard: typeof ClipboardAddonType; image: typeof ImageAddonType; ligatures: typeof LigaturesAddonType; + progress: typeof ProgressAddonType; search: typeof SearchAddonType; serialize: typeof SerializeAddonType; unicode11: typeof Unicode11AddonType; @@ -42,6 +44,7 @@ export class XtermAddonImporter { case 'clipboard': addon = (await importAMDNodeModule('@xterm/addon-clipboard', 'lib/addon-clipboard.js')).ClipboardAddon as IXtermAddonNameToCtor[T]; break; case 'image': addon = (await importAMDNodeModule('@xterm/addon-image', 'lib/addon-image.js')).ImageAddon as IXtermAddonNameToCtor[T]; break; case 'ligatures': addon = (await importAMDNodeModule('@xterm/addon-ligatures', 'lib/addon-ligatures.js')).LigaturesAddon as IXtermAddonNameToCtor[T]; break; + case 'progress': addon = (await importAMDNodeModule('@xterm/addon-progress', 'lib/addon-progress.js')).ProgressAddon as IXtermAddonNameToCtor[T]; break; case 'search': addon = (await importAMDNodeModule('@xterm/addon-search', 'lib/addon-search.js')).SearchAddon as IXtermAddonNameToCtor[T]; break; case 'serialize': addon = (await importAMDNodeModule('@xterm/addon-serialize', 'lib/addon-serialize.js')).SerializeAddon as IXtermAddonNameToCtor[T]; break; case 'unicode11': addon = (await importAMDNodeModule('@xterm/addon-unicode11', 'lib/addon-unicode11.js')).Unicode11Addon as IXtermAddonNameToCtor[T]; break; diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index ac41efa9c1b1..4a094ab458cf 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -43,6 +43,7 @@ import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../.. import { scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from '../../../../../platform/theme/common/colorRegistry.js'; import { XtermAddonImporter } from './xtermAddonImporter.js'; import { equals } from '../../../../../base/common/objects.js'; +import type { IProgressState } from '@xterm/addon-progress'; const enum RenderConstants { SmoothScrollDuration = 125 @@ -99,6 +100,8 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach private _isPhysicalMouseWheel = MouseWheelClassifier.INSTANCE.isPhysicalMouseWheel(); private _lastInputEvent: string | undefined; get lastInputEvent(): string | undefined { return this._lastInputEvent; } + private _progressState: IProgressState = { state: 0, value: 0 }; + get progressState(): IProgressState { return this._progressState; } // Always on addons private _markNavigationAddon: MarkNavigationAddon; @@ -141,6 +144,8 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach readonly onDidChangeFocus = this._onDidChangeFocus.event; private readonly _onDidDispose = this._register(new Emitter()); readonly onDidDispose = this._onDidDispose.event; + private readonly _onDidChangeProgress = this._register(new Emitter()); + readonly onDidChangeProgress = this._onDidChangeProgress.event; get markTracker(): IMarkTracker { return this._markNavigationAddon; } get shellIntegration(): IShellIntegration { return this._shellIntegrationAddon; } @@ -286,6 +291,22 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach }); this.raw.loadAddon(this._clipboardAddon); }); + this._xtermAddonLoader.importAddon('progress').then(ProgressAddon => { + if (this._store.isDisposed) { + return; + } + const progressAddon = this._instantiationService.createInstance(ProgressAddon); + this.raw.loadAddon(progressAddon); + const updateProgress = () => { + if (!equals(this._progressState, progressAddon.progress)) { + this._progressState = progressAddon.progress; + this._onDidChangeProgress.fire(this._progressState); + } + }; + // TODO: Remove ! when addon-progress updates + this._register(progressAddon.onChange!(() => updateProgress())); + updateProgress(); + }); this._anyTerminalFocusContextKey = TerminalContextKeys.focusInAny.bindTo(contextKeyService); this._anyFocusedTerminalHasSelection = TerminalContextKeys.textSelectedInFocused.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 632311c17343..663011266aaa 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -21,6 +21,7 @@ const terminalDescriptors = '\n- ' + [ '`\${workspaceFolderName}`: ' + localize('workspaceFolderName', "the `name` of the workspace in which the terminal was launched"), '`\${local}`: ' + localize('local', "indicates a local terminal in a remote workspace"), '`\${process}`: ' + localize('process', "the name of the terminal process"), + '`\${progress}`: ' + localize('progress', "the progress state as reported by the `OSC 9;4` sequence"), '`\${separator}`: ' + localize('separator', "a conditional separator {0} that only shows when surrounded by variables with values or static text.", '(` - `)'), '`\${sequence}`: ' + localize('sequence', "the name provided to the terminal by the process"), '`\${task}`: ' + localize('task', "indicates this terminal is associated with a task"), From af929ee6b3740768357ed65c721d8487b8f2cd99 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 9 Jan 2025 06:21:35 -0800 Subject: [PATCH 0438/3587] Scroll to bottom instead of restoring state on run recent Fixes #237572 --- .../history/browser/terminalRunRecentQuickPick.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/history/browser/terminalRunRecentQuickPick.ts b/src/vs/workbench/contrib/terminalContrib/history/browser/terminalRunRecentQuickPick.ts index 5227135598d3..4c06a09c9795 100644 --- a/src/vs/workbench/contrib/terminalContrib/history/browser/terminalRunRecentQuickPick.ts +++ b/src/vs/workbench/contrib/terminalContrib/history/browser/terminalRunRecentQuickPick.ts @@ -335,11 +335,13 @@ export async function showRunRecentQuickPick( text = result.rawLabel; } quickPick.hide(); + terminalScrollStateSaved = false; + instance.xterm?.markTracker.clear(); + instance.scrollToBottom(); instance.runCommand(text, !quickPick.keyMods.alt); if (quickPick.keyMods.alt) { instance.focus(); } - restoreScrollState(); })); disposables.add(quickPick.onDidHide(() => restoreScrollState())); if (value) { From f53e4ea0e30d39e8b8c83dc8a96803f320b86188 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 9 Jan 2025 06:27:18 -0800 Subject: [PATCH 0439/3587] Update addon-progress and remove ! --- package-lock.json | 96 +++++++++---------- package.json | 20 ++-- remote/package-lock.json | 96 +++++++++---------- remote/package.json | 20 ++-- remote/web/package-lock.json | 88 ++++++++--------- remote/web/package.json | 18 ++-- .../terminal/browser/xterm/xtermTerminal.ts | 3 +- 7 files changed, 170 insertions(+), 171 deletions(-) diff --git a/package-lock.json b/package-lock.json index b8b4e100fdd7..027efc4c8fa2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,16 +27,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.78", - "@xterm/addon-image": "^0.9.0-beta.95", - "@xterm/addon-ligatures": "^0.10.0-beta.95", - "@xterm/addon-progress": "^0.2.0-beta.1", - "@xterm/addon-search": "^0.16.0-beta.95", - "@xterm/addon-serialize": "^0.14.0-beta.95", - "@xterm/addon-unicode11": "^0.9.0-beta.95", - "@xterm/addon-webgl": "^0.19.0-beta.95", - "@xterm/headless": "^5.6.0-beta.95", - "@xterm/xterm": "^5.6.0-beta.95", + "@xterm/addon-clipboard": "^0.2.0-beta.79", + "@xterm/addon-image": "^0.9.0-beta.96", + "@xterm/addon-ligatures": "^0.10.0-beta.96", + "@xterm/addon-progress": "^0.2.0-beta.2", + "@xterm/addon-search": "^0.16.0-beta.96", + "@xterm/addon-serialize": "^0.14.0-beta.96", + "@xterm/addon-unicode11": "^0.9.0-beta.96", + "@xterm/addon-webgl": "^0.19.0-beta.96", + "@xterm/headless": "^5.6.0-beta.96", + "@xterm/xterm": "^5.6.0-beta.96", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -3459,30 +3459,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.78", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.78.tgz", - "integrity": "sha512-t9TG0WTkkSWZb9YL69aIUNXw9uHtozsMH5CNj5v+OfDgR0VMmJLjgVQEnOT7WyoFhQXOKgGzEZ1JfLQA8mbznA==", + "version": "0.2.0-beta.79", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.79.tgz", + "integrity": "sha512-zpwf43fzBG01fA8E75JQ/jsm/85bHCtFMOcURNAEbCoj0RSpZaq4rE5cPoy49IhYnctPZdYNxEU/+PzgtsSQ6Q==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.95.tgz", - "integrity": "sha512-qbYmhFW9XxQ0N6sCZx91ilz74hB7mKhezGv/MnUkUsoo6HW4AuuWeohlTlpWJh025RulLndRMoVuacyVlEtpgQ==", + "version": "0.9.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.96.tgz", + "integrity": "sha512-LDA03uA5gTBfFeEIUdHXoCpxGHuPIke03aSXZc6cllRRp9jCaOK2kbWwTOoXfzRvjreKK6pxD0vJcepwdKaNcw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.95.tgz", - "integrity": "sha512-JCsKuQUgnOcHGE1pbRXaRgLEt0aCgIPWW3Fo642YV1tmoOJxKyEM6IqqkXgmW0JRodRfy5xs2X3pUdqJvU4OzQ==", + "version": "0.10.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.96.tgz", + "integrity": "sha512-uI86EDxCab345YR/Piwzs3R82DptKp/PC+sMKSX24hehhvWVAkpZus0toGhYwLZxF1r0U6yw4Oq0aCEzW362dQ==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -3492,64 +3492,64 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.1", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.1.tgz", - "integrity": "sha512-lgaXmHvU3GYRiwUSet0SJ4DoofhwjcmNSsPkf4WWnHxpb7TakoJPWx9ItO2h9EswwBETjYGWut7GkQi2MwO5Rg==", + "version": "0.2.0-beta.2", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.2.tgz", + "integrity": "sha512-5BcRexNJanTMG2N9TjZpDcaemDqQuvNkGN8HK3509+QNmqBF9sinJXSBWWNwn+o/f4OheBtBmlmC5mDanh4kig==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.95.tgz", - "integrity": "sha512-R7NFwRpV6sq6ELcJN9jkVZkSvo7LBDjqkZ3FmpCS2aumubHPNf1oHkejOYTbOKbHnABNM5Mp6Y32HVKgLRnPtQ==", + "version": "0.16.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.96.tgz", + "integrity": "sha512-AEEXqxkT6OgDOII4SxQJFKn7NhM7PnFeMLxQwrRQzZyrtFPg5peCesA07xHYvYzh0aKQ1PVvgycfrPgbPEeQDQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.95.tgz", - "integrity": "sha512-IiNo02PZLcRSN//+FlIKmm3zMwhjw9dLqViqths7YoADMLAl51VDfUEVz+jlu6pHuJ2msl0n8uWggS45aNTO9g==", + "version": "0.14.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.96.tgz", + "integrity": "sha512-+00J2K3WsbMPy02UlehNumen//Opmr5B0MBrS4KFqhRwVaO8RD0hMPfr8IuhY3YqPaVny4HMOU88yaWtscxl7A==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.95.tgz", - "integrity": "sha512-GR6sAg+UcLAzOXl7ewzchkEKt3eH0GRciEqk3kgPOi70RHAnEoKtKtXN/Jdz5eV32I17wU4rUwwGHGc+mMNHxw==", + "version": "0.9.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.96.tgz", + "integrity": "sha512-jBUSErJtrf6Jhz3itT5JXLeIIl5BkJ1ii8/YdzZQMh9602ZLYvqwS+7ih2FzYYjk0pZ+E1y4sYbT5FpBlVOM5w==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.95.tgz", - "integrity": "sha512-HUr5YTKLifhupP4cQGk7Oq4ydnRYks8060ueFDLAOGcI7gki17a+QTy5ZYzmeArrWwSrGxnO2TsyUZZu8ziJMg==", + "version": "0.19.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.96.tgz", + "integrity": "sha512-iAno2CMYRBHasr5aE3I7WN6DZ0mlHsEAIYAhvdhH+be1YxX4/DceBAtN0Ak7nFcQ70ZbtYzs1aNLTVKtli2TKA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.95.tgz", - "integrity": "sha512-drsSE8HVuzfyeemdWaSHlzBnkwW2tu4b2uB7NYF9Yu5gvcwbt60+Roih2LlaAO9mpUz5oU3L6PVP9SCjZXJhDg==", + "version": "5.6.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.96.tgz", + "integrity": "sha512-lLumuO7sQbPNIxQkXa3CxwQWNlO4C5XF80VRgQ4PzjUcwPWGkGtkJJG9jcucHAKymQnH6AokCW171mcveU8cgg==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.95.tgz", - "integrity": "sha512-gG4ZjYrdob77QiWFultDhquYFYm2hldFN1jX0lA6WQp7H7utxW29VvCf/4hCW/qqFg56V3R76HiwXzKQMhzIiQ==", + "version": "5.6.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.96.tgz", + "integrity": "sha512-XdOZyAaqOW67J3kJGJDgVb5skTD2nDQLmBICyhQ0cEqThTKrX5CzB11RswG6rZE8dI+nDE+pc93NjAMXSrQgFg==", "license": "MIT" }, "node_modules/@xtuc/ieee754": { diff --git a/package.json b/package.json index 2a306c5eecc6..74e51726d194 100644 --- a/package.json +++ b/package.json @@ -85,16 +85,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.78", - "@xterm/addon-image": "^0.9.0-beta.95", - "@xterm/addon-ligatures": "^0.10.0-beta.95", - "@xterm/addon-progress": "^0.2.0-beta.1", - "@xterm/addon-search": "^0.16.0-beta.95", - "@xterm/addon-serialize": "^0.14.0-beta.95", - "@xterm/addon-unicode11": "^0.9.0-beta.95", - "@xterm/addon-webgl": "^0.19.0-beta.95", - "@xterm/headless": "^5.6.0-beta.95", - "@xterm/xterm": "^5.6.0-beta.95", + "@xterm/addon-clipboard": "^0.2.0-beta.79", + "@xterm/addon-image": "^0.9.0-beta.96", + "@xterm/addon-ligatures": "^0.10.0-beta.96", + "@xterm/addon-progress": "^0.2.0-beta.2", + "@xterm/addon-search": "^0.16.0-beta.96", + "@xterm/addon-serialize": "^0.14.0-beta.96", + "@xterm/addon-unicode11": "^0.9.0-beta.96", + "@xterm/addon-webgl": "^0.19.0-beta.96", + "@xterm/headless": "^5.6.0-beta.96", + "@xterm/xterm": "^5.6.0-beta.96", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", diff --git a/remote/package-lock.json b/remote/package-lock.json index e15bd112fa4e..d2fa6337bd8d 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -20,16 +20,16 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.78", - "@xterm/addon-image": "^0.9.0-beta.95", - "@xterm/addon-ligatures": "^0.10.0-beta.95", - "@xterm/addon-progress": "^0.2.0-beta.1", - "@xterm/addon-search": "^0.16.0-beta.95", - "@xterm/addon-serialize": "^0.14.0-beta.95", - "@xterm/addon-unicode11": "^0.9.0-beta.95", - "@xterm/addon-webgl": "^0.19.0-beta.95", - "@xterm/headless": "^5.6.0-beta.95", - "@xterm/xterm": "^5.6.0-beta.95", + "@xterm/addon-clipboard": "^0.2.0-beta.79", + "@xterm/addon-image": "^0.9.0-beta.96", + "@xterm/addon-ligatures": "^0.10.0-beta.96", + "@xterm/addon-progress": "^0.2.0-beta.2", + "@xterm/addon-search": "^0.16.0-beta.96", + "@xterm/addon-serialize": "^0.14.0-beta.96", + "@xterm/addon-unicode11": "^0.9.0-beta.96", + "@xterm/addon-webgl": "^0.19.0-beta.96", + "@xterm/headless": "^5.6.0-beta.96", + "@xterm/xterm": "^5.6.0-beta.96", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -522,30 +522,30 @@ "hasInstallScript": true }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.78", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.78.tgz", - "integrity": "sha512-t9TG0WTkkSWZb9YL69aIUNXw9uHtozsMH5CNj5v+OfDgR0VMmJLjgVQEnOT7WyoFhQXOKgGzEZ1JfLQA8mbznA==", + "version": "0.2.0-beta.79", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.79.tgz", + "integrity": "sha512-zpwf43fzBG01fA8E75JQ/jsm/85bHCtFMOcURNAEbCoj0RSpZaq4rE5cPoy49IhYnctPZdYNxEU/+PzgtsSQ6Q==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.95.tgz", - "integrity": "sha512-qbYmhFW9XxQ0N6sCZx91ilz74hB7mKhezGv/MnUkUsoo6HW4AuuWeohlTlpWJh025RulLndRMoVuacyVlEtpgQ==", + "version": "0.9.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.96.tgz", + "integrity": "sha512-LDA03uA5gTBfFeEIUdHXoCpxGHuPIke03aSXZc6cllRRp9jCaOK2kbWwTOoXfzRvjreKK6pxD0vJcepwdKaNcw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.95.tgz", - "integrity": "sha512-JCsKuQUgnOcHGE1pbRXaRgLEt0aCgIPWW3Fo642YV1tmoOJxKyEM6IqqkXgmW0JRodRfy5xs2X3pUdqJvU4OzQ==", + "version": "0.10.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.96.tgz", + "integrity": "sha512-uI86EDxCab345YR/Piwzs3R82DptKp/PC+sMKSX24hehhvWVAkpZus0toGhYwLZxF1r0U6yw4Oq0aCEzW362dQ==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -555,64 +555,64 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.1", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.1.tgz", - "integrity": "sha512-lgaXmHvU3GYRiwUSet0SJ4DoofhwjcmNSsPkf4WWnHxpb7TakoJPWx9ItO2h9EswwBETjYGWut7GkQi2MwO5Rg==", + "version": "0.2.0-beta.2", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.2.tgz", + "integrity": "sha512-5BcRexNJanTMG2N9TjZpDcaemDqQuvNkGN8HK3509+QNmqBF9sinJXSBWWNwn+o/f4OheBtBmlmC5mDanh4kig==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.95.tgz", - "integrity": "sha512-R7NFwRpV6sq6ELcJN9jkVZkSvo7LBDjqkZ3FmpCS2aumubHPNf1oHkejOYTbOKbHnABNM5Mp6Y32HVKgLRnPtQ==", + "version": "0.16.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.96.tgz", + "integrity": "sha512-AEEXqxkT6OgDOII4SxQJFKn7NhM7PnFeMLxQwrRQzZyrtFPg5peCesA07xHYvYzh0aKQ1PVvgycfrPgbPEeQDQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.95.tgz", - "integrity": "sha512-IiNo02PZLcRSN//+FlIKmm3zMwhjw9dLqViqths7YoADMLAl51VDfUEVz+jlu6pHuJ2msl0n8uWggS45aNTO9g==", + "version": "0.14.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.96.tgz", + "integrity": "sha512-+00J2K3WsbMPy02UlehNumen//Opmr5B0MBrS4KFqhRwVaO8RD0hMPfr8IuhY3YqPaVny4HMOU88yaWtscxl7A==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.95.tgz", - "integrity": "sha512-GR6sAg+UcLAzOXl7ewzchkEKt3eH0GRciEqk3kgPOi70RHAnEoKtKtXN/Jdz5eV32I17wU4rUwwGHGc+mMNHxw==", + "version": "0.9.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.96.tgz", + "integrity": "sha512-jBUSErJtrf6Jhz3itT5JXLeIIl5BkJ1ii8/YdzZQMh9602ZLYvqwS+7ih2FzYYjk0pZ+E1y4sYbT5FpBlVOM5w==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.95.tgz", - "integrity": "sha512-HUr5YTKLifhupP4cQGk7Oq4ydnRYks8060ueFDLAOGcI7gki17a+QTy5ZYzmeArrWwSrGxnO2TsyUZZu8ziJMg==", + "version": "0.19.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.96.tgz", + "integrity": "sha512-iAno2CMYRBHasr5aE3I7WN6DZ0mlHsEAIYAhvdhH+be1YxX4/DceBAtN0Ak7nFcQ70ZbtYzs1aNLTVKtli2TKA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.95.tgz", - "integrity": "sha512-drsSE8HVuzfyeemdWaSHlzBnkwW2tu4b2uB7NYF9Yu5gvcwbt60+Roih2LlaAO9mpUz5oU3L6PVP9SCjZXJhDg==", + "version": "5.6.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.96.tgz", + "integrity": "sha512-lLumuO7sQbPNIxQkXa3CxwQWNlO4C5XF80VRgQ4PzjUcwPWGkGtkJJG9jcucHAKymQnH6AokCW171mcveU8cgg==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.95.tgz", - "integrity": "sha512-gG4ZjYrdob77QiWFultDhquYFYm2hldFN1jX0lA6WQp7H7utxW29VvCf/4hCW/qqFg56V3R76HiwXzKQMhzIiQ==", + "version": "5.6.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.96.tgz", + "integrity": "sha512-XdOZyAaqOW67J3kJGJDgVb5skTD2nDQLmBICyhQ0cEqThTKrX5CzB11RswG6rZE8dI+nDE+pc93NjAMXSrQgFg==", "license": "MIT" }, "node_modules/agent-base": { diff --git a/remote/package.json b/remote/package.json index fc0e67e5c703..53829a45faa2 100644 --- a/remote/package.json +++ b/remote/package.json @@ -15,16 +15,16 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.78", - "@xterm/addon-image": "^0.9.0-beta.95", - "@xterm/addon-ligatures": "^0.10.0-beta.95", - "@xterm/addon-progress": "^0.2.0-beta.1", - "@xterm/addon-search": "^0.16.0-beta.95", - "@xterm/addon-serialize": "^0.14.0-beta.95", - "@xterm/addon-unicode11": "^0.9.0-beta.95", - "@xterm/addon-webgl": "^0.19.0-beta.95", - "@xterm/headless": "^5.6.0-beta.95", - "@xterm/xterm": "^5.6.0-beta.95", + "@xterm/addon-clipboard": "^0.2.0-beta.79", + "@xterm/addon-image": "^0.9.0-beta.96", + "@xterm/addon-ligatures": "^0.10.0-beta.96", + "@xterm/addon-progress": "^0.2.0-beta.2", + "@xterm/addon-search": "^0.16.0-beta.96", + "@xterm/addon-serialize": "^0.14.0-beta.96", + "@xterm/addon-unicode11": "^0.9.0-beta.96", + "@xterm/addon-webgl": "^0.19.0-beta.96", + "@xterm/headless": "^5.6.0-beta.96", + "@xterm/xterm": "^5.6.0-beta.96", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 5bb8aa2115ef..552a94343257 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -13,15 +13,15 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.78", - "@xterm/addon-image": "^0.9.0-beta.95", - "@xterm/addon-ligatures": "^0.10.0-beta.95", - "@xterm/addon-progress": "^0.2.0-beta.1", - "@xterm/addon-search": "^0.16.0-beta.95", - "@xterm/addon-serialize": "^0.14.0-beta.95", - "@xterm/addon-unicode11": "^0.9.0-beta.95", - "@xterm/addon-webgl": "^0.19.0-beta.95", - "@xterm/xterm": "^5.6.0-beta.95", + "@xterm/addon-clipboard": "^0.2.0-beta.79", + "@xterm/addon-image": "^0.9.0-beta.96", + "@xterm/addon-ligatures": "^0.10.0-beta.96", + "@xterm/addon-progress": "^0.2.0-beta.2", + "@xterm/addon-search": "^0.16.0-beta.96", + "@xterm/addon-serialize": "^0.14.0-beta.96", + "@xterm/addon-unicode11": "^0.9.0-beta.96", + "@xterm/addon-webgl": "^0.19.0-beta.96", + "@xterm/xterm": "^5.6.0-beta.96", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", @@ -90,30 +90,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.78", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.78.tgz", - "integrity": "sha512-t9TG0WTkkSWZb9YL69aIUNXw9uHtozsMH5CNj5v+OfDgR0VMmJLjgVQEnOT7WyoFhQXOKgGzEZ1JfLQA8mbznA==", + "version": "0.2.0-beta.79", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.79.tgz", + "integrity": "sha512-zpwf43fzBG01fA8E75JQ/jsm/85bHCtFMOcURNAEbCoj0RSpZaq4rE5cPoy49IhYnctPZdYNxEU/+PzgtsSQ6Q==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.95.tgz", - "integrity": "sha512-qbYmhFW9XxQ0N6sCZx91ilz74hB7mKhezGv/MnUkUsoo6HW4AuuWeohlTlpWJh025RulLndRMoVuacyVlEtpgQ==", + "version": "0.9.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.96.tgz", + "integrity": "sha512-LDA03uA5gTBfFeEIUdHXoCpxGHuPIke03aSXZc6cllRRp9jCaOK2kbWwTOoXfzRvjreKK6pxD0vJcepwdKaNcw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.95.tgz", - "integrity": "sha512-JCsKuQUgnOcHGE1pbRXaRgLEt0aCgIPWW3Fo642YV1tmoOJxKyEM6IqqkXgmW0JRodRfy5xs2X3pUdqJvU4OzQ==", + "version": "0.10.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.96.tgz", + "integrity": "sha512-uI86EDxCab345YR/Piwzs3R82DptKp/PC+sMKSX24hehhvWVAkpZus0toGhYwLZxF1r0U6yw4Oq0aCEzW362dQ==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -123,58 +123,58 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.1", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.1.tgz", - "integrity": "sha512-lgaXmHvU3GYRiwUSet0SJ4DoofhwjcmNSsPkf4WWnHxpb7TakoJPWx9ItO2h9EswwBETjYGWut7GkQi2MwO5Rg==", + "version": "0.2.0-beta.2", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.2.tgz", + "integrity": "sha512-5BcRexNJanTMG2N9TjZpDcaemDqQuvNkGN8HK3509+QNmqBF9sinJXSBWWNwn+o/f4OheBtBmlmC5mDanh4kig==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.95.tgz", - "integrity": "sha512-R7NFwRpV6sq6ELcJN9jkVZkSvo7LBDjqkZ3FmpCS2aumubHPNf1oHkejOYTbOKbHnABNM5Mp6Y32HVKgLRnPtQ==", + "version": "0.16.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.96.tgz", + "integrity": "sha512-AEEXqxkT6OgDOII4SxQJFKn7NhM7PnFeMLxQwrRQzZyrtFPg5peCesA07xHYvYzh0aKQ1PVvgycfrPgbPEeQDQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.95.tgz", - "integrity": "sha512-IiNo02PZLcRSN//+FlIKmm3zMwhjw9dLqViqths7YoADMLAl51VDfUEVz+jlu6pHuJ2msl0n8uWggS45aNTO9g==", + "version": "0.14.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.96.tgz", + "integrity": "sha512-+00J2K3WsbMPy02UlehNumen//Opmr5B0MBrS4KFqhRwVaO8RD0hMPfr8IuhY3YqPaVny4HMOU88yaWtscxl7A==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.95.tgz", - "integrity": "sha512-GR6sAg+UcLAzOXl7ewzchkEKt3eH0GRciEqk3kgPOi70RHAnEoKtKtXN/Jdz5eV32I17wU4rUwwGHGc+mMNHxw==", + "version": "0.9.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.96.tgz", + "integrity": "sha512-jBUSErJtrf6Jhz3itT5JXLeIIl5BkJ1ii8/YdzZQMh9602ZLYvqwS+7ih2FzYYjk0pZ+E1y4sYbT5FpBlVOM5w==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.95.tgz", - "integrity": "sha512-HUr5YTKLifhupP4cQGk7Oq4ydnRYks8060ueFDLAOGcI7gki17a+QTy5ZYzmeArrWwSrGxnO2TsyUZZu8ziJMg==", + "version": "0.19.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.96.tgz", + "integrity": "sha512-iAno2CMYRBHasr5aE3I7WN6DZ0mlHsEAIYAhvdhH+be1YxX4/DceBAtN0Ak7nFcQ70ZbtYzs1aNLTVKtli2TKA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.95" + "@xterm/xterm": "^5.6.0-beta.96" } }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.95", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.95.tgz", - "integrity": "sha512-gG4ZjYrdob77QiWFultDhquYFYm2hldFN1jX0lA6WQp7H7utxW29VvCf/4hCW/qqFg56V3R76HiwXzKQMhzIiQ==", + "version": "5.6.0-beta.96", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.96.tgz", + "integrity": "sha512-XdOZyAaqOW67J3kJGJDgVb5skTD2nDQLmBICyhQ0cEqThTKrX5CzB11RswG6rZE8dI+nDE+pc93NjAMXSrQgFg==", "license": "MIT" }, "node_modules/font-finder": { diff --git a/remote/web/package.json b/remote/web/package.json index 5f1cd6bb0fdf..f345c5d08d92 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -8,15 +8,15 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.78", - "@xterm/addon-image": "^0.9.0-beta.95", - "@xterm/addon-ligatures": "^0.10.0-beta.95", - "@xterm/addon-progress": "^0.2.0-beta.1", - "@xterm/addon-search": "^0.16.0-beta.95", - "@xterm/addon-serialize": "^0.14.0-beta.95", - "@xterm/addon-unicode11": "^0.9.0-beta.95", - "@xterm/addon-webgl": "^0.19.0-beta.95", - "@xterm/xterm": "^5.6.0-beta.95", + "@xterm/addon-clipboard": "^0.2.0-beta.79", + "@xterm/addon-image": "^0.9.0-beta.96", + "@xterm/addon-ligatures": "^0.10.0-beta.96", + "@xterm/addon-progress": "^0.2.0-beta.2", + "@xterm/addon-search": "^0.16.0-beta.96", + "@xterm/addon-serialize": "^0.14.0-beta.96", + "@xterm/addon-unicode11": "^0.9.0-beta.96", + "@xterm/addon-webgl": "^0.19.0-beta.96", + "@xterm/xterm": "^5.6.0-beta.96", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 4a094ab458cf..d66941ee3678 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -303,8 +303,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach this._onDidChangeProgress.fire(this._progressState); } }; - // TODO: Remove ! when addon-progress updates - this._register(progressAddon.onChange!(() => updateProgress())); + this._register(progressAddon.onChange(() => updateProgress())); updateProgress(); }); From 86b257ea82bca74e1fd719f952f0a9eee658fedc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 9 Jan 2025 06:38:58 -0800 Subject: [PATCH 0440/3587] Clear progress state when any command finishes --- .../contrib/terminal/browser/xterm/xtermTerminal.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index d66941ee3678..e753e86f97fe 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -44,6 +44,7 @@ import { scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSl import { XtermAddonImporter } from './xtermAddonImporter.js'; import { equals } from '../../../../../base/common/objects.js'; import type { IProgressState } from '@xterm/addon-progress'; +import type { CommandDetectionCapability } from '../../../../../platform/terminal/common/capabilities/commandDetectionCapability.js'; const enum RenderConstants { SmoothScrollDuration = 125 @@ -305,6 +306,18 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach }; this._register(progressAddon.onChange(() => updateProgress())); updateProgress(); + const commandDetection = this._capabilities.get(TerminalCapability.CommandDetection); + if (commandDetection) { + commandDetection.onCommandFinished(() => progressAddon.progress = { state: 0, value: 0 }); + } else { + const disposable = this._capabilities.onDidAddCapability(e => { + if (e.id === TerminalCapability.CommandDetection) { + (e.capability as CommandDetectionCapability).onCommandFinished(() => progressAddon.progress = { state: 0, value: 0 }); + this._store.delete(disposable); + } + }); + this._store.add(disposable); + } }); this._anyTerminalFocusContextKey = TerminalContextKeys.focusInAny.bindTo(contextKeyService); From 051d2dcfa25cedd28fef871dfeb74792a48bc3cd Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 9 Jan 2025 15:42:35 +0100 Subject: [PATCH 0441/3587] apply edits: show progress while computing edits --- .../browser/actions/codeBlockOperations.ts | 111 ++++++++++++------ .../browser/chatEditing/chatEditingSession.ts | 5 + .../chat/common/chatCodeMapperService.ts | 14 +-- 3 files changed, 86 insertions(+), 44 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index c43362c46504..5fafadbd6c02 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { AsyncIterableObject } from '../../../../../base/common/async.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { CharCode } from '../../../../../base/common/charCode.js'; import { isCancellationError } from '../../../../../base/common/errors.js'; import { isEqual } from '../../../../../base/common/resources.js'; @@ -21,13 +21,14 @@ import { localize } from '../../../../../nls.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js'; import { insertCell } from '../../../notebook/browser/controller/cellOperations.js'; import { IActiveNotebookEditor, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; import { CellKind, NOTEBOOK_EDITOR_ID } from '../../../notebook/common/notebookCommon.js'; -import { ICodeMapperRequest, ICodeMapperResponse, ICodeMapperService } from '../../common/chatCodeMapperService.js'; +import { ICodeMapperCodeBlock, ICodeMapperRequest, ICodeMapperResponse, ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { ChatUserAction, IChatService } from '../../common/chatService.js'; import { isResponseVM } from '../../common/chatViewModel.js'; import { ICodeBlockActionContext } from '../codeBlockPart.js'; @@ -111,7 +112,8 @@ export class ApplyCodeBlockOperation { @IFileService private readonly fileService: IFileService, @IDialogService private readonly dialogService: IDialogService, @ILogService private readonly logService: ILogService, - @ICodeMapperService private readonly codeMapperService: ICodeMapperService + @ICodeMapperService private readonly codeMapperService: ICodeMapperService, + @IProgressService private readonly progressService: IProgressService ) { } @@ -178,49 +180,88 @@ export class ApplyCodeBlockOperation { } private async handleTextEditor(codeEditor: IActiveCodeEditor, codeBlockContext: ICodeBlockActionContext): Promise { - if (isReadOnly(codeEditor.getModel(), this.textFileService)) { + const activeModel = codeEditor.getModel(); + if (isReadOnly(activeModel, this.textFileService)) { this.notify(localize('applyCodeBlock.readonly', "Cannot apply code block to read-only file.")); return undefined; } - const cancellationTokenSource = new CancellationTokenSource(); - try { - const activeModel = codeEditor.getModel(); - const resource = codeBlockContext.codemapperUri ?? activeModel.uri; - let codeMapper: string | undefined; - - const iterable = new AsyncIterableObject(async executor => { - const request: ICodeMapperRequest = { - codeBlocks: [{ code: codeBlockContext.code, resource, markdownBeforeBlock: undefined }] - }; - const response: ICodeMapperResponse = { - textEdit: (target: URI, edit: TextEdit[]) => { - executor.emitOne(edit); - } - }; - const result = await this.codeMapperService.mapCode(request, response, cancellationTokenSource.token); - codeMapper = result?.providerName; - if (result?.errorMessage) { - executor.reject(new Error(result.errorMessage)); - } - }); + const resource = codeBlockContext.codemapperUri ?? activeModel.uri; + const codeBlock = { code: codeBlockContext.code, resource, markdownBeforeBlock: undefined }; + + const codeMapper = this.codeMapperService.providers[0]?.displayName; + if (!codeMapper) { + this.notify(localize('applyCodeBlock.noCodeMapper', "No code mapper available.")); + return undefined; + } - const editorToApply = await this.codeEditorService.openCodeEditor({ resource }, codeEditor); - let result = false; - if (editorToApply && editorToApply.hasModel()) { + const editorToApply = await this.codeEditorService.openCodeEditor({ resource }, codeEditor); + let result = false; + if (editorToApply && editorToApply.hasModel()) { + + const cancellationTokenSource = new CancellationTokenSource(); + try { + const iterable = await this.progressService.withProgress>( + { location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true }, + async progress => { + progress.report({ message: localize('applyCodeBlock.progress', "Applying code block using {0}...", codeMapper) }); + const editsIterable = this.getEdits(codeBlock, cancellationTokenSource.token); + return await this.waitForFirstElement(editsIterable); + }, + () => cancellationTokenSource.cancel() + ); result = await this.applyWithInlinePreview(iterable, editorToApply, cancellationTokenSource); + } catch (e) { + if (!isCancellationError(e)) { + this.notify(localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message)); + } + } finally { + cancellationTokenSource.dispose(); } - return { editsProposed: result, codeMapper }; - } catch (e) { - if (!isCancellationError(e)) { - this.notify(localize('applyCodeBlock.error', "An error occurred while applying the code block.")); + } + return { + editsProposed: result, + codeMapper + }; + } + + private getEdits(codeBlock: ICodeMapperCodeBlock, token: CancellationToken): AsyncIterable { + return new AsyncIterableObject(async executor => { + const request: ICodeMapperRequest = { + codeBlocks: [codeBlock] + }; + const response: ICodeMapperResponse = { + textEdit: (target: URI, edit: TextEdit[]) => { + executor.emitOne(edit); + } + }; + const result = await this.codeMapperService.mapCode(request, response, token); + if (result?.errorMessage) { + executor.reject(new Error(result.errorMessage)); } - } finally { - cancellationTokenSource.dispose(); + }); + } + + private async waitForFirstElement(iterable: AsyncIterable): Promise> { + const iterator = iterable[Symbol.asyncIterator](); + const firstResult = await iterator.next(); + + if (firstResult.done) { + return { + async *[Symbol.asyncIterator]() { + return; + } + }; } - return undefined; + return { + async *[Symbol.asyncIterator]() { + yield firstResult.value; + yield* iterable; + } + }; } + private async applyWithInlinePreview(edits: AsyncIterable, codeEditor: IActiveCodeEditor, tokenSource: CancellationTokenSource): Promise { const inlineChatController = InlineChatController.get(codeEditor); if (inlineChatController) { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 134c3de87edb..3845b0642f9c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -640,6 +640,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._onDidChange.fire(ChatEditingSessionChangeType.Other); } + /** + * Retrieves or creates a modified file entry. + * + * @returns The modified file entry. + */ private async _getOrCreateModifiedFileEntry(resource: URI, responseModel: IModifiedEntryTelemetryInfo): Promise { const existingEntry = this._entriesObs.get().find(e => isEqual(e.modifiedURI, resource)); if (existingEntry) { diff --git a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts index f8aab6da4b34..809a37714805 100644 --- a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts +++ b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts @@ -34,20 +34,17 @@ export interface ICodeMapperProvider { export const ICodeMapperService = createDecorator('codeMapperService'); -export interface ICodeMapperServiceResult extends ICodeMapperResult { - readonly providerName: string; -} - export interface ICodeMapperService { readonly _serviceBrand: undefined; + readonly providers: ICodeMapperProvider[]; registerCodeMapperProvider(handle: number, provider: ICodeMapperProvider): IDisposable; - mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise; + mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise; } export class CodeMapperService implements ICodeMapperService { _serviceBrand: undefined; - private readonly providers: ICodeMapperProvider[] = []; + public readonly providers: ICodeMapperProvider[] = []; registerCodeMapperProvider(handle: number, provider: ICodeMapperProvider): IDisposable { this.providers.push(provider); @@ -64,11 +61,10 @@ export class CodeMapperService implements ICodeMapperService { async mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken) { for (const provider of this.providers) { const result = await provider.mapCode(request, response, token); - if (result) { - return { providerName: provider.displayName, ...result }; - } else if (token.isCancellationRequested) { + if (token.isCancellationRequested) { return undefined; } + return result; } return undefined; } From 067f61b47213f77a563667fd1e13b89a0c1eac26 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 9 Jan 2025 15:56:04 +0100 Subject: [PATCH 0442/3587] linux - also change `window.customTitleBarVisibility` based on experiment (#237576) --- src/vs/workbench/electron-sandbox/desktop.main.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index 105275414263..04b054017cf0 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -85,7 +85,12 @@ export class DesktopMain extends Disposable { // Apply custom title override to defaults if any if (isLinux && product.quality === 'stable' && this.configuration.overrideDefaultTitlebarStyle === 'custom') { const configurationRegistry = Registry.as(Extensions.Configuration); - configurationRegistry.registerDefaultConfigurations([{ overrides: { 'window.titleBarStyle': 'custom' } }]); + configurationRegistry.registerDefaultConfigurations([{ + overrides: { + 'window.titleBarStyle': 'custom', + 'window.customTitleBarVisibility': 'auto' + } + }]); } } From 5a932a6ac3fc431776c4fd9505f8b1f556be6699 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 9 Jan 2025 15:56:33 +0100 Subject: [PATCH 0443/3587] suggest - show previous item when reveal an in-the-middle item to indicate there are higher ranked suggestions (#237577) --- src/vs/editor/contrib/suggest/browser/suggestWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index e1bf843a994b..0ad604159e1a 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -570,7 +570,7 @@ export class SuggestWidget implements IDisposable { try { this._list.splice(0, this._list.length, this._completionModel.items); this._setState(isFrozen ? State.Frozen : State.Open); - this._list.reveal(selectionIndex, 0); + this._list.reveal(selectionIndex, 0, selectionIndex === 0 ? 0 : this.getLayoutInfo().itemHeight * 0.33); this._list.setFocus(noFocus ? [] : [selectionIndex]); } finally { this._onDidFocus.resume(); From 6eef1e20d7c0ba58c07515b18fb36987fb702852 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 9 Jan 2025 16:04:27 +0100 Subject: [PATCH 0444/3587] simpler recovery fix for #237551 (#237578) --- .../common/extensionGalleryService.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 7ed28238492f..53aedc82f10b 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -757,7 +757,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi version: this.productService.version, date: this.productService.date } - }, false); + }, undefined, extensionInfo.preRelease ? 'prerelease' : 'release'); if (extension) { result.push(extension); @@ -990,7 +990,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi if (hasAllVersions) { const extensions: IGalleryExtension[] = []; for (const rawGalleryExtension of rawGalleryExtensions) { - const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria, true, context); + const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria, context); if (extension) { extensions.push(extension); } @@ -1019,7 +1019,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi continue; } } - const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria, false, context); + const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria, context); if (!extension /** Need all versions if the extension is a pre-release version but * - the query is to look for a release version or @@ -1060,7 +1060,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return { extensions: result.sort((a, b) => a[0] - b[0]).map(([, extension]) => extension), total }; } - private async toGalleryExtensionWithCriteria(rawGalleryExtension: IRawGalleryExtension, criteria: IExtensionCriteria, hasAllVersions: boolean, queryContext?: IStringDictionary): Promise { + private async toGalleryExtensionWithCriteria(rawGalleryExtension: IRawGalleryExtension, criteria: IExtensionCriteria, queryContext?: IStringDictionary, versionType?: 'release' | 'prerelease' | 'any'): Promise { const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId }; const version = criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version; @@ -1082,7 +1082,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi extensionIdentifier.id, rawGalleryExtensionVersion, rawGalleryExtension.publisher.displayName, - includePreRelease ? (hasAllVersions ? 'any' : 'prerelease') : 'release', + versionType ?? (includePreRelease ? 'any' : 'release'), criteria.compatible, allTargetPlatforms, criteria.targetPlatform, From 77027e72e5f497e72edeba40aa9360a0865eec08 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 9 Jan 2025 16:28:50 +0100 Subject: [PATCH 0445/3587] Support filtering in output view (#237581) * implement log filtering as a editor contrib * show all log levels by default --- .../browser/parts/views/viewFilter.ts | 2 +- .../output/browser/output.contribution.ts | 79 ++++- .../contrib/output/browser/outputServices.ts | 111 ++++++- .../contrib/output/browser/outputView.ts | 286 +++++++++++++++++- .../common/outputChannelModelService.ts | 2 +- .../services/output/common/output.ts | 29 +- 6 files changed, 488 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/browser/parts/views/viewFilter.ts b/src/vs/workbench/browser/parts/views/viewFilter.ts index 9eae6e9bc5f8..ee6f952a249e 100644 --- a/src/vs/workbench/browser/parts/views/viewFilter.ts +++ b/src/vs/workbench/browser/parts/views/viewFilter.ts @@ -95,7 +95,7 @@ export class FilterWidget extends Widget { @IKeybindingService private readonly keybindingService: IKeybindingService ) { super(); - this.delayedFilterUpdate = new Delayer(400); + this.delayedFilterUpdate = new Delayer(300); this._register(toDisposable(() => this.delayedFilterUpdate.cancel())); if (options.focusContextKey) { diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 7288357d606a..0066f704b1fd 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -10,7 +10,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuId, registerAction2, Action2, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { OutputService } from './outputServices.js'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT } from '../../../services/output/common/output.js'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, OUTPUT_FILTER_FOCUS_CONTEXT } from '../../../services/output/common/output.js'; import { OutputViewPane } from './outputView.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; @@ -23,7 +23,7 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions, Configur import { IQuickPickItem, IQuickInputService, IQuickPickSeparator, QuickPickInput } from '../../../../platform/quickinput/common/quickInput.js'; import { AUX_WINDOW_GROUP, AUX_WINDOW_GROUP_TYPE, IEditorService } from '../../../services/editor/common/editorService.js'; import { assertIsDefined } from '../../../../base/common/types.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, ContextKeyExpression } from '../../../../platform/contextkey/common/contextkey.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; @@ -37,6 +37,9 @@ import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.j import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../platform/accessibility/common/accessibility.js'; import { IsWindowsContext } from '../../../../platform/contextkey/common/contextkeys.js'; import { FocusedViewContext } from '../../../common/contextkeys.js'; +import { localize, localize2 } from '../../../../nls.js'; +import { viewFilterSubmenu } from '../../../browser/parts/views/viewFilter.js'; +import { ViewAction } from '../../../browser/parts/views/viewPane.js'; // Register Service registerSingleton(IOutputService, OutputService, InstantiationType.Delayed); @@ -107,6 +110,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { this.registerShowLogsAction(); this.registerOpenLogFileAction(); this.registerConfigureActiveOutputLogLevelAction(); + this.registerFilterActions(); } private registerSwitchOutputAction(): void { @@ -527,6 +531,77 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { })); } + private registerFilterActions(): void { + let order = 0; + const registerLogLevel = (logLevel: LogLevel, toggled: ContextKeyExpression) => { + this._register(registerAction2(class extends ViewAction { + constructor() { + super({ + id: `workbench.actions.${OUTPUT_VIEW_ID}.toggle.${LogLevelToString(logLevel)}`, + title: LogLevelToLocalizedString(logLevel).value, + metadata: { + description: localize2('toggleTraceDescription', "Show or hide {0} messages in the output", LogLevelToString(logLevel)) + }, + toggled, + menu: { + id: viewFilterSubmenu, + group: '1_filter', + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE), + order: order++ + }, + viewId: OUTPUT_VIEW_ID + }); + } + async runInView(serviceAccessor: ServicesAccessor, view: OutputViewPane): Promise { + this.toggleLogLevelFilter(serviceAccessor.get(IOutputService), logLevel); + } + private toggleLogLevelFilter(outputService: IOutputService, logLevel: LogLevel): void { + switch (logLevel) { + case LogLevel.Trace: + outputService.filters.trace = !outputService.filters.trace; + break; + case LogLevel.Debug: + outputService.filters.debug = !outputService.filters.debug; + break; + case LogLevel.Info: + outputService.filters.info = !outputService.filters.info; + break; + case LogLevel.Warning: + outputService.filters.warning = !outputService.filters.warning; + break; + case LogLevel.Error: + outputService.filters.error = !outputService.filters.error; + break; + } + } + })); + }; + + registerLogLevel(LogLevel.Trace, SHOW_TRACE_FILTER_CONTEXT); + registerLogLevel(LogLevel.Debug, SHOW_DEBUG_FILTER_CONTEXT); + registerLogLevel(LogLevel.Info, SHOW_INFO_FILTER_CONTEXT); + registerLogLevel(LogLevel.Warning, SHOW_WARNING_FILTER_CONTEXT); + registerLogLevel(LogLevel.Error, SHOW_ERROR_FILTER_CONTEXT); + + this._register(registerAction2(class extends ViewAction { + constructor() { + super({ + id: `workbench.actions.${OUTPUT_VIEW_ID}.clearFilterText`, + title: localize('clearFiltersText', "Clear filters text"), + keybinding: { + when: OUTPUT_FILTER_FOCUS_CONTEXT, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Escape + }, + viewId: OUTPUT_VIEW_ID + }); + } + async runInView(serviceAccessor: ServicesAccessor, outputView: OutputViewPane): Promise { + outputView.clearFilterText(); + } + })); + } + } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(OutputContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 25950e9f7e90..299491940ce0 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -10,7 +10,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; -import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT } from '../../../services/output/common/output.js'; +import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, IOutputViewFilters, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT } from '../../../services/output/common/output.js'; import { OutputLinkProvider } from './outputLinkProvider.js'; import { ITextModelService, ITextModelContentProvider } from '../../../../editor/common/services/resolverService.js'; import { ITextModel } from '../../../../editor/common/model.js'; @@ -64,6 +64,104 @@ class OutputChannel extends Disposable implements IOutputChannel { } } +interface IOutputFilterOptions { + filterHistory: string[]; + trace: boolean; + debug: boolean; + info: boolean; + warning: boolean; + error: boolean; +} + +class OutputViewFilters extends Disposable implements IOutputViewFilters { + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + constructor( + options: IOutputFilterOptions, + private readonly contextKeyService: IContextKeyService + ) { + super(); + + this._trace.set(options.trace); + this._debug.set(options.debug); + this._info.set(options.info); + this._warning.set(options.warning); + this._error.set(options.error); + + this.filterHistory = options.filterHistory; + } + + filterHistory: string[]; + + private _filterText = ''; + get text(): string { + return this._filterText; + } + set text(filterText: string) { + if (this._filterText !== filterText) { + this._filterText = filterText; + this._onDidChange.fire(); + } + } + + private readonly _trace = SHOW_TRACE_FILTER_CONTEXT.bindTo(this.contextKeyService); + get trace(): boolean { + return !!this._trace.get(); + } + set trace(trace: boolean) { + if (this._trace.get() !== trace) { + this._trace.set(trace); + this._onDidChange.fire(); + } + } + + private readonly _debug = SHOW_DEBUG_FILTER_CONTEXT.bindTo(this.contextKeyService); + get debug(): boolean { + return !!this._debug.get(); + } + set debug(debug: boolean) { + if (this._debug.get() !== debug) { + this._debug.set(debug); + this._onDidChange.fire(); + } + } + + private readonly _info = SHOW_INFO_FILTER_CONTEXT.bindTo(this.contextKeyService); + get info(): boolean { + return !!this._info.get(); + } + set info(info: boolean) { + if (this._info.get() !== info) { + this._info.set(info); + this._onDidChange.fire(); + } + } + + private readonly _warning = SHOW_WARNING_FILTER_CONTEXT.bindTo(this.contextKeyService); + get warning(): boolean { + return !!this._warning.get(); + } + set warning(warning: boolean) { + if (this._warning.get() !== warning) { + this._warning.set(warning); + this._onDidChange.fire(); + } + } + + private readonly _error = SHOW_ERROR_FILTER_CONTEXT.bindTo(this.contextKeyService); + get error(): boolean { + return !!this._error.get(); + } + set error(error: boolean) { + if (this._error.get() !== error) { + this._error.set(error); + this._onDidChange.fire(); + } + } +} + export class OutputService extends Disposable implements IOutputService, ITextModelContentProvider { declare readonly _serviceBrand: undefined; @@ -81,6 +179,8 @@ export class OutputService extends Disposable implements IOutputService, ITextMo private readonly activeOutputChannelLevelContext: IContextKey; private readonly activeOutputChannelLevelIsDefaultContext: IContextKey; + readonly filters: OutputViewFilters; + constructor( @IStorageService private readonly storageService: IStorageService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -135,6 +235,15 @@ export class OutputService extends Disposable implements IOutputService, ITextMo })); this._register(this.lifecycleService.onDidShutdown(() => this.dispose())); + + this.filters = this._register(new OutputViewFilters({ + filterHistory: [], + trace: true, + debug: true, + info: true, + warning: true, + error: true + }, contextKeyService)); } provideTextContent(resource: URI): Promise | null { diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 4d280d00857c..c32d92d14b6d 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -7,20 +7,20 @@ import * as nls from '../../../../nls.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { IEditorOptions as ICodeEditorOptions } from '../../../../editor/common/config/editorOptions.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IStorageService } from '../../../../platform/storage/common/storage.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IContextKeyService, IContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IEditorOpenContext } from '../../../common/editor.js'; import { AbstractTextResourceEditor } from '../../../browser/parts/editor/textResourceEditor.js'; -import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK } from '../../../services/output/common/output.js'; +import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT } from '../../../services/output/common/output.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { CursorChangeReason } from '../../../../editor/common/cursorEvents.js'; -import { ViewPane, IViewPaneOptions } from '../../../browser/parts/views/viewPane.js'; +import { IViewPaneOptions, FilterViewPane } from '../../../browser/parts/views/viewPane.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IViewDescriptorService } from '../../../common/views.js'; @@ -35,8 +35,19 @@ import { ServiceCollection } from '../../../../platform/instantiation/common/ser import { IEditorConfiguration } from '../../../browser/parts/editor/textEditor.js'; import { computeEditorAriaLabel } from '../../../browser/editor.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; - -export class OutputViewPane extends ViewPane { +import { localize } from '../../../../nls.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { LogLevel } from '../../../../platform/log/common/log.js'; +import { IEditorContributionDescription, EditorExtensionsRegistry, EditorContributionInstantiation, EditorContributionCtor } from '../../../../editor/browser/editorExtensions.js'; +import { ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { IEditorContribution, IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; +import { IModelDeltaDecoration, ITextModel } from '../../../../editor/common/model.js'; +import { Range } from '../../../../editor/common/core/range.js'; +import { FindDecorations } from '../../../../editor/contrib/find/browser/findDecorations.js'; +import { Memento, MementoObject } from '../../../common/memento.js'; +import { Markers } from '../../markers/common/markers.js'; + +export class OutputViewPane extends FilterViewPane { private readonly editor: OutputEditor; private channelId: string | undefined; @@ -46,6 +57,9 @@ export class OutputViewPane extends ViewPane { get scrollLock(): boolean { return !!this.scrollLockContextKey.get(); } set scrollLock(scrollLock: boolean) { this.scrollLockContextKey.set(scrollLock); } + private readonly memento: Memento; + private readonly panelState: MementoObject; + constructor( options: IViewPaneOptions, @IKeybindingService keybindingService: IKeybindingService, @@ -58,8 +72,31 @@ export class OutputViewPane extends ViewPane { @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, @IHoverService hoverService: IHoverService, + @IOutputService private readonly outputService: IOutputService, + @IStorageService storageService: IStorageService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); + const memento = new Memento(Markers.MARKERS_VIEW_STORAGE_ID, storageService); + const viewState = memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); + super({ + ...options, + filterOptions: { + placeholder: localize('outputView.filter.placeholder', "Filter"), + focusContextKey: OUTPUT_FILTER_FOCUS_CONTEXT.key, + text: viewState['filter'] || '', + history: [] + } + }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); + this.memento = memento; + this.panelState = viewState; + + const filters = outputService.filters; + filters.text = this.panelState['filter'] || ''; + filters.trace = this.panelState['showTrace'] ?? true; + filters.debug = this.panelState['showDebug'] ?? true; + filters.info = this.panelState['showInfo'] ?? true; + filters.warning = this.panelState['showWarning'] ?? true; + filters.error = this.panelState['showError'] ?? true; + this.scrollLockContextKey = CONTEXT_OUTPUT_SCROLL_LOCK.bindTo(this.contextKeyService); const editorInstantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); @@ -69,6 +106,10 @@ export class OutputViewPane extends ViewPane { this.updateActions(); })); this._register(this.onDidChangeBodyVisibility(() => this.onDidChangeVisibility(this.isBodyVisible()))); + this._register(this.filterWidget.onDidChangeFilterText(text => outputService.filters.text = text)); + + this.checkMoreFilters(); + this._register(outputService.filters.onDidChange(() => this.checkMoreFilters())); } showChannel(channel: IOutputChannel, preserveFocus: boolean): void { @@ -85,6 +126,10 @@ export class OutputViewPane extends ViewPane { this.editorPromise?.then(() => this.editor.focus()); } + public clearFilterText(): void { + this.filterWidget.setFilterText(''); + } + protected override renderBody(container: HTMLElement): void { super.renderBody(container); this.editor.create(container); @@ -114,8 +159,7 @@ export class OutputViewPane extends ViewPane { })); } - protected override layoutBody(height: number, width: number): void { - super.layoutBody(height, width); + protected layoutBodyContent(height: number, width: number): void { this.editor.layout(new Dimension(width, height)); } @@ -138,6 +182,11 @@ export class OutputViewPane extends ViewPane { } + public checkMoreFilters(): void { + const filters = this.outputService.filters; + this.filterWidget.checkMoreFilters(!filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error); + } + private clearInput(): void { this.channelId = undefined; this.editor.clearInput(); @@ -148,9 +197,22 @@ export class OutputViewPane extends ViewPane { return this.instantiationService.createInstance(TextResourceEditorInput, channel.uri, nls.localize('output model title', "{0} - Output", channel.label), nls.localize('channel', "Output channel for '{0}'", channel.label), undefined, undefined); } + override saveState(): void { + const filters = this.outputService.filters; + this.panelState['filter'] = filters.text; + this.panelState['showTrace'] = filters.trace; + this.panelState['showDebug'] = filters.debug; + this.panelState['showInfo'] = filters.info; + this.panelState['showWarning'] = filters.warning; + this.panelState['showError'] = filters.error; + + this.memento.saveMemento(); + super.saveState(); + } + } -class OutputEditor extends AbstractTextResourceEditor { +export class OutputEditor extends AbstractTextResourceEditor { private readonly resourceContext: ResourceContextKey; constructor( @@ -260,4 +322,210 @@ class OutputEditor extends AbstractTextResourceEditor { CONTEXT_IN_OUTPUT.bindTo(scopedContextKeyService).set(true); } } + + private _getContributions(): IEditorContributionDescription[] { + return [ + ...EditorExtensionsRegistry.getEditorContributions(), + { + id: FilterController.ID, + ctor: FilterController as EditorContributionCtor, + instantiation: EditorContributionInstantiation.Eager + } + ]; + } + + protected override getCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { + return { contributions: this._getContributions() }; + } + +} + + +interface ILogEntry { + readonly logLevel: LogLevel; + readonly lineRange: [number, number]; +} + +const logEntryRegex = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \[(info|trace|debug|error|warn)\]/; + +export class FilterController extends Disposable implements IEditorContribution { + + public static readonly ID = 'output.editor.contrib.filterController'; + + private readonly modelDisposables: DisposableStore = this._register(new DisposableStore()); + private hiddenAreas: Range[] = []; + private readonly decorationsCollection: IEditorDecorationsCollection; + + private logEntries: ILogEntry[] | undefined; + + constructor( + private readonly editor: ICodeEditor, + @IOutputService private readonly outputService: IOutputService + ) { + super(); + this.decorationsCollection = editor.createDecorationsCollection(); + this._register(editor.onDidChangeModel(() => this.onDidChangeModel())); + this._register(this.outputService.filters.onDidChange(() => editor.hasModel() && this.filter(editor.getModel()))); + } + + private onDidChangeModel(): void { + this.modelDisposables.clear(); + this.logEntries = undefined; + this.hiddenAreas = []; + + if (!this.editor.hasModel()) { + return; + } + + const model = this.editor.getModel(); + this.computeLogEntries(model); + this.filter(model); + + const computeEndLineNumber = () => { + const endLineNumber = model.getLineCount(); + return endLineNumber > 1 && model.getLineMaxColumn(endLineNumber) === 1 ? endLineNumber - 1 : endLineNumber; + }; + + let endLineNumber = computeEndLineNumber(); + + this.modelDisposables.add(model.onDidChangeContent(e => { + if (e.changes.every(e => e.range.startLineNumber > endLineNumber)) { + const filterFrom = this.logEntries?.length ?? endLineNumber + 1; + if (this.logEntries) { + this.computeLogEntriesIncremental(model, endLineNumber + 1); + } + this.filterIncremental(model, filterFrom); + } else { + this.computeLogEntries(model); + this.filter(model); + } + endLineNumber = computeEndLineNumber(); + })); + } + + private computeLogEntries(model: ITextModel): void { + this.logEntries = undefined; + const firstLine = model.getLineContent(1); + if (!logEntryRegex.test(firstLine)) { + return; + } + + this.logEntries = []; + this.computeLogEntriesIncremental(model, 1); + } + + private computeLogEntriesIncremental(model: ITextModel, fromLine: number): void { + if (!this.logEntries) { + return; + } + + const lineCount = model.getLineCount(); + for (let lineNumber = fromLine; lineNumber <= lineCount; lineNumber++) { + const lineContent = model.getLineContent(lineNumber); + const match = logEntryRegex.exec(lineContent); + if (match) { + const logLevel = this.parseLogLevel(match[2]); + const startLine = lineNumber; + let endLine = lineNumber; + + while (endLine < lineCount) { + const nextLineContent = model.getLineContent(endLine + 1); + if (model.getLineFirstNonWhitespaceColumn(endLine + 1) === 0 || logEntryRegex.test(nextLineContent)) { + break; + } + endLine++; + } + + this.logEntries.push({ logLevel, lineRange: [startLine, endLine] }); + lineNumber = endLine; + } + } + } + + private filter(model: ITextModel): void { + this.hiddenAreas = []; + this.decorationsCollection.clear(); + this.filterIncremental(model, 0); + } + + private filterIncremental(model: ITextModel, from: number): void { + const filters = this.outputService.filters; + const findMatchesDecorations: IModelDeltaDecoration[] = []; + + if (this.logEntries) { + const hasLogLevelFilter = !filters.trace || !filters.debug || !filters.info || !filters.warning || filters.error; + if (hasLogLevelFilter || filters.text) { + for (let i = from; i < this.logEntries.length; i++) { + const entry = this.logEntries[i]; + if (hasLogLevelFilter && !this.shouldShowEntry(entry, filters)) { + this.hiddenAreas.push(new Range(entry.lineRange[0], 1, entry.lineRange[1], model.getLineMaxColumn(entry.lineRange[1]))); + continue; + } + if (filters.text) { + const matches = model.findMatches(filters.text, new Range(entry.lineRange[0], 1, entry.lineRange[1], model.getLineLastNonWhitespaceColumn(entry.lineRange[1])), false, false, null, false); + if (matches.length) { + for (const match of matches) { + findMatchesDecorations.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION }); + } + } else { + this.hiddenAreas.push(new Range(entry.lineRange[0], 1, entry.lineRange[1], model.getLineMaxColumn(entry.lineRange[1]))); + } + } + } + } + } else { + if (filters.text) { + const lineCount = model.getLineCount(); + for (let lineNumber = from + 1; lineNumber <= lineCount; lineNumber++) { + const lineRange = new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber)); + const matches = model.findMatches(filters.text, lineRange, false, false, null, false); + if (matches.length) { + for (const match of matches) { + findMatchesDecorations.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION }); + } + } else { + this.hiddenAreas.push(lineRange); + } + } + } + } + + this.editor.setHiddenAreas(this.hiddenAreas, this); + if (findMatchesDecorations.length) { + this.decorationsCollection.append(findMatchesDecorations); + } + } + + private shouldShowEntry(entry: ILogEntry, filters: IOutputViewFilters): boolean { + switch (entry.logLevel) { + case LogLevel.Trace: + return filters.trace; + case LogLevel.Debug: + return filters.debug; + case LogLevel.Info: + return filters.info; + case LogLevel.Warning: + return filters.warning; + case LogLevel.Error: + return filters.error; + } + return true; + } + + private parseLogLevel(level: string): LogLevel { + switch (level.toLowerCase()) { + case 'trace': + return LogLevel.Trace; + case 'debug': + return LogLevel.Debug; + case 'info': + return LogLevel.Info; + case 'warn': + return LogLevel.Warning; + case 'error': + return LogLevel.Error; + default: + throw new Error(`Unknown log level: ${level}`); + } + } } diff --git a/src/vs/workbench/contrib/output/common/outputChannelModelService.ts b/src/vs/workbench/contrib/output/common/outputChannelModelService.ts index 3f500babda69..f075abc08e6c 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModelService.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModelService.ts @@ -22,7 +22,7 @@ export interface IOutputChannelModelService { } -export class OutputChannelModelService { +export class OutputChannelModelService implements IOutputChannelModelService { declare readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index c43058f79703..4cacd7947132 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -35,16 +35,28 @@ export const LOG_MODE_ID = 'log'; export const OUTPUT_VIEW_ID = 'workbench.panel.output'; export const CONTEXT_IN_OUTPUT = new RawContextKey('inOutput', false); - export const CONTEXT_ACTIVE_FILE_OUTPUT = new RawContextKey('activeLogOutput', false); - export const CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE = new RawContextKey('activeLogOutput.levelSettable', false); - export const CONTEXT_ACTIVE_OUTPUT_LEVEL = new RawContextKey('activeLogOutput.level', ''); - export const CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT = new RawContextKey('activeLogOutput.levelIsDefault', false); - export const CONTEXT_OUTPUT_SCROLL_LOCK = new RawContextKey(`outputView.scrollLock`, false); +export const ACTIVE_OUTPUT_CHANNEL_CONTEXT = new RawContextKey('activeOutputChannel', ''); +export const SHOW_TRACE_FILTER_CONTEXT = new RawContextKey('output.filter.trace', true); +export const SHOW_DEBUG_FILTER_CONTEXT = new RawContextKey('output.filter.debug', true); +export const SHOW_INFO_FILTER_CONTEXT = new RawContextKey('output.filter.info', true); +export const SHOW_WARNING_FILTER_CONTEXT = new RawContextKey('output.filter.warning', true); +export const SHOW_ERROR_FILTER_CONTEXT = new RawContextKey('output.filter.error', true); +export const OUTPUT_FILTER_FOCUS_CONTEXT = new RawContextKey('outputFilterFocus', false); + +export interface IOutputViewFilters { + readonly onDidChange: Event; + text: string; + trace: boolean; + debug: boolean; + info: boolean; + warning: boolean; + error: boolean; +} export const IOutputService = createDecorator('outputService'); @@ -54,6 +66,11 @@ export const IOutputService = createDecorator('outputService'); export interface IOutputService { readonly _serviceBrand: undefined; + /** + * Output view filters. + */ + readonly filters: IOutputViewFilters; + /** * Given the channel id returns the output channel instance. * Channel should be first registered via OutputChannelRegistry. @@ -213,5 +230,3 @@ class OutputChannelRegistry implements IOutputChannelRegistry { } Registry.add(Extensions.OutputChannels, new OutputChannelRegistry()); - -export const ACTIVE_OUTPUT_CHANNEL_CONTEXT = new RawContextKey('activeOutputChannel', ''); From da4fcc54ac894e44af8eda1507e1a065575802cf Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Thu, 9 Jan 2025 14:50:44 +0100 Subject: [PATCH 0446/3587] @vscode/proxy-agent 0.29.0 --- .nvmrc | 2 +- build/npm/preinstall.js | 4 ++-- package-lock.json | 18 +++++++++--------- package.json | 2 +- remote/package-lock.json | 18 +++++++++--------- remote/package.json | 2 +- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.nvmrc b/.nvmrc index 2a393af592b8..d4b7699d36ca 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.18.0 +20.18.1 diff --git a/build/npm/preinstall.js b/build/npm/preinstall.js index 6619382d2477..31821ee2393d 100644 --- a/build/npm/preinstall.js +++ b/build/npm/preinstall.js @@ -9,8 +9,8 @@ const minorNodeVersion = parseInt(nodeVersion[2]); const patchNodeVersion = parseInt(nodeVersion[3]); if (!process.env['VSCODE_SKIP_NODE_VERSION_CHECK']) { - if (majorNodeVersion < 20 || (majorNodeVersion === 20 && minorNodeVersion < 18)) { - console.error('\x1b[1;31m*** Please use Node.js v20.18.0 or later for development.\x1b[0;0m'); + if (majorNodeVersion < 20 || (majorNodeVersion === 20 && minorNodeVersion < 18) || (majorNodeVersion === 20 && minorNodeVersion === 18 && patchNodeVersion < 1)) { + console.error('\x1b[1;31m*** Please use Node.js v20.18.1 or later for development.\x1b[0;0m'); throw new Error(); } } diff --git a/package-lock.json b/package-lock.json index 9a706f0ae8aa..e16e5fa2309e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.8", - "@vscode/proxy-agent": "^0.28.0", + "@vscode/proxy-agent": "^0.29.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", @@ -2848,9 +2848,9 @@ } }, "node_modules/@vscode/proxy-agent": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.28.0.tgz", - "integrity": "sha512-7rYF8ju0dP/ASpjjnuOCvzRosGLoKz0WOyNohREUskRdrvMEnYuEUXy84lHlH+4+MD8CZZjw2SUzhjHaJK1hxg==", + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.29.0.tgz", + "integrity": "sha512-zwpDvm5rwtJjXZv4TC8IXFRDDOU+fUNRe3asmls92Tz0dM0AJ8/WVfNgki5YOKxQMjVzWHAt0w53ZJxXj567EQ==", "license": "MIT", "dependencies": { "@tootallnate/once": "^3.0.0", @@ -2859,7 +2859,7 @@ "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "socks-proxy-agent": "^8.0.1", - "undici": "^6.20.1" + "undici": "^7.2.0" }, "optionalDependencies": { "@vscode/windows-ca-certs": "^0.3.1" @@ -17782,12 +17782,12 @@ "dev": true }, "node_modules/undici": { - "version": "6.20.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.20.1.tgz", - "integrity": "sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.2.0.tgz", + "integrity": "sha512-klt+0S55GBViA9nsq48/NSCo4YX5mjydjypxD7UmHh/brMu8h/Mhd/F7qAeoH2NOO8SDTk6kjnTFc4WpzmfYpQ==", "license": "MIT", "engines": { - "node": ">=18.17" + "node": ">=20.18.1" } }, "node_modules/undici-types": { diff --git a/package.json b/package.json index 58b368af92e6..19ce015d288c 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.8", - "@vscode/proxy-agent": "^0.28.0", + "@vscode/proxy-agent": "^0.29.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", diff --git a/remote/package-lock.json b/remote/package-lock.json index 26c6690ae30d..c8100866d15a 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -13,7 +13,7 @@ "@parcel/watcher": "2.5.0", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.28.0", + "@vscode/proxy-agent": "^0.29.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/tree-sitter-wasm": "^0.0.5", @@ -418,9 +418,9 @@ "integrity": "sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg==" }, "node_modules/@vscode/proxy-agent": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.28.0.tgz", - "integrity": "sha512-7rYF8ju0dP/ASpjjnuOCvzRosGLoKz0WOyNohREUskRdrvMEnYuEUXy84lHlH+4+MD8CZZjw2SUzhjHaJK1hxg==", + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.29.0.tgz", + "integrity": "sha512-zwpDvm5rwtJjXZv4TC8IXFRDDOU+fUNRe3asmls92Tz0dM0AJ8/WVfNgki5YOKxQMjVzWHAt0w53ZJxXj567EQ==", "license": "MIT", "dependencies": { "@tootallnate/once": "^3.0.0", @@ -429,7 +429,7 @@ "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "socks-proxy-agent": "^8.0.1", - "undici": "^6.20.1" + "undici": "^7.2.0" }, "optionalDependencies": { "@vscode/windows-ca-certs": "^0.3.1" @@ -1398,12 +1398,12 @@ } }, "node_modules/undici": { - "version": "6.20.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.20.1.tgz", - "integrity": "sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.2.0.tgz", + "integrity": "sha512-klt+0S55GBViA9nsq48/NSCo4YX5mjydjypxD7UmHh/brMu8h/Mhd/F7qAeoH2NOO8SDTk6kjnTFc4WpzmfYpQ==", "license": "MIT", "engines": { - "node": ">=18.17" + "node": ">=20.18.1" } }, "node_modules/universalify": { diff --git a/remote/package.json b/remote/package.json index 44281195658d..be199fcd0ae1 100644 --- a/remote/package.json +++ b/remote/package.json @@ -8,7 +8,7 @@ "@parcel/watcher": "2.5.0", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.28.0", + "@vscode/proxy-agent": "^0.29.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/tree-sitter-wasm": "^0.0.5", From e8184ed571509b6bc1f7752ff2216116f6194ff3 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 9 Jan 2025 17:58:03 +0100 Subject: [PATCH 0447/3587] real fix for #236429 (#237479) --- .../common/configurationModels.ts | 14 +++++--- .../test/browser/configurationService.test.ts | 35 ++++++++++++++++++- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index 400ae1033cb9..468614153ed5 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -727,7 +727,7 @@ export class Configuration { } getValue(section: string | undefined, overrides: IConfigurationOverrides, workspace: Workspace | undefined): any { - const consolidateConfigurationModel = this.getConsolidatedConfigurationModel(section, overrides, workspace); + const consolidateConfigurationModel = this.getConsolidatedConfigurationModel(overrides, workspace); return consolidateConfigurationModel.getValue(section); } @@ -755,7 +755,7 @@ export class Configuration { } inspect(key: string, overrides: IConfigurationOverrides, workspace: Workspace | undefined): IConfigurationValue { - const consolidateConfigurationModel = this.getConsolidatedConfigurationModel(key, overrides, workspace); + const consolidateConfigurationModel = this.getConsolidatedConfigurationModel(overrides, workspace); const folderConfigurationModel = this.getFolderConfigurationModelForResource(overrides.resource, workspace); const memoryConfigurationModel = overrides.resource ? this._memoryConfigurationByResource.get(overrides.resource) || this._memoryConfiguration : this._memoryConfiguration; const overrideIdentifiers = new Set(); @@ -965,13 +965,17 @@ export class Configuration { return this._folderConfigurations; } - private getConsolidatedConfigurationModel(section: string | undefined, overrides: IConfigurationOverrides, workspace: Workspace | undefined): ConfigurationModel { + private getConsolidatedConfigurationModel(overrides: IConfigurationOverrides, workspace: Workspace | undefined): ConfigurationModel { let configurationModel = this.getConsolidatedConfigurationModelForResource(overrides, workspace); if (overrides.overrideIdentifier) { configurationModel = configurationModel.override(overrides.overrideIdentifier); } - if (!this._policyConfiguration.isEmpty() && this._policyConfiguration.getValue(section) !== undefined) { - configurationModel = configurationModel.merge(this._policyConfiguration); + if (!this._policyConfiguration.isEmpty()) { + // clone by merging + configurationModel = configurationModel.merge(); + for (const key of this._policyConfiguration.keys) { + configurationModel.setValue(key, this._policyConfiguration.getValue(key)); + } } return configurationModel; } diff --git a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts index 833dec287e06..eacc1bacd4a4 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -779,6 +779,14 @@ suite('WorkspaceConfigurationService - Folder', () => { minimumVersion: '1.0.0', } }, + 'configurationService.folder.policyObjectSetting': { + 'type': 'object', + 'default': {}, + policy: { + name: 'configurationService.folder.policyObjectSetting', + minimumVersion: '1.0.0', + } + }, } }); @@ -827,7 +835,19 @@ suite('WorkspaceConfigurationService - Folder', () => { }); test('defaults', () => { - assert.deepStrictEqual(testObject.getValue('configurationService'), { 'folder': { 'applicationSetting': 'isSet', 'machineSetting': 'isSet', 'machineOverridableSetting': 'isSet', 'testSetting': 'isSet', 'languageSetting': 'isSet', 'restrictedSetting': 'isSet', 'policySetting': 'isSet' } }); + assert.deepStrictEqual(testObject.getValue('configurationService'), + { + 'folder': { + 'applicationSetting': 'isSet', + 'machineSetting': 'isSet', + 'machineOverridableSetting': 'isSet', + 'testSetting': 'isSet', + 'languageSetting': 'isSet', + 'restrictedSetting': 'isSet', + 'policySetting': 'isSet', + 'policyObjectSetting': {} + } + }); }); test('globals override defaults', () => runWithFakedTimers({ useFakeTimers: true }, async () => { @@ -1052,6 +1072,19 @@ suite('WorkspaceConfigurationService - Folder', () => { assert.strictEqual(testObject.inspect('configurationService.folder.policySetting').policyValue, undefined); })); + test('policy value override all for object type setting', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await runWithFakedTimers({ useFakeTimers: true }, async () => { + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await fileService.writeFile(environmentService.policyFile!, VSBuffer.fromString('{ "configurationService.folder.policyObjectSetting": {"a": true} }')); + return promise; + }); + + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.policyObjectSetting": {"b": true} }')); + await testObject.reloadConfiguration(); + + assert.deepStrictEqual(testObject.getValue('configurationService.folder.policyObjectSetting'), { a: true }); + })); + test('reload configuration emits events after global configuraiton changes', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "testworkbench.editor.tabs": true }')); const target = sinon.spy(); From 4a8978f7de8ddf071a0dad7da4ff51abda765977 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 9 Jan 2025 15:31:20 -0600 Subject: [PATCH 0448/3587] clean up terminal suggest code, fix bugs (#237599) --- .../src/terminalSuggestMain.ts | 173 ++++++++++++------ .../src/test/terminalSuggestMain.test.ts | 6 +- .../browser/terminalCompletionService.ts | 2 +- .../browser/terminalCompletionService.test.ts | 4 +- .../suggest/browser/simpleSuggestWidget.ts | 2 +- 5 files changed, 119 insertions(+), 68 deletions(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 13ff032037ac..72cc348e2eba 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -164,7 +164,7 @@ function createCompletionItem(cursorPosition: number, prefix: string, label: str label, detail: description ?? '', replacementIndex: cursorPosition - lastWord.length, - replacementLength: lastWord.length > 0 ? lastWord.length : cursorPosition, + replacementLength: lastWord.length, kind: kind ?? vscode.TerminalCompletionItemKind.Method }; } @@ -246,21 +246,33 @@ export function asArray(x: T | T[]): T[] { return Array.isArray(x) ? x : [x]; } -export async function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalContext: { commandLine: string; cursorPosition: number }, availableCommands: string[], prefix: string, shellIntegrationCwd?: vscode.Uri, token?: vscode.CancellationToken): Promise<{ items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; cwd?: vscode.Uri }> { +export async function getCompletionItemsFromSpecs( + specs: Fig.Spec[], + terminalContext: { commandLine: string; cursorPosition: number }, + availableCommands: string[], + prefix: string, + shellIntegrationCwd?: vscode.Uri, + token?: vscode.CancellationToken +): Promise<{ items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; cwd?: vscode.Uri }> { const items: vscode.TerminalCompletionItem[] = []; let filesRequested = false; let foldersRequested = false; + const firstCommand = getFirstCommand(terminalContext.commandLine); + const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1); + for (const spec of specs) { const specLabels = getLabel(spec); + if (!specLabels) { continue; } + for (const specLabel of specLabels) { - if (!availableCommands.includes(specLabel) || (token && token?.isCancellationRequested)) { + if (!availableCommands.includes(specLabel) || (token && token.isCancellationRequested)) { continue; } - // + if ( // If the prompt is empty !terminalContext.commandLine @@ -270,76 +282,38 @@ export async function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalCon // push it to the completion items items.push(createCompletionItem(terminalContext.cursorPosition, prefix, specLabel)); } + if (!terminalContext.commandLine.startsWith(specLabel)) { // the spec label is not the first word in the command line, so do not provide options or args continue; } - const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1); - if ('options' in spec && spec.options) { - for (const option of spec.options) { - const optionLabels = getLabel(option); - if (!optionLabels) { - continue; - } - for (const optionLabel of optionLabels) { - if (!items.find(i => i.label === optionLabel) && optionLabel.startsWith(prefix) || (prefix.length > specLabel.length && prefix.trim() === specLabel)) { - items.push(createCompletionItem(terminalContext.cursorPosition, prefix, optionLabel, option.description, vscode.TerminalCompletionItemKind.Flag)); - } - const expectedText = `${specLabel} ${optionLabel} `; - if (!precedingText.includes(expectedText)) { - continue; - } - const indexOfPrecedingText = terminalContext.commandLine.lastIndexOf(expectedText); - const currentPrefix = precedingText.slice(indexOfPrecedingText + expectedText.length); - const argsCompletions = getCompletionItemsFromArgs(option.args, currentPrefix, terminalContext); - if (!argsCompletions) { - continue; - } - const argCompletions = argsCompletions.items; - foldersRequested = foldersRequested || argsCompletions.foldersRequested; - filesRequested = filesRequested || argsCompletions.filesRequested; - let cwd: vscode.Uri | undefined; - if (shellIntegrationCwd && (filesRequested || foldersRequested)) { - cwd = await resolveCwdFromPrefix(prefix, shellIntegrationCwd) ?? shellIntegrationCwd; - } - return { items: argCompletions, filesRequested, foldersRequested, cwd }; - } - } + + const argsCompletionResult = handleArguments(specLabel, spec, terminalContext, precedingText); + if (argsCompletionResult) { + items.push(...argsCompletionResult.items); + filesRequested ||= argsCompletionResult.filesRequested; + foldersRequested ||= argsCompletionResult.foldersRequested; } - if ('args' in spec && asArray(spec.args)) { - const expectedText = `${specLabel} `; - if (!precedingText.includes(expectedText)) { - continue; - } - const indexOfPrecedingText = terminalContext.commandLine.lastIndexOf(expectedText); - const currentPrefix = precedingText.slice(indexOfPrecedingText + expectedText.length); - const argsCompletions = getCompletionItemsFromArgs(spec.args, currentPrefix, terminalContext); - if (!argsCompletions) { - continue; - } - items.push(...argsCompletions.items); - filesRequested = filesRequested || argsCompletions.filesRequested; - foldersRequested = foldersRequested || argsCompletions.foldersRequested; + + const optionsCompletionResult = handleOptions(specLabel, spec, terminalContext, precedingText, prefix); + if (optionsCompletionResult) { + items.push(...optionsCompletionResult.items); + filesRequested ||= optionsCompletionResult.filesRequested; + foldersRequested ||= optionsCompletionResult.foldersRequested; } } } const shouldShowResourceCompletions = - ( - // If the command line is empty - terminalContext.commandLine.trim().length === 0 - // or no completions are found and the prefix is empty - || !items?.length - // or all of the items are '.' or '..' IE file paths - || items.length && items.every(i => ['.', '..'].includes(i.label)) - ) - // and neither files nor folders are going to be requested (for a specific spec's argument) - && (!filesRequested && !foldersRequested); + (!terminalContext.commandLine.trim() || !items.length) && + !filesRequested && + !foldersRequested; const shouldShowCommands = !terminalContext.commandLine.substring(0, terminalContext.cursorPosition).trimStart().includes(' '); - if (shouldShowCommands && (filesRequested === foldersRequested)) { + + if (shouldShowCommands && !filesRequested && !foldersRequested) { // Include builitin/available commands in the results - const labels = new Set(items.map(i => i.label)); + const labels = new Set(items.map((i) => i.label)); for (const command of availableCommands) { if (!labels.has(command)) { items.push(createCompletionItem(terminalContext.cursorPosition, prefix, command)); @@ -351,13 +325,90 @@ export async function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalCon filesRequested = true; foldersRequested = true; } + let cwd: vscode.Uri | undefined; if (shellIntegrationCwd && (filesRequested || foldersRequested)) { cwd = await resolveCwdFromPrefix(prefix, shellIntegrationCwd) ?? shellIntegrationCwd; } + return { items, filesRequested, foldersRequested, cwd }; } +function handleArguments(specLabel: string, spec: Fig.Spec, terminalContext: { commandLine: string; cursorPosition: number }, precedingText: string): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean } | undefined { + let args; + if ('args' in spec && spec.args && asArray(spec.args)) { + args = asArray(spec.args); + } + const expectedText = `${specLabel} `; + + if (!precedingText.includes(expectedText)) { + return; + } + + const currentPrefix = precedingText.slice(precedingText.lastIndexOf(expectedText) + expectedText.length); + const argsCompletions = getCompletionItemsFromArgs(args, currentPrefix, terminalContext); + + if (!argsCompletions) { + return; + } + + return argsCompletions; +} + +function handleOptions(specLabel: string, spec: Fig.Spec, terminalContext: { commandLine: string; cursorPosition: number }, precedingText: string, prefix: string): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean } | undefined { + let options; + if ('options' in spec && spec.options) { + options = spec.options; + } + if (!options) { + return; + } + + const optionItems: vscode.TerminalCompletionItem[] = []; + + for (const option of options) { + const optionLabels = getLabel(option); + + if (!optionLabels) { + continue; + } + + for (const optionLabel of optionLabels) { + if ( + // Already includes this option + optionItems.find((i) => i.label === optionLabel) + ) { + continue; + } + + optionItems.push( + createCompletionItem( + terminalContext.cursorPosition, + prefix, + optionLabel, + option.description, + vscode.TerminalCompletionItemKind.Flag + ) + ); + + const expectedText = `${specLabel} ${optionLabel} `; + if (!precedingText.includes(expectedText)) { + continue; + } + + const currentPrefix = precedingText.slice(precedingText.lastIndexOf(expectedText) + expectedText.length); + const argsCompletions = getCompletionItemsFromArgs(option.args, currentPrefix, terminalContext); + + if (argsCompletions) { + return { items: argsCompletions.items, filesRequested: argsCompletions.filesRequested, foldersRequested: argsCompletions.foldersRequested }; + } + } + } + + return { items: optionItems, filesRequested: false, foldersRequested: false }; +} + + function getCompletionItemsFromArgs(args: Fig.SingleOrArray | undefined, currentPrefix: string, terminalContext: { commandLine: string; cursorPosition: number }): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean } | undefined { if (!args) { return; diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index 70694606d99c..6e4520e2df14 100644 --- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -62,9 +62,9 @@ function createCodeTestSpecs(executable: string): ITestSpec2[] { { input: `${executable} --merge ./file1 ./file2 ./base |`, expectedResourceRequests: { type: 'files', cwd: testCwd } }, { input: `${executable} --goto |`, expectedResourceRequests: { type: 'files', cwd: testCwd } }, { input: `${executable} --user-data-dir |`, expectedResourceRequests: { type: 'folders', cwd: testCwd } }, - { input: `${executable} --profile |` }, - { input: `${executable} --install-extension |` }, - { input: `${executable} --uninstall-extension |` }, + { input: `${executable} --profile |`, expectedResourceRequests: { type: 'both', cwd: testCwd } }, + { input: `${executable} --install-extension |`, expectedResourceRequests: { type: 'both', cwd: testCwd } }, + { input: `${executable} --uninstall-extension |`, expectedResourceRequests: { type: 'both', cwd: testCwd } }, { input: `${executable} --log |`, expectedCompletions: logOptions }, { input: `${executable} --sync |`, expectedCompletions: syncOptions }, { input: `${executable} --extensions-dir |`, expectedResourceRequests: { type: 'folders', cwd: testCwd } }, diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index ebbeae34b598..aa81849cb8dc 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -250,7 +250,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo isDirectory, isFile: kind === TerminalCompletionItemKind.File, replacementIndex: cursorPosition - lastWord.length, - replacementLength: lastWord.length > 0 ? lastWord.length : cursorPosition + replacementLength: lastWord.length }); } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index 1ce22b4138c9..ebfa97fe98b3 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -84,7 +84,7 @@ suite('TerminalCompletionService', () => { isDirectory: true, isFile: false, replacementIndex: 1, - replacementLength: 1 + replacementLength: 0 }]); }); test('.|', async () => { @@ -132,7 +132,7 @@ suite('TerminalCompletionService', () => { isDirectory: true, isFile: false, replacementIndex: 3, - replacementLength: 3 + replacementLength: 0 }]); }); test('cd .|', async () => { diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index af70d32b896d..5e48a435c41b 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -517,7 +517,7 @@ export class SimpleSuggestWidget extends Disposable { const preferredWidth = this._completionModel ? this._completionModel.stats.pLabelLen * info.typicalHalfwidthCharacterWidth : width; // height math - const fullHeight = info.statusBarHeight + this._list.contentHeight + info.borderHeight; + const fullHeight = info.statusBarHeight + this._list.contentHeight + this._messageElement.clientHeight + info.borderHeight; const minHeight = info.itemHeight + info.statusBarHeight; // const editorBox = dom.getDomNodePagePosition(this.editor.getDomNode()); // const cursorBox = this.editor.getScrolledVisiblePosition(this.editor.getPosition()); From f74e6beed9c3e5185853ddf65bba2b8d10d916b0 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:39:37 -0800 Subject: [PATCH 0449/3587] Expose shell's environment - pwsh (#237415) * start terminal shell env proposed * fix typo * progress on shellEnvDetectionCapability, mainThreadTerminalSI * update IShellEnvDetectionCapability interface * touch up on $shellEnvChange * adjust IShellEnvDetectionCapability * properly listen to envChangeEvent Co-authored-by: Daniel Imms * Serialize env map, expose on exthost * start adding to zsh script * receive environment variable in extension host, properly escape " Co-authored-by: Daniel Imms * clean up * Add TODO: properly escape double quotes, figure out why JSON parse fails for bash Co-authored-by: Daniel Imms * Fix nonce check, ignore PS1 for now in bash * Add some simple PS1 string tests to deserializeMessage * New approach of sending env entries separately * be able to get EnvSingleVar * few comments * add function signature for start, set, end environment var * implement EnvStart, EnvEntry, EnvEnd for single env entry * deserialize env value for EnvEntry * Remove unncessary comments * only leave pwsh in this PR and exclude other shells * keep exlcuding other shell env - only pwsh should remain * Update src/vs/workbench/api/common/extHostTerminalShellIntegration.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/api/common/extHostTerminalShellIntegration.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/api/common/extHost.protocol.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/capabilities/capabilities.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * add comment for ShellEnvDetection * change envs in shellEnvDetectionCapability to env * Mention escaping character for EnvJSON similar to commandLine * Do not fire env event if env has not changed * add link to CommandLine * follow main branch format so I avoid merge conflict * remove resolved TODO * Update src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * use vscode object equals --------- Co-authored-by: Daniel Imms Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> --- .../common/extensionsApiProposals.ts | 3 ++ .../common/capabilities/capabilities.ts | 16 +++++++- .../shellEnvDetectionCapability.ts | 39 +++++++++++++++++++ .../common/xterm/shellIntegrationAddon.ts | 39 ++++++++++++++++++- .../mainThreadTerminalShellIntegration.ts | 8 ++++ .../workbench/api/common/extHost.protocol.ts | 1 + .../common/extHostTerminalShellIntegration.ts | 23 ++++++++++- .../common/scripts/shellIntegration.ps1 | 10 +++++ .../xterm/shellIntegrationAddon.test.ts | 2 + .../vscode.proposed.terminalShellEnv.d.ts | 18 +++++++++ 10 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts create mode 100644 src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index cced7c27af0d..ad633927aeaf 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -349,6 +349,9 @@ const _allApiProposals = { terminalSelection: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalSelection.d.ts', }, + terminalShellEnv: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts', + }, testObserver: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', }, diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index 6de15be87fa7..50f39332a431 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -70,7 +70,13 @@ export const enum TerminalCapability { * the request (task, debug, etc) provides an ID, optional marker, hoverMessage, and hidden property. When * hidden is not provided, a generic decoration is added to the buffer and overview ruler. */ - BufferMarkDetection + BufferMarkDetection, + + /** + * The terminal can detect the latest environment of user's current shell. + */ + ShellEnvDetection, + } /** @@ -133,6 +139,7 @@ export interface ITerminalCapabilityImplMap { [TerminalCapability.NaiveCwdDetection]: INaiveCwdDetectionCapability; [TerminalCapability.PartialCommandDetection]: IPartialCommandDetectionCapability; [TerminalCapability.BufferMarkDetection]: IBufferMarkCapability; + [TerminalCapability.ShellEnvDetection]: IShellEnvDetectionCapability; } export interface ICwdDetectionCapability { @@ -143,6 +150,13 @@ export interface ICwdDetectionCapability { updateCwd(cwd: string): void; } +export interface IShellEnvDetectionCapability { + readonly type: TerminalCapability.ShellEnvDetection; + readonly onDidChangeEnv: Event>; + get env(): Map; + setEnvironment(envs: { [key: string]: string | undefined } | undefined, isTrusted: boolean): void; +} + export const enum CommandInvalidationReason { Windows = 'windows', NoProblemsReported = 'noProblemsReported' diff --git a/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts new file mode 100644 index 000000000000..95e1d827dc43 --- /dev/null +++ b/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IShellEnvDetectionCapability, TerminalCapability } from './capabilities.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { equals } from '../../../../base/common/objects.js'; + +export class ShellEnvDetectionCapability extends Disposable implements IShellEnvDetectionCapability { + readonly type = TerminalCapability.ShellEnvDetection; + + private readonly _env: Map = new Map(); + get env(): Map { return this._env; } + + private readonly _onDidChangeEnv = this._register(new Emitter>()); + readonly onDidChangeEnv = this._onDidChangeEnv.event; + + setEnvironment(env: { [key: string]: string | undefined }, isTrusted: boolean): void { + if (!isTrusted) { + return; + } + + if (equals(this._env, env)) { + return; + } + + this._env.clear(); + for (const [key, value] of Object.entries(env)) { + if (value !== undefined) { + this._env.set(key, value); + } + } + + // Convert to event and fire event + this._onDidChangeEnv.fire(this._env); + } +} diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index 8221ad5c282f..ee7dbff267e5 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -8,7 +8,7 @@ import { Disposable, dispose, IDisposable, toDisposable } from '../../../../base import { TerminalCapabilityStore } from '../capabilities/terminalCapabilityStore.js'; import { CommandDetectionCapability } from '../capabilities/commandDetectionCapability.js'; import { CwdDetectionCapability } from '../capabilities/cwdDetectionCapability.js'; -import { IBufferMarkCapability, ICommandDetectionCapability, ICwdDetectionCapability, ISerializedCommandDetectionCapability, TerminalCapability } from '../capabilities/capabilities.js'; +import { IBufferMarkCapability, ICommandDetectionCapability, ICwdDetectionCapability, ISerializedCommandDetectionCapability, IShellEnvDetectionCapability, TerminalCapability } from '../capabilities/capabilities.js'; import { PartialCommandDetectionCapability } from '../capabilities/partialCommandDetectionCapability.js'; import { ILogService } from '../../../log/common/log.js'; import { ITelemetryService } from '../../../telemetry/common/telemetry.js'; @@ -18,6 +18,7 @@ import type { ITerminalAddon, Terminal } from '@xterm/headless'; import { URI } from '../../../../base/common/uri.js'; import { sanitizeCwd } from '../terminalEnvironment.js'; import { removeAnsiEscapeCodesFromPrompt } from '../../../../base/common/strings.js'; +import { ShellEnvDetectionCapability } from '../capabilities/shellEnvDetectionCapability.js'; /** @@ -224,6 +225,20 @@ const enum VSCodeOscPt { * WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script. */ SetMark = 'SetMark', + + /** + * Sends the shell's complete environment in JSON format. + * + * Format: `OSC 633 ; EnvJson ; ; ` + * + * - `Environment` - A stringified JSON object containing the shell's complete environment. The + * variables and values use the same encoding rules as the {@link CommandLine} sequence. + * - `Nonce` - An _mandatory_ nonce to ensure the sequence is not malicious. + * + * WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script. + */ + EnvJson = 'EnvJson', + } /** @@ -418,6 +433,19 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati this._createOrGetCommandDetection(this._terminal).handleContinuationEnd(); return true; } + case VSCodeOscPt.EnvJson: { + const arg0 = args[0]; + const arg1 = args[1]; + if (arg0 !== undefined) { + try { + const env = JSON.parse(deserializeMessage(arg0)); + this._createOrGetShellEnvDetection().setEnvironment(env, arg1 === this._nonce); + } catch (e) { + this._logService.warn('Failed to parse environment from shell integration sequence', arg0); + } + } + return true; + } case VSCodeOscPt.RightPromptStart: { this._createOrGetCommandDetection(this._terminal).handleRightPromptStart(); return true; @@ -614,6 +642,15 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati } return bufferMarkDetection; } + + protected _createOrGetShellEnvDetection(): IShellEnvDetectionCapability { + let shellEnvDetection = this.capabilities.get(TerminalCapability.ShellEnvDetection); + if (!shellEnvDetection) { + shellEnvDetection = this._register(new ShellEnvDetectionCapability()); + this.capabilities.add(TerminalCapability.ShellEnvDetection, shellEnvDetection); + } + return shellEnvDetection; + } } export function deserializeMessage(message: string): string { diff --git a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts index e2ee50cc3a46..bfe720fc0872 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts @@ -60,6 +60,14 @@ export class MainThreadTerminalShellIntegration extends Disposable implements Ma this._proxy.$cwdChange(e.instance.instanceId, this._convertCwdToUri(e.data)); })); + // onDidChangeTerminalShellIntegration via env + const envChangeEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.ShellEnvDetection, e => e.onDidChangeEnv)); + this._store.add(envChangeEvent.event(e => { + const keysArr = Array.from(e.data.keys()); + const valuesArr = Array.from(e.data.values()); + this._proxy.$shellEnvChange(e.instance.instanceId, keysArr, valuesArr); + })); + // onDidStartTerminalShellExecution const commandDetectionStartEvent = this._store.add(this._terminalService.createOnInstanceCapabilityEvent(TerminalCapability.CommandDetection, e => e.onCommandExecuted)); let currentCommand: ITerminalCommand | undefined; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 9b7b0424e7a8..9155c298fc3c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2439,6 +2439,7 @@ export interface ExtHostTerminalShellIntegrationShape { $shellExecutionStart(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, cwd: UriComponents | undefined): void; $shellExecutionEnd(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, exitCode: number | undefined): void; $shellExecutionData(instanceId: number, data: string): void; + $shellEnvChange(instanceId: number, shellEnvKeys: string[], shellEnvValues: string[]): void; $cwdChange(instanceId: number, cwd: UriComponents | undefined): void; $closeTerminal(instanceId: number): void; } diff --git a/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts b/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts index 7a1c24029d6a..076f1b725cc3 100644 --- a/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts +++ b/src/vs/workbench/api/common/extHostTerminalShellIntegration.ts @@ -131,6 +131,10 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH this._activeShellIntegrations.get(instanceId)?.emitData(data); } + public $shellEnvChange(instanceId: number, shellEnvKeys: string[], shellEnvValues: string[]): void { + this._activeShellIntegrations.get(instanceId)?.setEnv(shellEnvKeys, shellEnvValues); + } + public $cwdChange(instanceId: number, cwd: UriComponents | undefined): void { this._activeShellIntegrations.get(instanceId)?.setCwd(URI.revive(cwd)); } @@ -147,6 +151,7 @@ class InternalTerminalShellIntegration extends Disposable { get currentExecution(): InternalTerminalShellExecution | undefined { return this._currentExecution; } private _ignoreNextExecution: boolean = false; + private _env: { [key: string]: string | undefined } | undefined; private _cwd: URI | undefined; readonly store: DisposableStore = this._register(new DisposableStore()); @@ -171,6 +176,9 @@ class InternalTerminalShellIntegration extends Disposable { get cwd(): URI | undefined { return that._cwd; }, + get env(): { [key: string]: string | undefined } | undefined { + return that._env; + }, // executeCommand(commandLine: string): vscode.TerminalShellExecution; // executeCommand(executable: string, args: string[]): vscode.TerminalShellExecution; executeCommand(commandLineOrExecutable: string, args?: string[]): vscode.TerminalShellExecution { @@ -232,6 +240,15 @@ class InternalTerminalShellIntegration extends Disposable { } } + setEnv(keys: string[], values: string[]): void { + const env: { [key: string]: string | undefined } = {}; + for (let i = 0; i < keys.length; i++) { + env[keys[i]] = values[i]; + } + this._env = env; + this._fireChangeEvent(); + } + setCwd(cwd: URI | undefined): void { let wasChanged = false; if (URI.isUri(this._cwd)) { @@ -241,9 +258,13 @@ class InternalTerminalShellIntegration extends Disposable { } if (wasChanged) { this._cwd = cwd; - this._onDidRequestChangeShellIntegration.fire({ terminal: this._terminal, shellIntegration: this.value }); + this._fireChangeEvent(); } } + + private _fireChangeEvent() { + this._onDidRequestChangeShellIntegration.fire({ terminal: this._terminal, shellIntegration: this.value }); + } } class InternalTerminalShellExecution { diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 index 117a22854055..4c5a7059a512 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 @@ -88,6 +88,16 @@ function Global:Prompt() { # Current working directory # OSC 633 ; = ST $Result += if ($pwd.Provider.Name -eq 'FileSystem') { "$([char]0x1b)]633;P;Cwd=$(__VSCode-Escape-Value $pwd.ProviderPath)`a" } + + # Send current environment variables as JSON + # OSC 633 ; Env ; ; + if ($isStable -eq "0") { + $envMap = @{} + Get-ChildItem Env: | ForEach-Object { $envMap[$_.Name] = $_.Value } + $envJson = $envMap | ConvertTo-Json -Compress + $Result += "$([char]0x1b)]633;EnvJson;$(__VSCode-Escape-Value $envJson);$Nonce`a" + } + # Before running the original prompt, put $? back to what it was: if ($FakeCode -ne 0) { Write-Error "failure" -ea ignore diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts index 74c18f3557bd..532950a3908c 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts @@ -287,6 +287,8 @@ suite('ShellIntegrationAddon', () => { ['escaped newline (upper hex)', `${Backslash}x0A`, Newline], ['escaped backslash followed by literal "x0a" is not a newline', `${Backslash}${Backslash}x0a`, `${Backslash}x0a`], ['non-initial escaped backslash followed by literal "x0a" is not a newline', `foo${Backslash}${Backslash}x0a`, `foo${Backslash}x0a`], + ['PS1 simple', '[\\u@\\h \\W]\\$', '[\\u@\\h \\W]\\$'], + ['PS1 VSC SI', `${Backslash}x1b]633;A${Backslash}x07\\[${Backslash}x1b]0;\\u@\\h:\\w\\a\\]${Backslash}x1b]633;B${Backslash}x07`, '\x1b]633;A\x07\\[\x1b]0;\\u@\\h:\\w\\a\\]\x1b]633;B\x07'] ]; cases.forEach(([title, input, expected]) => { diff --git a/src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts b/src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts new file mode 100644 index 000000000000..965d48e164b4 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + // @anthonykim1 @tyriar https://github.com/microsoft/vscode/issues/227467 + + export interface TerminalShellIntegration { + /** + * The environment of the shell process. This is undefined if the shell integration script + * does not send the environment. + */ + readonly env: { [key: string]: string | undefined } | undefined; + } + + // TODO: Is it fine that this shares onDidChangeTerminalShellIntegration with cwd and the shellIntegration object itself? +} From ddf38410466a54f9feca8d6624a12aa7d3600835 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 10 Jan 2025 00:19:31 +0100 Subject: [PATCH 0450/3587] Inline edits UI enhancements (#237611) inline edits UI changes --- .../view/inlineEdits/sideBySideDiff.ts | 111 ++++++++++-------- .../browser/view/inlineEdits/utils.ts | 2 + .../view/inlineEdits/wordReplacementView.ts | 2 +- 3 files changed, 62 insertions(+), 53 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index e1feaa2eb8b7..38bf37a2698f 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -147,9 +147,11 @@ export class InlineEditsSideBySideDiff extends Disposable { return; } - this.previewEditor.layout({ height: layoutInfo.editHeight, width: layoutInfo.previewEditorWidth }); - const topEdit = layoutInfo.edit1; + const bottomEdit = layoutInfo.edit2; + + this.previewEditor.layout({ height: bottomEdit.y - topEdit.y, width: layoutInfo.previewEditorWidth }); + this.previewEditor.updateOptions({ padding: { top: layoutInfo.padding, bottom: layoutInfo.padding } }); this._editorContainer.element.style.top = `${topEdit.y}px`; this._editorContainer.element.style.left = `${topEdit.x}px`; })); @@ -261,6 +263,7 @@ export class InlineEditsSideBySideDiff extends Disposable { overviewRulerLanes: 0, lineDecorationsWidth: 0, lineNumbersMinChars: 0, + revealHorizontalRightPadding: 0, bracketPairColorization: { enabled: true, independentColorPoolPerBracketType: false }, scrollBeyondLastLine: false, scrollbar: { @@ -440,8 +443,10 @@ export class InlineEditsSideBySideDiff extends Disposable { const previewEditorWidth = Math.min(previewContentWidth, remainingWidthRightOfEditor + editorLayout.width - editorLayout.contentLeft - codeEditDist); + const PADDING = 4; + const edit1 = new Point(left + codeEditDist, selectionTop); - const edit2 = new Point(left + codeEditDist, selectionTop + editHeight); + const edit2 = new Point(left + codeEditDist, selectionTop + editHeight + PADDING * 2); return { code1, @@ -456,7 +461,9 @@ export class InlineEditsSideBySideDiff extends Disposable { maxContentWidth, shouldShowShadow: clipped, desiredPreviewEditorScrollLeft, - previewEditorWidth + previewEditorWidth, + padding: PADDING, + borderRadius: PADDING }; }); @@ -484,13 +491,28 @@ export class InlineEditsSideBySideDiff extends Disposable { private readonly _extendedModifiedPath = derived(reader => { const layoutInfo = this._previewEditorLayoutInfo.read(reader); if (!layoutInfo) { return undefined; } - const width = layoutInfo.previewEditorWidth; + const width = layoutInfo.previewEditorWidth + layoutInfo.padding; + + + const topLeft = layoutInfo.edit1; + const topRight = layoutInfo.edit1.deltaX(width); + const topRightBefore = topRight.deltaX(-layoutInfo.borderRadius); + const topRightAfter = topRight.deltaY(layoutInfo.borderRadius); + + const bottomLeft = layoutInfo.edit2; + const bottomRight = bottomLeft.deltaX(width); + const bottomRightBefore = bottomRight.deltaY(-layoutInfo.borderRadius); + const bottomRightAfter = bottomRight.deltaX(-layoutInfo.borderRadius); + const extendedModifiedPathBuilder = new PathBuilder() .moveTo(layoutInfo.code1) - .lineTo(layoutInfo.edit1) - .lineTo(layoutInfo.edit1.deltaX(width)) - .lineTo(layoutInfo.edit2.deltaX(width)) - .lineTo(layoutInfo.edit2); + .lineTo(topLeft) + .lineTo(topRightBefore) + .curveTo(topRight, topRightAfter) + .lineTo(bottomRightBefore) + .curveTo(bottomRight, bottomRightAfter) + .lineTo(bottomLeft); + if (layoutInfo.edit2.y !== layoutInfo.code2.y) { extendedModifiedPathBuilder.curveTo2(layoutInfo.edit2.deltaX(-20), layoutInfo.code2.deltaX(20), layoutInfo.code2.deltaX(0)); } @@ -545,6 +567,19 @@ export class InlineEditsSideBySideDiff extends Disposable { }), ]).keepUpdated(this._store); + private readonly _middleBorderWithShadow = n.div({ + class: ['middleBorderWithShadow'], + style: { + position: 'absolute', + display: this._previewEditorLayoutInfo.map(i => i?.shouldShowShadow ? 'block' : 'none'), + width: '6px', + boxShadow: 'var(--vscode-scrollbar-shadow) -6px 0 6px -6px inset', + left: this._previewEditorLayoutInfo.map(i => i ? i.code1.x - 6 : 0), + top: this._previewEditorLayoutInfo.map(i => i ? i.code1.y : 0), + height: this._previewEditorLayoutInfo.map(i => i ? i.code2.y - i.code1.y : 0), + }, + }, []).keepUpdated(this._store); + private readonly _foregroundSvg = n.svg({ transform: 'translate(-0.5 -0.5)', style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, @@ -552,7 +587,6 @@ export class InlineEditsSideBySideDiff extends Disposable { const layoutInfoObs = mapOutFalsy(this._previewEditorLayoutInfo).read(reader); if (!layoutInfoObs) { return undefined; } - const shadowWidth = 6; return [ n.svgElem('path', { class: 'originalOverlay', @@ -579,46 +613,19 @@ export class InlineEditsSideBySideDiff extends Disposable { strokeWidth: '1px', } }), - - ...(!layoutInfoObs.map(i => i.shouldShowShadow).read(reader) - ? [ - n.svgElem('path', { - class: 'middleBorder', - d: layoutInfoObs.map(layoutInfo => new PathBuilder() - .moveTo(layoutInfo.code1) - .lineTo(layoutInfo.code2) - .build() - ), - style: { - stroke: 'var(--vscode-inlineEdit-modifiedBorder)', - strokeWidth: '1px' - } - }) - ] - : [ - n.svgElem('defs', {}, [ - n.svgElem('linearGradient', { id: 'gradient', x1: '0%', x2: '100%', }, [ - n.svgElem('stop', { - offset: '0%', - style: { stopColor: 'var(--vscode-inlineEdit-modifiedBorder)', stopOpacity: '0', } - }), - n.svgElem('stop', { - offset: '100%', - style: { stopColor: 'var(--vscode-inlineEdit-modifiedBorder)', stopOpacity: '1', } - }) - ]) - ]), - n.svgElem('rect', { - class: 'middleBorderWithShadow', - x: layoutInfoObs.map(layoutInfo => layoutInfo.code1.x - shadowWidth), - y: layoutInfoObs.map(layoutInfo => layoutInfo.code1.y), - width: shadowWidth, - height: layoutInfoObs.map(layoutInfo => layoutInfo.code2.y - layoutInfo.code1.y), - fill: 'url(#gradient)', - style: { strokeWidth: '0', stroke: 'transparent', } - }) - ] - ) + n.svgElem('path', { + class: 'middleBorder', + d: layoutInfoObs.map(layoutInfo => new PathBuilder() + .moveTo(layoutInfo.code1) + .lineTo(layoutInfo.code2) + .build() + ), + style: { + display: layoutInfoObs.map(i => i.shouldShowShadow ? 'none' : 'block'), + stroke: 'var(--vscode-inlineEdit-modifiedBorder)', + strokeWidth: '1px' + } + }) ]; })).keepUpdated(this._store); @@ -634,7 +641,7 @@ export class InlineEditsSideBySideDiff extends Disposable { }, }, [ this._backgroundSvg, - derived(this, reader => this._shouldOverflow.read(reader) ? [] : [this._foregroundBackgroundSvg, this._editorContainer, this._foregroundSvg]), + derived(this, reader => this._shouldOverflow.read(reader) ? [] : [this._foregroundBackgroundSvg, this._editorContainer, this._foregroundSvg, this._middleBorderWithShadow]), ]).keepUpdated(this._store); private readonly _overflowView = n.div({ @@ -645,6 +652,6 @@ export class InlineEditsSideBySideDiff extends Disposable { display: this._display, }, }, [ - derived(this, reader => this._shouldOverflow.read(reader) ? [this._foregroundBackgroundSvg, this._editorContainer, this._foregroundSvg] : []), + derived(this, reader => this._shouldOverflow.read(reader) ? [this._foregroundBackgroundSvg, this._editorContainer, this._foregroundSvg, this._middleBorderWithShadow] : []), ]).keepUpdated(this._store); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts index 57e0eb3393e0..a3b822d875d8 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts @@ -392,11 +392,13 @@ function setClassName(domNode: Element, className: string) { function resolve(value: ValueOrList, reader: IReader | undefined, cb: (val: T) => void): void { if (isObservable(value)) { cb(value.read(reader)); + return; } if (Array.isArray(value)) { for (const v of value) { resolve(v, reader, cb); } + return; } cb(value as any); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index 19f28075c336..9797502d7b03 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -24,7 +24,7 @@ export const transparentHoverBackground = registerColor( 'inlineEdit.wordReplacementView.background', { light: transparent(editorHoverStatusBarBackground, 0.1), - dark: transparent(editorHoverStatusBarBackground, 0.5), + dark: transparent(editorHoverStatusBarBackground, 0.1), hcLight: transparent(editorHoverStatusBarBackground, 0.1), hcDark: transparent(editorHoverStatusBarBackground, 0.1), }, From a26fe3e4666aae75fdbfaacf7be153a07bdd12e8 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 10 Jan 2025 00:20:57 +0100 Subject: [PATCH 0451/3587] Git - do not show "Open on GitHub" action for commits that have not been pushed to the remote (#237605) --- extensions/git/src/blame.ts | 4 +++- extensions/git/src/git.ts | 9 ++++++++ extensions/git/src/repository.ts | 30 ++++++++++++++++++++++++++ extensions/git/src/timelineProvider.ts | 4 +++- 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 110b4601b15f..8ad5703bbff5 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -218,7 +218,9 @@ export class GitBlameController { // Remote commands const defaultRemote = repository.getDefaultRemote(); - if (defaultRemote?.fetchUrl) { + const unpublishedCommits = await repository.getUnpublishedCommits(); + + if (defaultRemote?.fetchUrl && !unpublishedCommits.has(blameInformation.hash)) { remoteSourceCommands.push(...await getRemoteSourceControlHistoryItemCommands(defaultRemote.fetchUrl)); } } diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index b000245a0f2e..7e2fd062f888 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -2848,6 +2848,15 @@ export class Repository { return commits[0]; } + async revList(ref1: string, ref2: string): Promise { + const result = await this.exec(['rev-list', `${ref1}..${ref2}`]); + if (result.stderr) { + return []; + } + + return result.stdout.trim().split('\n'); + } + async revParse(ref: string): Promise { try { const result = await fs.readFile(path.join(this.dotGit.path, ref), 'utf8'); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index c576bc790b08..9b70bef793cb 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -841,6 +841,7 @@ export class Repository implements Disposable { private isRepositoryHuge: false | { limit: number } = false; private didWarnAboutLimit = false; + private unpublishedCommits: Set | undefined = undefined; private branchProtection = new Map(); private commitCommandCenter: CommitCommandsCenter; private resourceCommandResolver = new ResourceCommandResolver(this); @@ -2270,6 +2271,17 @@ export class Repository implements Disposable { this.isCherryPickInProgress(), this.getInputTemplate()]); + // Reset the list of unpublished commits if HEAD has + // changed (ex: checkout, fetch, pull, push, publish, etc.). + // The list of unpublished commits will be computed lazily + // on demand. + if (this.HEAD?.name !== HEAD?.name || + this.HEAD?.commit !== HEAD?.commit || + this.HEAD?.ahead !== HEAD?.ahead || + this.HEAD?.upstream !== HEAD?.upstream) { + this.unpublishedCommits = undefined; + } + this._HEAD = HEAD; this._remotes = remotes!; this._submodules = submodules!; @@ -2746,6 +2758,24 @@ export class Repository implements Disposable { return false; } + async getUnpublishedCommits(): Promise> { + if (this.unpublishedCommits) { + return this.unpublishedCommits; + } + + if (this.HEAD && this.HEAD.name && this.HEAD.upstream && this.HEAD.ahead && this.HEAD.ahead > 0) { + const ref1 = `${this.HEAD.upstream.remote}/${this.HEAD.upstream.name}`; + const ref2 = this.HEAD.name; + + const revList = await this.repository.revList(ref1, ref2); + this.unpublishedCommits = new Set(revList); + } else { + this.unpublishedCommits = new Set(); + } + + return this.unpublishedCommits; + } + dispose(): void { this.disposables = dispose(this.disposables); } diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index fbf9b139a0a6..7a4111857d9d 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -215,6 +215,7 @@ export class GitTimelineProvider implements TimelineProvider { const openComparison = l10n.t('Open Comparison'); const defaultRemote = repo.getDefaultRemote(); + const unpublishedCommits = await repo.getUnpublishedCommits(); const remoteSourceCommands: Command[] = defaultRemote?.fetchUrl ? await getRemoteSourceControlHistoryItemCommands(defaultRemote.fetchUrl) : []; @@ -230,7 +231,8 @@ export class GitTimelineProvider implements TimelineProvider { item.description = c.authorName; } - item.setItemDetails(uri, c.hash, c.authorName!, c.authorEmail, dateFormatter.format(date), message, c.shortStat, remoteSourceCommands); + const commitRemoteSourceCommands = !unpublishedCommits.has(c.hash) ? remoteSourceCommands : []; + item.setItemDetails(uri, c.hash, c.authorName!, c.authorEmail, dateFormatter.format(date), message, c.shortStat, commitRemoteSourceCommands); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { From 4dd6e8e17f38bfd82b3815681189e878932869e6 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 9 Jan 2025 17:58:39 -0600 Subject: [PATCH 0452/3587] add `delete` as default keybinding for removing file from working set, ctx key (#237513) * fix #10338 * add ctx key for working set, use it for the when clause --- .../contrib/chat/browser/chatEditing/chatEditingActions.ts | 5 +++++ src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 6 ++++++ src/vs/workbench/contrib/chat/common/chatContextKeys.ts | 1 + 3 files changed, 12 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 8a2ac519e78d..9e4102b58384 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -98,6 +98,11 @@ registerAction2(class RemoveFileFromWorkingSet extends WorkingSetAction { order: 5, group: 'navigation' }], + keybinding: { + primary: KeyCode.Delete, + when: ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), ChatContextKeys.inChatEditWorkingSet), + weight: KeybindingWeight.WorkbenchContrib, + } }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index cc9ebf1b7baf..3dad10ec25c7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -149,6 +149,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge readonly onDidAcceptFollowup = this._onDidAcceptFollowup.event; private readonly _attachmentModel: ChatAttachmentModel; + private _inChatEditWorkingSetCtx: IContextKey | undefined; + public get attachmentModel(): ChatAttachmentModel { return this._attachmentModel; } @@ -643,6 +645,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ChatContextKeys.inChatInput.bindTo(inputScopedContextKeyService).set(true); const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, inputScopedContextKeyService]))); + this._inChatEditWorkingSetCtx = ChatContextKeys.inChatEditWorkingSet.bindTo(this.contextKeyService); + const { historyNavigationBackwardsEnablement, historyNavigationForwardsEnablement } = this._register(registerAndCreateHistoryNavigationContext(inputScopedContextKeyService, this)); this.historyNavigationBackwardsEnablement = historyNavigationBackwardsEnablement; this.historyNavigationForewardsEnablement = historyNavigationForwardsEnablement; @@ -1268,6 +1272,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Working set const workingSetContainer = innerContainer.querySelector('.chat-editing-session-list') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-list')); + this._register(addDisposableListener(workingSetContainer, 'focusin', () => this._inChatEditWorkingSetCtx?.set(true))); + this._register(addDisposableListener(workingSetContainer, 'focusout', () => this._inChatEditWorkingSetCtx?.set(false))); if (!this._chatEditList) { this._chatEditList = this._chatEditsListPool.get(); const list = this._chatEditList.object; diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 68c50c90600a..e9fa7f0ac91a 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -28,6 +28,7 @@ export namespace ChatContextKeys { export const inputHasFocus = new RawContextKey('chatInputHasFocus', false, { type: 'boolean', description: localize('interactiveInputHasFocus', "True when the chat input has focus.") }); export const inChatInput = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); export const inChatSession = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); + export const inChatEditWorkingSet = new RawContextKey('inChatEditWorkingSet', false, { type: 'boolean', description: localize('inChatEditWorkingSet', "True when focus is in the chat edit working set, false otherwise.") }); export const supported = ContextKeyExpr.or(IsWebContext.toNegated(), RemoteNameContext.notEqualsTo('')); // supported on desktop and in web only with a remote connection export const enabled = new RawContextKey('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is activated with an implementation.") }); From 91990b4e6919ce44efdc7dd309c098cc94eae5c9 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 9 Jan 2025 21:37:35 -0800 Subject: [PATCH 0453/3587] Fix occasional streaming pauses (#237615) --- src/vs/workbench/contrib/chat/common/chatModel.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 2b23c26cffd6..5c0530f7aa32 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -352,7 +352,9 @@ export class Response extends Disposable implements IResponse { // The last part can't be merged with- not markdown, or markdown with different permissions this._responseParts.push(progress); } else { - lastResponsePart.content = appendMarkdownString(lastResponsePart.content, progress.content); + // Don't modify the current object, since it's being diffed by the renderer + const idx = this._responseParts.indexOf(lastResponsePart); + this._responseParts[idx] = { ...lastResponsePart, content: appendMarkdownString(lastResponsePart.content, progress.content) }; } this._updateRepr(quiet); } else if (progress.kind === 'textEdit') { From 8e81ea645d41e683ccae3dd454c70670c1a009cb Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 10 Jan 2025 06:43:21 +0100 Subject: [PATCH 0454/3587] watcher - defer `realpath` to only when needed (#237600) --- .../node/watcher/nodejs/nodejsWatcherLib.ts | 73 +++++++++---------- 1 file changed, 33 insertions(+), 40 deletions(-) diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts index 35ea5eea71f6..b016dde0a9ff 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts @@ -17,6 +17,7 @@ import { realpath } from '../../../../../base/node/extpath.js'; import { Promises } from '../../../../../base/node/pfs.js'; import { FileChangeType, IFileChange } from '../../../common/files.js'; import { ILogMessage, coalesceEvents, INonRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, isWatchRequestWithCorrelation } from '../../../common/watcher.js'; +import { Lazy } from '../../../../../base/common/lazy.js'; export class NodeJSFileWatcherLibrary extends Disposable { @@ -55,6 +56,28 @@ export class NodeJSFileWatcherLibrary extends Disposable { private readonly cts = new CancellationTokenSource(); + private readonly realPath = new Lazy(async () => { + + // This property is intentionally `Lazy` and not using `realcase()` as the counterpart + // in the recursive watcher because of the amount of paths this watcher is dealing with. + // We try as much as possible to avoid even needing `realpath()` if we can because even + // that method does an `lstat()` per segment of the path. + + let result = this.request.path; + + try { + result = await realpath(this.request.path); + + if (this.request.path !== result) { + this.trace(`correcting a path to watch that seems to be a symbolic link (original: ${this.request.path}, real: ${result})`); + } + } catch (error) { + // ignore + } + + return result; + }); + readonly ready = this.watch(); private _isReusingRecursiveWatcher = false; @@ -76,19 +99,13 @@ export class NodeJSFileWatcherLibrary extends Disposable { private async watch(): Promise { try { - const realPath = await this.normalizePath(this.request); + const stat = await promises.stat(this.request.path); if (this.cts.token.isCancellationRequested) { return; } - const stat = await promises.stat(realPath); - - if (this.cts.token.isCancellationRequested) { - return; - } - - this._register(await this.doWatch(realPath, stat.isDirectory())); + this._register(await this.doWatch(stat.isDirectory())); } catch (error) { if (error.code !== 'ENOENT') { this.error(error); @@ -106,46 +123,21 @@ export class NodeJSFileWatcherLibrary extends Disposable { this.onDidWatchFail?.(); } - private async normalizePath(request: INonRecursiveWatchRequest): Promise { - let realPath = request.path; - - try { - - // Check for symbolic link - realPath = await realpath(request.path); - - // Note: we used to also call `realcase()` here, but - // that operation is very expensive for large amounts - // of files and is actually not needed for single - // file/folder watching where we report on the original - // path anyway. - // (https://github.com/microsoft/vscode/issues/237351) - - if (request.path !== realPath) { - this.trace(`correcting a path to watch that seems to be a symbolic link (original: ${request.path}, real: ${realPath})`); - } - } catch (error) { - // ignore - } - - return realPath; - } - - private async doWatch(realPath: string, isDirectory: boolean): Promise { + private async doWatch(isDirectory: boolean): Promise { const disposables = new DisposableStore(); - if (this.doWatchWithExistingWatcher(realPath, isDirectory, disposables)) { + if (this.doWatchWithExistingWatcher(isDirectory, disposables)) { this.trace(`reusing an existing recursive watcher for ${this.request.path}`); this._isReusingRecursiveWatcher = true; } else { this._isReusingRecursiveWatcher = false; - await this.doWatchWithNodeJS(realPath, isDirectory, disposables); + await this.doWatchWithNodeJS(isDirectory, disposables); } return disposables; } - private doWatchWithExistingWatcher(realPath: string, isDirectory: boolean, disposables: DisposableStore): boolean { + private doWatchWithExistingWatcher(isDirectory: boolean, disposables: DisposableStore): boolean { if (isDirectory) { // Recursive watcher re-use is currently not enabled for when // folders are watched. this is because the dispatching in the @@ -162,7 +154,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { } if (error) { - const watchDisposable = await this.doWatch(realPath, isDirectory); + const watchDisposable = await this.doWatch(isDirectory); if (!disposables.isDisposed) { disposables.add(watchDisposable); } else { @@ -188,7 +180,8 @@ export class NodeJSFileWatcherLibrary extends Disposable { return false; } - private async doWatchWithNodeJS(realPath: string, isDirectory: boolean, disposables: DisposableStore): Promise { + private async doWatchWithNodeJS(isDirectory: boolean, disposables: DisposableStore): Promise { + const realPath = await this.realPath.value; // macOS: watching samba shares can crash VSCode so we do // a simple check for the file path pointing to /Volumes @@ -407,7 +400,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { if (fileExists) { this.onFileChange({ resource: requestResource, type: FileChangeType.UPDATED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */); - watcherDisposables.add(await this.doWatch(realPath, false)); + watcherDisposables.add(await this.doWatch(false)); } // File seems to be really gone, so emit a deleted and failed event From f80816ab8e21c65ed7f1f7e08ccdbffae63610c6 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 10 Jan 2025 08:41:08 +0100 Subject: [PATCH 0455/3587] fix leaking disposables (#237586) * chore - mark disposables from registry#register functions as disposables so that they don't appear as leaking * fix various leaking disposables --- src/vs/editor/common/languages.ts | 2 +- .../languageConfigurationRegistry.ts | 10 +++--- .../contrib/codelens/browser/codeLensCache.ts | 2 +- .../contrib/codelens/browser/codelens.ts | 13 +++++--- .../hover/browser/contentHoverRendered.ts | 3 +- .../editor/contrib/links/browser/getLinks.ts | 32 +++++++++++-------- src/vs/monaco.d.ts | 2 +- src/vs/platform/actions/common/actions.ts | 10 +++--- src/vs/platform/commands/common/commands.ts | 4 +-- .../extensions/browser/extensionsList.ts | 4 +-- .../contrib/scm/browser/scmViewPane.ts | 11 +++++-- .../browser/authenticationUsageService.ts | 4 +-- 12 files changed, 57 insertions(+), 40 deletions(-) diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 0a0545a891ab..bef68de579de 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -2107,7 +2107,7 @@ export interface CodeLens { export interface CodeLensList { lenses: CodeLens[]; - dispose(): void; + dispose?(): void; } export interface CodeLensProvider { diff --git a/src/vs/editor/common/languages/languageConfigurationRegistry.ts b/src/vs/editor/common/languages/languageConfigurationRegistry.ts index bf0516d531b2..f42b06d0b746 100644 --- a/src/vs/editor/common/languages/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/languages/languageConfigurationRegistry.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from '../../../base/common/event.js'; -import { Disposable, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { Disposable, IDisposable, markAsSingleton, toDisposable } from '../../../base/common/lifecycle.js'; import * as strings from '../../../base/common/strings.js'; import { ITextModel } from '../model.js'; import { DEFAULT_WORD_REGEXP, ensureValidWordDefinition } from '../core/wordHelper.js'; @@ -202,7 +202,7 @@ class ComposedLanguageConfiguration { ); this._entries.push(entry); this._resolved = null; - return toDisposable(() => { + return markAsSingleton(toDisposable(() => { for (let i = 0; i < this._entries.length; i++) { if (this._entries[i] === entry) { this._entries.splice(i, 1); @@ -210,7 +210,7 @@ class ComposedLanguageConfiguration { break; } } - }); + })); } public getResolvedConfiguration(): ResolvedLanguageConfiguration | null { @@ -332,10 +332,10 @@ export class LanguageConfigurationRegistry extends Disposable { const disposable = entries.register(configuration, priority); this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageId)); - return toDisposable(() => { + return markAsSingleton(toDisposable(() => { disposable.dispose(); this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageId)); - }); + })); } public getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration | null { diff --git a/src/vs/editor/contrib/codelens/browser/codeLensCache.ts b/src/vs/editor/contrib/codelens/browser/codeLensCache.ts index 5f479695091d..6dcde99c3699 100644 --- a/src/vs/editor/contrib/codelens/browser/codeLensCache.ts +++ b/src/vs/editor/contrib/codelens/browser/codeLensCache.ts @@ -77,7 +77,7 @@ export class CodeLensCache implements ICodeLensCache { }; }); const copyModel = new CodeLensModel(); - copyModel.add({ lenses: copyItems, dispose: () => { } }, this._fakeProvider); + copyModel.add({ lenses: copyItems }, this._fakeProvider); const item = new CacheItem(model.getLineCount(), copyModel); this._cache.set(model.uri.toString(), item); diff --git a/src/vs/editor/contrib/codelens/browser/codelens.ts b/src/vs/editor/contrib/codelens/browser/codelens.ts index 0a777339b04e..be24cae08b01 100644 --- a/src/vs/editor/contrib/codelens/browser/codelens.ts +++ b/src/vs/editor/contrib/codelens/browser/codelens.ts @@ -5,7 +5,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { illegalArgument, onUnexpectedExternalError } from '../../../../base/common/errors.js'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { DisposableStore, isDisposable } from '../../../../base/common/lifecycle.js'; import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { ITextModel } from '../../../common/model.js'; @@ -24,18 +24,21 @@ export class CodeLensModel { lenses: CodeLensItem[] = []; - private readonly _disposables = new DisposableStore(); + private _store: DisposableStore | undefined; dispose(): void { - this._disposables.dispose(); + this._store?.dispose(); } get isDisposed(): boolean { - return this._disposables.isDisposed; + return this._store?.isDisposed ?? false; } add(list: CodeLensList, provider: CodeLensProvider): void { - this._disposables.add(list); + if (isDisposable(list)) { + this._store ??= new DisposableStore(); + this._store.add(list); + } for (const symbol of list.lenses) { this.lenses.push({ symbol, provider }); } diff --git a/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts b/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts index 81ccfcbc60b2..a80bdb7966fb 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts @@ -273,6 +273,7 @@ class RenderedContentHoverParts extends Disposable { ...hoverContext }; const disposables = new DisposableStore(); + disposables.add(statusBar); for (const participant of participants) { const renderedHoverParts = this._renderHoverPartsForParticipant(hoverParts, participant, hoverRenderingContext); disposables.add(renderedHoverParts); @@ -294,7 +295,7 @@ class RenderedContentHoverParts extends Disposable { actions: renderedStatusBar.actions, }); } - return toDisposable(() => { disposables.dispose(); }); + return disposables; } private _renderHoverPartsForParticipant(hoverParts: IHoverPart[], participant: IEditorHoverParticipant, hoverRenderingContext: IEditorHoverRenderContext): IRenderedHoverParts { diff --git a/src/vs/editor/contrib/links/browser/getLinks.ts b/src/vs/editor/contrib/links/browser/getLinks.ts index cd2e19c756e0..be8a31af119b 100644 --- a/src/vs/editor/contrib/links/browser/getLinks.ts +++ b/src/vs/editor/contrib/links/browser/getLinks.ts @@ -70,6 +70,8 @@ export class Link implements ILink { export class LinksList { + static readonly Empty = new LinksList([]); + readonly links: Link[]; private readonly _disposables = new DisposableStore(); @@ -137,27 +139,31 @@ export class LinksList { } -export function getLinks(providers: LanguageFeatureRegistry, model: ITextModel, token: CancellationToken): Promise { - +export async function getLinks(providers: LanguageFeatureRegistry, model: ITextModel, token: CancellationToken): Promise { const lists: [ILinksList, LinkProvider][] = []; // ask all providers for links in parallel - const promises = providers.ordered(model).reverse().map((provider, i) => { - return Promise.resolve(provider.provideLinks(model, token)).then(result => { + const promises = providers.ordered(model).reverse().map(async (provider, i) => { + try { + const result = await provider.provideLinks(model, token); if (result) { lists[i] = [result, provider]; } - }, onUnexpectedExternalError); - }); - - return Promise.all(promises).then(() => { - const result = new LinksList(coalesce(lists)); - if (!token.isCancellationRequested) { - return result; + } catch (err) { + onUnexpectedExternalError(err); } - result.dispose(); - return new LinksList([]); }); + + await Promise.all(promises); + + let res = new LinksList(coalesce(lists)); + + if (token.isCancellationRequested) { + res.dispose(); + res = LinksList.Empty; + } + + return res; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index bd27d4799411..5db429fff414 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -8060,7 +8060,7 @@ declare namespace monaco.languages { export interface CodeLensList { lenses: CodeLens[]; - dispose(): void; + dispose?(): void; } export interface CodeLensProvider { diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 2b784b5bf5e1..9f1452f4b65c 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -5,7 +5,7 @@ import { IAction, SubmenuAction } from '../../../base/common/actions.js'; import { Event, MicrotaskEmitter } from '../../../base/common/event.js'; -import { DisposableStore, dispose, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { DisposableStore, dispose, IDisposable, markAsSingleton, toDisposable } from '../../../base/common/lifecycle.js'; import { LinkedList } from '../../../base/common/linkedList.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { ICommandAction, ICommandActionTitle, Icon, ILocalizedString } from '../../action/common/action.js'; @@ -397,11 +397,11 @@ export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry { this._commands.set(command.id, command); this._onDidChangeMenu.fire(MenuRegistryChangeEvent.for(MenuId.CommandPalette)); - return toDisposable(() => { + return markAsSingleton(toDisposable(() => { if (this._commands.delete(command.id)) { this._onDidChangeMenu.fire(MenuRegistryChangeEvent.for(MenuId.CommandPalette)); } - }); + })); } getCommand(id: string): ICommandAction | undefined { @@ -422,10 +422,10 @@ export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry { } const rm = list.push(item); this._onDidChangeMenu.fire(MenuRegistryChangeEvent.for(id)); - return toDisposable(() => { + return markAsSingleton(toDisposable(() => { rm(); this._onDidChangeMenu.fire(MenuRegistryChangeEvent.for(id)); - }); + })); } appendMenuItems(items: Iterable<{ id: MenuId; item: IMenuItem | ISubmenuItem }>): IDisposable { diff --git a/src/vs/platform/commands/common/commands.ts b/src/vs/platform/commands/common/commands.ts index 53e32c4842e1..b6e7bc6fe501 100644 --- a/src/vs/platform/commands/common/commands.ts +++ b/src/vs/platform/commands/common/commands.ts @@ -6,7 +6,7 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { Iterable } from '../../../base/common/iterator.js'; import { IJSONSchema } from '../../../base/common/jsonSchema.js'; -import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { IDisposable, markAsSingleton, toDisposable } from '../../../base/common/lifecycle.js'; import { LinkedList } from '../../../base/common/linkedList.js'; import { TypeConstraint, validateConstraints } from '../../../base/common/types.js'; import { ILocalizedString } from '../../action/common/action.js'; @@ -121,7 +121,7 @@ export const CommandsRegistry: ICommandRegistry = new class implements ICommandR // tell the world about this command this._onDidRegisterCommand.fire(id); - return ret; + return markAsSingleton(ret); } registerCommandAlias(oldId: string, newId: string): IDisposable { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 2afa3e73bbf6..7293bfef2b0a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -114,7 +114,7 @@ export class Renderer implements IPagedRenderer { focusOnlyEnabledItems: true }); actionbar.setFocusable(false); - actionbar.onDidRun(({ error }) => error && this.notificationService.error(error)); + const actionBarListener = actionbar.onDidRun(({ error }) => error && this.notificationService.error(error)); const extensionStatusIconAction = this.instantiationService.createInstance(ExtensionStatusAction); const actions = [ @@ -150,7 +150,7 @@ export class Renderer implements IPagedRenderer { const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets]); actionbar.push(actions, { icon: true, label: true }); - const disposable = combinedDisposable(...actions, ...widgets, actionbar, extensionContainers); + const disposable = combinedDisposable(...actions, ...widgets, actionbar, actionBarListener, extensionContainers); return { root, element, icon, name, installCount, ratings, description, publisherDisplayName, disposables: [disposable], actionbar, diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 8a05bb6c06f7..a786b73aab1a 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -21,7 +21,7 @@ import { IContextKeyService, IContextKey, ContextKeyExpr, RawContextKey } from ' import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, MenuRegistry, Action2, IMenu } from '../../../../platform/actions/common/actions.js'; -import { IAction, ActionRunner, Action, Separator, IActionRunner } from '../../../../base/common/actions.js'; +import { IAction, ActionRunner, Action, Separator, IActionRunner, toAction } from '../../../../base/common/actions.js'; import { ActionBar, IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IThemeService, IFileIconTheme } from '../../../../platform/theme/common/themeService.js'; import { isSCMResource, isSCMResourceGroup, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMResourceNode, connectPrimaryMenu } from './util.js'; @@ -2988,7 +2988,14 @@ export class SCMActionButton implements IDisposable { for (let index = 0; index < button.secondaryCommands.length; index++) { const commands = button.secondaryCommands[index]; for (const command of commands) { - actions.push(new Action(command.id, command.title, undefined, true, async () => await this.executeCommand(command.id, ...(command.arguments || [])))); + actions.push(toAction({ + id: command.id, + label: command.title, + enabled: true, + run: async () => { + await this.executeCommand(command.id, ...(command.arguments || [])); + } + })); } if (commands.length) { actions.push(new Separator()); diff --git a/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts b/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts index 514e1ce72da7..08f8436fdf79 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts @@ -81,11 +81,11 @@ export class AuthenticationUsageService extends Disposable implements IAuthentic } } - this._authenticationService.onDidRegisterAuthenticationProvider( + this._register(this._authenticationService.onDidRegisterAuthenticationProvider( provider => this._queue.queue( () => this._addExtensionsToCache(provider.id) ) - ); + )); } async initializeExtensionUsageCache(): Promise { From e1e2944acf490f5490b3250bf23147ff8a7bb0b3 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 10 Jan 2025 11:03:18 +0100 Subject: [PATCH 0456/3587] fixes https://github.com/microsoft/vscode/issues/237171 (#237628) --- src/vs/workbench/contrib/inlineChat/common/inlineChat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index b541cb0e6214..e478a0fdac46 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -140,7 +140,7 @@ export const inlineChatInputBackground = registerColor('inlineChatInput.backgrou export const inlineChatDiffInserted = registerColor('inlineChatDiff.inserted', transparent(diffInserted, .5), localize('inlineChatDiff.inserted', "Background color of inserted text in the interactive editor input")); export const overviewRulerInlineChatDiffInserted = registerColor('editorOverviewRuler.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorOverviewRuler.inlineChatInserted', 'Overview ruler marker color for inline chat inserted content.')); -export const minimapInlineChatDiffInserted = registerColor('editorOverviewRuler.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorOverviewRuler.inlineChatInserted', 'Overview ruler marker color for inline chat inserted content.')); +export const minimapInlineChatDiffInserted = registerColor('editorMinimap.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorMinimap.inlineChatInserted', 'Minimap marker color for inline chat inserted content.')); export const inlineChatDiffRemoved = registerColor('inlineChatDiff.removed', transparent(diffRemoved, .5), localize('inlineChatDiff.removed', "Background color of removed text in the interactive editor input")); export const overviewRulerInlineChatDiffRemoved = registerColor('editorOverviewRuler.inlineChatRemoved', { dark: transparent(diffRemoved, 0.6), light: transparent(diffRemoved, 0.8), hcDark: transparent(diffRemoved, 0.6), hcLight: transparent(diffRemoved, 0.8) }, localize('editorOverviewRuler.inlineChatRemoved', 'Overview ruler marker color for inline chat removed content.')); From 7faf268c1b3f31e33e12c76a62d818a3bf67c689 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 10 Jan 2025 11:07:59 +0100 Subject: [PATCH 0457/3587] fix https://github.com/microsoft/vscode/issues/235322 (#237630) --- src/vs/base/common/errors.ts | 11 ++++++++++- .../api/browser/mainThreadLanguageModels.ts | 3 ++- .../workbench/api/common/extHostLanguageModels.ts | 2 +- src/vs/workbench/api/common/extHostTypes.ts | 13 +++++++++++-- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/vs/base/common/errors.ts b/src/vs/base/common/errors.ts index a6930ef51cf5..888aa3b06302 100644 --- a/src/vs/base/common/errors.ts +++ b/src/vs/base/common/errors.ts @@ -126,9 +126,14 @@ export interface SerializedError { readonly message: string; readonly stack: string; readonly noTelemetry: boolean; + readonly code?: string; readonly cause?: SerializedError; } +type ErrorWithCode = Error & { + code: string | undefined; +}; + export function transformErrorForSerialization(error: Error): SerializedError; export function transformErrorForSerialization(error: any): any; export function transformErrorForSerialization(error: any): any { @@ -141,7 +146,8 @@ export function transformErrorForSerialization(error: any): any { message, stack, noTelemetry: ErrorNoTelemetry.isErrorNoTelemetry(error), - cause: cause ? transformErrorForSerialization(cause) : undefined + cause: cause ? transformErrorForSerialization(cause) : undefined, + code: (error).code }; } @@ -159,6 +165,9 @@ export function transformErrorFromSerialization(data: SerializedError): Error { } error.message = data.message; error.stack = data.stack; + if (data.code) { + (error).code = data.code; + } if (data.cause) { error.cause = transformErrorFromSerialization(data.cause); } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index 659f026e78d1..1b1cb20d72b6 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -20,6 +20,7 @@ import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticati import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; import { IExtensionService } from '../../services/extensions/common/extensions.js'; import { ExtHostContext, ExtHostLanguageModelsShape, MainContext, MainThreadLanguageModelsShape } from '../common/extHost.protocol.js'; +import { LanguageModelError } from '../common/extHostTypes.js'; @extHostNamedCustomer(MainContext.MainThreadLanguageModels) export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { @@ -97,7 +98,7 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { if (data) { this._pendingProgress.delete(requestId); if (err) { - const error = transformErrorFromSerialization(err); + const error = LanguageModelError.tryDeserialize(err) ?? transformErrorFromSerialization(err); data.stream.reject(error); data.defer.error(error); } else { diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index b5267b761c74..9264a42d2899 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -434,7 +434,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { if (error) { // we error the stream because that's the only way to signal // that the request has failed - data.res.reject(transformErrorFromSerialization(error)); + data.res.reject(extHostTypes.LanguageModelError.tryDeserialize(error) ?? transformErrorFromSerialization(error)); } else { data.res.resolve(); } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 667b018aa332..abb6a3e737b2 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -7,7 +7,7 @@ import type * as vscode from 'vscode'; import { asArray, coalesceInPlace, equals } from '../../../base/common/arrays.js'; -import { illegalArgument } from '../../../base/common/errors.js'; +import { illegalArgument, SerializedError } from '../../../base/common/errors.js'; import { IRelativePattern } from '../../../base/common/glob.js'; import { MarkdownString as BaseMarkdownString, MarkdownStringTrustedOptions } from '../../../base/common/htmlContent.js'; import { ResourceMap } from '../../../base/common/map.js'; @@ -4847,6 +4847,8 @@ export class LanguageModelChatAssistantMessage { export class LanguageModelError extends Error { + static readonly #name = 'LanguageModelError'; + static NotFound(message?: string): LanguageModelError { return new LanguageModelError(message, LanguageModelError.NotFound.name); } @@ -4859,11 +4861,18 @@ export class LanguageModelError extends Error { return new LanguageModelError(message, LanguageModelError.Blocked.name); } + static tryDeserialize(data: SerializedError): LanguageModelError | undefined { + if (data.name !== LanguageModelError.#name) { + return undefined; + } + return new LanguageModelError(data.message, data.code, data.cause); + } + readonly code: string; constructor(message?: string, code?: string, cause?: Error) { super(message, { cause }); - this.name = 'LanguageModelError'; + this.name = LanguageModelError.#name; this.code = code ?? ''; } From 3073dc1fb489badd5a07f20af503c408780a51f7 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:00:33 +0100 Subject: [PATCH 0458/3587] padding between statusbar and inline edit (#237632) fixes #11597 --- .../browser/view/inlineEdits/gutterIndicatorView.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index e0a1ce126d0e..dd4d8852858d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -116,7 +116,8 @@ export class InlineEditsGutterIndicator extends Disposable { const layout = this._editorObs.layoutInfo.read(reader); - const fullViewPort = Rect.fromLeftTopRightBottom(0, 0, layout.width, layout.height); + const bottomPadding = 1; + const fullViewPort = Rect.fromLeftTopRightBottom(0, 0, layout.width, layout.height - bottomPadding); const viewPortWithStickyScroll = fullViewPort.withTop(this._stickyScrollHeight.read(reader)); const targetVertRange = s.lineOffsetRange.read(reader); From eba8049c8033d9e88b79a4ea0975ecf1ef1d26ed Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 10 Jan 2025 13:41:55 +0100 Subject: [PATCH 0459/3587] improve error handling from unpkg service (#237638) --- .../common/extensionGalleryService.ts | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 53aedc82f10b..9d139598a74b 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -742,10 +742,17 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return; } + if (!EXTENSION_IDENTIFIER_REGEX.test(extensionInfo.id)) { + toQuery.push(extensionInfo); + return; + } + try { const rawGalleryExtension = await this.getLatestRawGalleryExtension(extensionInfo.id, token); if (!rawGalleryExtension) { - toQuery.push(extensionInfo); + if (extensionInfo.uuid) { + toQuery.push(extensionInfo); + } return; } @@ -1190,16 +1197,12 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } } - private async getLatestRawGalleryExtension(extensionId: string, token: CancellationToken): Promise { + private async getLatestRawGalleryExtension(extensionId: string, token: CancellationToken): Promise { let errorCode: string | undefined; const stopWatch = new StopWatch(); try { const [publisher, name] = extensionId.split('.'); - if (!publisher || !name) { - errorCode = 'InvalidExtensionId'; - return undefined; - } const uri = URI.parse(format2(this.extensionUrlTemplate!, { publisher, name })); const commonHeaders = await this.commonHeadersPromise; const headers = { @@ -1216,20 +1219,21 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi timeout: 10000 /*10s*/ }, token); + if (context.res.statusCode === 404) { + errorCode = 'NotFound'; + return null; + } + if (context.res.statusCode && context.res.statusCode !== 200) { errorCode = `GalleryServiceError:` + context.res.statusCode; - this.logService.warn('Error getting latest version of the extension', extensionId, context.res.statusCode); - return undefined; + throw new Error('Unexpected HTTP response: ' + context.res.statusCode); } const result = await asJson(context); - if (result) { - return result; + if (!result) { + errorCode = 'NoData'; } - - errorCode = 'NoData'; - this.logService.warn('Error getting latest version of the extension', extensionId, errorCode); - + return result; } catch (error) { @@ -1243,6 +1247,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi ? ExtensionGalleryErrorCode.Timeout : ExtensionGalleryErrorCode.Failed; } + throw error; } finally { @@ -1260,8 +1265,6 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi }; this.telemetryService.publicLog2('galleryService:getLatest', { extension: extensionId, duration: stopWatch.elapsed(), errorCode }); } - - return undefined; } async reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise { From 9bb633aaf79329f3f42812ce5fed241dd67776c2 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:52:01 +0100 Subject: [PATCH 0460/3587] GitHub - add "Open on GitHub" action to the SCM graph (#237635) --- extensions/github/package.json | 22 ++++++++++-- extensions/github/src/commands.ts | 34 +++++++++++++++++-- extensions/github/tsconfig.json | 1 + .../contrib/scm/browser/scmHistoryViewPane.ts | 3 +- 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/extensions/github/package.json b/extensions/github/package.json index f99a41d5979e..5ad7b7493486 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -27,9 +27,11 @@ } }, "enabledApiProposals": [ - "contribShareMenu", - "contribEditSessions", "canonicalUriProvider", + "contribEditSessions", + "contribShareMenu", + "contribSourceControlHistoryItemMenu", + "scmHistoryProvider", "shareProvider" ], "contributes": { @@ -54,6 +56,11 @@ "command": "github.openOnVscodeDev", "title": "Open in vscode.dev", "icon": "$(globe)" + }, + { + "command": "github.openOnGitHub2", + "title": "Open on GitHub", + "icon": "$(github)" } ], "continueEditSession": [ @@ -71,6 +78,10 @@ "command": "github.publish", "when": "git-base.gitEnabled && workspaceFolderCount != 0 && remoteName != 'codespaces'" }, + { + "command": "github.openOnGitHub2", + "when": "false" + }, { "command": "github.copyVscodeDevLink", "when": "false" @@ -127,6 +138,13 @@ "when": "github.hasGitHubRepo && resourceScheme != untitled && remoteName != 'codespaces'", "group": "0_vscode@0" } + ], + "scm/historyItem/context": [ + { + "command": "github.openOnGitHub2", + "when": "github.hasGitHubRepo", + "group": "0_view@2" + } ] }, "configuration": [ diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 2fc47e096f68..0889628cf0d0 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { API as GitAPI } from './typings/git'; +import { API as GitAPI, RefType } from './typings/git'; import { publishRepository } from './publish'; -import { DisposableStore } from './util'; +import { DisposableStore, getRepositoryFromUrl } from './util'; import { LinkContext, getCommitLink, getLink, getVscodeDevHost } from './links'; async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean, context: LinkContext, includeRange = true) { @@ -62,6 +62,36 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { vscode.env.openExternal(vscode.Uri.parse(link)); })); + disposables.add(vscode.commands.registerCommand('github.openOnGitHub2', async (repository: vscode.SourceControl, historyItem: vscode.SourceControlHistoryItem) => { + if (!repository || !historyItem) { + return; + } + + const apiRepository = gitAPI.repositories.find(r => r.rootUri.fsPath === repository.rootUri?.fsPath); + if (!apiRepository) { + return; + } + + // Get the unique remotes that contain the commit + const branches = await apiRepository.getBranches({ contains: historyItem.id }); + const remoteNames = new Set(branches.filter(b => b.type === RefType.RemoteHead && b.remote).map(b => b.remote!)); + + // GitHub remotes that contain the commit + const remotes = apiRepository.state.remotes + .filter(r => remoteNames.has(r.name) && r.fetchUrl && getRepositoryFromUrl(r.fetchUrl)); + + if (remotes.length === 0) { + vscode.window.showInformationMessage(vscode.l10n.t('No GitHub remotes found that contain this commit.')); + return; + } + + // Default remote (origin, or the first remote) + const defaultRemote = remotes.find(r => r.name === 'origin') ?? remotes[0]; + + const link = getCommitLink(defaultRemote.fetchUrl!, historyItem.id); + vscode.env.openExternal(vscode.Uri.parse(link)); + })); + disposables.add(vscode.commands.registerCommand('github.openOnVscodeDev', async () => { return openVscodeDevLink(gitAPI); })); diff --git a/extensions/github/tsconfig.json b/extensions/github/tsconfig.json index 452c74b8bc40..6b9a6e7db0a3 100644 --- a/extensions/github/tsconfig.json +++ b/extensions/github/tsconfig.json @@ -11,6 +11,7 @@ "src/**/*", "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts", + "../../src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts", "../../src/vscode-dts/vscode.proposed.shareProvider.d.ts" ] } diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 1c3f71ac1ca9..436a02762954 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -243,8 +243,9 @@ registerAction2(class extends Action2 { menu: [ { id: MenuId.SCMChangesContext, + when: ContextKeyExpr.equals('config.multiDiffEditor.experimental.enabled', true), group: '0_view', - when: ContextKeyExpr.equals('config.multiDiffEditor.experimental.enabled', true) + order: 1 } ] }); From 54e4744e7dd24ff32c388cef3161f0b5a69a0ac2 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 10 Jan 2025 14:00:39 +0100 Subject: [PATCH 0461/3587] watcher - more perf improvements for large workspaces (#237640) --- src/vs/base/node/extpath.ts | 44 +--------- src/vs/base/test/node/extpath.test.ts | 26 +----- .../node/watcher/parcel/parcelWatcher.ts | 57 ++++++------ .../files/test/node/nodejsWatcher.test.ts | 6 +- .../files/test/node/parcelWatcher.test.ts | 86 ++++++++++--------- 5 files changed, 85 insertions(+), 134 deletions(-) diff --git a/src/vs/base/node/extpath.ts b/src/vs/base/node/extpath.ts index 4fa8d7a0b2b1..60fbdd6e4c7c 100644 --- a/src/vs/base/node/extpath.ts +++ b/src/vs/base/node/extpath.ts @@ -8,7 +8,7 @@ import { CancellationToken } from '../common/cancellation.js'; import { basename, dirname, join, normalize, sep } from '../common/path.js'; import { isLinux } from '../common/platform.js'; import { rtrim } from '../common/strings.js'; -import { Promises, readdirSync } from './pfs.js'; +import { Promises } from './pfs.js'; /** * Copied from: https://github.com/microsoft/vscode-node-debug/blob/master/src/node/pathUtilities.ts#L83 @@ -17,48 +17,8 @@ import { Promises, readdirSync } from './pfs.js'; * On a case insensitive file system, the returned path might differ from the original path by character casing. * On a case sensitive file system, the returned path will always be identical to the original path. * In case of errors, null is returned. But you cannot use this function to verify that a path exists. - * realcaseSync does not handle '..' or '.' path segments and it does not take the locale into account. + * realcase does not handle '..' or '.' path segments and it does not take the locale into account. */ -export function realcaseSync(path: string): string | null { - if (isLinux) { - // This method is unsupported on OS that have case sensitive - // file system where the same path can exist in different forms - // (see also https://github.com/microsoft/vscode/issues/139709) - return path; - } - - const dir = dirname(path); - if (path === dir) { // end recursion - return path; - } - - const name = (basename(path) /* can be '' for windows drive letters */ || path).toLowerCase(); - try { - const entries = readdirSync(dir); - const found = entries.filter(e => e.toLowerCase() === name); // use a case insensitive search - if (found.length === 1) { - // on a case sensitive filesystem we cannot determine here, whether the file exists or not, hence we need the 'file exists' precondition - const prefix = realcaseSync(dir); // recurse - if (prefix) { - return join(prefix, found[0]); - } - } else if (found.length > 1) { - // must be a case sensitive $filesystem - const ix = found.indexOf(name); - if (ix >= 0) { // case sensitive - const prefix = realcaseSync(dir); // recurse - if (prefix) { - return join(prefix, found[ix]); - } - } - } - } catch (error) { - // silently ignore error - } - - return null; -} - export async function realcase(path: string, token?: CancellationToken): Promise { if (isLinux) { // This method is unsupported on OS that have case sensitive diff --git a/src/vs/base/test/node/extpath.test.ts b/src/vs/base/test/node/extpath.test.ts index 9dbdf1ac87e3..c86f3cea0d1e 100644 --- a/src/vs/base/test/node/extpath.test.ts +++ b/src/vs/base/test/node/extpath.test.ts @@ -6,7 +6,7 @@ import * as fs from 'fs'; import assert from 'assert'; import { tmpdir } from 'os'; -import { realcase, realcaseSync, realpath, realpathSync } from '../../node/extpath.js'; +import { realcase, realpath, realpathSync } from '../../node/extpath.js'; import { Promises } from '../../node/pfs.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../common/utils.js'; import { flakySuite, getRandomTestPath } from './testUtils.js'; @@ -24,30 +24,6 @@ flakySuite('Extpath', () => { return Promises.rm(testDir); }); - test('realcaseSync', async () => { - - // assume case insensitive file system - if (process.platform === 'win32' || process.platform === 'darwin') { - const upper = testDir.toUpperCase(); - const real = realcaseSync(upper); - - if (real) { // can be null in case of permission errors - assert.notStrictEqual(real, upper); - assert.strictEqual(real.toUpperCase(), upper); - assert.strictEqual(real, testDir); - } - } - - // linux, unix, etc. -> assume case sensitive file system - else { - let real = realcaseSync(testDir); - assert.strictEqual(real, testDir); - - real = realcaseSync(testDir.toUpperCase()); - assert.strictEqual(real, testDir.toUpperCase()); - } - }); - test('realcase', async () => { // assume case insensitive file system diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index f6eea7fb0e52..c778b998140b 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import parcelWatcher from '@parcel/watcher'; -import { statSync, unlinkSync } from 'fs'; +import { promises } from 'fs'; import { tmpdir, homedir } from 'os'; import { URI } from '../../../../../base/common/uri.js'; import { DeferredPromise, RunOnceScheduler, RunOnceWorker, ThrottledWorker } from '../../../../../base/common/async.js'; @@ -18,7 +18,7 @@ import { TernarySearchTree } from '../../../../../base/common/ternarySearchTree. import { normalizeNFC } from '../../../../../base/common/normalization.js'; import { normalize, join } from '../../../../../base/common/path.js'; import { isLinux, isMacintosh, isWindows } from '../../../../../base/common/platform.js'; -import { realcaseSync, realpathSync } from '../../../../../base/node/extpath.js'; +import { realcase, realpath } from '../../../../../base/node/extpath.js'; import { FileChangeType, IFileChange } from '../../../common/files.js'; import { coalesceEvents, IRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, IWatcherErrorEvent } from '../../../common/watcher.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; @@ -201,7 +201,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise { // Figure out duplicates to remove from the requests - requests = this.removeDuplicateRequests(requests); + requests = await this.removeDuplicateRequests(requests); // Figure out which watchers to start and which to stop const requestsToStart: IRecursiveWatchRequest[] = []; @@ -232,7 +232,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS // Start watching as instructed for (const request of requestsToStart) { if (request.pollingInterval) { - this.startPolling(request, request.pollingInterval); + await this.startPolling(request, request.pollingInterval); } else { await this.startWatching(request); } @@ -247,7 +247,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS return isLinux ? path : path.toLowerCase() /* ignore path casing */; } - private startPolling(request: IRecursiveWatchRequest, pollingInterval: number, restarts = 0): void { + private async startPolling(request: IRecursiveWatchRequest, pollingInterval: number, restarts = 0): Promise { const cts = new CancellationTokenSource(); const instance = new DeferredPromise(); @@ -268,13 +268,13 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS watcher.worker.dispose(); pollingWatcher.dispose(); - unlinkSync(snapshotFile); + await promises.unlink(snapshotFile); } ); this._watchers.set(this.requestToWatcherKey(request), watcher); // Path checks for symbolic links / wrong casing - const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); + const { realPath, realPathDiffers, realPathLength } = await this.normalizePath(request); this.trace(`Started watching: '${realPath}' with polling interval '${pollingInterval}'`); @@ -343,7 +343,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS this._watchers.set(this.requestToWatcherKey(request), watcher); // Path checks for symbolic links / wrong casing - const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); + const { realPath, realPathDiffers, realPathLength } = await this.normalizePath(request); try { const parcelWatcherLib = parcelWatcher; @@ -471,7 +471,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS } } - private normalizePath(request: IRecursiveWatchRequest): { realPath: string; realPathDiffers: boolean; realPathLength: number } { + private async normalizePath(request: IRecursiveWatchRequest): Promise<{ realPath: string; realPathDiffers: boolean; realPathLength: number }> { let realPath = request.path; let realPathDiffers = false; let realPathLength = request.path.length; @@ -479,12 +479,12 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS try { // First check for symbolic link - realPath = realpathSync(request.path); + realPath = await realpath(request.path); // Second check for casing difference // Note: this will be a no-op on Linux platforms if (request.path === realPath) { - realPath = realcaseSync(request.path) ?? request.path; + realPath = await realcase(request.path) ?? request.path; } // Correct watch path as needed @@ -615,7 +615,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS // Start watcher again counting the restarts if (watcher.request.pollingInterval) { - this.startPolling(watcher.request, watcher.request.pollingInterval, watcher.restarts + 1); + await this.startPolling(watcher.request, watcher.request.pollingInterval, watcher.restarts + 1); } else { await this.startWatching(watcher.request, watcher.restarts + 1); } @@ -640,7 +640,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS } } - protected removeDuplicateRequests(requests: IRecursiveWatchRequest[], validatePaths = true): IRecursiveWatchRequest[] { + protected async removeDuplicateRequests(requests: IRecursiveWatchRequest[], validatePaths = true): Promise { // Sort requests by path length to have shortest first // to have a way to prevent children to be watched if @@ -686,26 +686,29 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS for (const request of requestsForCorrelation.values()) { - // Check for overlapping requests + // Check for overlapping request paths (but preserve symbolic links) if (requestTrie.findSubstr(request.path)) { - try { - const realpath = realpathSync(request.path); - if (realpath === request.path) { - this.trace(`ignoring a request for watching who's parent is already watched: ${this.requestToString(request)}`); + if (requestTrie.has(request.path)) { + this.trace(`ignoring a request for watching who's path is already watched: ${this.requestToString(request)}`); + } else { + try { + if (!(await promises.lstat(request.path)).isSymbolicLink()) { + this.trace(`ignoring a request for watching who's parent is already watched: ${this.requestToString(request)}`); - continue; - } - } catch (error) { - this.trace(`ignoring a request for watching who's realpath failed to resolve: ${this.requestToString(request)} (error: ${error})`); + continue; + } + } catch (error) { + this.trace(`ignoring a request for watching who's lstat failed to resolve: ${this.requestToString(request)} (error: ${error})`); - this._onDidWatchFail.fire(request); + this._onDidWatchFail.fire(request); - continue; + continue; + } } } // Check for invalid paths - if (validatePaths && !this.isPathValid(request.path)) { + if (validatePaths && !(await this.isPathValid(request.path))) { this._onDidWatchFail.fire(request); continue; @@ -720,9 +723,9 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS return normalizedRequests; } - private isPathValid(path: string): boolean { + private async isPathValid(path: string): Promise { try { - const stat = statSync(path); + const stat = await promises.stat(path); if (!stat.isDirectory()) { this.trace(`ignoring a path for watching that is a file and not a folder: ${path}`); diff --git a/src/vs/platform/files/test/node/nodejsWatcher.test.ts b/src/vs/platform/files/test/node/nodejsWatcher.test.ts index 9a234b80cf57..2e748a7c5322 100644 --- a/src/vs/platform/files/test/node/nodejsWatcher.test.ts +++ b/src/vs/platform/files/test/node/nodejsWatcher.test.ts @@ -72,7 +72,11 @@ suite.skip('File Watcher (node.js)', function () { setup(async () => { await createWatcher(undefined); - testDir = URI.file(getRandomTestPath(tmpdir(), 'vsctests', 'filewatcher')).fsPath; + // Rule out strange testing conditions by using the realpath + // here. for example, on macOS the tmp dir is potentially a + // symlink in some of the root folders, which is a rather + // unrealisic case for the file watcher. + testDir = URI.file(getRandomTestPath(fs.realpathSync(tmpdir()), 'vsctests', 'filewatcher')).fsPath; const sourceDir = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/service').fsPath; diff --git a/src/vs/platform/files/test/node/parcelWatcher.test.ts b/src/vs/platform/files/test/node/parcelWatcher.test.ts index 8b1824af3bd8..e2038a17f50e 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.test.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.test.ts @@ -32,14 +32,14 @@ export class TestParcelWatcher extends ParcelWatcher { readonly onWatchFail = this._onDidWatchFail.event; - testRemoveDuplicateRequests(paths: string[], excludes: string[] = []): string[] { + async testRemoveDuplicateRequests(paths: string[], excludes: string[] = []): Promise { // Work with strings as paths to simplify testing const requests: IRecursiveWatchRequest[] = paths.map(path => { return { path, excludes, recursive: true }; }); - return this.removeDuplicateRequests(requests, false /* validate paths skipped for tests */).map(request => request.path); + return (await this.removeDuplicateRequests(requests, false /* validate paths skipped for tests */)).map(request => request.path); } protected override getUpdateWatchersDelay(): number { @@ -97,7 +97,11 @@ suite.skip('File Watcher (parcel)', function () { } }); - testDir = URI.file(getRandomTestPath(tmpdir(), 'vsctests', 'filewatcher')).fsPath; + // Rule out strange testing conditions by using the realpath + // here. for example, on macOS the tmp dir is potentially a + // symlink in some of the root folders, which is a rather + // unrealisic case for the file watcher. + testDir = URI.file(getRandomTestPath(realpathSync(tmpdir()), 'vsctests', 'filewatcher')).fsPath; const sourceDir = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/service').fsPath; @@ -636,34 +640,34 @@ suite.skip('File Watcher (parcel)', function () { return basicCrudTest(join(testDir, 'newFile.txt'), correlationId); }); - test('should not exclude roots that do not overlap', () => { + test('should not exclude roots that do not overlap', async () => { if (isWindows) { - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a']), ['C:\\a']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a']), ['C:\\a']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); } else { - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a']), ['/a']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a', '/b']), ['/a', '/b']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a']), ['/a']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/b']), ['/a', '/b']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']); } }); - test('should remove sub-folders of other paths', () => { + test('should remove sub-folders of other paths', async () => { if (isWindows) { - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\a\\b']), ['C:\\a']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\a\\b']), ['C:\\a']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']); } else { - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a', '/a/b']), ['/a']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a', '/b', '/a/b']), ['/a', '/b']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']); - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/a', '/a/b', '/a/c/d']), ['/a']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/a/b']), ['/a']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/b', '/a/b']), ['/a', '/b']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']); + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/a', '/a/b', '/a/c/d']), ['/a']); } }); - test('should ignore when everything excluded', () => { - assert.deepStrictEqual(watcher.testRemoveDuplicateRequests(['/foo/bar', '/bar'], ['**', 'something']), []); + test('should ignore when everything excluded', async () => { + assert.deepStrictEqual(await watcher.testRemoveDuplicateRequests(['/foo/bar', '/bar'], ['**', 'something']), []); }); test('watching same or overlapping paths supported when correlation is applied', async () => { @@ -786,17 +790,19 @@ suite.skip('File Watcher (parcel)', function () { const filePath = join(folderPath, 'newFile.txt'); await basicCrudTest(filePath); - onDidWatchFail = Event.toPromise(watcher.onWatchFail); - await Promises.rm(folderPath); - await onDidWatchFail; + if (!reuseExistingWatcher) { + onDidWatchFail = Event.toPromise(watcher.onWatchFail); + await Promises.rm(folderPath); + await onDidWatchFail; - changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED); - onDidWatch = Event.toPromise(watcher.onDidWatch); - await promises.mkdir(folderPath); - await changeFuture; - await onDidWatch; + changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED); + onDidWatch = Event.toPromise(watcher.onDidWatch); + await promises.mkdir(folderPath); + await changeFuture; + await onDidWatch; - await basicCrudTest(filePath); + await basicCrudTest(filePath); + } } (isWindows /* Windows: times out for some reason */ ? test.skip : test)('watch requests support suspend/resume (folder, exist in beginning, not reusing watcher)', async () => { @@ -820,17 +826,19 @@ suite.skip('File Watcher (parcel)', function () { const filePath = join(folderPath, 'newFile.txt'); await basicCrudTest(filePath); - const onDidWatchFail = Event.toPromise(watcher.onWatchFail); - await Promises.rm(folderPath); - await onDidWatchFail; + if (!reuseExistingWatcher) { + const onDidWatchFail = Event.toPromise(watcher.onWatchFail); + await Promises.rm(folderPath); + await onDidWatchFail; - const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED); - const onDidWatch = Event.toPromise(watcher.onDidWatch); - await promises.mkdir(folderPath); - await changeFuture; - await onDidWatch; + const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED); + const onDidWatch = Event.toPromise(watcher.onDidWatch); + await promises.mkdir(folderPath); + await changeFuture; + await onDidWatch; - await basicCrudTest(filePath); + await basicCrudTest(filePath); + } } test('watch request reuses another recursive watcher even when requests are coming in at the same time', async function () { From 9df82a4e49821e6486aa81f1f9d23600b2126848 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 10 Jan 2025 14:28:50 +0100 Subject: [PATCH 0462/3587] add chat overlay for /edit diff editors (#237643) * make /edit actions and rendering work inside diff editor showing /edit-changes * align naming to Discard and Discard this Change fixes https://github.com/microsoft/vscode-copilot/issues/10954 --- .../browser/widget/diffEditor/commands.ts | 28 +++++--- .../chat/browser/chatEditing/chatEditing.ts | 20 ++++++ .../contrib/chat/browser/chatEditorActions.ts | 48 +++++++++----- .../chat/browser/chatEditorController.ts | 66 ++++++++++++++----- .../contrib/chat/browser/chatEditorOverlay.ts | 18 +++-- .../codeEditor/browser/toggleWordWrap.ts | 23 ++----- 6 files changed, 136 insertions(+), 67 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/chatEditing/chatEditing.ts diff --git a/src/vs/editor/browser/widget/diffEditor/commands.ts b/src/vs/editor/browser/widget/diffEditor/commands.ts index 9e04b2aad05e..0d0676fea621 100644 --- a/src/vs/editor/browser/widget/diffEditor/commands.ts +++ b/src/vs/editor/browser/widget/diffEditor/commands.ts @@ -20,6 +20,7 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind import './registrations.contribution.js'; import { DiffEditorSelectionHunkToolbarContext } from './features/gutterFeature.js'; import { URI } from '../../../../base/common/uri.js'; +import { EditorOption } from '../../../common/config/editorOptions.js'; export class ToggleCollapseUnchangedRegions extends Action2 { constructor() { @@ -255,7 +256,7 @@ export function findFocusedDiffEditor(accessor: ServicesAccessor): IDiffEditor | if (activeElement) { for (const d of diffEditors) { const container = d.getContainerDomNode(); - if (isElementOrParentOf(container, activeElement)) { + if (container.contains(activeElement)) { return d; } } @@ -264,13 +265,24 @@ export function findFocusedDiffEditor(accessor: ServicesAccessor): IDiffEditor | return null; } -function isElementOrParentOf(elementOrParent: Element, element: Element): boolean { - let e: Element | null = element; - while (e) { - if (e === elementOrParent) { - return true; + +/** + * If `editor` is the original or modified editor of a diff editor, it returns it. + * It returns null otherwise. + */ +export function findDiffEditorContainingCodeEditor(accessor: ServicesAccessor, editor: ICodeEditor): IDiffEditor | null { + if (!editor.getOption(EditorOption.inDiffEditor)) { + return null; + } + + const codeEditorService = accessor.get(ICodeEditorService); + + for (const diffEditor of codeEditorService.listDiffEditors()) { + const originalEditor = diffEditor.getOriginalEditor(); + const modifiedEditor = diffEditor.getModifiedEditor(); + if (originalEditor === editor || modifiedEditor === editor) { + return diffEditor; } - e = e.parentElement; } - return false; + return null; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditing.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditing.ts new file mode 100644 index 000000000000..7258043aa36b --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditing.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isEqual } from '../../../../../base/common/resources.js'; +import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; +import { findDiffEditorContainingCodeEditor } from '../../../../../editor/browser/widget/diffEditor/commands.js'; +import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IModifiedFileEntry } from '../../common/chatEditingService.js'; + +export function isDiffEditorForEntry(accessor: ServicesAccessor, entry: IModifiedFileEntry, editor: ICodeEditor) { + const diffEditor = findDiffEditorContainingCodeEditor(accessor, editor); + if (!diffEditor) { + return false; + } + const originalModel = diffEditor.getOriginalEditor().getModel(); + const modifiedModel = diffEditor.getModifiedEditor().getModel(); + return isEqual(originalModel?.uri, entry.originalURI) && isEqual(modifiedModel?.uri, entry.modifiedURI); +} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts index 731a921c0cad..75822df0d721 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; import { localize2 } from '../../../../nls.js'; import { EditorAction2, ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { Codicon } from '../../../../base/common/codicons.js'; @@ -54,7 +54,10 @@ abstract class NavigateAction extends Action2 { const chatEditingService = accessor.get(IChatEditingService); const editorService = accessor.get(IEditorService); - const editor = editorService.activeTextEditorControl; + let editor = editorService.activeTextEditorControl; + if (isDiffEditor(editor)) { + editor = editor.getModifiedEditor(); + } if (!isCodeEditor(editor) || !editor.hasModel()) { return; } @@ -152,8 +155,13 @@ abstract class AcceptDiscardAction extends Action2 { let uri = getNotebookEditorFromEditorPane(editorService.activeEditorPane)?.textModel?.uri; if (!uri) { - const editor = editorService.activeTextEditorControl; - uri = isCodeEditor(editor) && editor.hasModel() ? editor.getModel().uri : undefined; + let editor = editorService.activeTextEditorControl; + if (isDiffEditor(editor)) { + editor = editor.getModifiedEditor(); + } + uri = isCodeEditor(editor) && editor.hasModel() + ? editor.getModel().uri + : undefined; } if (!uri) { return; @@ -190,12 +198,11 @@ export class RejectAction extends AcceptDiscardAction { } } -class UndoHunkAction extends EditorAction2 { +class RejectHunkAction extends EditorAction2 { constructor() { super({ id: 'chatEditor.action.undoHunk', - title: localize2('undo', 'Undo this Change'), - shortTitle: localize2('undo2', 'Undo'), + title: localize2('undo', 'Discard this Change'), category: CHAT_CATEGORY, precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), icon: Codicon.discard, @@ -213,7 +220,7 @@ class UndoHunkAction extends EditorAction2 { } override runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]) { - ChatEditorController.get(editor)?.undoNearestChange(args[0]); + ChatEditorController.get(editor)?.rejectNearestChange(args[0]); } } @@ -222,7 +229,6 @@ class AcceptHunkAction extends EditorAction2 { super({ id: 'chatEditor.action.acceptHunk', title: localize2('acceptHunk', 'Accept this Change'), - shortTitle: localize2('acceptHunk2', 'Accept'), category: CHAT_CATEGORY, precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), icon: Codicon.check, @@ -244,23 +250,31 @@ class AcceptHunkAction extends EditorAction2 { } } -class OpenDiffFromHunkAction extends EditorAction2 { +class OpenDiffAction extends EditorAction2 { constructor() { super({ id: 'chatEditor.action.diffHunk', - title: localize2('diff', 'Open Diff'), + title: localize2('diff', 'Toggle Diff Editor'), category: CHAT_CATEGORY, + toggled: { + condition: EditorContextKeys.inDiffEditor, + icon: Codicon.goToFile, + }, precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), icon: Codicon.diffSingle, - menu: { + menu: [{ id: MenuId.ChatEditingEditorHunk, order: 10 - } + }, { + id: MenuId.ChatEditingEditorContent, + group: 'a_resolve', + order: 2, + }] }); } override runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]) { - ChatEditorController.get(editor)?.openDiff(args[0]); + ChatEditorController.get(editor)?.toggleDiff(args[0]); } } @@ -268,8 +282,8 @@ export function registerChatEditorActions() { registerAction2(class NextAction extends NavigateAction { constructor() { super(true); } }); registerAction2(class PrevAction extends NavigateAction { constructor() { super(false); } }); registerAction2(AcceptAction); - registerAction2(RejectAction); - registerAction2(UndoHunkAction); registerAction2(AcceptHunkAction); - registerAction2(OpenDiffFromHunkAction); + registerAction2(RejectAction); + registerAction2(RejectHunkAction); + registerAction2(OpenDiffAction); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index bf0c199267f3..cfe1214f7d86 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -31,6 +31,10 @@ import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/a import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; import { minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../../scm/common/quickDiff.js'; import { DetailedLineRangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; +import { isDiffEditorForEntry } from './chatEditing/chatEditing.js'; +import { basename } from '../../../../base/common/resources.js'; +import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; +import { IEditorIdentifier } from '../../../common/editor.js'; export const ctxHasEditorModification = new RawContextKey('chat.hasEditorModifications', undefined, localize('chat.hasEditorModifications', "The current editor contains chat modifications")); export const ctxHasRequestInProgress = new RawContextKey('chat.ctxHasRequestInProgress', false, localize('chat.ctxHasRequestInProgress', "The current editor shows a file from an edit session which is still in progress")); @@ -67,6 +71,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IChatEditingService private readonly _chatEditingService: IChatEditingService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IEditorService private readonly _editorService: IEditorService, @IContextKeyService contextKeyService: IContextKeyService, ) { @@ -104,11 +109,6 @@ export class ChatEditorController extends Disposable implements IEditorContribut this._register(autorunWithStore((r, store) => { - if (this._editor.getOption(EditorOption.inDiffEditor)) { - this._clearRendering(); - return; - } - const currentEditorEntry = entryForEditor.read(r); if (!currentEditorEntry) { @@ -117,6 +117,11 @@ export class ChatEditorController extends Disposable implements IEditorContribut return; } + if (this._editor.getOption(EditorOption.inDiffEditor) && !_instantiationService.invokeFunction(isDiffEditorForEntry, currentEditorEntry.entry, this._editor)) { + this._clearRendering(); + return; + } + const { session, entry } = currentEditorEntry; const entryIndex = session.entries.read(r).indexOf(entry); @@ -508,7 +513,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut return closestWidget; } - undoNearestChange(closestWidget: DiffHunkWidget | undefined): void { + rejectNearestChange(closestWidget: DiffHunkWidget | undefined): void { closestWidget = closestWidget ?? this._findClosestWidget(); if (closestWidget instanceof DiffHunkWidget) { closestWidget.reject(); @@ -524,7 +529,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut } } - async openDiff(widget: DiffHunkWidget | undefined): Promise { + async toggleDiff(widget: DiffHunkWidget | undefined): Promise { if (!this._editor.hasModel()) { return; } @@ -544,22 +549,50 @@ export class ChatEditorController extends Disposable implements IEditorContribut } } - if (widget instanceof DiffHunkWidget) { + if (!(widget instanceof DiffHunkWidget)) { + return; + } - const lineNumber = widget.getStartLineNumber(); - const position = lineNumber ? new Position(lineNumber, 1) : undefined; - let selection = this._editor.getSelection(); - if (position && !selection.containsPosition(position)) { - selection = Selection.fromPositions(position); - } + const lineNumber = widget.getStartLineNumber(); + const position = lineNumber ? new Position(lineNumber, 1) : undefined; + let selection = this._editor.getSelection(); + if (position && !selection.containsPosition(position)) { + selection = Selection.fromPositions(position); + } + + const isDiffEditor = this._editor.getOption(EditorOption.inDiffEditor); + + if (isDiffEditor) { + // normal EDITOR + await this._editorService.openEditor({ resource: widget.entry.modifiedURI }); + + } else { + // DIFF editor + const defaultAgentName = this._chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession)?.fullName; const diffEditor = await this._editorService.openEditor({ original: { resource: widget.entry.originalURI, options: { selection: undefined } }, modified: { resource: widget.entry.modifiedURI, options: { selection } }, + label: defaultAgentName + ? localize('diff.agent', '{0} (changes from {1})', basename(widget.entry.modifiedURI), defaultAgentName) + : localize('diff.generic', '{0} (changes from chat)', basename(widget.entry.modifiedURI)) }); - // this is needed, passing the selection doesn't seem to work - diffEditor?.getControl()?.setSelection(selection); + if (diffEditor && diffEditor.input) { + const editorIdent: IEditorIdentifier = { editor: diffEditor.input, groupId: diffEditor.group.id }; + + // this is needed, passing the selection doesn't seem to work + diffEditor.getControl()?.setSelection(selection); + + // close diff editor when entry is decided + const d = autorun(r => { + const state = widget.entry.state.read(r); + if (state === WorkingSetEntryState.Accepted || state === WorkingSetEntryState.Rejected) { + d.dispose(); + this._editorService.closeEditor(editorIdent); + } + }); + } } } } @@ -597,6 +630,7 @@ class DiffHunkWidget implements IOverlayWidget { }); this._store.add(toolbar); + this._store.add(toolbar.actionRunner.onWillRun(_ => _editor.focus())); this._editor.addOverlayWidget(this); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index 4b0dd3c8111f..6306e2361ce6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/chatEditorOverlay.css'; import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { autorun, observableFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; @@ -18,7 +17,6 @@ import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/ed import { Range } from '../../../../editor/common/core/range.js'; import { IActionRunner } from '../../../../base/common/actions.js'; import { $, append, EventLike, reset } from '../../../../base/browser/dom.js'; -import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { Codicon } from '../../../../base/common/codicons.js'; @@ -27,6 +25,9 @@ import { localize } from '../../../../nls.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { AcceptAction, RejectAction } from './chatEditorActions.js'; import { ChatEditorController } from './chatEditorController.js'; +import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; +import { isDiffEditorForEntry } from './chatEditing/chatEditing.js'; +import './media/chatEditorOverlay.css'; class ChatEditorOverlayWidget implements IOverlayWidget { @@ -273,14 +274,11 @@ export class ChatEditorOverlayController implements IEditorContribution { constructor( private readonly _editor: ICodeEditor, @IChatEditingService chatEditingService: IChatEditingService, - @IInstantiationService instaService: IInstantiationService, + @IInstantiationService instaService: IInstantiationService ) { const modelObs = observableFromEvent(this._editor.onDidChangeModel, () => this._editor.getModel()); const widget = this._store.add(instaService.createInstance(ChatEditorOverlayWidget, this._editor)); - if (this._editor.getOption(EditorOption.inDiffEditor)) { - return; - } this._store.add(autorun(r => { const model = modelObs.read(r); @@ -303,13 +301,19 @@ export class ChatEditorOverlayController implements IEditorContribution { return; } + const entry = entries[idx]; + + if (this._editor.getOption(EditorOption.inDiffEditor) && !instaService.invokeFunction(isDiffEditorForEntry, entry, this._editor)) { + widget.hide(); + return; + } + const isModifyingOrModified = entries.some(e => e.state.read(r) === WorkingSetEntryState.Modified || e.isCurrentlyBeingModified.read(r)); if (!isModifyingOrModified) { widget.hide(); return; } - const entry = entries[idx]; if (entry.state.read(r) === WorkingSetEntryState.Accepted || entry.state.read(r) === WorkingSetEntryState.Rejected) { widget.hide(); return; diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts index 76bc1b9ddb07..524d34577777 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts @@ -12,6 +12,7 @@ import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.j import { IActiveCodeEditor, ICodeEditor, IDiffEditor } from '../../../../editor/browser/editorBrowser.js'; import { EditorAction, EditorContributionInstantiation, ServicesAccessor, registerDiffEditorContribution, registerEditorAction, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { findDiffEditorContainingCodeEditor } from '../../../../editor/browser/widget/diffEditor/commands.js'; import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; import { IDiffEditorContribution, IEditorContribution } from '../../../../editor/common/editorCommon.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; @@ -19,6 +20,7 @@ import { ITextModel } from '../../../../editor/common/model.js'; import * as nls from '../../../../nls.js'; import { MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; @@ -68,6 +70,7 @@ class ToggleWordWrapAction extends EditorAction { public run(accessor: ServicesAccessor, editor: ICodeEditor): void { const codeEditorService = accessor.get(ICodeEditorService); + const instaService = accessor.get(IInstantiationService); if (!canToggleWordWrap(codeEditorService, editor)) { return; @@ -93,7 +96,7 @@ class ToggleWordWrapAction extends EditorAction { writeTransientState(model, newState, codeEditorService); // if we are in a diff editor, update the other editor (if possible) - const diffEditor = findDiffEditorContainingCodeEditor(editor, codeEditorService); + const diffEditor = instaService.invokeFunction(findDiffEditorContainingCodeEditor, editor); if (diffEditor) { const originalEditor = diffEditor.getOriginalEditor(); const modifiedEditor = diffEditor.getModifiedEditor(); @@ -106,24 +109,6 @@ class ToggleWordWrapAction extends EditorAction { } } -/** - * If `editor` is the original or modified editor of a diff editor, it returns it. - * It returns null otherwise. - */ -function findDiffEditorContainingCodeEditor(editor: ICodeEditor, codeEditorService: ICodeEditorService): IDiffEditor | null { - if (!editor.getOption(EditorOption.inDiffEditor)) { - return null; - } - for (const diffEditor of codeEditorService.listDiffEditors()) { - const originalEditor = diffEditor.getOriginalEditor(); - const modifiedEditor = diffEditor.getModifiedEditor(); - if (originalEditor === editor || modifiedEditor === editor) { - return diffEditor; - } - } - return null; -} - class ToggleWordWrapController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.toggleWordWrapController'; From c6954727db5ca92d778ac74097953f7968b62e72 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:33:10 +0100 Subject: [PATCH 0463/3587] Git - fix timeline item hover rendering (#237641) --- extensions/git/src/timelineProvider.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 7a4111857d9d..0599f80c0545 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -69,19 +69,20 @@ export class GitTimelineItem extends TimelineItem { if (shortStat) { this.tooltip.appendMarkdown(`---\n\n`); + const labels: string[] = []; if (shortStat.insertions) { - this.tooltip.appendMarkdown(`${shortStat.insertions === 1 ? + labels.push(`${shortStat.insertions === 1 ? l10n.t('{0} insertion{1}', shortStat.insertions, '(+)') : l10n.t('{0} insertions{1}', shortStat.insertions, '(+)')}`); } if (shortStat.deletions) { - this.tooltip.appendMarkdown(`, ${shortStat.deletions === 1 ? + labels.push(`${shortStat.deletions === 1 ? l10n.t('{0} deletion{1}', shortStat.deletions, '(-)') : l10n.t('{0} deletions{1}', shortStat.deletions, '(-)')}`); } - this.tooltip.appendMarkdown(`\n\n`); + this.tooltip.appendMarkdown(`${labels.join(', ')}\n\n`); } if (hash) { From 496eb294d656861c7f9d4492f2a7026df3809de9 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:45:09 +0100 Subject: [PATCH 0464/3587] Git/SCM - update "view" to "open" in the command titles (#237645) --- extensions/git/package.nls.json | 8 ++++---- extensions/git/src/blame.ts | 4 ++-- extensions/git/src/timelineProvider.ts | 2 +- .../multiDiffEditor/browser/scmMultiDiffSourceResolver.ts | 2 +- .../workbench/contrib/scm/browser/scmHistoryViewPane.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 0fb33c69b48a..5233f764f131 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -122,10 +122,10 @@ "command.timelineCompareWithSelected": "Compare with Selected", "command.manageUnsafeRepositories": "Manage Unsafe Repositories", "command.openRepositoriesInParentFolders": "Open Repositories In Parent Folders", - "command.viewChanges": "View Changes", - "command.viewStagedChanges": "View Staged Changes", - "command.viewUntrackedChanges": "View Untracked Changes", - "command.viewCommit": "View Commit", + "command.viewChanges": "Open Changes", + "command.viewStagedChanges": "Open Staged Changes", + "command.viewUntrackedChanges": "Open Untracked Changes", + "command.viewCommit": "Open Commit", "command.api.getRepositories": "Get Repositories", "command.api.getRepositoryState": "Get Repository State", "command.api.getRemoteSources": "Get Remote Sources", diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 8ad5703bbff5..9ab8e3e58bd8 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -281,7 +281,7 @@ export class GitBlameController { // Commands const hash = commitInformation?.hash ?? blameInformation.hash; - markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, hash]))} "${l10n.t('View Commit')}")`); + markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, hash]))} "${l10n.t('Open Commit')}")`); markdownString.appendMarkdown(' '); markdownString.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`); @@ -727,7 +727,7 @@ class GitBlameStatusBarItem { this._statusBarItem.text = `$(git-commit) ${this._controller.formatBlameInformationMessage(window.activeTextEditor.document.uri, template, blameInformation[0].blameInformation)}`; this._statusBarItem.tooltip = await this._controller.getBlameInformationHover(window.activeTextEditor.document.uri, blameInformation[0].blameInformation); this._statusBarItem.command = { - title: l10n.t('View Commit'), + title: l10n.t('Open Commit'), command: 'git.viewCommit', arguments: [window.activeTextEditor.document.uri, blameInformation[0].blameInformation.hash] } satisfies Command; diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 0599f80c0545..b243d72ed4be 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -88,7 +88,7 @@ export class GitTimelineItem extends TimelineItem { if (hash) { this.tooltip.appendMarkdown(`---\n\n`); - this.tooltip.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(uri, hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([uri, hash]))} "${l10n.t('View Commit')}")`); + this.tooltip.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(uri, hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([uri, hash]))} "${l10n.t('Open Commit')}")`); this.tooltip.appendMarkdown(' '); this.tooltip.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`); diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts index cdd3aac6b293..ec198f8ad634 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts @@ -139,7 +139,7 @@ export class OpenScmGroupAction extends Action2 { constructor() { super({ id: '_workbench.openScmMultiDiffEditor', - title: localize2('viewChanges', 'View Changes'), + title: localize2('openChanges', 'Open Changes'), f1: false }); } diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 436a02762954..7b9e08f87e15 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -238,7 +238,7 @@ registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.scm.action.graph.viewChanges', - title: localize('viewChanges', "View Changes"), + title: localize('openChanges', "Open Changes"), f1: false, menu: [ { From 8f0dfcdf4e1e5c619fd776d9cdbf8d210758e4a7 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:47:17 +0100 Subject: [PATCH 0465/3587] Fix gutter menu mouse interaction (#237647) fix gutter menu mouse interaction --- .../controller/inlineCompletionsController.ts | 18 ++++++++-- .../browser/view/inlineCompletionsView.ts | 5 +-- .../view/inlineEdits/gutterIndicatorView.ts | 36 +++++++++++++------ .../browser/view/inlineEdits/view.ts | 4 ++- .../view/inlineEdits/viewAndDiffProducer.ts | 5 +-- 5 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index b2b4b8ca6c69..a0fbec729203 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -8,7 +8,7 @@ import { timeout } from '../../../../../base/common/async.js'; import { cancelOnDispose } from '../../../../../base/common/cancellation.js'; import { createHotClass } from '../../../../../base/common/hotReloadHelpers.js'; import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { ITransaction, autorun, derived, derivedDisposable, derivedObservableWithCache, observableFromEvent, observableSignal, runOnChange, runOnChangeWithStore, transaction, waitForState } from '../../../../../base/common/observable.js'; +import { ITransaction, autorun, derived, derivedDisposable, derivedObservableWithCache, observableFromEvent, observableSignal, observableValue, runOnChange, runOnChangeWithStore, transaction, waitForState } from '../../../../../base/common/observable.js'; import { isUndefined } from '../../../../../base/common/types.js'; import { localize } from '../../../../../nls.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; @@ -82,6 +82,13 @@ export class InlineCompletionsController extends Disposable { { min: 50, max: 50 } ); + private readonly _focusIsInMenu = observableValue(this, false); + private readonly _focusIsInEditorOrMenu = derived(this, reader => { + const editorHasFocus = this._editorObs.isFocused.read(reader); + const menuHasFocus = this._focusIsInMenu.read(reader); + return editorHasFocus || menuHasFocus; + }); + private readonly _cursorIsInIndentation = derived(this, reader => { const cursorPos = this._editorObs.cursorPosition.read(reader); if (cursorPos === null) { return false; } @@ -114,7 +121,7 @@ export class InlineCompletionsController extends Disposable { private readonly _hideInlineEditOnSelectionChange = this._editorObs.getOption(EditorOption.inlineSuggest).map(val => true); - protected readonly _view = this._register(new InlineCompletionsView(this.editor, this.model, this._instantiationService)); + protected readonly _view = this._register(new InlineCompletionsView(this.editor, this.model, this._focusIsInMenu, this._instantiationService)); constructor( public readonly editor: ICodeEditor, @@ -190,7 +197,12 @@ export class InlineCompletionsController extends Disposable { } })); - this._register(this.editor.onDidBlurEditorWidget(() => { + this._register(autorun(reader => { + const isFocused = this._focusIsInEditorOrMenu.read(reader); + if (isFocused) { + return; + } + // This is a hidden setting very useful for debugging if (this._contextKeyService.getContextKeyValue('accessibleViewIsShown') || this._configurationService.getValue('editor.inlineSuggest.keepOnBlur') diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts index fa303f3fc6e3..faa3c671e122 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts @@ -6,7 +6,7 @@ import { createStyleSheetFromObservable } from '../../../../../base/browser/domObservable.js'; import { readHotReloadableExport } from '../../../../../base/common/hotReloadHelpers.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { derived, mapObservableArrayCached, derivedDisposable, constObservable, derivedObservableWithCache, IObservable } from '../../../../../base/common/observable.js'; +import { derived, mapObservableArrayCached, derivedDisposable, constObservable, derivedObservableWithCache, IObservable, ISettableObservable } from '../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../browser/observableCodeEditor.js'; @@ -38,7 +38,7 @@ export class InlineCompletionsView extends Disposable { if (!this._everHadInlineEdit.read(reader)) { return undefined; } - return this._instantiationService.createInstance(InlineEditsViewAndDiffProducer.hot.read(reader), this._editor, this._inlineEdit, this._model); + return this._instantiationService.createInstance(InlineEditsViewAndDiffProducer.hot.read(reader), this._editor, this._inlineEdit, this._model, this._focusIsInMenu); }) .recomputeInitiallyAndOnChange(this._store); @@ -48,6 +48,7 @@ export class InlineCompletionsView extends Disposable { constructor( private readonly _editor: ICodeEditor, private readonly _model: IObservable, + private readonly _focusIsInMenu: ISettableObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index dd4d8852858d..d0cdbc360b38 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -5,8 +5,8 @@ import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { IObservable, autorun, constObservable, derived, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { IObservable, ISettableObservable, autorun, constObservable, derived, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { buttonBackground, buttonForeground, buttonSecondaryBackground, buttonSecondaryForeground } from '../../../../../../platform/theme/common/colorRegistry.js'; @@ -23,6 +23,7 @@ import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; import { GutterIndicatorMenuContent } from './gutterIndicatorMenu.js'; import { mapOutFalsy, n, rectToProps } from './utils.js'; import { localize } from '../../../../../../nls.js'; +import { trackFocus } from '../../../../../../base/browser/dom.js'; export const inlineEditIndicatorPrimaryForeground = registerColor( 'inlineEdit.gutterIndicator.primaryForeground', buttonForeground, @@ -73,6 +74,7 @@ export class InlineEditsGutterIndicator extends Disposable { private readonly _originalRange: IObservable, private readonly _model: IObservable, private readonly _shouldShowHover: IObservable, + private readonly _focusIsInMenu: ISettableObservable, @IHoverService private readonly _hoverService: HoverService, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { @@ -159,12 +161,18 @@ export class InlineEditsGutterIndicator extends Disposable { if (this._layout.map(d => d && d.docked).read(reader)) { return { selectionOverride: 'accept' as const, - action: () => { this._model.get()?.accept(); } + action: () => { + this._editorObs.editor.focus(); + this._model.get()?.accept(); + } }; } else { return { selectionOverride: 'jump' as const, - action: () => { this._model.get()?.jump(); } + action: () => { + this._editorObs.editor.focus(); + this._model.get()?.jump(); + } }; } }); @@ -179,35 +187,43 @@ export class InlineEditsGutterIndicator extends Disposable { return; } - const content = this._instantiationService.createInstance( + const disposableStore = new DisposableStore(); + const content = disposableStore.add(this._instantiationService.createInstance( GutterIndicatorMenuContent, this._hoverSelectionOverride, (focusEditor) => { - h?.dispose(); if (focusEditor) { this._editorObs.editor.focus(); } + h?.dispose(); }, this._model.map((m, r) => m?.state.read(r)?.inlineCompletion?.inlineCompletion.source.inlineCompletions.commands), - ).toDisposableLiveElement(); + ).toDisposableLiveElement()); + + const focusTracker = disposableStore.add(trackFocus(content.element)); + disposableStore.add(focusTracker.onDidBlur(() => this._focusIsInMenu.set(false, undefined))); + disposableStore.add(focusTracker.onDidFocus(() => this._focusIsInMenu.set(true, undefined))); + disposableStore.add(toDisposable(() => this._focusIsInMenu.set(false, undefined))); + const h = this._hoverService.showHover({ target: this._iconRef.element, content: content.element, }) as HoverWidget | undefined; if (h) { this._hoverVisible = true; - h.onDispose(() => { - content.dispose(); + h.onDispose(() => { // TODO:@hediet fix leak + disposableStore.dispose(); this._hoverVisible = false; }); } else { - content.dispose(); + disposableStore.dispose(); } } private readonly _indicator = n.div({ class: 'inline-edits-view-gutter-indicator', onclick: () => this._onClickAction.get().action(), + tabIndex: 0, style: { position: 'absolute', overflow: 'visible', diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 4282852af531..8e01c35c5bca 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { autorunWithStore, derived, IObservable, IReader, mapObservableArrayCached } from '../../../../../../base/common/observable.js'; +import { autorunWithStore, derived, IObservable, IReader, ISettableObservable, mapObservableArrayCached } from '../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; @@ -37,6 +37,7 @@ export class InlineEditsView extends Disposable { private readonly _editor: ICodeEditor, private readonly _edit: IObservable, private readonly _model: IObservable, + private readonly _focusIsInMenu: ISettableObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); @@ -143,6 +144,7 @@ export class InlineEditsView extends Disposable { this._uiState.map(s => s && s.originalDisplayRange), this._model, this._sideBySide.isHovered, + this._focusIsInMenu, )); } else { store.add(new InlineEditsIndicator( diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts index f56b4d2c9867..a0e9d4d1268a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts @@ -8,7 +8,7 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js import { equalsIfDefined, itemEquals } from '../../../../../../base/common/equals.js'; import { createHotClass } from '../../../../../../base/common/hotReloadHelpers.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { derivedDisposable, ObservablePromise, derived, IObservable, derivedOpts } from '../../../../../../base/common/observable.js'; +import { derivedDisposable, ObservablePromise, derived, IObservable, derivedOpts, ISettableObservable } from '../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { IDiffProviderFactoryService } from '../../../../../browser/widget/diffEditor/diffProviderFactoryService.js'; @@ -99,13 +99,14 @@ export class InlineEditsViewAndDiffProducer extends Disposable { private readonly _editor: ICodeEditor, private readonly _edit: IObservable, private readonly _model: IObservable, + private readonly _focusIsInMenu: ISettableObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IDiffProviderFactoryService private readonly _diffProviderFactoryService: IDiffProviderFactoryService, @IModelService private readonly _modelService: IModelService ) { super(); - this._register(this._instantiationService.createInstance(InlineEditsView, this._editor, this._inlineEdit, this._model)); + this._register(this._instantiationService.createInstance(InlineEditsView, this._editor, this._inlineEdit, this._model, this._focusIsInMenu)); } } From 7bfaf7350194d4f8766b84e8febcfd104a7f265a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 10 Jan 2025 06:05:34 -0800 Subject: [PATCH 0466/3587] Use terminal shell env when resolving commands in path (#237588) --------- Co-authored-by: Daniel Imms Co-authored-by: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> --- extensions/terminal-suggest/package.json | 3 +- .../src/terminalSuggestMain.ts | 31 ++++++++++++++----- extensions/terminal-suggest/tsconfig.json | 3 +- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/extensions/terminal-suggest/package.json b/extensions/terminal-suggest/package.json index 611ef00ed4ce..981112636bcb 100644 --- a/extensions/terminal-suggest/package.json +++ b/extensions/terminal-suggest/package.json @@ -14,7 +14,8 @@ "Other" ], "enabledApiProposals": [ - "terminalCompletionProvider" + "terminalCompletionProvider", + "terminalShellEnv" ], "scripts": { "compile": "npx gulp compile-extension:terminal-suggest", diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 72cc348e2eba..f146f932372e 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -11,6 +11,7 @@ import codeInsidersCompletionSpec from './completions/code-insiders'; import codeCompletionSpec from './completions/code'; import cdSpec from './completions/cd'; +let cachedAvailableCommandsPath: string | undefined; let cachedAvailableCommands: Set | undefined; const cachedBuiltinCommands: Map = new Map(); @@ -80,7 +81,7 @@ export async function activate(context: vscode.ExtensionContext) { return; } - const commandsInPath = await getCommandsInPath(); + const commandsInPath = await getCommandsInPath(terminal.shellIntegration?.env); const builtinCommands = getBuiltinCommands(shellPath); if (!commandsInPath || !builtinCommands) { return; @@ -129,6 +130,7 @@ export async function resolveCwdFromPrefix(prefix: string, currentCwd?: vscode.U // Resolve the absolute path of the prefix const resolvedPath = path.resolve(currentCwd?.fsPath, relativeFolder); + const stat = await fs.stat(resolvedPath); // Check if the resolved path exists and is a directory @@ -187,15 +189,30 @@ async function isExecutable(filePath: string): Promise { } } -async function getCommandsInPath(): Promise | undefined> { - if (cachedAvailableCommands) { - return cachedAvailableCommands; +async function getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise | undefined> { + // Get PATH value + let pathValue: string | undefined; + if (osIsWindows()) { + const caseSensitivePathKey = Object.keys(env).find(key => key.toLowerCase() === 'path'); + if (caseSensitivePathKey) { + pathValue = env[caseSensitivePathKey]; + } + } else { + pathValue = env.PATH; } - const paths = osIsWindows() ? process.env.PATH?.split(';') : process.env.PATH?.split(':'); - if (!paths) { + if (pathValue === undefined) { return; } - const pathSeparator = osIsWindows() ? '\\' : '/'; + + // Check cache + if (cachedAvailableCommands && cachedAvailableCommandsPath === pathValue) { + return cachedAvailableCommands; + } + + // Extract executables from PATH + const isWindows = osIsWindows(); + const paths = pathValue.split(isWindows ? ';' : ':'); + const pathSeparator = isWindows ? '\\' : '/'; const executables = new Set(); for (const path of paths) { try { diff --git a/extensions/terminal-suggest/tsconfig.json b/extensions/terminal-suggest/tsconfig.json index 151a29616bb2..df4e9764caf1 100644 --- a/extensions/terminal-suggest/tsconfig.json +++ b/extensions/terminal-suggest/tsconfig.json @@ -16,6 +16,7 @@ "src/**/*", "src/completions/index.d.ts", "../../src/vscode-dts/vscode.d.ts", - "../../src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts" + "../../src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts", + "../../src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts" ] } From 52a5dd113f1733ec7a03bdc7716e5bf93ae5c8f3 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 10 Jan 2025 15:20:14 +0100 Subject: [PATCH 0467/3587] revert change (#237591) --- .../extensionManagement/common/allowedExtensionsService.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts b/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts index 7a2892fd098f..0d00bc2bfeb3 100644 --- a/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts +++ b/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts @@ -54,8 +54,7 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte } private getAllowedExtensionsValue(): AllowedExtensionsConfigValueType | undefined { - const inspectValue = this.configurationService.inspect(AllowedExtensionsConfigKey); - const value = inspectValue.policyValue ?? inspectValue.userValue ?? inspectValue.defaultValue; + const value = this.configurationService.getValue(AllowedExtensionsConfigKey); if (!isObject(value) || Array.isArray(value)) { return undefined; } From 24f6dafe20207435512305b426e351cdaf0a1da2 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 10 Jan 2025 15:24:34 +0100 Subject: [PATCH 0468/3587] debt - remove inline chat preview mode (#237654) --- .../inlineChat/browser/inlineChatActions.ts | 22 +- .../browser/inlineChatController.ts | 34 +- .../inlineChat/browser/inlineChatSession.ts | 6 +- .../browser/inlineChatSessionService.ts | 3 +- .../browser/inlineChatSessionServiceImpl.ts | 5 +- .../browser/inlineChatStrategies.ts | 317 ++++++------------ .../browser/inlineChatZoneWidget.ts | 8 +- .../contrib/inlineChat/common/inlineChat.ts | 18 - .../test/browser/inlineChatController.test.ts | 7 +- .../test/browser/inlineChatSession.test.ts | 31 +- .../controller/chat/cellChatActions.ts | 3 +- 11 files changed, 141 insertions(+), 313 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 30dd09c6e485..561bef81e76d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diff import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { InlineChatController, InlineChatRunOptions } from './inlineChatController.js'; -import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES, CTX_INLINE_CHAT_POSSIBLE, ACTION_START } from '../common/inlineChat.js'; +import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES, CTX_INLINE_CHAT_POSSIBLE, ACTION_START } from '../common/inlineChat.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, IAction2Options, MenuId } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -264,7 +264,7 @@ export class AcceptChanges extends AbstractInlineChatAction { shortTitle: localize('apply2', 'Accept'), icon: Codicon.check, f1: true, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, ContextKeyExpr.or(CTX_INLINE_CHAT_DOCUMENT_CHANGED.toNegated(), CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Preview))), + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE), keybinding: [{ weight: KeybindingWeight.WorkbenchContrib + 10, primary: KeyMod.CtrlCmd | KeyCode.Enter, @@ -300,16 +300,6 @@ export class DiscardHunkAction extends AbstractInlineChatAction { icon: Codicon.chromeClose, precondition: CTX_INLINE_CHAT_VISIBLE, menu: [{ - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 2, - when: ContextKeyExpr.and( - ChatContextKeys.inputHasText.toNegated(), - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate(), - CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.MessagesAndEdits), - CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live) - ), - }, { id: MENU_INLINE_CHAT_ZONE, group: 'navigation', order: 2 @@ -392,10 +382,7 @@ export class CloseAction extends AbstractInlineChatAction { order: 1, when: ContextKeyExpr.and( CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate(), - ContextKeyExpr.or( - CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.Messages), - CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Preview) - ) + CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.Messages) ), }] }); @@ -506,7 +493,7 @@ export class ToggleDiffForChange extends AbstractInlineChatAction { constructor() { super({ id: ACTION_TOGGLE_DIFF, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live), CTX_INLINE_CHAT_CHANGE_HAS_DIFF), + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_CHANGE_HAS_DIFF), title: localize2('showChanges', 'Toggle Changes'), icon: Codicon.diffSingle, toggled: { @@ -515,7 +502,6 @@ export class ToggleDiffForChange extends AbstractInlineChatAction { menu: [{ id: MENU_INLINE_CHAT_WIDGET_STATUS, group: 'zzz', - when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live)), order: 1, }, { id: MENU_INLINE_CHAT_ZONE, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 9b4f17b41730..d32358f537db 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -43,12 +43,12 @@ import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from '../../chat/common/chatModel.js'; import { IChatService } from '../../chat/common/chatService.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; -import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js'; import { IInlineChatSavingService } from './inlineChatSavingService.js'; import { HunkInformation, Session, StashedSession } from './inlineChatSession.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { InlineChatError } from './inlineChatSessionServiceImpl.js'; -import { EditModeStrategy, HunkAction, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from './inlineChatStrategies.js'; +import { HunkAction, IEditObserver, LiveStrategy, ProgressingEditsOptions } from './inlineChatStrategies.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; export const enum State { @@ -131,7 +131,7 @@ export class InlineChatController implements IEditorContribution { private readonly _sessionStore = this._store.add(new DisposableStore()); private readonly _stashedSession = this._store.add(new MutableDisposable()); private _session?: Session; - private _strategy?: EditModeStrategy; + private _strategy?: LiveStrategy; constructor( private readonly _editor: ICodeEditor, @@ -255,10 +255,6 @@ export class InlineChatController implements IEditorContribution { return INLINE_CHAT_ID; } - private _getMode(): EditMode { - return this._configurationService.getValue(InlineChatConfigKeys.Mode); - } - getWidgetPosition(): Position | undefined { return this._ui.value.position; } @@ -338,7 +334,7 @@ export class InlineChatController implements IEditorContribution { try { session = await this._inlineChatSessionService.createSession( this._editor, - { editMode: this._getMode(), wholeRange: options.initialRange }, + { wholeRange: options.initialRange }, createSessionCts.token ); } catch (error) { @@ -371,15 +367,7 @@ export class InlineChatController implements IEditorContribution { await session.chatModel.waitForInitialization(); // create a new strategy - switch (session.editMode) { - case EditMode.Preview: - this._strategy = this._instaService.createInstance(PreviewStrategy, session, this._editor, this._ui.value); - break; - case EditMode.Live: - default: - this._strategy = this._instaService.createInstance(LiveStrategy, session, this._editor, this._ui.value, session.headless); - break; - } + this._strategy = this._instaService.createInstance(LiveStrategy, session, this._editor, this._ui.value, session.headless); this._session = session; return State.INIT_UI; @@ -658,7 +646,6 @@ export class InlineChatController implements IEditorContribution { const newSession = await this._inlineChatSessionService.createSession( newEditor, { - editMode: this._getMode(), session: this._session, }, CancellationToken.None); // TODO@ulugbekna: add proper cancellation? @@ -1116,13 +1103,8 @@ export class InlineChatController implements IEditorContribution { finishExistingSession(): void { if (this._session) { - if (this._session.editMode === EditMode.Preview) { - this._log('finishing existing session, using CANCEL', this._session.editMode); - this.cancelSession(); - } else { - this._log('finishing existing session, using APPLY', this._session.editMode); - this.acceptSession(); - } + this._log('finishing existing session, using APPLY'); + this.acceptSession(); } } @@ -1157,7 +1139,7 @@ export class InlineChatController implements IEditorContribution { return false; } - const session = await this._inlineChatSessionService.createSession(this._editor, { editMode: EditMode.Live, wholeRange: anchor, headless: true }, token); + const session = await this._inlineChatSessionService.createSession(this._editor, { wholeRange: anchor, headless: true }, token); if (!session) { return false; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index 83d2d451dec1..c153bb89559e 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -6,7 +6,7 @@ import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IIdentifiedSingleEditOperation, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/common/model.js'; -import { EditMode, CTX_INLINE_CHAT_HAS_STASHED_SESSION } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_HAS_STASHED_SESSION } from '../common/inlineChat.js'; import { IRange, Range } from '../../../../editor/common/core/range.js'; import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; import { EditOperation, ISingleEditOperation } from '../../../../editor/common/core/editOperation.js'; @@ -36,7 +36,6 @@ export type TelemetryData = { finishedByEdit: boolean; startTime: string; endTime: string; - editMode: string; acceptedHunks: number; discardedHunks: number; responseTypes: string; @@ -53,7 +52,6 @@ export type TelemetryDataClassification = { finishedByEdit: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Did edits cause the session to terminate' }; startTime: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When the session started' }; endTime: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When the session ended' }; - editMode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'What edit mode was choosen: live, livePreview, preview' }; acceptedHunks: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of accepted hunks' }; discardedHunks: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of discarded hunks' }; responseTypes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Comma separated list of response types like edits, message, mixed' }; @@ -125,7 +123,6 @@ export class Session { private readonly _versionByRequest = new Map(); constructor( - readonly editMode: EditMode, readonly headless: boolean, /** * The URI of the document which is being EditorEdit @@ -154,7 +151,6 @@ export class Session { finishedByEdit: false, rounds: '', undos: '', - editMode, unstashed: 0, acceptedHunks: 0, discardedHunks: 0, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts index 233e79d76e31..6c4541c633ed 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts @@ -10,7 +10,6 @@ import { IActiveCodeEditor, ICodeEditor } from '../../../../editor/browser/edito import { IRange } from '../../../../editor/common/core/range.js'; import { IValidEditOperation } from '../../../../editor/common/model.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { EditMode } from '../common/inlineChat.js'; import { Session, StashedSession } from './inlineChatSession.js'; export interface ISessionKeyComputer { @@ -36,7 +35,7 @@ export interface IInlineChatSessionService { onDidStashSession: Event; onDidEndSession: Event; - createSession(editor: IActiveCodeEditor, options: { editMode: EditMode; wholeRange?: IRange; session?: Session; headless?: boolean }, token: CancellationToken): Promise; + createSession(editor: IActiveCodeEditor, options: { wholeRange?: IRange; session?: Session; headless?: boolean }, token: CancellationToken): Promise; moveSession(session: Session, newEditor: ICodeEditor): void; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 758764720e84..284212f668f2 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -22,7 +22,7 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { DEFAULT_EDITOR_ASSOCIATION } from '../../../common/editor.js'; import { ChatAgentLocation, IChatAgentService } from '../../chat/common/chatAgents.js'; import { IChatService } from '../../chat/common/chatService.js'; -import { CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_POSSIBLE, EditMode } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_POSSIBLE } from '../common/inlineChat.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js'; import { HunkData, Session, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession.js'; @@ -88,7 +88,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { this._sessions.clear(); } - async createSession(editor: IActiveCodeEditor, options: { editMode: EditMode; headless?: boolean; wholeRange?: Range; session?: Session }, token: CancellationToken): Promise { + async createSession(editor: IActiveCodeEditor, options: { headless?: boolean; wholeRange?: Range; session?: Session }, token: CancellationToken): Promise { const agent = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Editor); @@ -197,7 +197,6 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { } const session = new Session( - options.editMode, options.headless ?? false, targetUri, textModel0, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 4d8b814e0dfb..2518e2f29305 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { WindowIntervalTimer } from '../../../../base/browser/dom.js'; -import { coalesceInPlace } from '../../../../base/common/arrays.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; @@ -27,9 +26,8 @@ import { SaveReason } from '../../../common/editor.js'; import { countWords } from '../../chat/common/chatWordCounter.js'; import { HunkInformation, Session, HunkState } from './inlineChatSession.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; -import { ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, MENU_INLINE_CHAT_ZONE, minimapInlineChatDiffInserted, overviewRulerInlineChatDiffInserted } from '../common/inlineChat.js'; +import { ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, InlineChatConfigKeys, MENU_INLINE_CHAT_ZONE, minimapInlineChatDiffInserted, overviewRulerInlineChatDiffInserted } from '../common/inlineChat.js'; import { assertType } from '../../../../base/common/types.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; import { performAsyncTextEdit, asProgressiveEdit } from './utils.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -57,183 +55,7 @@ export const enum HunkAction { ToggleDiff } -export abstract class EditModeStrategy { - - protected static _decoBlock = ModelDecorationOptions.register({ - description: 'inline-chat', - showIfCollapsed: false, - isWholeLine: true, - }); - - protected readonly _store = new DisposableStore(); - protected readonly _onDidAccept = this._store.add(new Emitter()); - protected readonly _onDidDiscard = this._store.add(new Emitter()); - - - readonly onDidAccept: Event = this._onDidAccept.event; - readonly onDidDiscard: Event = this._onDidDiscard.event; - - constructor( - protected readonly _session: Session, - protected readonly _editor: ICodeEditor, - protected readonly _zone: InlineChatZoneWidget, - @ITextFileService private readonly _textFileService: ITextFileService, - @IInstantiationService protected readonly _instaService: IInstantiationService, - ) { } - - dispose(): void { - this._store.dispose(); - } - - performHunkAction(_hunk: HunkInformation | undefined, action: HunkAction) { - if (action === HunkAction.Accept) { - this._onDidAccept.fire(); - } else if (action === HunkAction.Discard) { - this._onDidDiscard.fire(); - } - } - - protected async _doApplyChanges(ignoreLocal: boolean): Promise { - - const untitledModels: IUntitledTextEditorModel[] = []; - - const editor = this._instaService.createInstance(DefaultChatTextEditor); - - - for (const request of this._session.chatModel.getRequests()) { - - if (!request.response?.response) { - continue; - } - - for (const item of request.response.response.value) { - if (item.kind !== 'textEditGroup') { - continue; - } - if (ignoreLocal && isEqual(item.uri, this._session.textModelN.uri)) { - continue; - } - - await editor.apply(request.response, item, undefined); - - if (item.uri.scheme === Schemas.untitled) { - const untitled = this._textFileService.untitled.get(item.uri); - if (untitled) { - untitledModels.push(untitled); - } - } - } - } - - for (const untitledModel of untitledModels) { - if (!untitledModel.isDisposed()) { - await untitledModel.resolve(); - await untitledModel.save({ reason: SaveReason.EXPLICIT }); - } - } - } - - abstract apply(): Promise; - - cancel() { - return this._session.hunkData.discardAll(); - } - - - - abstract makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, timings: ProgressingEditsOptions, undoStopBefore: boolean): Promise; - - abstract makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean): Promise; - - abstract renderChanges(): Promise; - - abstract hasFocus(): boolean; - - getWholeRangeDecoration(): IModelDeltaDecoration[] { - const ranges = [this._session.wholeRange.value]; - const newDecorations = ranges.map(range => range.isEmpty() ? undefined : ({ range, options: EditModeStrategy._decoBlock })); - coalesceInPlace(newDecorations); - return newDecorations; - } -} - -export class PreviewStrategy extends EditModeStrategy { - - private readonly _ctxDocumentChanged: IContextKey; - - constructor( - session: Session, - editor: ICodeEditor, - zone: InlineChatZoneWidget, - @IModelService modelService: IModelService, - @IContextKeyService contextKeyService: IContextKeyService, - @ITextFileService textFileService: ITextFileService, - @IInstantiationService instaService: IInstantiationService - ) { - super(session, editor, zone, textFileService, instaService); - - this._ctxDocumentChanged = CTX_INLINE_CHAT_DOCUMENT_CHANGED.bindTo(contextKeyService); - - const baseModel = modelService.getModel(session.targetUri)!; - Event.debounce(baseModel.onDidChangeContent.bind(baseModel), () => { }, 350)(_ => { - if (!baseModel.isDisposed() && !session.textModel0.isDisposed()) { - this._ctxDocumentChanged.set(session.hasChangedText); - } - }, undefined, this._store); - } - - override dispose(): void { - this._ctxDocumentChanged.reset(); - super.dispose(); - } - - override async apply() { - await super._doApplyChanges(false); - } - - override async makeChanges(): Promise { - } - - override async makeProgressiveChanges(): Promise { - } - - override async renderChanges(): Promise { } - - hasFocus(): boolean { - return this._zone.widget.hasFocus(); - } -} - - -export interface ProgressingEditsOptions { - duration: number; - token: CancellationToken; -} - - - -type HunkDisplayData = { - - decorationIds: string[]; - - diffViewZoneId: string | undefined; - diffViewZone: IViewZone; - - lensActionsViewZoneIds?: string[]; - - distance: number; - position: Position; - acceptHunk: () => void; - discardHunk: () => void; - toggleDiff?: () => any; - remove(): void; - move: (next: boolean) => void; - - hunk: HunkInformation; -}; - - -export class LiveStrategy extends EditModeStrategy { +export class LiveStrategy { private readonly _decoInsertedText = ModelDecorationOptions.register({ description: 'inline-modified-line', @@ -255,17 +77,23 @@ export class LiveStrategy extends EditModeStrategy { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, }); + protected readonly _store = new DisposableStore(); + protected readonly _onDidAccept = this._store.add(new Emitter()); + protected readonly _onDidDiscard = this._store.add(new Emitter()); private readonly _ctxCurrentChangeHasDiff: IContextKey; private readonly _ctxCurrentChangeShowsDiff: IContextKey; - private readonly _progressiveEditingDecorations: IEditorDecorationsCollection; private readonly _lensActionsFactory: ConflictActionsFactory; private _editCount: number = 0; + private readonly _hunkData = new Map(); + + readonly onDidAccept: Event = this._onDidAccept.event; + readonly onDidDiscard: Event = this._onDidDiscard.event; constructor( - session: Session, - editor: ICodeEditor, - zone: InlineChatZoneWidget, + protected readonly _session: Session, + protected readonly _editor: ICodeEditor, + protected readonly _zone: InlineChatZoneWidget, private readonly _showOverlayToolbar: boolean, @IContextKeyService contextKeyService: IContextKeyService, @IEditorWorkerService protected readonly _editorWorkerService: IEditorWorkerService, @@ -273,21 +101,19 @@ export class LiveStrategy extends EditModeStrategy { @IConfigurationService private readonly _configService: IConfigurationService, @IMenuService private readonly _menuService: IMenuService, @IContextKeyService private readonly _contextService: IContextKeyService, - @ITextFileService textFileService: ITextFileService, - @IInstantiationService instaService: IInstantiationService + @ITextFileService private readonly _textFileService: ITextFileService, + @IInstantiationService protected readonly _instaService: IInstantiationService ) { - super(session, editor, zone, textFileService, instaService); this._ctxCurrentChangeHasDiff = CTX_INLINE_CHAT_CHANGE_HAS_DIFF.bindTo(contextKeyService); this._ctxCurrentChangeShowsDiff = CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF.bindTo(contextKeyService); this._progressiveEditingDecorations = this._editor.createDecorationsCollection(); this._lensActionsFactory = this._store.add(new ConflictActionsFactory(this._editor)); - } - override dispose(): void { + dispose(): void { this._resetDiff(); - super.dispose(); + this._store.dispose(); } private _resetDiff(): void { @@ -297,29 +123,29 @@ export class LiveStrategy extends EditModeStrategy { this._progressiveEditingDecorations.clear(); - for (const data of this._hunkDisplayData.values()) { + for (const data of this._hunkData.values()) { data.remove(); } } - override async apply() { + async apply() { this._resetDiff(); if (this._editCount > 0) { this._editor.pushUndoStop(); } - await super._doApplyChanges(true); + await this._doApplyChanges(true); } - override cancel() { + cancel() { this._resetDiff(); - return super.cancel(); + return this._session.hunkData.discardAll(); } - override async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean): Promise { + async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean): Promise { return this._makeChanges(edits, obs, undefined, undefined, undoStopBefore); } - override async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions, undoStopBefore: boolean): Promise { + async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions, undoStopBefore: boolean): Promise { // add decorations once per line that got edited const progress = new Progress(edits => { @@ -373,7 +199,7 @@ export class LiveStrategy extends EditModeStrategy { } } - override performHunkAction(hunk: HunkInformation | undefined, action: HunkAction) { + performHunkAction(hunk: HunkInformation | undefined, action: HunkAction) { const displayData = this._findDisplayData(hunk); if (!displayData) { @@ -404,14 +230,14 @@ export class LiveStrategy extends EditModeStrategy { let result: HunkDisplayData | undefined; if (hunkInfo) { // use context hunk (from tool/buttonbar) - result = this._hunkDisplayData.get(hunkInfo); + result = this._hunkData.get(hunkInfo); } if (!result && this._zone.position) { // find nearest from zone position const zoneLine = this._zone.position.lineNumber; let distance: number = Number.MAX_SAFE_INTEGER; - for (const candidate of this._hunkDisplayData.values()) { + for (const candidate of this._hunkData.values()) { if (candidate.hunk.getState() !== HunkState.Pending) { continue; } @@ -433,14 +259,12 @@ export class LiveStrategy extends EditModeStrategy { if (!result) { // fallback: first hunk that is pending - result = Iterable.first(Iterable.filter(this._hunkDisplayData.values(), candidate => candidate.hunk.getState() === HunkState.Pending)); + result = Iterable.first(Iterable.filter(this._hunkData.values(), candidate => candidate.hunk.getState() === HunkState.Pending)); } return result; } - private readonly _hunkDisplayData = new Map(); - - override async renderChanges() { + async renderChanges() { this._progressiveEditingDecorations.clear(); @@ -450,7 +274,7 @@ export class LiveStrategy extends EditModeStrategy { changeDecorationsAndViewZones(this._editor, (decorationsAccessor, viewZoneAccessor) => { - const keysNow = new Set(this._hunkDisplayData.keys()); + const keysNow = new Set(this._hunkData.keys()); widgetData = undefined; for (const hunkData of this._session.hunkData.getInfo()) { @@ -458,7 +282,7 @@ export class LiveStrategy extends EditModeStrategy { keysNow.delete(hunkData); const hunkRanges = hunkData.getRangesN(); - let data = this._hunkDisplayData.get(hunkData); + let data = this._hunkData.get(hunkData); if (!data) { // first time -> create decoration const decorationIds: string[] = []; @@ -570,7 +394,7 @@ export class LiveStrategy extends EditModeStrategy { decorationsAccessor.removeDecoration(decorationId); } if (data.diffViewZoneId) { - viewZoneAccessor.removeZone(data.diffViewZoneId); + viewZoneAccessor.removeZone(data.diffViewZoneId!); } data.decorationIds = []; data.diffViewZoneId = undefined; @@ -583,11 +407,11 @@ export class LiveStrategy extends EditModeStrategy { }; const move = (next: boolean) => { - const keys = Array.from(this._hunkDisplayData.keys()); + const keys = Array.from(this._hunkData.keys()); const idx = keys.indexOf(hunkData); const nextIdx = (idx + (next ? 1 : -1) + keys.length) % keys.length; if (nextIdx !== idx) { - const nextData = this._hunkDisplayData.get(keys[nextIdx])!; + const nextData = this._hunkData.get(keys[nextIdx])!; this._zone.updatePositionAndHeight(nextData?.position); renderHunks(); } @@ -613,7 +437,7 @@ export class LiveStrategy extends EditModeStrategy { move, }; - this._hunkDisplayData.set(hunkData, data); + this._hunkData.set(hunkData, data); } else if (hunkData.getState() !== HunkState.Pending) { data.remove(); @@ -634,9 +458,9 @@ export class LiveStrategy extends EditModeStrategy { } for (const key of keysNow) { - const data = this._hunkDisplayData.get(key); + const data = this._hunkData.get(key); if (data) { - this._hunkDisplayData.delete(key); + this._hunkData.delete(key); data.remove(); } } @@ -652,7 +476,7 @@ export class LiveStrategy extends EditModeStrategy { this._ctxCurrentChangeHasDiff.set(Boolean(widgetData.toggleDiff)); - } else if (this._hunkDisplayData.size > 0) { + } else if (this._hunkData.size > 0) { // everything accepted or rejected let oneAccepted = false; for (const hunkData of this._session.hunkData.getInfo()) { @@ -678,12 +502,77 @@ export class LiveStrategy extends EditModeStrategy { return this._zone.widget.hasFocus(); } - override getWholeRangeDecoration(): IModelDeltaDecoration[] { + getWholeRangeDecoration(): IModelDeltaDecoration[] { // don't render the blue in live mode return []; } + + private async _doApplyChanges(ignoreLocal: boolean): Promise { + + const untitledModels: IUntitledTextEditorModel[] = []; + + const editor = this._instaService.createInstance(DefaultChatTextEditor); + + + for (const request of this._session.chatModel.getRequests()) { + + if (!request.response?.response) { + continue; + } + + for (const item of request.response.response.value) { + if (item.kind !== 'textEditGroup') { + continue; + } + if (ignoreLocal && isEqual(item.uri, this._session.textModelN.uri)) { + continue; + } + + await editor.apply(request.response, item, undefined); + + if (item.uri.scheme === Schemas.untitled) { + const untitled = this._textFileService.untitled.get(item.uri); + if (untitled) { + untitledModels.push(untitled); + } + } + } + } + + for (const untitledModel of untitledModels) { + if (!untitledModel.isDisposed()) { + await untitledModel.resolve(); + await untitledModel.save({ reason: SaveReason.EXPLICIT }); + } + } + } +} + +export interface ProgressingEditsOptions { + duration: number; + token: CancellationToken; } +type HunkDisplayData = { + + decorationIds: string[]; + + diffViewZoneId: string | undefined; + diffViewZone: IViewZone; + + lensActionsViewZoneIds?: string[]; + + distance: number; + position: Position; + acceptHunk: () => void; + discardHunk: () => void; + toggleDiff?: () => any; + remove(): void; + move: (next: boolean) => void; + + hunk: HunkInformation; +}; + function changeDecorationsAndViewZones(editor: ICodeEditor, callback: (accessor: IModelDecorationsChangeAccessor, viewZoneAccessor: IViewZoneChangeAccessor) => void): void { editor.changeDecorations(decorationsAccessor => { editor.changeViewZones(viewZoneAccessor => { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index ba1d884af6c8..783e6f05f592 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -22,7 +22,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; import { isResponseVM } from '../../chat/common/chatViewModel.js'; -import { ACTION_REGENERATE_RESPONSE, ACTION_REPORT_ISSUE, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, EditMode, InlineChatConfigKeys, MENU_INLINE_CHAT_WIDGET_SECONDARY, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; +import { ACTION_REGENERATE_RESPONSE, ACTION_REPORT_ISSUE, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET_SECONDARY, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; export class InlineChatZoneWidget extends ZoneWidget { @@ -84,10 +84,8 @@ export class InlineChatZoneWidget extends ZoneWidget { }, rendererOptions: { renderTextEditsAsSummary: (uri) => { - // render edits as summary only when using Live mode and when - // dealing with the current file in the editor - return isEqual(uri, editor.getModel()?.uri) - && configurationService.getValue(InlineChatConfigKeys.Mode) === EditMode.Live; + // render when dealing with the current file in the editor + return isEqual(uri, editor.getModel()?.uri); }, renderDetectedCommandsWithRequest: true, } diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index e478a0fdac46..19f27bf3ad6a 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -13,7 +13,6 @@ import { diffInserted, diffRemoved, editorWidgetBackground, editorWidgetBorder, // settings export const enum InlineChatConfigKeys { - Mode = 'inlineChat.mode', FinishOnType = 'inlineChat.finishOnType', AcceptedOrDiscardBeforeSave = 'inlineChat.acceptedOrDiscardBeforeSave', StartWithOverlayWidget = 'inlineChat.startWithOverlayWidget', @@ -23,24 +22,9 @@ export const enum InlineChatConfigKeys { LineNLHint = 'inlineChat.lineNaturalLanguageHint' } -export const enum EditMode { - Live = 'live', - Preview = 'preview' -} - Registry.as(Extensions.Configuration).registerConfiguration({ id: 'editor', properties: { - [InlineChatConfigKeys.Mode]: { - description: localize('mode', "Configure if changes crafted with inline chat are applied directly to the document or are previewed first."), - default: EditMode.Live, - type: 'string', - enum: [EditMode.Live, EditMode.Preview], - markdownEnumDescriptions: [ - localize('mode.live', "Changes are applied directly to the document, can be highlighted via inline diffs, and accepted/discarded by hunks. Ending a session will keep the changes."), - localize('mode.preview', "Changes are previewed only and need to be accepted via the apply button. Ending a session will discard the changes."), - ] - }, [InlineChatConfigKeys.FinishOnType]: { description: localize('finishOnType', "Whether to finish an inline chat session when typing outside of changed regions."), default: false, @@ -103,10 +87,8 @@ export const CTX_INLINE_CHAT_INNER_CURSOR_END = new RawContextKey('inli export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input")); export const CTX_INLINE_CHAT_HAS_STASHED_SESSION = new RawContextKey('inlineChatHasStashedSession', false, localize('inlineChatHasStashedSession', "Whether interactive editor has kept a session for quick restore")); export const CTX_INLINE_CHAT_USER_DID_EDIT = new RawContextKey('inlineChatUserDidEdit', undefined, localize('inlineChatUserDidEdit', "Whether the user did changes ontop of the inline chat")); -export const CTX_INLINE_CHAT_DOCUMENT_CHANGED = new RawContextKey('inlineChatDocumentChanged', false, localize('inlineChatDocumentChanged', "Whether the document has changed concurrently")); export const CTX_INLINE_CHAT_CHANGE_HAS_DIFF = new RawContextKey('inlineChatChangeHasDiff', false, localize('inlineChatChangeHasDiff', "Whether the current change supports showing a diff")); export const CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF = new RawContextKey('inlineChatChangeShowsDiff', false, localize('inlineChatChangeShowsDiff', "Whether the current change showing a diff")); -export const CTX_INLINE_CHAT_EDIT_MODE = new RawContextKey('config.inlineChat.mode', EditMode.Live); export const CTX_INLINE_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('inlineChatRequestInProgress', false, localize('inlineChatRequestInProgress', "Whether an inline chat request is currently in progress")); export const CTX_INLINE_CHAT_RESPONSE_TYPE = new RawContextKey('inlineChatResponseType', InlineChatResponseType.None, localize('inlineChatResponseTypes', "What type was the responses have been receieved, nothing yet, just messages, or messaged and local edits")); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 75db4dbb7f1f..aaeb5de7b5a1 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -35,7 +35,7 @@ import { ChatAgentLocation, ChatAgentService, IChatAgentData, IChatAgentNameServ import { IChatResponseViewModel } from '../../../chat/common/chatViewModel.js'; import { InlineChatController, State } from '../../browser/inlineChatController.js'; import { Session } from '../../browser/inlineChatSession.js'; -import { CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, InlineChatConfigKeys, InlineChatResponseType } from '../../common/inlineChat.js'; +import { CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, InlineChatConfigKeys, InlineChatResponseType } from '../../common/inlineChat.js'; import { TestViewsService, workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; import { IChatProgress, IChatService } from '../../../chat/common/chatService.js'; @@ -207,7 +207,7 @@ suite('InlineChatController', function () { configurationService = instaService.get(IConfigurationService) as TestConfigurationService; configurationService.setUserConfiguration('chat', { editor: { fontSize: 14, fontFamily: 'default' } }); - configurationService.setUserConfiguration('inlineChat', { mode: EditMode.Live }); + configurationService.setUserConfiguration('editor', {}); contextKeyService = instaService.get(IContextKeyService) as MockContextKeyService; @@ -404,7 +404,6 @@ suite('InlineChatController', function () { test.skip('UI is streaming edits minutes after the response is finished #3345', async function () { - configurationService.setUserConfiguration(InlineChatConfigKeys.Mode, EditMode.Live); return runWithFakedTimers({ maxTaskCount: Number.MAX_SAFE_INTEGER }, async () => { @@ -845,7 +844,7 @@ suite('InlineChatController', function () { model.setValue(''); - const newSession = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const newSession = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(newSession); await chatService.sendRequest(newSession.chatModel.sessionId, 'Existing', { location: ChatAgentLocation.Editor }); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts index 665206f55b03..880552dd7ba1 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -30,7 +30,6 @@ import { IInlineChatSavingService } from '../../browser/inlineChatSavingService. import { HunkState, Session } from '../../browser/inlineChatSession.js'; import { IInlineChatSessionService } from '../../browser/inlineChatSessionService.js'; import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl.js'; -import { EditMode } from '../../common/inlineChat.js'; import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { assertType } from '../../../../../base/common/types.js'; @@ -178,7 +177,7 @@ suite('InlineChatSession', function () { test('Create, release', async function () { - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); inlineChatSessionService.releaseSession(session); }); @@ -187,7 +186,7 @@ suite('InlineChatSession', function () { const decorationCountThen = model.getAllDecorations().length; - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); assert.ok(session.textModelN === model); @@ -213,7 +212,7 @@ suite('InlineChatSession', function () { test('HunkData, accept', async function () { - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); @@ -234,7 +233,7 @@ suite('InlineChatSession', function () { test('HunkData, reject', async function () { - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); @@ -257,7 +256,7 @@ suite('InlineChatSession', function () { model.setValue('one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven\ntwelwe\nthirteen\nfourteen\nfifteen\nsixteen\nseventeen\neighteen\nnineteen\n'); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); assert.ok(session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer())); @@ -306,7 +305,7 @@ suite('InlineChatSession', function () { const lines = ['one', 'two', 'three']; model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI WAS HERE\n')]); @@ -323,7 +322,7 @@ suite('InlineChatSession', function () { const lines = ['one', 'two', 'three', 'four', 'five']; model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI_EDIT\n')]); @@ -348,7 +347,7 @@ suite('InlineChatSession', function () { const lines = ['one', 'two', 'three']; model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI WAS HERE\n')]); @@ -364,7 +363,7 @@ suite('InlineChatSession', function () { const lines = ['one', 'two', 'three']; model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI WAS HERE\n')]); @@ -387,7 +386,7 @@ suite('InlineChatSession', function () { const lines = ['one', 'two', 'three', 'four', 'five']; model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI_EDIT\n')]); @@ -414,7 +413,7 @@ suite('InlineChatSession', function () { const lines = ['one', 'two', 'three', 'four', 'five']; model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI_EDIT\n')]); @@ -444,7 +443,7 @@ suite('InlineChatSession', function () { test('HunkData, accept, discardAll', async function () { - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); @@ -466,7 +465,7 @@ suite('InlineChatSession', function () { test('HunkData, discardAll return undo edits', async function () { - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); @@ -509,7 +508,7 @@ suite('InlineChatSession', function () { }`; model.setValue(origValue); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); const fakeRequest = new class extends mock() { @@ -543,7 +542,7 @@ suite('InlineChatSession', function () { if (n === 2) return 1; return fib(n - 1) + fib(n - 2); }`); - const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(session); await makeEditAsAi([EditOperation.replace(new Range(5, 1, 6, Number.MAX_SAFE_INTEGER), ` diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts index e32df03950a2..c367f5a89884 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts @@ -15,7 +15,7 @@ import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/con import { InputFocusedContextKey } from '../../../../../../platform/contextkey/common/contextkeys.js'; import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_EDIT_MODE, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_VISIBLE, EditMode, InlineChatResponseType, MENU_INLINE_CHAT_WIDGET_STATUS } from '../../../../inlineChat/common/inlineChat.js'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_VISIBLE, InlineChatResponseType, MENU_INLINE_CHAT_WIDGET_STATUS } from '../../../../inlineChat/common/inlineChat.js'; import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_HAS_AGENT, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_STATUS } from './notebookChatContext.js'; import { NotebookChatController } from './notebookChatController.js'; import { CELL_TITLE_CELL_GROUP_ID, INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, getContextFromActiveEditor, getEditorFromArgsOrActivePane } from '../coreActions.js'; @@ -684,7 +684,6 @@ export class AcceptChangesAndRun extends AbstractInlineChatAction { precondition: ContextKeyExpr.and( NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), CTX_INLINE_CHAT_VISIBLE, - ContextKeyExpr.or(CTX_INLINE_CHAT_DOCUMENT_CHANGED.toNegated(), CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Preview)) ), keybinding: undefined, menu: [{ From 5f294d09205248e13de135be36d6af809178f7ef Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 10 Jan 2025 15:25:04 +0100 Subject: [PATCH 0469/3587] clean up getting compatible version in gallery service (#237653) --- .../common/extensionGalleryService.ts | 149 ++++++++++++------ 1 file changed, 98 insertions(+), 51 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 9d139598a74b..1262c42d93a8 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -10,7 +10,7 @@ import { CancellationError, getErrorMessage, isCancellationError } from '../../. import { IPager } from '../../../base/common/paging.js'; import { isWeb, platform } from '../../../base/common/platform.js'; import { arch } from '../../../base/common/process.js'; -import { isBoolean } from '../../../base/common/types.js'; +import { isBoolean, isString } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { IHeaders, IRequestContext, IRequestOptions, isOfflineError } from '../../../base/parts/request/common/request.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; @@ -297,14 +297,27 @@ type GalleryServiceAdditionalQueryEvent = { readonly count: number; }; -interface IExtensionCriteria { +type ExtensionsCriteria = { readonly productVersion: IProductVersion; readonly targetPlatform: TargetPlatform; readonly compatible: boolean; readonly includePreRelease: boolean | (IExtensionIdentifier & { includePreRelease: boolean })[]; readonly versions?: (IExtensionIdentifier & { version: string })[]; +}; + +const enum VersionKind { + Release, + Prerelease, + Latest } +type ExtensionVersionCriteria = { + readonly productVersion: IProductVersion; + readonly targetPlatform: TargetPlatform; + readonly compatible: boolean; + readonly version: VersionKind | string; +}; + class Query { constructor(private state = DefaultQueryState) { } @@ -756,18 +769,21 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return; } - const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, { - targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, - includePreRelease: !!extensionInfo.preRelease, - compatible: !!options.compatible, - productVersion: options.productVersion ?? { - version: this.productService.version, - date: this.productService.date - } - }, undefined, extensionInfo.preRelease ? 'prerelease' : 'release'); + const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension); + const rawGalleryExtensionVersion = await this.getRawGalleryExtensionVersion( + rawGalleryExtension, + { + targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, + compatible: !!options.compatible, + productVersion: options.productVersion ?? { + version: this.productService.version, + date: this.productService.date + }, + version: extensionInfo.preRelease ? VersionKind.Prerelease : VersionKind.Release + }, allTargetPlatforms); - if (extension) { - result.push(extension); + if (rawGalleryExtensionVersion) { + result.push(toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms)); } // report telemetry @@ -868,13 +884,30 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return areApiProposalsCompatible(enabledApiProposals); } - private async isValidVersion(extension: string, rawGalleryExtensionVersion: IRawGalleryExtensionVersion, publisherDisplayName: string, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { - const targetPlatformForExtension = getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion); - if (!isTargetPlatformCompatible(targetPlatformForExtension, allTargetPlatforms, targetPlatform)) { - return false; + private async isValidVersion( + extension: string, + rawGalleryExtensionVersion: IRawGalleryExtensionVersion, + { targetPlatform, compatible, productVersion, version }: ExtensionVersionCriteria, + publisherDisplayName: string, + allTargetPlatforms: TargetPlatform[] + ): Promise { + + // Specific version + if (isString(version)) { + if (rawGalleryExtensionVersion.version !== version) { + return false; + } } - if (versionType !== 'any' && isPreReleaseVersion(rawGalleryExtensionVersion) !== (versionType === 'prerelease')) { + // Prerelease or release version kind + else if (version === VersionKind.Release || version === VersionKind.Prerelease) { + if (isPreReleaseVersion(rawGalleryExtensionVersion) !== (version === VersionKind.Prerelease)) { + return false; + } + } + + const targetPlatformForExtension = getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion); + if (!isTargetPlatformCompatible(targetPlatformForExtension, allTargetPlatforms, targetPlatform)) { return false; } @@ -963,7 +996,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return { firstPage: extensions, total, pageSize: query.pageSize, getPage }; } - private async queryGalleryExtensions(query: Query, criteria: IExtensionCriteria, token: CancellationToken): Promise<{ extensions: IGalleryExtension[]; total: number }> { + private async queryGalleryExtensions(query: Query, criteria: ExtensionsCriteria, token: CancellationToken): Promise<{ extensions: IGalleryExtension[]; total: number }> { const flags = query.flags; /** @@ -997,9 +1030,21 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi if (hasAllVersions) { const extensions: IGalleryExtension[] = []; for (const rawGalleryExtension of rawGalleryExtensions) { - const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria, context); - if (extension) { - extensions.push(extension); + const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension); + const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId }; + const rawGalleryExtensionVersion = await this.getRawGalleryExtensionVersion( + rawGalleryExtension, + { + compatible: criteria.compatible, + targetPlatform: criteria.targetPlatform, + productVersion: criteria.productVersion, + version: criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version + ?? (criteria.includePreRelease ? VersionKind.Latest : VersionKind.Release) + }, + allTargetPlatforms + ); + if (rawGalleryExtensionVersion) { + extensions.push(toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, context)); } } return { extensions, total }; @@ -1011,22 +1056,29 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const rawGalleryExtension = rawGalleryExtensions[index]; const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId }; const includePreRelease = isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : !!criteria.includePreRelease.find(extensionIdentifierWithPreRelease => areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier))?.includePreRelease; + const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension); if (criteria.compatible) { - /** Skip if requested for a web-compatible extension and it is not a web extension. - * All versions are not needed in this case - */ - if (isNotWebExtensionInWebTargetPlatform(getAllTargetPlatforms(rawGalleryExtension), criteria.targetPlatform)) { + // Skip looking for all versions if requested for a web-compatible extension and it is not a web extension. + if (isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, criteria.targetPlatform)) { continue; } - /** - * Skip if the extension is not allowed. - * All versions are not needed in this case - */ + // Skip looking for all versions if the extension is not allowed. if (this.allowedExtensionsService.isAllowed({ id: extensionIdentifier.id, publisherDisplayName: rawGalleryExtension.publisher.displayName }) !== true) { continue; } } - const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria, context); + const rawGalleryExtensionVersion = await this.getRawGalleryExtensionVersion( + rawGalleryExtension, + { + compatible: criteria.compatible, + targetPlatform: criteria.targetPlatform, + productVersion: criteria.productVersion, + version: criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version + ?? (criteria.includePreRelease ? VersionKind.Latest : VersionKind.Release) + }, + allTargetPlatforms + ); + const extension = rawGalleryExtensionVersion ? toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, context) : null; if (!extension /** Need all versions if the extension is a pre-release version but * - the query is to look for a release version or @@ -1067,38 +1119,29 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return { extensions: result.sort((a, b) => a[0] - b[0]).map(([, extension]) => extension), total }; } - private async toGalleryExtensionWithCriteria(rawGalleryExtension: IRawGalleryExtension, criteria: IExtensionCriteria, queryContext?: IStringDictionary, versionType?: 'release' | 'prerelease' | 'any'): Promise { - + private async getRawGalleryExtensionVersion(rawGalleryExtension: IRawGalleryExtension, criteria: ExtensionVersionCriteria, allTargetPlatforms: TargetPlatform[]): Promise { const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId }; - const version = criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version; - const includePreRelease = isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : !!criteria.includePreRelease.find(extensionIdentifierWithPreRelease => areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier))?.includePreRelease; - const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension); const rawGalleryExtensionVersions = sortExtensionVersions(rawGalleryExtension.versions, criteria.targetPlatform); if (criteria.compatible && isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, criteria.targetPlatform)) { return null; } + const version = isString(criteria.version) ? criteria.version : undefined; + for (let index = 0; index < rawGalleryExtensionVersions.length; index++) { const rawGalleryExtensionVersion = rawGalleryExtensionVersions[index]; - if (version && rawGalleryExtensionVersion.version !== version) { - continue; - } - // Allow any version if includePreRelease flag is set otherwise only release versions are allowed if (await this.isValidVersion( extensionIdentifier.id, rawGalleryExtensionVersion, + criteria, rawGalleryExtension.publisher.displayName, - versionType ?? (includePreRelease ? 'any' : 'release'), - criteria.compatible, - allTargetPlatforms, - criteria.targetPlatform, - criteria.productVersion) + allTargetPlatforms) ) { if (criteria.compatible && !this.areApiProposalsCompatible(extensionIdentifier, getEnabledApiProposals(rawGalleryExtensionVersion))) { continue; } - return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, queryContext); + return rawGalleryExtensionVersion; } if (version && rawGalleryExtensionVersion.version === version) { return null; @@ -1113,7 +1156,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi * Fallback: Return the latest version * This can happen when the extension does not have a release version or does not have a version compatible with the given target platform. */ - return toExtension(rawGalleryExtension, rawGalleryExtension.versions[0], allTargetPlatforms); + return rawGalleryExtension.versions[0]; } private async queryRawGalleryExtensions(query: Query, token: CancellationToken): Promise { @@ -1415,17 +1458,21 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } const validVersions: IRawGalleryExtensionVersion[] = []; + const productVersion = { version: this.productService.version, date: this.productService.date }; await Promise.all(galleryExtensions[0].versions.map(async (version) => { try { if ( (await this.isValidVersion( extensionIdentifier.id, version, + { + compatible: true, + productVersion, + targetPlatform, + version: includePreRelease ? VersionKind.Latest : VersionKind.Release + }, galleryExtensions[0].publisher.displayName, - includePreRelease ? 'any' : 'release', - true, - allTargetPlatforms, - targetPlatform)) + allTargetPlatforms)) && this.areApiProposalsCompatible(extensionIdentifier, getEnabledApiProposals(version)) ) { validVersions.push(version); From d593bfefbc6149517be3cdc4e0651ae94d5672e6 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:36:50 +0100 Subject: [PATCH 0470/3587] Rename "Jump" to "Go To" in Gutter Indicator Menu (#237658) goto instead of jump --- .../browser/view/inlineEdits/gutterIndicatorMenu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts index 95168505a402..bfaa95386cfa 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts @@ -55,7 +55,7 @@ export class GutterIndicatorMenuContent { return hoverContent([ // TODO: make header dynamic, get from extension header(localize('inlineEdit', "Inline Edit")), - option(createOptionArgs({ id: 'jump', title: localize('jump', "Jump"), icon: Codicon.arrowRight, commandId: new JumpToNextInlineEdit().id })), + option(createOptionArgs({ id: 'jump', title: localize('goto', "Go To"), icon: Codicon.arrowRight, commandId: new JumpToNextInlineEdit().id })), option(createOptionArgs({ id: 'accept', title: localize('accept', "Accept"), icon: Codicon.check, commandId: new AcceptInlineCompletion().id })), option(createOptionArgs({ id: 'reject', title: localize('reject', "Reject"), icon: Codicon.close, commandId: new HideInlineCompletion().id })), separator(), From af47cbf5c839247388256f55fc23c765d988aa42 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:42:47 +0100 Subject: [PATCH 0471/3587] Show Tab keybindings regardless of applicability (#237660) show Tab keybinding even if not applicable --- .../browser/view/inlineEdits/gutterIndicatorMenu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts index bfaa95386cfa..70160ee12b87 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts @@ -71,7 +71,7 @@ export class GutterIndicatorMenuContent { if (!commandId) { return constObservable(undefined); } - return observableFromEvent(this._contextKeyService.onDidChangeContext, () => this._keybindingService.lookupKeybinding(commandId, this._contextKeyService, true)); + return observableFromEvent(this._contextKeyService.onDidChangeContext, () => this._keybindingService.lookupKeybinding(commandId)); // TODO: use contextkeyservice to use different renderings } } From 55c35c836f7526ec12928baf3592dd8bd72147c3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 10 Jan 2025 07:11:38 -0800 Subject: [PATCH 0472/3587] Automate some pulling completions from upstream --- .eslint-ignore | 1 + build/filters.js | 3 + extensions/terminal-suggest/.gitignore | 1 + .../terminal-suggest/scripts/clone-fig.ps1 | 1 + .../terminal-suggest/scripts/clone-fig.sh | 1 + .../terminal-suggest/scripts/update-specs.js | 16 ++ .../terminal-suggest/scripts/update-specs.ps1 | 1 + .../terminal-suggest/scripts/update-specs.sh | 1 + .../src/completions/index.d.ts | 2 +- .../src/completions/upstream/echo.ts | 42 ++++ .../src/completions/upstream/ls.ts | 227 ++++++++++++++++++ .../src/completions/upstream/mkdir.ts | 37 +++ .../src/completions/upstream/rm.ts | 43 ++++ .../src/completions/upstream/rmdir.ts | 18 ++ .../src/completions/upstream/touch.ts | 59 +++++ extensions/terminal-suggest/src/constants.ts | 13 + .../src/terminalSuggestMain.ts | 12 +- extensions/terminal-suggest/tsconfig.json | 4 + 18 files changed, 479 insertions(+), 3 deletions(-) create mode 100644 extensions/terminal-suggest/.gitignore create mode 100644 extensions/terminal-suggest/scripts/clone-fig.ps1 create mode 100644 extensions/terminal-suggest/scripts/clone-fig.sh create mode 100644 extensions/terminal-suggest/scripts/update-specs.js create mode 100644 extensions/terminal-suggest/scripts/update-specs.ps1 create mode 100644 extensions/terminal-suggest/scripts/update-specs.sh create mode 100644 extensions/terminal-suggest/src/completions/upstream/echo.ts create mode 100644 extensions/terminal-suggest/src/completions/upstream/ls.ts create mode 100644 extensions/terminal-suggest/src/completions/upstream/mkdir.ts create mode 100644 extensions/terminal-suggest/src/completions/upstream/rm.ts create mode 100644 extensions/terminal-suggest/src/completions/upstream/rmdir.ts create mode 100644 extensions/terminal-suggest/src/completions/upstream/touch.ts create mode 100644 extensions/terminal-suggest/src/constants.ts diff --git a/.eslint-ignore b/.eslint-ignore index 12da4a432e15..6fbdf94696e6 100644 --- a/.eslint-ignore +++ b/.eslint-ignore @@ -12,6 +12,7 @@ **/extensions/markdown-math/notebook-out/** **/extensions/notebook-renderers/renderer-out/index.js **/extensions/simple-browser/media/index.js +**/extensions/terminal-suggest/src/completions/** **/extensions/typescript-language-features/test-workspace/** **/extensions/typescript-language-features/extension.webpack.config.js **/extensions/typescript-language-features/extension-browser.webpack.config.js diff --git a/build/filters.js b/build/filters.js index 1705d4b32c33..70e175463dc5 100644 --- a/build/filters.js +++ b/build/filters.js @@ -49,6 +49,7 @@ module.exports.unicodeFilter = [ '!extensions/ipynb/notebook-out/**', '!extensions/notebook-renderers/renderer-out/**', '!extensions/php-language-features/src/features/phpGlobalFunctions.ts', + '!extensions/terminal-suggest/src/completions/upstream/**', '!extensions/typescript-language-features/test-workspace/**', '!extensions/vscode-api-tests/testWorkspace/**', '!extensions/vscode-api-tests/testWorkspace2/**', @@ -88,6 +89,7 @@ module.exports.indentationFilter = [ '!test/automation/out/**', '!test/monaco/out/**', '!test/smoke/out/**', + '!extensions/terminal-suggest/src/completions/upstream/**', '!extensions/typescript-language-features/test-workspace/**', '!extensions/typescript-language-features/resources/walkthroughs/**', '!extensions/typescript-language-features/package-manager/node-maintainer/**', @@ -170,6 +172,7 @@ module.exports.copyrightFilter = [ '!extensions/markdown-math/notebook-out/**', '!extensions/ipynb/notebook-out/**', '!extensions/simple-browser/media/codicon.css', + '!extensions/terminal-suggest/src/completions/upstream/**', '!extensions/typescript-language-features/node-maintainer/**', '!extensions/html-language-features/server/src/modes/typescript/*', '!extensions/*/server/bin/*', diff --git a/extensions/terminal-suggest/.gitignore b/extensions/terminal-suggest/.gitignore new file mode 100644 index 000000000000..76b510c710f0 --- /dev/null +++ b/extensions/terminal-suggest/.gitignore @@ -0,0 +1 @@ +third_party/ diff --git a/extensions/terminal-suggest/scripts/clone-fig.ps1 b/extensions/terminal-suggest/scripts/clone-fig.ps1 new file mode 100644 index 000000000000..11ec13e560eb --- /dev/null +++ b/extensions/terminal-suggest/scripts/clone-fig.ps1 @@ -0,0 +1 @@ +git clone https://github.com/withfig/autocomplete third_party/autocomplete diff --git a/extensions/terminal-suggest/scripts/clone-fig.sh b/extensions/terminal-suggest/scripts/clone-fig.sh new file mode 100644 index 000000000000..11ec13e560eb --- /dev/null +++ b/extensions/terminal-suggest/scripts/clone-fig.sh @@ -0,0 +1 @@ +git clone https://github.com/withfig/autocomplete third_party/autocomplete diff --git a/extensions/terminal-suggest/scripts/update-specs.js b/extensions/terminal-suggest/scripts/update-specs.js new file mode 100644 index 000000000000..3573c6664d6d --- /dev/null +++ b/extensions/terminal-suggest/scripts/update-specs.js @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const fs = require('fs'); +const path = require('path'); + +const upstreamSpecs = require('../out/constants.js').upstreamSpecs; + +const extRoot = path.resolve(path.join(__dirname, '..')); +for (const spec of upstreamSpecs) { + const source = path.join(extRoot, `third_party/autocomplete/src/${spec}.ts`); + const destination = path.join(extRoot, `src/completions/upstream/${spec}.ts`); + fs.copyFileSync(source, destination); +} diff --git a/extensions/terminal-suggest/scripts/update-specs.ps1 b/extensions/terminal-suggest/scripts/update-specs.ps1 new file mode 100644 index 000000000000..0f1291903791 --- /dev/null +++ b/extensions/terminal-suggest/scripts/update-specs.ps1 @@ -0,0 +1 @@ +node "$PSScriptRoot/update-specs.js" diff --git a/extensions/terminal-suggest/scripts/update-specs.sh b/extensions/terminal-suggest/scripts/update-specs.sh new file mode 100644 index 000000000000..4efd5bbf20dc --- /dev/null +++ b/extensions/terminal-suggest/scripts/update-specs.sh @@ -0,0 +1 @@ +node ./update-specs.js diff --git a/extensions/terminal-suggest/src/completions/index.d.ts b/extensions/terminal-suggest/src/completions/index.d.ts index e56afd803ca4..de76233ecb47 100644 --- a/extensions/terminal-suggest/src/completions/index.d.ts +++ b/extensions/terminal-suggest/src/completions/index.d.ts @@ -666,7 +666,7 @@ declare namespace Fig { * }, * ``` */ - generateSpec?: (tokens: string[], executeCommand: ExecuteCommandFunction) => Promise; + generateSpec?: (tokens: string[], executeCommand: ExecuteCommandFunction) => Promise; /** * Generating a spec can be expensive, but due to current guarantees they are not cached. diff --git a/extensions/terminal-suggest/src/completions/upstream/echo.ts b/extensions/terminal-suggest/src/completions/upstream/echo.ts new file mode 100644 index 000000000000..8ca21b858b39 --- /dev/null +++ b/extensions/terminal-suggest/src/completions/upstream/echo.ts @@ -0,0 +1,42 @@ +const environmentVariableGenerator: Fig.Generator = { + custom: async (tokens, _, context) => { + if (tokens.length < 3 || tokens[tokens.length - 1].startsWith("$")) { + return Object.keys(context.environmentVariables).map((suggestion) => ({ + name: `$${suggestion}`, + type: "arg", + description: "Environment Variable", + })); + } else { + return []; + } + }, + trigger: "$", +}; + +const completionSpec: Fig.Spec = { + name: "echo", + description: "Write arguments to the standard output", + args: { + name: "string", + isVariadic: true, + optionsCanBreakVariadicArg: false, + suggestCurrentToken: true, + generators: environmentVariableGenerator, + }, + options: [ + { + name: "-n", + description: "Do not print the trailing newline character", + }, + { + name: "-e", + description: "Interpret escape sequences", + }, + { + name: "-E", + description: "Disable escape sequences", + }, + ], +}; + +export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/ls.ts b/extensions/terminal-suggest/src/completions/upstream/ls.ts new file mode 100644 index 000000000000..91afc0b75ed9 --- /dev/null +++ b/extensions/terminal-suggest/src/completions/upstream/ls.ts @@ -0,0 +1,227 @@ +const completionSpec: Fig.Spec = { + name: "ls", + description: "List directory contents", + args: { + isVariadic: true, + template: ["filepaths", "folders"], + filterStrategy: "fuzzy", + }, + options: [ + { + name: "-@", + description: + "Display extended attribute keys and sizes in long (-l) output", + }, + { + name: "-1", + description: + "(The numeric digit ``one''.) Force output to be one entry per line. This is the default when output is not to a terminal", + }, + { + name: "-A", + description: + "List all entries except for . and ... Always set for the super-user", + }, + { + name: "-a", + description: "Include directory entries whose names begin with a dot (.)", + }, + { + name: "-B", + description: + "Force printing of non-printable characters (as defined by ctype(3) and current locale settings) in file names as xxx, where xxx is the numeric value of the character in octal", + }, + { + name: "-b", + description: "As -B, but use C escape codes whenever possible", + }, + { + name: "-C", + description: + "Force multi-column output; this is the default when output is to a terminal", + }, + { + name: "-c", + description: + "Use time when file status was last changed for sorting (-t) or long printing (-l)", + }, + { + name: "-d", + description: + "Directories are listed as plain files (not searched recursively)", + }, + { + name: "-e", + description: + "Print the Access Control List (ACL) associated with the file, if present, in long (-l) output", + }, + { + name: "-F", + description: + "Display a slash (/) immediately after each pathname that is a directory, an asterisk (*) after each that is executable, an at sign (@) after each symbolic link, an equals sign (=) after each socket, a percent sign (%) after each whiteout, and a vertical bar (|) after each that is a FIFO", + }, + { + name: "-f", + description: "Output is not sorted. This option turns on the -a option", + }, + { + name: "-G", + description: + "Enable colorized output. This option is equivalent to defining CLICOLOR in the environment. (See below.)", + }, + { + name: "-g", + description: + "This option is only available for compatibility with POSIX; it is used to display the group name in the long (-l) format output (the owner name is suppressed)", + }, + { + name: "-H", + description: + "Symbolic links on the command line are followed. This option is assumed if none of the -F, -d, or -l options are specified", + }, + { + name: "-h", + description: + "When used with the -l option, use unit suffixes: Byte, Kilobyte, Megabyte, Gigabyte, Terabyte and Petabyte in order to reduce the number of digits to three or less using base 2 for sizes", + }, + { + name: "-i", + description: + "For each file, print the file's file serial number (inode number)", + }, + { + name: "-k", + description: + "If the -s option is specified, print the file size allocation in kilobytes, not blocks. This option overrides the environment variable BLOCKSIZE", + }, + { + name: "-L", + description: + "Follow all symbolic links to final target and list the file or directory the link references rather than the link itself. This option cancels the -P option", + }, + { + name: "-l", + description: + "(The lowercase letter ``ell''.) List in long format. (See below.) A total sum for all the file sizes is output on a line before the long listing", + }, + { + name: "-m", + description: + "Stream output format; list files across the page, separated by commas", + }, + { + name: "-n", + description: + "Display user and group IDs numerically, rather than converting to a user or group name in a long (-l) output. This option turns on the -l option", + }, + { + name: "-O", + description: "Include the file flags in a long (-l) output", + }, + { name: "-o", description: "List in long format, but omit the group id" }, + { + name: "-P", + description: + "If argument is a symbolic link, list the link itself rather than the object the link references. This option cancels the -H and -L options", + }, + { + name: "-p", + description: + "Write a slash (`/') after each filename if that file is a directory", + }, + { + name: "-q", + description: + "Force printing of non-graphic characters in file names as the character `?'; this is the default when output is to a terminal", + }, + { name: "-R", description: "Recursively list subdirectories encountered" }, + { + name: "-r", + description: + "Reverse the order of the sort to get reverse lexicographical order or the oldest entries first (or largest files last, if combined with sort by size", + }, + { name: "-S", description: "Sort files by size" }, + { + name: "-s", + description: + "Display the number of file system blocks actually used by each file, in units of 512 bytes, where partial units are rounded up to the next integer value. If the output is to a terminal, a total sum for all the file sizes is output on a line before the listing. The environment variable BLOCKSIZE overrides the unit size of 512 bytes", + }, + { + name: "-T", + description: + "When used with the -l (lowercase letter ``ell'') option, display complete time information for the file, including month, day, hour, minute, second, and year", + }, + { + name: "-t", + description: + "Sort by time modified (most recently modified first) before sorting the operands by lexicographical order", + }, + { + name: "-u", + description: + "Use time of last access, instead of last modification of the file for sorting (-t) or long printing (-l)", + }, + { + name: "-U", + description: + "Use time of file creation, instead of last modification for sorting (-t) or long output (-l)", + }, + { + name: "-v", + description: + "Force unedited printing of non-graphic characters; this is the default when output is not to a terminal", + }, + { + name: "-W", + description: "Display whiteouts when scanning directories. (-S) flag)", + }, + { + name: "-w", + description: + "Force raw printing of non-printable characters. This is the default when output is not to a terminal", + }, + { + name: "-x", + description: + "The same as -C, except that the multi-column output is produced with entries sorted across, rather than down, the columns", + }, + { + name: "-%", + description: + "Distinguish dataless files and directories with a '%' character in long (-l) output, and don't materialize dataless directories when listing them", + }, + { + name: "-,", + description: `When the -l option is set, print file sizes grouped and separated by thousands using the non-monetary separator returned +by localeconv(3), typically a comma or period. If no locale is set, or the locale does not have a non-monetary separator, this +option has no effect. This option is not defined in IEEE Std 1003.1-2001 (“POSIX.1”)`, + dependsOn: ["-l"], + }, + { + name: "--color", + description: `Output colored escape sequences based on when, which may be set to either always, auto, or never`, + requiresSeparator: true, + args: { + name: "when", + suggestions: [ + { + name: ["always", "yes", "force"], + description: "Will make ls always output color", + }, + { + name: "auto", + description: + "Will make ls output escape sequences based on termcap(5), but only if stdout is a tty and either the -G flag is specified or the COLORTERM environment variable is set and not empty", + }, + { + name: ["never", "no", "none"], + description: + "Will disable color regardless of environment variables", + }, + ], + }, + }, + ], +}; + +export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/mkdir.ts b/extensions/terminal-suggest/src/completions/upstream/mkdir.ts new file mode 100644 index 000000000000..90a6530c3bda --- /dev/null +++ b/extensions/terminal-suggest/src/completions/upstream/mkdir.ts @@ -0,0 +1,37 @@ +const completionSpec: Fig.Spec = { + name: "mkdir", + description: "Make directories", + args: { + name: "directory name", + template: "folders", + suggestCurrentToken: true, + }, + options: [ + { + name: ["-m", "--mode"], + description: "Set file mode (as in chmod), not a=rwx - umask", + args: { name: "MODE" }, + }, + { + name: ["-p", "--parents"], + description: "No error if existing, make parent directories as needed", + }, + { + name: ["-v", "--verbose"], + description: "Print a message for each created directory", + }, + { + name: ["-Z", "--context"], + description: + "Set the SELinux security context of each created directory to CTX", + args: { name: "CTX" }, + }, + { name: "--help", description: "Display this help and exit" }, + { + name: "--version", + description: "Output version information and exit", + }, + ], +}; + +export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/rm.ts b/extensions/terminal-suggest/src/completions/upstream/rm.ts new file mode 100644 index 000000000000..7b52f909527a --- /dev/null +++ b/extensions/terminal-suggest/src/completions/upstream/rm.ts @@ -0,0 +1,43 @@ +const completionSpec: Fig.Spec = { + name: "rm", + description: "Remove directory entries", + args: { + isVariadic: true, + template: ["folders", "filepaths"], + }, + + options: [ + { + name: ["-r", "-R"], + description: + "Recursive. Attempt to remove the file hierarchy rooted in each file argument", + isDangerous: true, + }, + { + name: "-P", + description: "Overwrite regular files before deleting them", + isDangerous: true, + }, + { + name: "-d", + description: + "Attempt to remove directories as well as other types of files", + }, + { + name: "-f", + description: + "⚠️ Attempt to remove the files without prompting for confirmation", + isDangerous: true, + }, + { + name: "-i", + description: "Request confirmation before attempting to remove each file", + }, + { + name: "-v", + description: "Be verbose when deleting files", + }, + ], +}; + +export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/rmdir.ts b/extensions/terminal-suggest/src/completions/upstream/rmdir.ts new file mode 100644 index 000000000000..92790e75d0dd --- /dev/null +++ b/extensions/terminal-suggest/src/completions/upstream/rmdir.ts @@ -0,0 +1,18 @@ +const completionSpec: Fig.Spec = { + name: "rmdir", + description: "Remove directories", + args: { + isVariadic: true, + template: "folders", + }, + + options: [ + { + name: "-p", + description: "Remove each directory of path", + isDangerous: true, + }, + ], +}; + +export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/touch.ts b/extensions/terminal-suggest/src/completions/upstream/touch.ts new file mode 100644 index 000000000000..452088783139 --- /dev/null +++ b/extensions/terminal-suggest/src/completions/upstream/touch.ts @@ -0,0 +1,59 @@ +const completionSpec: Fig.Spec = { + name: "touch", + description: "Change file access and modification times", + args: { + name: "file", + isVariadic: true, + template: "folders", + suggestCurrentToken: true, + }, + options: [ + { + name: "-A", + description: + "Adjust the access and modification time stamps for the file by the specified value", + args: { + name: "time", + description: "[-][[hh]mm]SS", + }, + }, + { name: "-a", description: "Change the access time of the file" }, + { + name: "-c", + description: "Do not create the file if it does not exist", + }, + { + name: "-f", + description: + "Attempt to force the update, even if the file permissions do not currently permit it", + }, + { + name: "-h", + description: + "If the file is a symbolic link, change the times of the link itself rather than the file that the link points to", + }, + { + name: "-m", + description: "Change the modification time of the file", + }, + { + name: "-r", + description: + "Use the access and modifications times from the specified file instead of the current time of day", + args: { + name: "file", + }, + }, + { + name: "-t", + description: + "Change the access and modification times to the specified time instead of the current time of day", + args: { + name: "timestamp", + description: "[[CC]YY]MMDDhhmm[.SS]", + }, + }, + ], +}; + +export default completionSpec; diff --git a/extensions/terminal-suggest/src/constants.ts b/extensions/terminal-suggest/src/constants.ts new file mode 100644 index 000000000000..37d08189da65 --- /dev/null +++ b/extensions/terminal-suggest/src/constants.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const upstreamSpecs = [ + 'echo', + 'ls', + 'mkdir', + 'rm', + 'rmdir', + 'touch', +]; diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index f146f932372e..4ed1a26fe6d3 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -7,15 +7,23 @@ import * as os from 'os'; import * as fs from 'fs/promises'; import * as path from 'path'; import { ExecOptionsWithStringEncoding, execSync } from 'child_process'; -import codeInsidersCompletionSpec from './completions/code-insiders'; +import { upstreamSpecs } from './constants'; import codeCompletionSpec from './completions/code'; import cdSpec from './completions/cd'; +import codeInsidersCompletionSpec from './completions/code-insiders'; let cachedAvailableCommandsPath: string | undefined; let cachedAvailableCommands: Set | undefined; const cachedBuiltinCommands: Map = new Map(); -export const availableSpecs = [codeCompletionSpec, codeInsidersCompletionSpec, cdSpec]; +export const availableSpecs: Fig.Spec[] = [ + cdSpec, + codeInsidersCompletionSpec, + codeCompletionSpec, +]; +for (const spec of upstreamSpecs) { + availableSpecs.push(require(`./completions/upstream/${spec}`).default); +} function getBuiltinCommands(shell: string): string[] | undefined { try { diff --git a/extensions/terminal-suggest/tsconfig.json b/extensions/terminal-suggest/tsconfig.json index df4e9764caf1..f3d3aa73975c 100644 --- a/extensions/terminal-suggest/tsconfig.json +++ b/extensions/terminal-suggest/tsconfig.json @@ -11,6 +11,10 @@ "strict": true, "esModuleInterop": true, "skipLibCheck": true, + + // Needed to suppress warnings in upstream completions + "noImplicitReturns": false, + "noUnusedParameters": false }, "include": [ "src/**/*", From ff5cbfc70e6996e2d567ae750ab786a75bf43abc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 10 Jan 2025 08:28:30 -0800 Subject: [PATCH 0473/3587] Don't draw ruler behind line numbers Fixes #237667 --- src/vs/editor/browser/gpu/rectangleRenderer.ts | 7 +++++++ src/vs/editor/browser/gpu/viewGpuContext.ts | 14 +++++++++++--- .../browser/viewParts/viewLinesGpu/viewLinesGpu.ts | 14 ++++---------- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/vs/editor/browser/gpu/rectangleRenderer.ts b/src/vs/editor/browser/gpu/rectangleRenderer.ts index d0e087dbbc8a..5be1db2f1623 100644 --- a/src/vs/editor/browser/gpu/rectangleRenderer.ts +++ b/src/vs/editor/browser/gpu/rectangleRenderer.ts @@ -6,6 +6,7 @@ import { getActiveWindow } from '../../../base/browser/dom.js'; import { Event } from '../../../base/common/event.js'; import { IReference, MutableDisposable } from '../../../base/common/lifecycle.js'; +import type { IObservable } from '../../../base/common/observable.js'; import { EditorOption } from '../../common/config/editorOptions.js'; import { ViewEventHandler } from '../../common/viewEventHandler.js'; import type { ViewScrollChangedEvent } from '../../common/viewEvents.js'; @@ -56,6 +57,8 @@ export class RectangleRenderer extends ViewEventHandler { constructor( private readonly _context: ViewContext, + private readonly _contentLeft: IObservable, + private readonly _devicePixelRatio: IObservable, private readonly _canvas: HTMLCanvasElement, private readonly _ctx: GPUCanvasContext, device: Promise, @@ -281,6 +284,10 @@ export class RectangleRenderer extends ViewEventHandler { pass.setVertexBuffer(0, this._vertexBuffer); pass.setBindGroup(0, this._bindGroup); + // Only draw the content area + const contentLeft = Math.ceil(this._contentLeft.get() * this._devicePixelRatio.get()); + pass.setScissorRect(contentLeft, 0, this._canvas.width - contentLeft, this._canvas.height); + pass.draw(quadVertices.length / 2, this._shapeCollection.entryCount); pass.end(); diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 54e4a4892bd8..60db4cdd04db 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -21,7 +21,7 @@ import { RectangleRenderer } from './rectangleRenderer.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; import { DecorationCssRuleExtractor } from './decorationCssRuleExtractor.js'; import { Event } from '../../../base/common/event.js'; -import type { IEditorOptions } from '../../common/config/editorOptions.js'; +import { EditorOption, type IEditorOptions } from '../../common/config/editorOptions.js'; import { InlineDecorationType } from '../../common/viewModel.js'; const enum GpuRenderLimits { @@ -79,6 +79,7 @@ export class ViewGpuContext extends Disposable { readonly canvasDevicePixelDimensions: IObservable<{ width: number; height: number }>; readonly devicePixelRatio: IObservable; + readonly contentLeft: IObservable; constructor( context: ViewContext, @@ -115,8 +116,6 @@ export class ViewGpuContext extends Disposable { } }); - this.rectangleRenderer = this._instantiationService.createInstance(RectangleRenderer, context, this.canvas.domNode, this.ctx, this.device); - const dprObs = observableValue(this, getActiveWindow().devicePixelRatio); this._register(addDisposableListener(getActiveWindow(), 'resize', () => { dprObs.set(getActiveWindow().devicePixelRatio, undefined); @@ -135,6 +134,15 @@ export class ViewGpuContext extends Disposable { } )); this.canvasDevicePixelDimensions = canvasDevicePixelDimensions; + + const contentLeft = observableValue(this, 0); + this._register(this.configurationService.onDidChangeConfiguration(e => { + contentLeft.set(context.configuration.options.get(EditorOption.layoutInfo).contentLeft, undefined); + })); + this.contentLeft = contentLeft; + + + this.rectangleRenderer = this._instantiationService.createInstance(RectangleRenderer, context, this.contentLeft, this.devicePixelRatio, this.canvas.domNode, this.ctx, this.device); } /** diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index 678dec6ab99e..7be81a4dc999 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -5,7 +5,7 @@ import { getActiveWindow } from '../../../../base/browser/dom.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; -import { autorun, observableValue, runOnChange } from '../../../../base/common/observable.js'; +import { autorun, runOnChange } from '../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; @@ -61,8 +61,6 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { private _renderStrategy!: IGpuRenderStrategy; - private _contentLeftObs = observableValue('contentLeft', 0); - constructor( context: ViewContext, private readonly _viewGpuContext: ViewGpuContext, @@ -160,7 +158,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { this._register(runOnChange(this._viewGpuContext.canvasDevicePixelDimensions, ({ width, height }) => { this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, updateBufferValues(width, height)); })); - this._register(runOnChange(this._contentLeftObs, () => { + this._register(runOnChange(this._viewGpuContext.contentLeft, () => { this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, updateBufferValues()); })); } @@ -381,6 +379,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { // from that side. Luckily rendering is cheap, it's only when uploaded data changes does it // start to cost. + override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { return true; } override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { return true; } override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { return true; } override onFlushed(e: viewEvents.ViewFlushedEvent): boolean { return true; } @@ -394,11 +393,6 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { override onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { return true; } override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { return true; } - override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { - this._contentLeftObs.set(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft, undefined); - return true; - } - // #endregion public renderText(viewportData: ViewportData): void { @@ -427,7 +421,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { pass.setVertexBuffer(0, this._vertexBuffer); // Only draw the content area - const contentLeft = Math.ceil(this._contentLeftObs.get() * this._viewGpuContext.devicePixelRatio.get()); + const contentLeft = Math.ceil(this._viewGpuContext.contentLeft.get() * this._viewGpuContext.devicePixelRatio.get()); pass.setScissorRect(contentLeft, 0, this.canvas.width - contentLeft, this.canvas.height); pass.setBindGroup(0, this._bindGroup); From a514d865ee12b689bcf726fd5b3f0a5739f49776 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 10 Jan 2025 17:32:50 +0100 Subject: [PATCH 0474/3587] Fix NES UI cutoff to the right of the editor (#237668) fixes https://github.com/microsoft/vscode-copilot/issues/11582 --- .../browser/view/inlineEdits/sideBySideDiff.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index 38bf37a2698f..2235a94e05bf 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -388,8 +388,8 @@ export class InlineEditsSideBySideDiff extends Disposable { const editorContentAreaWidth = editorLayout.contentWidth - editorLayout.verticalScrollbarWidth; const editorBoundingClientRect = this._editor.getContainerDomNode().getBoundingClientRect(); const clientContentAreaRight = editorLayout.contentLeft + editorLayout.contentWidth + editorBoundingClientRect.left; - const remainingWidthRightOfContent = getWindow(this._editor.getContainerDomNode()).outerWidth - clientContentAreaRight; - const remainingWidthRightOfEditor = getWindow(this._editor.getContainerDomNode()).outerWidth - editorBoundingClientRect.right; + const remainingWidthRightOfContent = getWindow(this._editor.getContainerDomNode()).innerWidth - clientContentAreaRight; + const remainingWidthRightOfEditor = getWindow(this._editor.getContainerDomNode()).innerWidth - editorBoundingClientRect.right; const desiredMinimumWidth = Math.min(editorLayout.contentWidth * 0.3, previewContentWidth, 100); const IN_EDITOR_DISPLACEMENT = 0; const maximumAvailableWidth = IN_EDITOR_DISPLACEMENT + remainingWidthRightOfContent; From 2b4d91e3649548860e4012d530cf5886ea0e7dbe Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 10 Jan 2025 08:46:17 -0800 Subject: [PATCH 0475/3587] fix: confirm before deleting file with pending edits (#237670) --- .../browser/chatEditing/chatEditingActions.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 9e4102b58384..d24387e023e9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -107,7 +107,27 @@ registerAction2(class RemoveFileFromWorkingSet extends WorkingSetAction { } async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, chatWidget: IChatWidget, ...uris: URI[]): Promise { + const dialogService = accessor.get(IDialogService); + + const pendingEntries = currentEditingSession.entries.get().filter((entry) => uris.includes(entry.modifiedURI) && entry.state.get() === WorkingSetEntryState.Modified); + if (pendingEntries.length > 0) { + // Ask for confirmation if there are any pending edits + const file = pendingEntries.length > 1 + ? localize('chat.editing.removeFile.confirmationmanyFiles', "{0} files", pendingEntries.length) + : basename(pendingEntries[0].modifiedURI); + const confirmation = await dialogService.confirm({ + title: localize('chat.editing.removeFile.confirmation.title', "Remove {0} from working set?", file), + message: localize('chat.editing.removeFile.confirmation.message', "This will remove {0} from your working set and undo the edits made to it. Do you want to proceed?", file), + primaryButton: localize('chat.editing.removeFile.confirmation.primaryButton', "Yes"), + type: 'info' + }); + if (!confirmation.confirmed) { + return; + } + } + // Remove from working set + await currentEditingSession.reject(...uris); currentEditingSession.remove(WorkingSetEntryRemovalReason.User, ...uris); // Remove from chat input part From 672240b1f1d9e54c6424e362697b07eda8c16835 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 10 Jan 2025 18:04:08 +0100 Subject: [PATCH 0476/3587] Redo is a no-op after undo of file creation (fix #236984) (#237674) --- src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index 237b768fbd42..858d20cd458e 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -255,15 +255,16 @@ class DeleteOperation implements IFileOperation { // read file contents for undo operation. when a file is too large it won't be restored let fileContent: IFileContent | undefined; - if (!edit.undoesCreate && !edit.options.folder && !(typeof edit.options.maxSize === 'number' && fileStat.size > edit.options.maxSize)) { + const isSizeLimitExceeded = typeof edit.options.maxSize === 'number' && fileStat.size > edit.options.maxSize; + if (!edit.undoesCreate && !edit.options.folder && !isSizeLimitExceeded) { try { fileContent = await this._fileService.readFile(edit.oldUri); } catch (err) { this._logService.error(err); } } - if (fileContent !== undefined) { - undoes.push(new CreateEdit(edit.oldUri, edit.options, fileContent.value)); + if (!fileContent || !isSizeLimitExceeded) { + undoes.push(new CreateEdit(edit.oldUri, edit.options, fileContent?.value)); } } From 2f35e8d7ce847a64d59001fa3b46c318b8944c49 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:13:56 -0800 Subject: [PATCH 0477/3587] Set up for font-weight support on GPU renderer Part of #237584 --- src/vs/base/common/color.ts | 10 ++-- src/vs/base/test/common/color.test.ts | 60 +++++++++---------- .../browser/gpu/fullFileRenderStrategy.ts | 21 ++++++- .../browser/gpu/raster/glyphRasterizer.ts | 6 +- src/vs/editor/browser/gpu/viewGpuContext.ts | 20 +++++-- 5 files changed, 74 insertions(+), 43 deletions(-) diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts index 8b1a68294a50..9f20f7080ab5 100644 --- a/src/vs/base/common/color.ts +++ b/src/vs/base/common/color.ts @@ -535,17 +535,17 @@ export class Color { return this._toString; } - private _toNumber24Bit?: number; - toNumber24Bit(): number { - if (!this._toNumber24Bit) { - this._toNumber24Bit = ( + private _toNumber32Bit?: number; + toNumber32Bit(): number { + if (!this._toNumber32Bit) { + this._toNumber32Bit = ( this.rgba.r /* */ << 24 | this.rgba.g /* */ << 16 | this.rgba.b /* */ << 8 | this.rgba.a * 0xFF << 0 ) >>> 0; } - return this._toNumber24Bit; + return this._toNumber32Bit; } static getLighterColor(of: Color, relative: Color, factor?: number): Color { diff --git a/src/vs/base/test/common/color.test.ts b/src/vs/base/test/common/color.test.ts index c0f439d744ed..116ebba41191 100644 --- a/src/vs/base/test/common/color.test.ts +++ b/src/vs/base/test/common/color.test.ts @@ -118,40 +118,40 @@ suite('Color', () => { }); }); - suite('toNumber24Bit', () => { + suite('toNumber32Bit', () => { test('alpha channel', () => { - assert.deepStrictEqual(Color.fromHex('#00000000').toNumber24Bit(), 0x00000000); - assert.deepStrictEqual(Color.fromHex('#00000080').toNumber24Bit(), 0x00000080); - assert.deepStrictEqual(Color.fromHex('#000000FF').toNumber24Bit(), 0x000000FF); + assert.deepStrictEqual(Color.fromHex('#00000000').toNumber32Bit(), 0x00000000); + assert.deepStrictEqual(Color.fromHex('#00000080').toNumber32Bit(), 0x00000080); + assert.deepStrictEqual(Color.fromHex('#000000FF').toNumber32Bit(), 0x000000FF); }); test('opaque', () => { - assert.deepStrictEqual(Color.fromHex('#000000').toNumber24Bit(), 0x000000FF); - assert.deepStrictEqual(Color.fromHex('#FFFFFF').toNumber24Bit(), 0xFFFFFFFF); - assert.deepStrictEqual(Color.fromHex('#FF0000').toNumber24Bit(), 0xFF0000FF); - assert.deepStrictEqual(Color.fromHex('#00FF00').toNumber24Bit(), 0x00FF00FF); - assert.deepStrictEqual(Color.fromHex('#0000FF').toNumber24Bit(), 0x0000FFFF); - assert.deepStrictEqual(Color.fromHex('#FFFF00').toNumber24Bit(), 0xFFFF00FF); - assert.deepStrictEqual(Color.fromHex('#00FFFF').toNumber24Bit(), 0x00FFFFFF); - assert.deepStrictEqual(Color.fromHex('#FF00FF').toNumber24Bit(), 0xFF00FFFF); - assert.deepStrictEqual(Color.fromHex('#C0C0C0').toNumber24Bit(), 0xC0C0C0FF); - assert.deepStrictEqual(Color.fromHex('#808080').toNumber24Bit(), 0x808080FF); - assert.deepStrictEqual(Color.fromHex('#800000').toNumber24Bit(), 0x800000FF); - assert.deepStrictEqual(Color.fromHex('#808000').toNumber24Bit(), 0x808000FF); - assert.deepStrictEqual(Color.fromHex('#008000').toNumber24Bit(), 0x008000FF); - assert.deepStrictEqual(Color.fromHex('#800080').toNumber24Bit(), 0x800080FF); - assert.deepStrictEqual(Color.fromHex('#008080').toNumber24Bit(), 0x008080FF); - assert.deepStrictEqual(Color.fromHex('#000080').toNumber24Bit(), 0x000080FF); - assert.deepStrictEqual(Color.fromHex('#010203').toNumber24Bit(), 0x010203FF); - assert.deepStrictEqual(Color.fromHex('#040506').toNumber24Bit(), 0x040506FF); - assert.deepStrictEqual(Color.fromHex('#070809').toNumber24Bit(), 0x070809FF); - assert.deepStrictEqual(Color.fromHex('#0a0A0a').toNumber24Bit(), 0x0a0A0aFF); - assert.deepStrictEqual(Color.fromHex('#0b0B0b').toNumber24Bit(), 0x0b0B0bFF); - assert.deepStrictEqual(Color.fromHex('#0c0C0c').toNumber24Bit(), 0x0c0C0cFF); - assert.deepStrictEqual(Color.fromHex('#0d0D0d').toNumber24Bit(), 0x0d0D0dFF); - assert.deepStrictEqual(Color.fromHex('#0e0E0e').toNumber24Bit(), 0x0e0E0eFF); - assert.deepStrictEqual(Color.fromHex('#0f0F0f').toNumber24Bit(), 0x0f0F0fFF); - assert.deepStrictEqual(Color.fromHex('#a0A0a0').toNumber24Bit(), 0xa0A0a0FF); + assert.deepStrictEqual(Color.fromHex('#000000').toNumber32Bit(), 0x000000FF); + assert.deepStrictEqual(Color.fromHex('#FFFFFF').toNumber32Bit(), 0xFFFFFFFF); + assert.deepStrictEqual(Color.fromHex('#FF0000').toNumber32Bit(), 0xFF0000FF); + assert.deepStrictEqual(Color.fromHex('#00FF00').toNumber32Bit(), 0x00FF00FF); + assert.deepStrictEqual(Color.fromHex('#0000FF').toNumber32Bit(), 0x0000FFFF); + assert.deepStrictEqual(Color.fromHex('#FFFF00').toNumber32Bit(), 0xFFFF00FF); + assert.deepStrictEqual(Color.fromHex('#00FFFF').toNumber32Bit(), 0x00FFFFFF); + assert.deepStrictEqual(Color.fromHex('#FF00FF').toNumber32Bit(), 0xFF00FFFF); + assert.deepStrictEqual(Color.fromHex('#C0C0C0').toNumber32Bit(), 0xC0C0C0FF); + assert.deepStrictEqual(Color.fromHex('#808080').toNumber32Bit(), 0x808080FF); + assert.deepStrictEqual(Color.fromHex('#800000').toNumber32Bit(), 0x800000FF); + assert.deepStrictEqual(Color.fromHex('#808000').toNumber32Bit(), 0x808000FF); + assert.deepStrictEqual(Color.fromHex('#008000').toNumber32Bit(), 0x008000FF); + assert.deepStrictEqual(Color.fromHex('#800080').toNumber32Bit(), 0x800080FF); + assert.deepStrictEqual(Color.fromHex('#008080').toNumber32Bit(), 0x008080FF); + assert.deepStrictEqual(Color.fromHex('#000080').toNumber32Bit(), 0x000080FF); + assert.deepStrictEqual(Color.fromHex('#010203').toNumber32Bit(), 0x010203FF); + assert.deepStrictEqual(Color.fromHex('#040506').toNumber32Bit(), 0x040506FF); + assert.deepStrictEqual(Color.fromHex('#070809').toNumber32Bit(), 0x070809FF); + assert.deepStrictEqual(Color.fromHex('#0a0A0a').toNumber32Bit(), 0x0a0A0aFF); + assert.deepStrictEqual(Color.fromHex('#0b0B0b').toNumber32Bit(), 0x0b0B0bFF); + assert.deepStrictEqual(Color.fromHex('#0c0C0c').toNumber32Bit(), 0x0c0C0cFF); + assert.deepStrictEqual(Color.fromHex('#0d0D0d').toNumber32Bit(), 0x0d0D0dFF); + assert.deepStrictEqual(Color.fromHex('#0e0E0e').toNumber32Bit(), 0x0e0E0eFF); + assert.deepStrictEqual(Color.fromHex('#0f0F0f').toNumber32Bit(), 0x0f0F0fFF); + assert.deepStrictEqual(Color.fromHex('#a0A0a0').toNumber32Bit(), 0xa0A0a0FF); }); }); diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index a1d5358123e1..c7a708734639 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -386,7 +386,16 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend if (!parsedColor) { throw new BugIndicatingError('Invalid color format ' + value); } - charMetadata = parsedColor.toNumber24Bit(); + charMetadata = parsedColor.toNumber32Bit(); + break; + } + case 'font-weight': { + const parsedValue = parseCssFontWeight(value); + if (parsedValue >= 400) { + // TODO: Set bold (https://github.com/microsoft/vscode/issues/237584) + } else { + // TODO: Set normal (https://github.com/microsoft/vscode/issues/237584) + } break; } default: throw new BugIndicatingError('Unexpected inline decoration style'); @@ -485,3 +494,13 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend this._queuedBufferUpdates[1].push(e); } } + +function parseCssFontWeight(value: string) { + switch (value) { + case 'lighter': + case 'normal': return 400; + case 'bolder': + case 'bold': return 700; + } + return parseInt(value); +} diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 159c3ad46c97..6287bfb8d3b3 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -94,7 +94,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { public _rasterizeGlyph( chars: string, - metadata: number, + tokenMetadata: number, charMetadata: number, colorMap: string[], ): Readonly { @@ -109,7 +109,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); const fontSb = new StringBuilder(200); - const fontStyle = TokenMetadata.getFontStyle(metadata); + const fontStyle = TokenMetadata.getFontStyle(tokenMetadata); if (fontStyle & FontStyle.Italic) { fontSb.appendString('italic '); } @@ -128,7 +128,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { if (charMetadata) { this._ctx.fillStyle = `#${charMetadata.toString(16).padStart(8, '0')}`; } else { - this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(metadata)]; + this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(tokenMetadata)]; } this._ctx.textBaseline = 'top'; diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 54e4a4892bd8..2670da06b3f6 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -170,7 +170,7 @@ export class ViewGpuContext extends Disposable { return false; } for (const r of rule.style) { - if (!gpuSupportedDecorationCssRules.includes(r)) { + if (!supportsCssRule(r, rule.style)) { return false; } } @@ -220,8 +220,8 @@ export class ViewGpuContext extends Disposable { return false; } for (const r of rule.style) { - if (!gpuSupportedDecorationCssRules.includes(r)) { - problemRules.push(r); + if (!supportsCssRule(r, rule.style)) { + problemRules.push(`${r}: ${rule.style[r as any]}`); return false; } } @@ -249,8 +249,20 @@ export class ViewGpuContext extends Disposable { } /** - * A list of fully supported decoration CSS rules that can be used in the GPU renderer. + * A list of supported decoration CSS rules that can be used in the GPU renderer. */ const gpuSupportedDecorationCssRules = [ 'color', + // TODO: https://github.com/microsoft/vscode/issues/237584 + // 'font-weight', ]; + +function supportsCssRule(rule: string, style: CSSStyleDeclaration) { + if (!gpuSupportedDecorationCssRules.includes(rule)) { + return false; + } + // Check for values that aren't supported + switch (rule) { + default: return true; + } +} From 88e6face01795b161523824ba075444fad55466a Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 10 Jan 2025 18:15:49 +0100 Subject: [PATCH 0478/3587] chore - remove listener "GC'ed before dispose" tracking because we now have this for all disposables (#237656) --- src/vs/base/common/event.ts | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index d49f2ef276f4..fe10aaf1c892 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -13,12 +13,6 @@ import { IObservable, IObservableWithChange, IObserver } from './observable.js'; import { StopWatch } from './stopwatch.js'; import { MicrotaskDelay } from './symbols.js'; -// ----------------------------------------------------------------------------------------------------------------------- -// Uncomment the next line to print warnings whenever a listener is GC'ed without having been disposed. This is a LEAK. -// ----------------------------------------------------------------------------------------------------------------------- -const _enableListenerGCedWarning = false - // || Boolean("TRUE") // causes a linter warning so that it cannot be pushed - ; // ----------------------------------------------------------------------------------------------------------------------- // Uncomment the next line to print warnings whenever an emitter with listeners is disposed. That is a sign of code smell. @@ -984,28 +978,6 @@ const forEachListener = (listeners: ListenerOrListeners, fn: (c: ListenerC } }; - -let _listenerFinalizers: FinalizationRegistry | undefined; - -if (_enableListenerGCedWarning) { - const leaks: string[] = []; - - setInterval(() => { - if (leaks.length === 0) { - return; - } - console.warn('[LEAKING LISTENERS] GC\'ed these listeners that were NOT yet disposed:'); - console.warn(leaks.join('\n')); - leaks.length = 0; - }, 3000); - - _listenerFinalizers = new FinalizationRegistry(heldValue => { - if (typeof heldValue === 'string') { - leaks.push(heldValue); - } - }); -} - /** * The Emitter can be used to expose an Event to the public * to fire it from the insides. @@ -1161,7 +1133,6 @@ export class Emitter { const result = toDisposable(() => { - _listenerFinalizers?.unregister(result); removeMonitor?.(); this._removeListener(contained); }); @@ -1171,12 +1142,6 @@ export class Emitter { disposables.push(result); } - if (_listenerFinalizers) { - const stack = new Error().stack!.split('\n').slice(2, 3).join('\n').trim(); - const match = /(file:|vscode-file:\/\/vscode-app)?(\/[^:]*:\d+:\d+)/.exec(stack); - _listenerFinalizers.register(result, match?.[2] ?? stack, result); - } - return result; }; From 4d68b698c74d7ad41308463ca891892a456798c8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:33:26 -0800 Subject: [PATCH 0479/3587] Create decoration style set that holds css styles Part of #234473 --- src/vs/editor/browser/gpu/atlas/atlas.ts | 2 +- .../editor/browser/gpu/atlas/textureAtlas.ts | 20 ++++----- .../browser/gpu/atlas/textureAtlasPage.ts | 12 +++--- .../{ => css}/decorationCssRuleExtractor.ts | 4 +- .../browser/gpu/css/decorationStyleCache.ts | 42 +++++++++++++++++++ .../media/decorationCssRuleExtractor.css | 0 .../browser/gpu/fullFileRenderStrategy.ts | 8 ++-- .../browser/gpu/raster/glyphRasterizer.ts | 19 +++++---- src/vs/editor/browser/gpu/raster/raster.ts | 4 +- src/vs/editor/browser/gpu/viewGpuContext.ts | 8 +++- .../gpu/decorationCssRulerExtractor.test.ts | 2 +- 11 files changed, 86 insertions(+), 35 deletions(-) rename src/vs/editor/browser/gpu/{ => css}/decorationCssRuleExtractor.ts (94%) create mode 100644 src/vs/editor/browser/gpu/css/decorationStyleCache.ts rename src/vs/editor/browser/gpu/{ => css}/media/decorationCssRuleExtractor.css (100%) diff --git a/src/vs/editor/browser/gpu/atlas/atlas.ts b/src/vs/editor/browser/gpu/atlas/atlas.ts index f4d87f2ae2ae..42d1eacaa891 100644 --- a/src/vs/editor/browser/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/gpu/atlas/atlas.ts @@ -106,4 +106,4 @@ export const enum UsagePreviewColors { Restricted = '#FF000088', } -export type GlyphMap = FourKeyMap; +export type GlyphMap = FourKeyMap; diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts index 9f527081e440..472f39e67098 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts @@ -110,7 +110,7 @@ export class TextureAtlas extends Disposable { this._onDidDeleteGlyphs.fire(); } - getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly { + getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly { // TODO: Encode font size and family into key // Ignore metadata that doesn't affect the glyph tokenMetadata &= ~(MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK); @@ -122,27 +122,27 @@ export class TextureAtlas extends Disposable { } // Try get the glyph, overflowing to a new page if necessary - return this._tryGetGlyph(this._glyphPageIndex.get(chars, tokenMetadata, charMetadata, rasterizer.cacheKey) ?? 0, rasterizer, chars, tokenMetadata, charMetadata); + return this._tryGetGlyph(this._glyphPageIndex.get(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey) ?? 0, rasterizer, chars, tokenMetadata, decorationStyleSetId); } - private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly { - this._glyphPageIndex.set(chars, tokenMetadata, charMetadata, rasterizer.cacheKey, pageIndex); + private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly { + this._glyphPageIndex.set(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey, pageIndex); return ( - this._pages[pageIndex].getGlyph(rasterizer, chars, tokenMetadata, charMetadata) + this._pages[pageIndex].getGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId) ?? (pageIndex + 1 < this._pages.length - ? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, tokenMetadata, charMetadata) + ? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, tokenMetadata, decorationStyleSetId) : undefined) - ?? this._getGlyphFromNewPage(rasterizer, chars, tokenMetadata, charMetadata) + ?? this._getGlyphFromNewPage(rasterizer, chars, tokenMetadata, decorationStyleSetId) ); } - private _getGlyphFromNewPage(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly { + private _getGlyphFromNewPage(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly { if (this._pages.length >= TextureAtlas.maximumPageCount) { throw new Error(`Attempt to create a texture atlas page past the limit ${TextureAtlas.maximumPageCount}`); } this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, this._pages.length, this.pageSize, this._allocatorType)); - this._glyphPageIndex.set(chars, tokenMetadata, charMetadata, rasterizer.cacheKey, this._pages.length - 1); - return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, tokenMetadata, charMetadata)!; + this._glyphPageIndex.set(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey, this._pages.length - 1); + return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId)!; } public getUsagePreview(): Promise { diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts index 9c09181751a7..4c48a3f70e22 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts @@ -65,20 +65,20 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla })); } - public getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly | undefined { + public getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly | undefined { // IMPORTANT: There are intentionally no intermediate variables here to aid in runtime // optimization as it's a very hot function - return this._glyphMap.get(chars, tokenMetadata, charMetadata, rasterizer.cacheKey) ?? this._createGlyph(rasterizer, chars, tokenMetadata, charMetadata); + return this._glyphMap.get(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey) ?? this._createGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId); } - private _createGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly | undefined { + private _createGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly | undefined { // Ensure the glyph can fit on the page if (this._glyphInOrderSet.size >= TextureAtlasPage.maximumGlyphCount) { return undefined; } // Rasterize and allocate the glyph - const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, tokenMetadata, charMetadata, this._colorMap); + const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, tokenMetadata, decorationStyleSetId, this._colorMap); const glyph = this._allocator.allocate(rasterizedGlyph); // Ensure the glyph was allocated @@ -89,7 +89,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla } // Save the glyph - this._glyphMap.set(chars, tokenMetadata, charMetadata, rasterizer.cacheKey, glyph); + this._glyphMap.set(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey, glyph); this._glyphInOrderSet.add(glyph); // Update page version and it's tracked used area @@ -101,7 +101,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla this._logService.trace('New glyph', { chars, tokenMetadata, - charMetadata, + decorationStyleSetId, rasterizedGlyph, glyph }); diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts similarity index 94% rename from src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts rename to src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts index ae36165b2e6f..43dbd85c87ac 100644 --- a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts +++ b/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, getActiveDocument } from '../../../base/browser/dom.js'; -import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { $, getActiveDocument } from '../../../../base/browser/dom.js'; +import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; import './media/decorationCssRuleExtractor.css'; /** diff --git a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts new file mode 100644 index 000000000000..0b4929103e8a --- /dev/null +++ b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface IDecorationStyleSet { + /** + * A 24-bit number representing `color`. + */ + color: number; +} + +export interface IDecorationStyleCacheEntry extends IDecorationStyleSet { + /** + * A unique identifier for this set of styles. + */ + id: number; +} + +export class DecorationStyleCache { + + private _nextId = 1; + + private readonly _cache = new Map(); + + getOrCreateEntry(color: number): number { + const id = this._nextId++; + const entry = { + id, + color, + }; + this._cache.set(id, entry); + return id; + } + + getStyleSet(id: number): IDecorationStyleSet | undefined { + if (id === 0) { + return undefined; + } + return this._cache.get(id); + } +} diff --git a/src/vs/editor/browser/gpu/media/decorationCssRuleExtractor.css b/src/vs/editor/browser/gpu/css/media/decorationCssRuleExtractor.css similarity index 100% rename from src/vs/editor/browser/gpu/media/decorationCssRuleExtractor.css rename to src/vs/editor/browser/gpu/css/media/decorationCssRuleExtractor.css diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index c7a708734639..054c13404814 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -256,7 +256,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend let tokenEndIndex = 0; let tokenMetadata = 0; - let charMetadata = 0; + let decorationStyleSetColor = 0; let lineData: ViewLineRenderingData; let decoration: InlineDecoration; @@ -360,7 +360,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend break; } chars = content.charAt(x); - charMetadata = 0; + decorationStyleSetColor = 0; // Apply supported inline decoration styles to the cell metadata for (decoration of lineData.inlineDecorations) { @@ -386,7 +386,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend if (!parsedColor) { throw new BugIndicatingError('Invalid color format ' + value); } - charMetadata = parsedColor.toNumber32Bit(); + decorationStyleSetColor = parsedColor.toNumber32Bit(); break; } case 'font-weight': { @@ -415,7 +415,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend continue; } - glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer.value, chars, tokenMetadata, charMetadata); + glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer.value, chars, tokenMetadata, ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor)); // TODO: Support non-standard character widths absoluteOffsetX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * dpr); diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 6287bfb8d3b3..4222932a6056 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -8,6 +8,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { StringBuilder } from '../../../common/core/stringBuilder.js'; import { FontStyle, TokenMetadata } from '../../../common/encodedTokenAttributes.js'; import { ensureNonNullable } from '../gpuUtils.js'; +import { ViewGpuContext } from '../viewGpuContext.js'; import { type IBoundingBox, type IGlyphRasterizer, type IRasterizedGlyph } from './raster.js'; let nextId = 0; @@ -40,7 +41,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { fontBoundingBoxAscent: 0, fontBoundingBoxDescent: 0, }; - private _workGlyphConfig: { chars: string | undefined; tokenMetadata: number; charMetadata: number } = { chars: undefined, tokenMetadata: 0, charMetadata: 0 }; + private _workGlyphConfig: { chars: string | undefined; tokenMetadata: number; decorationStyleSetId: number } = { chars: undefined, tokenMetadata: 0, decorationStyleSetId: 0 }; constructor( readonly fontSize: number, @@ -68,7 +69,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { public rasterizeGlyph( chars: string, tokenMetadata: number, - charMetadata: number, + decorationStyleSetId: number, colorMap: string[], ): Readonly { if (chars === '') { @@ -83,19 +84,19 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { // Check if the last glyph matches the config, reuse if so. This helps avoid unnecessary // work when the rasterizer is called multiple times like when the glyph doesn't fit into a // page. - if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.tokenMetadata === tokenMetadata && this._workGlyphConfig.charMetadata === charMetadata) { + if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.tokenMetadata === tokenMetadata && this._workGlyphConfig.decorationStyleSetId === decorationStyleSetId) { return this._workGlyph; } this._workGlyphConfig.chars = chars; this._workGlyphConfig.tokenMetadata = tokenMetadata; - this._workGlyphConfig.charMetadata = charMetadata; - return this._rasterizeGlyph(chars, tokenMetadata, charMetadata, colorMap); + this._workGlyphConfig.decorationStyleSetId = decorationStyleSetId; + return this._rasterizeGlyph(chars, tokenMetadata, decorationStyleSetId, colorMap); } public _rasterizeGlyph( chars: string, tokenMetadata: number, - charMetadata: number, + decorationStyleSetId: number, colorMap: string[], ): Readonly { const devicePixelFontSize = Math.ceil(this.fontSize * this.devicePixelRatio); @@ -105,6 +106,8 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._canvas.height = canvasDim; } + const decorationStyleSet = ViewGpuContext.decorationStyleCache.getStyleSet(decorationStyleSetId); + // TODO: Support workbench.fontAliasing this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); @@ -125,8 +128,8 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { const originX = devicePixelFontSize; const originY = devicePixelFontSize; - if (charMetadata) { - this._ctx.fillStyle = `#${charMetadata.toString(16).padStart(8, '0')}`; + if (decorationStyleSet?.color) { + this._ctx.fillStyle = `#${decorationStyleSet.color.toString(16).padStart(8, '0')}`; } else { this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(tokenMetadata)]; } diff --git a/src/vs/editor/browser/gpu/raster/raster.ts b/src/vs/editor/browser/gpu/raster/raster.ts index 989a4656415c..b93d8748978f 100644 --- a/src/vs/editor/browser/gpu/raster/raster.ts +++ b/src/vs/editor/browser/gpu/raster/raster.ts @@ -23,13 +23,13 @@ export interface IGlyphRasterizer { * emoji, etc. * @param tokenMetadata The token metadata of the glyph to rasterize. See {@link MetadataConsts} * for how this works. - * @param charMetadata The chracter metadata of the glyph to rasterize. + * @param decorationStyleSetId The id of the decoration style sheet. Zero means no decoration. * @param colorMap A theme's color map. */ rasterizeGlyph( chars: string, tokenMetadata: number, - charMetadata: number, + decorationStyleSetId: number, colorMap: string[], ): Readonly; } diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 2670da06b3f6..661a502ee1d6 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -19,10 +19,11 @@ import { GPULifecycle } from './gpuDisposable.js'; import { ensureNonNullable, observeDevicePixelDimensions } from './gpuUtils.js'; import { RectangleRenderer } from './rectangleRenderer.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; -import { DecorationCssRuleExtractor } from './decorationCssRuleExtractor.js'; +import { DecorationCssRuleExtractor } from './css/decorationCssRuleExtractor.js'; import { Event } from '../../../base/common/event.js'; import type { IEditorOptions } from '../../common/config/editorOptions.js'; import { InlineDecorationType } from '../../common/viewModel.js'; +import { DecorationStyleCache } from './css/decorationStyleCache.js'; const enum GpuRenderLimits { maxGpuLines = 3000, @@ -54,6 +55,11 @@ export class ViewGpuContext extends Disposable { return ViewGpuContext._decorationCssRuleExtractor; } + private static readonly _decorationStyleCache = new DecorationStyleCache(); + static get decorationStyleCache(): DecorationStyleCache { + return ViewGpuContext._decorationStyleCache; + } + private static _atlas: TextureAtlas | undefined; /** diff --git a/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts b/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts index 5828d479ab7a..c36d8ddadc1d 100644 --- a/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts +++ b/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts @@ -5,7 +5,7 @@ import { deepStrictEqual } from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { DecorationCssRuleExtractor } from '../../../browser/gpu/decorationCssRuleExtractor.js'; +import { DecorationCssRuleExtractor } from '../../../browser/gpu/css/decorationCssRuleExtractor.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { $, getActiveDocument } from '../../../../base/browser/dom.js'; From 7fbca61bdb5fee1a225ee512060d27924fc173b8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:39:27 -0800 Subject: [PATCH 0480/3587] Create cache for decoration styles Fixes #234473 --- .../editor/browser/gpu/css/decorationStyleCache.ts | 12 ++++++++++-- src/vs/editor/browser/gpu/fullFileRenderStrategy.ts | 11 ++++++++--- src/vs/editor/browser/gpu/raster/glyphRasterizer.ts | 8 ++++++-- src/vs/editor/browser/gpu/viewGpuContext.ts | 3 +-- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts index 0b4929103e8a..5e023e840784 100644 --- a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts +++ b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts @@ -7,7 +7,11 @@ export interface IDecorationStyleSet { /** * A 24-bit number representing `color`. */ - color: number; + color: number | undefined; + /** + * Whether the text should be rendered in bold. + */ + bold: boolean | undefined; } export interface IDecorationStyleCacheEntry extends IDecorationStyleSet { @@ -23,11 +27,15 @@ export class DecorationStyleCache { private readonly _cache = new Map(); - getOrCreateEntry(color: number): number { + getOrCreateEntry(color: number | undefined, bold: boolean | undefined): number { + if (color === undefined && bold === undefined) { + return 0; + } const id = this._nextId++; const entry = { id, color, + bold, }; this._cache.set(id, entry); return id; diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 054c13404814..0d597760bc87 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -256,7 +256,8 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend let tokenEndIndex = 0; let tokenMetadata = 0; - let decorationStyleSetColor = 0; + let decorationStyleSetBold: boolean | undefined; + let decorationStyleSetColor: number | undefined; let lineData: ViewLineRenderingData; let decoration: InlineDecoration; @@ -360,7 +361,8 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend break; } chars = content.charAt(x); - decorationStyleSetColor = 0; + decorationStyleSetColor = undefined; + decorationStyleSetBold = undefined; // Apply supported inline decoration styles to the cell metadata for (decoration of lineData.inlineDecorations) { @@ -392,8 +394,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend case 'font-weight': { const parsedValue = parseCssFontWeight(value); if (parsedValue >= 400) { + decorationStyleSetBold = true; // TODO: Set bold (https://github.com/microsoft/vscode/issues/237584) } else { + decorationStyleSetBold = false; // TODO: Set normal (https://github.com/microsoft/vscode/issues/237584) } break; @@ -415,7 +419,8 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend continue; } - glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer.value, chars, tokenMetadata, ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor)); + const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold); + glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer.value, chars, tokenMetadata, decorationStyleSetId); // TODO: Support non-standard character widths absoluteOffsetX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * dpr); diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 4222932a6056..0375d305f155 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -116,7 +116,11 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { if (fontStyle & FontStyle.Italic) { fontSb.appendString('italic '); } - if (fontStyle & FontStyle.Bold) { + if (decorationStyleSet?.bold !== undefined) { + if (decorationStyleSet.bold) { + fontSb.appendString('bold '); + } + } else if (fontStyle & FontStyle.Bold) { fontSb.appendString('bold '); } fontSb.appendString(`${devicePixelFontSize}px ${this.fontFamily}`); @@ -128,7 +132,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { const originX = devicePixelFontSize; const originY = devicePixelFontSize; - if (decorationStyleSet?.color) { + if (decorationStyleSet?.color !== undefined) { this._ctx.fillStyle = `#${decorationStyleSet.color.toString(16).padStart(8, '0')}`; } else { this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(tokenMetadata)]; diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 661a502ee1d6..6a55c16633a6 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -259,8 +259,7 @@ export class ViewGpuContext extends Disposable { */ const gpuSupportedDecorationCssRules = [ 'color', - // TODO: https://github.com/microsoft/vscode/issues/237584 - // 'font-weight', + 'font-weight', ]; function supportsCssRule(rule: string, style: CSSStyleDeclaration) { From 057f430c2579fa9d8cdb59615c3771ad75ef7b1e Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 10 Jan 2025 11:55:41 -0600 Subject: [PATCH 0481/3587] after timeout, cancel accessibility signal for chat (#237661) --- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index b4f126517730..aa4dc26c919a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1095,6 +1095,13 @@ export class ChatWidget extends Disposable implements IChatWidget { } } }); + + const RESPONSE_TIMEOUT = 20000; + setTimeout(() => { + // Stop the signal if the promise is still unresolved + this.chatAccessibilityService.acceptResponse(undefined, requestId, options?.isVoiceInput); + }, RESPONSE_TIMEOUT); + return result.responseCreatedPromise; } } From 67632b1e3778383e57f2f841f08520e18555c11d Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Fri, 10 Jan 2025 10:15:45 -0800 Subject: [PATCH 0482/3587] [prompt instructions]: initial implementation (#237604) * [instruction attachments]: initial implementation * [instruction attachments]: exclude already attached instruction files from the picker dialog * [instruction attachments]: improve CSS styles * [instruction attachments]: include worspace folder names when resolving specified prompt instruction file locations * [instruction attachments]: localize error message strings, improve CSS styles, fix the infinite dispose loop issue --- .../browser/actions/chatContextActions.ts | 76 ++++- .../browser/actions/chatExecuteActions.ts | 10 +- .../instructionAttachments.ts | 141 +++++++++ .../instructionsAttachment.ts | 196 +++++++++++++ .../chat/browser/chatAttachmentModel.ts | 22 +- .../chatInstructionAttachmentsModel.ts | 164 +++++++++++ .../chatInstructionsAttachment.ts | 274 ++++++++++++++++++ .../chatInstructionsFileLocator.ts | 182 ++++++++++++ .../contrib/chat/browser/chatInputPart.ts | 44 ++- .../contrib/chat/browser/chatWidget.ts | 4 +- .../chatDynamicVariables/chatFileReference.ts | 4 +- .../contrib/chat/browser/media/chat.css | 67 +++++ .../contrib/chat/common/chatContextKeys.ts | 1 + .../contrib/chat/common/chatService.ts | 5 + .../contrib/chat/common/chatServiceImpl.ts | 10 +- .../chat/common/promptFileReference.ts | 36 ++- .../test/common/promptFileReference.test.ts | 4 +- 17 files changed, 1226 insertions(+), 14 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionAttachments.ts create mode 100644 src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index c812ec0d053d..e74066f28970 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -65,7 +65,9 @@ export function registerChatContextActions() { /** * We fill the quickpick with these types, and enable some quick access providers */ -type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IQuickAccessQuickPickItem | IToolQuickPickItem | IImageQuickPickItem | IVariableQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem | IScreenShotQuickPickItem | IRelatedFilesQuickPickItem; +type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IQuickAccessQuickPickItem | IToolQuickPickItem | + IImageQuickPickItem | IVariableQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem | + IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IPromptInstructionsQuickPickItem; /** * These are the types that we can get out of the quick pick @@ -119,6 +121,17 @@ function isRelatedFileQuickPickItem(obj: unknown): obj is IRelatedFilesQuickPick ); } +/** + * Checks is a provided object is a prompt instructions quick pick item. + */ +function isPromptInstructionsQuickPickItem(obj: unknown): obj is IPromptInstructionsQuickPickItem { + if (!obj || typeof obj !== 'object') { + return false; + } + + return ('kind' in obj && obj.kind === 'prompt-instructions'); +} + interface IRelatedFilesQuickPickItem extends IQuickPickItem { kind: 'related-files'; id: string; @@ -177,6 +190,22 @@ interface IScreenShotQuickPickItem extends IQuickPickItem { icon?: ThemeIcon; } +/** + * Quick pick item for prompt instructions attachment. + */ +interface IPromptInstructionsQuickPickItem extends IQuickPickItem { + /** + * Unique kind identifier of the prompt instructions + * attachment quick pick item. + */ + kind: 'prompt-instructions'; + + /** + * The id of the qucik pick item. + */ + id: string; +} + abstract class AttachFileAction extends Action2 { getFiles(accessor: ServicesAccessor, ...args: any[]): URI[] { const textEditorService = accessor.get(IEditorService); @@ -544,6 +573,40 @@ export class AttachContextAction extends Action2 { if (blob) { toAttach.push(convertBufferToScreenshotVariable(blob)); } + } else if (isPromptInstructionsQuickPickItem(pick)) { + const { promptInstructions } = widget.attachmentModel; + + // find all prompt instruction files in the user project + // and present them to the user so they can select one + const filesPromise = promptInstructions.listNonAttachedFiles() + .then((files) => { + return files.map((file) => { + const result: IQuickPickItem & { value: URI } = { + type: 'item', + label: labelService.getUriBasenameLabel(file), + description: labelService.getUriLabel(dirname(file), { relative: true }), + tooltip: file.fsPath, + value: file, + }; + + return result; + }); + }); + + const selectedFile = await quickInputService.pick( + filesPromise, + { + placeHolder: localize('promptInstructions', 'Add prompt instructions file'), + canPickMany: false, + }); + + // if the quick pick dialog was dismissed, nothing to do + if (!selectedFile) { + return; + } + + // add selected prompt instructions reference to the chat attachments model + promptInstructions.add(selectedFile.value); } else { // Anything else is an attachment const attachmentPick = pick as IAttachmentQuickPickItem; @@ -757,6 +820,17 @@ export class AttachContextAction extends Action2 { } } + // if the `prompt instructions` feature is enabled, add + // the `Prompt Instructions` attachment type to the list + if (widget.attachmentModel.promptInstructions.featureEnabled) { + quickPickItems.push({ + kind: 'prompt-instructions', + id: 'prompt-instructions', + label: localize('chatContext.promptInstructions', 'Prompt Instructions'), + iconClass: ThemeIcon.asClassName(Codicon.lightbulbSparkle), + }); + } + function extractTextFromIconLabel(label: string | undefined): string { if (!label) { return ''; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index caf69f26262a..9b44062187db 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -46,13 +46,21 @@ export class ChatSubmitAction extends SubmitAction { static readonly ID = 'workbench.action.chat.submit'; constructor() { + const precondition = ContextKeyExpr.and( + // if the input has prompt instructions attached, allow submitting requests even + // without text present - having instructions is enough context for a request + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + ChatContextKeys.requestInProgress.negate(), + ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession), + ); + super({ id: ChatSubmitAction.ID, title: localize2('interactive.submit.label', "Send and Dispatch"), f1: false, category: CHAT_CATEGORY, icon: Codicon.send, - precondition: ContextKeyExpr.and(ChatContextKeys.inputHasText, ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession)), + precondition, keybinding: { when: ChatContextKeys.inChatInput, primary: KeyCode.Enter, diff --git a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionAttachments.ts b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionAttachments.ts new file mode 100644 index 000000000000..2e8d11ca7f51 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionAttachments.ts @@ -0,0 +1,141 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as dom from '../../../../../../base/browser/dom.js'; +import { ResourceLabels } from '../../../../../browser/labels.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { InstructionsAttachmentWidget } from './instructionsAttachment.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ChatInstructionAttachmentsModel } from '../../chatAttachmentModel/chatInstructionAttachmentsModel.js'; + +/** + * Widget fot a collection of prompt instructions attachments. + * See {@linkcode InstructionsAttachmentWidget}. + */ +export class InstructionAttachmentsWidget extends Disposable { + /** + * The root DOM node of the widget. + */ + public readonly domNode: HTMLElement; + + /** + * List of child instruction attachment widgets. + */ + private children: InstructionsAttachmentWidget[] = []; + + /** + * Get all `URI`s of all valid references, including all + * the possible references nested inside the children. + */ + public get references(): readonly URI[] { + return this.model.references; + } + + /** + * Check if child widget list is empty (no attachments present). + */ + public get empty(): boolean { + return this.children.length === 0; + } + + constructor( + private readonly model: ChatInstructionAttachmentsModel, + private readonly resourceLabels: ResourceLabels, + @IInstantiationService private readonly initService: IInstantiationService, + @ILogService private readonly logService: ILogService, + ) { + super(); + + this.render = this.render.bind(this); + this.domNode = dom.$('.chat-prompt-instructions-attachments'); + + this._register(this.model.onUpdate(this.render)); + + // when a new attachment model is added, create a new child widget for it + this.model.onAdd((attachment) => { + const widget = this.initService.createInstance( + InstructionsAttachmentWidget, + attachment, + this.resourceLabels, + ); + + // handle the child widget disposal event, removing it from the list + widget.onDispose(this.onChildDispose.bind(this, widget)); + + // register the new child widget + this.children.push(widget); + this.domNode.appendChild(widget.domNode); + this.render(); + }); + } + + /** + * Handle child widget disposal. + * @param widget The child widget that was disposed. + */ + public onChildDispose(widget: InstructionsAttachmentWidget): this { + // common prefix for all log messages + const logPrefix = `[onChildDispose] Widget for instructions attachment '${widget.uri.path}'`; + + // flag to check if the widget was found in the children list + let widgetExists = false; + + // filter out disposed child widget from the list + this.children = this.children.filter((child) => { + if (child === widget) { + // because we filter out all objects here it might be ok to have multiple of them, but + // it also highlights a potential issue in our logic somewhere else, so trace a warning here + if (widgetExists) { + this.logService.warn( + `${logPrefix} is present in the children references list multiple times.`, + ); + } + + widgetExists = true; + return false; + } + + return true; + }); + + // no widget was found in the children list, while it might be ok it also + // highlights a potential issue in our logic, so trace a warning here + if (!widgetExists) { + this.logService.warn( + `${logPrefix} was disposed, but was not found in the child references.`, + ); + } + + // remove the child widget root node from the DOM + this.domNode.removeChild(widget.domNode); + + // re-render the whole widget + return this.render(); + } + + /** + * Render this widget. + */ + private render(): this { + // set visibility of the root node based on the presence of attachments + dom.setVisibility(!this.empty, this.domNode); + + return this; + } + + /** + * Dispose of the widget, including all the child + * widget instances. + */ + public override dispose(): void { + for (const child of this.children) { + child.dispose(); + } + + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts new file mode 100644 index 000000000000..fd0484ceb5e7 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts @@ -0,0 +1,196 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../../../nls.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import * as dom from '../../../../../../base/browser/dom.js'; +import { Emitter } from '../../../../../../base/common/event.js'; +import { ResourceLabels } from '../../../../../browser/labels.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { ResourceContextKey } from '../../../../../common/contextkeys.js'; +import { Button } from '../../../../../../base/browser/ui/button/button.js'; +import { basename, dirname } from '../../../../../../base/common/resources.js'; +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 { 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 { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; +import { ChatInstructionsAttachmentModel } from '../../chatAttachmentModel/chatInstructionsAttachment.js'; +import { getDefaultHoverDelegate } from '../../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { getFlatContextMenuActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; + +/** + * Widget for a single prompt instructions attachment. + */ +export class InstructionsAttachmentWidget extends Disposable { + /** + * The root DOM node of the widget. + */ + public readonly domNode: HTMLElement; + + /** + * Get the `URI` associated with the model reference. + */ + public get uri(): URI { + return this.model.reference.uri; + } + + /** + * Event that fires when the object is disposed. + * + * See {@linkcode onDispose}. + */ + protected _onDispose = this._register(new Emitter()); + /** + * Subscribe to the `onDispose` event. + * @param callback Function to invoke on dispose. + */ + public onDispose(callback: () => unknown): this { + this._register(this._onDispose.event(callback)); + + return this; + } + + /** + * Temporary disposables used for rendering purposes. + */ + private readonly renderDisposables = this._register(new DisposableStore()); + + constructor( + private readonly model: ChatInstructionsAttachmentModel, + private readonly resourceLabels: ResourceLabels, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IHoverService private readonly hoverService: IHoverService, + @ILabelService private readonly labelService: ILabelService, + @IMenuService private readonly menuService: IMenuService, + @IFileService private readonly fileService: IFileService, + @ILanguageService private readonly languageService: ILanguageService, + @IModelService private readonly modelService: IModelService, + ) { + super(); + + this.domNode = dom.$('.chat-prompt-instructions-attachment.chat-attached-context-attachment.show-file-icons.implicit'); + + this.render = this.render.bind(this); + this.dispose = this.dispose.bind(this); + + this.model.onUpdate(this.render); + this.model.onDispose(this.dispose); + + this.render(); + } + + /** + * Render this widget. + */ + private render() { + dom.clearNode(this.domNode); + this.renderDisposables.clear(); + this.domNode.classList.remove('warning', 'error', 'disabled'); + + const { enabled, resolveIssue: errorCondition } = this.model; + if (!enabled) { + this.domNode.classList.add('disabled'); + } + + const label = this.resourceLabels.create(this.domNode, { supportIcons: true }); + const file = this.model.reference.uri; + + const fileBasename = basename(file); + const fileDirname = dirname(file); + const friendlyName = `${fileBasename} ${fileDirname}`; + const ariaLabel = localize('chat.instructionsAttachment', "Prompt instructions attachment, {0}", friendlyName); + + const uriLabel = this.labelService.getUriLabel(file, { relative: true }); + const currentFile = localize('openEditor', "Prompt instructions"); + const inactive = localize('enableHint', "disabled"); + const currentFileHint = currentFile + (enabled ? '' : ` (${inactive})`); + + let title = `${currentFileHint} ${uriLabel}`; + + // if there are some errors/warning during the process of resolving + // attachment references (including all the nested child references), + // add the issue details in the hover title for the attachment + if (errorCondition) { + const { type, message: details } = errorCondition; + this.domNode.classList.add(type); + + const errorCaption = type === 'warning' + ? localize('warning', "Warning") + : localize('error', "Error"); + + title += `\n-\n[${errorCaption}]: ${details}`; + } + + label.setFile(file, { + fileKind: FileKind.FILE, + hidePath: true, + range: undefined, + title, + icon: ThemeIcon.fromId(Codicon.lightbulbSparkle.id), + extraClasses: [], + }); + this.domNode.ariaLabel = ariaLabel; + this.domNode.tabIndex = 0; + + const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, localize('instructions', 'Instructions'))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), hintElement, title)); + + // create `toggle` enabled state button + const toggleButtonMsg = enabled + ? localize('disable', "Disable") + : localize('enable', "Enable"); + + const toggleButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: toggleButtonMsg })); + toggleButton.icon = enabled ? Codicon.eye : Codicon.eyeClosed; + this.renderDisposables.add(toggleButton.onDidClick((e) => { + e.stopPropagation(); + this.model.toggle(); + })); + + // create the `remove` button + const removeButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: localize('remove', "Remove") })); + removeButton.icon = Codicon.x; + this.renderDisposables.add(removeButton.onDidClick((e) => { + e.stopPropagation(); + this.model.dispose(); + })); + + // Context menu + const scopedContextKeyService = this.renderDisposables.add(this.contextKeyService.createScoped(this.domNode)); + + const resourceContextKey = this.renderDisposables.add( + new ResourceContextKey(scopedContextKeyService, this.fileService, this.languageService, this.modelService), + ); + resourceContextKey.set(file); + + this.renderDisposables.add(dom.addDisposableListener(this.domNode, dom.EventType.CONTEXT_MENU, async domEvent => { + const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent); + dom.EventHelper.stop(domEvent, true); + + this.contextMenuService.showContextMenu({ + contextKeyService: scopedContextKeyService, + getAnchor: () => event, + getActions: () => { + const menu = this.menuService.getMenuActions(MenuId.ChatInputResourceAttachmentContext, scopedContextKeyService, { arg: file }); + return getFlatContextMenuActions(menu); + }, + }); + })); + } + + public override dispose(): void { + this._onDispose.fire(); + + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts index 5dd6ddf986b9..2cb3ffb405a7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts @@ -10,8 +10,27 @@ import { URI } from '../../../../base/common/uri.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { IChatEditingService } from '../common/chatEditingService.js'; import { IChatRequestVariableEntry } from '../common/chatModel.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ChatInstructionAttachmentsModel } from './chatAttachmentModel/chatInstructionAttachmentsModel.js'; export class ChatAttachmentModel extends Disposable { + /** + * Collection on prompt instruction attachments. + */ + public readonly promptInstructions: ChatInstructionAttachmentsModel; + + constructor( + @IInstantiationService private readonly initService: IInstantiationService, + ) { + super(); + + this.promptInstructions = this._register( + this.initService.createInstance(ChatInstructionAttachmentsModel), + ).onUpdate(() => { + this._onDidChangeContext.fire(); + }); + } + private _attachments = new Map(); get attachments(): ReadonlyArray { return Array.from(this._attachments.values()); @@ -91,8 +110,9 @@ export class EditsAttachmentModel extends ChatAttachmentModel { constructor( @IChatEditingService private readonly _chatEditingService: IChatEditingService, + @IInstantiationService _initService: IInstantiationService, ) { - super(); + super(_initService); } private isExcludeFileAttachment(fileAttachmentId: string) { diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts new file mode 100644 index 000000000000..d981e0e655a4 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts @@ -0,0 +1,164 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Emitter } from '../../../../../base/common/event.js'; +import { ChatInstructionsFileLocator } from './chatInstructionsFileLocator.js'; +import { ChatInstructionsAttachmentModel } from './chatInstructionsAttachment.js'; +import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; + +/** + * Configuration setting name for the `prompt instructions` feature. + * Set to `true` to enable the feature for yourself. + */ +const PROMPT_INSTRUCTIONS_SETTING_NAME = 'chat.experimental.prompt-instructions.enabled'; + +/** + * Model for a collection of prompt instruction attachments. + * See {@linkcode ChatInstructionsAttachmentModel}. + */ +export class ChatInstructionAttachmentsModel extends Disposable { + /** + * Helper to locate prompt instruction files on the disk. + */ + private readonly instructionsFileReader: ChatInstructionsFileLocator; + + /** + * List of all prompt instruction attachments. + */ + private attachments: DisposableMap = + this._register(new DisposableMap()); + + /** + * Get all `URI`s of all valid references, including all + * the possible references nested inside the children. + */ + public get references(): readonly URI[] { + const result = []; + + for (const child of this.attachments.values()) { + result.push(...child.references); + } + + return result; + } + + /** + * Event that fires then this model is updated. + * + * See {@linkcode onUpdate}. + */ + protected _onUpdate = this._register(new Emitter()); + /** + * Subscribe to the `onUpdate` event. + * @param callback Function to invoke on update. + */ + public onUpdate(callback: () => unknown): this { + this._register(this._onUpdate.event(callback)); + + return this; + } + + /** + * Event that fires when a new prompt instruction attachment is added. + * See {@linkcode onAdd}. + */ + protected _onAdd = this._register(new Emitter()); + /** + * The `onAdd` event fires when a new prompt instruction attachment is added. + * + * @param callback Function to invoke on add. + */ + public onAdd(callback: (attachment: ChatInstructionsAttachmentModel) => unknown): this { + this._register(this._onAdd.event(callback)); + + return this; + } + + constructor( + @IInstantiationService private readonly initService: IInstantiationService, + @IConfigurationService private readonly configService: IConfigurationService, + ) { + super(); + + this._onUpdate.fire = this._onUpdate.fire.bind(this._onUpdate); + this.instructionsFileReader = initService.createInstance(ChatInstructionsFileLocator); + } + + /** + * Add a prompt instruction attachment instance with the provided `URI`. + * @param uri URI of the prompt instruction attachment to add. + */ + public add(uri: URI): this { + // if already exists, nothing to do + if (this.attachments.has(uri.path)) { + return this; + } + + const instruction = this.initService.createInstance(ChatInstructionsAttachmentModel, uri) + .onUpdate(this._onUpdate.fire) + .onDispose(() => { + // note! we have to use `deleteAndLeak` here, because the `*AndDispose` + // alternative results in an infinite loop of calling this callback + this.attachments.deleteAndLeak(uri.path); + this._onUpdate.fire(); + }); + + this.attachments.set(uri.path, instruction); + instruction.resolve(); + + this._onAdd.fire(instruction); + this._onUpdate.fire(); + + return this; + } + + /** + * Remove a prompt instruction attachment instance by provided `URI`. + * @param uri URI of the prompt instruction attachment to remove. + */ + public remove(uri: URI): this { + // if does not exist, nothing to do + if (!this.attachments.has(uri.path)) { + return this; + } + + this.attachments.deleteAndDispose(uri.path); + + return this; + } + + /** + * List prompt instruction files available and not attached yet. + */ + public async listNonAttachedFiles(): Promise { + return await this.instructionsFileReader.listFiles(this.references); + } + + /** + * Checks if the prompt instructions feature is enabled in the user settings. + * The setting can be set to `true`, `'true'`, `True`, `TRUE`, etc. + * All other values are treated as the `false` value, which is the `default`. + */ + public get featureEnabled(): boolean { + const value = this.configService.getValue(PROMPT_INSTRUCTIONS_SETTING_NAME); + + if (!value) { + return false; + } + + if (value === true) { + return true; + } + + if (typeof value === 'string' && value.toLowerCase().trim() === 'true') { + return true; + } + + return false; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts new file mode 100644 index 000000000000..40288ee891fb --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts @@ -0,0 +1,274 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../../nls.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { Emitter } from '../../../../../base/common/event.js'; +import { basename } from '../../../../../base/common/resources.js'; +import { assertDefined } from '../../../../../base/common/types.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { PromptFileReference, TErrorCondition } from '../../common/promptFileReference.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { FileOpenFailed, NonPromptSnippetFile, RecursiveReference } from '../../common/promptFileReferenceErrors.js'; + +/** + * Well-known localized error messages. + */ +const errorMessages = { + recursion: localize('chatPromptInstructionsRecursiveReference', 'Recursive reference found'), + fileOpenFailed: localize('chatPromptInstructionsFileOpenFailed', 'Failed to open file'), + brokenChild: localize('chatPromptInstructionsBrokenReference', 'Contains a broken reference that will be ignored'), +}; + +/** + * Object that represents an error that may occur during + * the process of resolving prompt instructions reference. + */ +interface IIssue { + /** + * Type of the failure. Currently all errors that occur on + * the "main" root reference directly attached to the chat + * are considered to be `error`s, while all failures on nested + * child references are considered to be `warning`s. + */ + type: 'error' | 'warning'; + + /** + * Error or warning message. + */ + message: string; +} + +/** + * Model for a single chat prompt instructions attachment. + */ +export class ChatInstructionsAttachmentModel extends Disposable { + /** + * Private reference of the underlying prompt instructions + * reference instance. + */ + private readonly _reference: PromptFileReference; + /** + * Get the prompt instructions reference instance. + */ + public get reference(): PromptFileReference { + return this._reference; + } + + + /** + * Get `URI` for the main reference and `URI`s of all valid + * child references it may contain. + */ + public get references(): readonly URI[] { + const { reference, enabled, resolveIssue } = this; + + // return no references if the attachment is disabled + if (!enabled) { + return []; + } + + // if the model has an error, return no references + if (resolveIssue && !(resolveIssue instanceof NonPromptSnippetFile)) { + return []; + } + + // otherwise return `URI` for the main reference and + // all valid child `URI` references it may contain + return [ + ...reference.validFileReferenceUris, + reference.uri, + ]; + } + + + /** + * If the prompt instructions reference (or any of its child references) has + * failed to resolve, this field contains the failure details, otherwise `undefined`. + * + * See {@linkcode IIssue}. + */ + public get resolveIssue(): IIssue | undefined { + const { errorCondition } = this._reference; + + const errorConditions = this.collectErrorConditions(); + if (errorConditions.length === 0) { + return undefined; + } + + const [firstError, ...restErrors] = errorConditions; + + // if the first error is the error of the root reference, + // then return it as an `error` otherwise use `warning` + const isRootError = (firstError === errorCondition); + const type = (isRootError) + ? 'error' + : 'warning'; + + const moreSuffix = restErrors.length > 0 + ? `\n-\n +${restErrors.length} more error${restErrors.length > 1 ? 's' : ''}` + : ''; + + const errorMessage = this.getErrorMessage(firstError, isRootError); + return { + type, + message: `${errorMessage}${moreSuffix}`, + }; + } + + /** + * Get message for the provided error condition object. + * + * @param error Error object. + * @param isRootError If the error happened on the the "main" root reference. + * @returns Error message. + */ + private getErrorMessage( + error: TErrorCondition, + isRootError: boolean, + ): string { + const { uri } = error; + + // if a child error - the error is somewhere in the nested references tree, + // then use message prefix to highlight that this is not a root error + const prefix = (!isRootError) + ? `${errorMessages.brokenChild}: ` + : ''; + + // if failed to open a file, return approprivate message and the file path + if (error instanceof FileOpenFailed) { + return `${prefix}${errorMessages.fileOpenFailed} '${uri.path}'.`; + } + + // if a recursion, provide the entire recursion path so users can use + // it for the debugging purposes + if (error instanceof RecursiveReference) { + const { recursivePath } = error; + + const recursivePathString = recursivePath + .map((path) => { + return basename(URI.file(path)); + }) + .join(' -> '); + + return `${prefix}${errorMessages.recursion}:\n${recursivePathString}`; + } + + return `${prefix}${error.message}`; + } + + /** + * Collect all failures that may have occurred during the process + * of resolving references in the entire references tree. + * + * @returns List of errors in the references tree. + */ + private collectErrorConditions(): TErrorCondition[] { + return this.reference + // get all references (including the root) as a flat array + .flatten() + // filter out children without error conditions or + // the ones that are non-prompt snippet files + .filter((childReference) => { + const { errorCondition } = childReference; + + return errorCondition && !(errorCondition instanceof NonPromptSnippetFile); + }) + // map to error condition objects + .map((childReference): TErrorCondition => { + const { errorCondition } = childReference; + + // `must` always be `true` because of the `filter` call above + assertDefined( + errorCondition, + `Error condition must be present for '${childReference.uri.path}'.`, + ); + + return errorCondition; + }); + } + + /** + * Event that fires when the error condition of the prompt + * reference changes. + * + * See {@linkcode onUpdate}. + */ + protected _onUpdate = this._register(new Emitter()); + /** + * Subscribe to the `onUpdate` event. + * @param callback Function to invoke on update. + */ + public onUpdate(callback: () => unknown): this { + this._register(this._onUpdate.event(callback)); + + return this; + } + + /** + * Event that fires when the object is disposed. + * + * See {@linkcode onDispose}. + */ + protected _onDispose = this._register(new Emitter()); + /** + * Subscribe to the `onDispose` event. + * @param callback Function to invoke on dispose. + */ + public onDispose(callback: () => unknown): this { + this._register(this._onDispose.event(callback)); + + return this; + } + + /** + * Private property to track the `enabled` state of the prompt + * instructions attachment. + */ + private _enabled: boolean = true; + /** + * Get the `enabled` state of the prompt instructions attachment. + */ + public get enabled(): boolean { + return this._enabled; + } + + constructor( + uri: URI, + @IInstantiationService private readonly initService: IInstantiationService, + ) { + super(); + + this._onUpdate.fire = this._onUpdate.fire.bind(this._onUpdate); + this._reference = this._register(this.initService.createInstance(PromptFileReference, uri)) + .onUpdate(this._onUpdate.fire); + } + + /** + * Start resolving the prompt instructions reference and child references + * that it may contain. + */ + public resolve(): this { + this._reference.resolve(); + + return this; + } + + /** + * Toggle the `enabled` state of the prompt instructions attachment. + */ + public toggle(): this { + this._enabled = !this._enabled; + this._onUpdate.fire(); + + return this; + } + + public override dispose(): void { + this._onDispose.fire(); + + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts new file mode 100644 index 000000000000..e31f5e0fe832 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts @@ -0,0 +1,182 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { dirname, extUri } from '../../../../../base/common/resources.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js'; + +/** + * Configuration setting name for the prompt instructions source folder paths. + */ +const PROMPT_FILES_LOCATION_SETTING_NAME = 'chat.experimental.prompt-files.location'; + +/** + * Default prompt instructions source folder paths. + */ +const PROMPT_FILES_DEFAULT_LOCATION = ['.copilot/prompts']; + +/** + * Extension of the prompt instructions files. + */ +const INSTRUCTIONS_FILE_EXTENSION = '.md'; + +/** + * Class to locate prompt instructions files. + */ +export class ChatInstructionsFileLocator { + constructor( + @IFileService private readonly fileService: IFileService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + @IConfigurationService private readonly configService: IConfigurationService, + ) { } + + /** + * List all prompt instructions files from the filesystem. + * + * @param exclude List of `URIs` to exclude from the result. + * @returns List of prompt instructions files found in the workspace. + */ + public async listFiles(exclude: ReadonlyArray): Promise { + // create a set from the list of URIs for convenience + const excludeSet: Set = new Set(); + for (const excludeUri of exclude) { + excludeSet.add(excludeUri.path); + } + + // filter out the excluded paths from the locations list + const locations = this.getSourceLocations() + .filter((location) => { + return !excludeSet.has(location.path); + }); + + return await this.findInstructionFiles(locations, excludeSet); + } + + /** + * Get all possible prompt instructions file locations based on the current + * workspace folder structure. + * + * @returns List of possible prompt instructions file locations. + */ + private getSourceLocations(): readonly URI[] { + const state = this.workspaceService.getWorkbenchState(); + + // nothing to do if the workspace is empty + if (state === WorkbenchState.EMPTY) { + return []; + } + + const sourceLocations = this.getSourceLocationsConfigValue(); + const result = []; + + // otherwise for each folder provided in the configuration, create + // a URI per each folder in the current workspace + const { folders } = this.workspaceService.getWorkspace(); + for (const folder of folders) { + for (const sourceFolderName of sourceLocations) { + const folderUri = extUri.resolvePath(folder.uri, sourceFolderName); + result.push(folderUri); + } + } + + // if inside a workspace, add the specified source locations inside the workspace + // root too, to allow users to use `.copilot/prompts` folder (or whatever they + // specify in the setting) in the workspace root + if (folders.length > 1) { + const workspaceRootUri = dirname(folders[0].uri); + for (const sourceFolderName of sourceLocations) { + const folderUri = extUri.resolvePath(workspaceRootUri, sourceFolderName); + result.push(folderUri); + } + } + + return result; + } + + /** + * Get the configuation value for the prompt instructions source locations. + * Defaults to {@linkcode PROMPT_FILES_DEFAULT_LOCATION} if the value is not set. + * + * @returns List of prompt instructions source locations that were provided in + * user settings. + */ + private getSourceLocationsConfigValue(): readonly string[] { + const value = this.configService.getValue(PROMPT_FILES_LOCATION_SETTING_NAME); + + if (value === undefined || value === null) { + return PROMPT_FILES_DEFAULT_LOCATION; + } + + if (typeof value === 'string') { + return [value]; + } + + // if not a string nor an array, return an empty array + if (!Array.isArray(value)) { + return []; + } + + // filter out non-string values from the list + const result = value.filter((item) => { + return typeof item === 'string'; + }); + + return result; + } + + /** + * Finds all existent prompt instruction files in the provided locations. + * + * @param locations List of locations to search for prompt instruction files in. + * @param exclude Map of `path -> boolean` to exclude from the result. + * @returns List of prompt instruction files found in the provided locations. + */ + private async findInstructionFiles( + locations: readonly URI[], + exclude: ReadonlySet, + ): Promise { + const results = await this.fileService.resolveAll( + locations.map((location) => { + return { resource: location }; + }), + ); + + const files = []; + for (const result of results) { + const { stat, success } = result; + + if (!success) { + continue; + } + + if (!stat || !stat.children) { + continue; + } + + for (const child of stat.children) { + const { name, resource, isDirectory } = child; + + if (isDirectory) { + continue; + } + + if (!name.endsWith(INSTRUCTIONS_FILE_EXTENSION)) { + continue; + } + + if (exclude.has(resource.path)) { + continue; + } + + files.push(resource); + } + } + + return files; + + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 3dad10ec25c7..f61955b5b5dd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -91,6 +91,7 @@ import { IChatHistoryEntry, IChatInputState, IChatWidgetHistoryService } from '. import { ILanguageModelChatMetadata, ILanguageModelsService } from '../common/languageModels.js'; import { CancelAction, ChatModelPickerActionId, ChatSubmitAction, ChatSubmitSecondaryAgentAction, IChatExecuteActionContext } from './actions/chatExecuteActions.js'; 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 { hookUpResourceAttachmentDragAndContextMenu, hookUpSymbolAttachmentDragAndContextMenu } from './chatContentParts/chatAttachmentsContentPart.js'; @@ -202,9 +203,28 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } + for (const uri of this.instructionAttachmentsPart.references) { + contextArr.push({ + id: 'vscode.prompt.instructions', + name: basename(uri.path), + value: uri, + isSelection: false, + enabled: true, + isFile: true, + isDynamic: true, + }); + } + return contextArr; } + /** + * Check if the chat input part has any prompt instruction attachments. + */ + public get hasInstructionAttachments(): boolean { + return !this.instructionAttachmentsPart.empty; + } + private _indexOfLastAttachedContextDeletedWithKeyboard: number = -1; private _implicitContext: ChatImplicitContext | undefined; @@ -260,6 +280,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private inputEditorHasText: IContextKey; private chatCursorAtTop: IContextKey; private inputEditorHasFocus: IContextKey; + /** + * Context key is set when prompt instructions are attached.3 + */ + private promptInstructionsAttached: IContextKey; private readonly _waitForPersistedLanguageModel = this._register(new MutableDisposable()); private _onDidChangeCurrentLanguageModel = this._register(new Emitter()); @@ -307,6 +331,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private readonly getInputState: () => IChatInputState; + /** + * Child widget of prompt instruction attachments. + * See {@linkcode InstructionAttachmentsWidget}. + */ + private instructionAttachmentsPart: InstructionAttachmentsWidget; + constructor( // private readonly editorOptions: ChatEditorOptions, // TODO this should be used private readonly location: ChatAgentLocation, @@ -356,6 +386,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.inputEditorHasText = ChatContextKeys.inputHasText.bindTo(contextKeyService); this.chatCursorAtTop = ChatContextKeys.inputCursorAtTop.bindTo(contextKeyService); this.inputEditorHasFocus = ChatContextKeys.inputHasFocus.bindTo(contextKeyService); + this.promptInstructionsAttached = ChatContextKeys.instructionsAttached.bindTo(contextKeyService); this.history = this.loadHistory(); this._register(this.historyService.onDidClearHistory(() => this.history = new HistoryNavigator2([{ text: '' }], 50, historyKeyFn))); @@ -370,6 +401,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._hasFileAttachmentContextKey = ChatContextKeys.hasFileAttachments.bindTo(contextKeyService); + this.instructionAttachmentsPart = this._register( + instantiationService.createInstance( + InstructionAttachmentsWidget, + this.attachmentModel.promptInstructions, + this._contextResourceLabels, + ), + ); + this.initSelectedModel(); } @@ -841,7 +880,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // 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.attachedContextContainer); + dom.setVisibility(Boolean(attachments.length) || Boolean(this.implicitContext?.value) || !this.instructionAttachmentsPart.empty, this.attachedContextContainer); if (!attachments.length) { this._indexOfLastAttachedContextDeletedWithKeyboard = -1; } @@ -851,6 +890,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge container.appendChild(implicitPart.domNode); } + this.promptInstructionsAttached.set(!this.instructionAttachmentsPart.empty); + container.appendChild(this.instructionAttachmentsPart.domNode); + const attachmentInitPromises: Promise[] = []; for (const [index, attachment] of attachments) { const widget = dom.append(container, $('.chat-attached-context-attachment.show-file-icons')); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index aa4dc26c919a..eea04f739842 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -650,7 +650,8 @@ export class ChatWidget extends Disposable implements IChatWidget { noCommandDetection: true, attempt: request.attempt + 1, location: this.location, - userSelectedModelId: this.input.currentLanguageModel + userSelectedModelId: this.input.currentLanguageModel, + hasInstructionAttachments: this.input.hasInstructionAttachments, }; this.chatService.resendRequest(request, options).catch(e => this.logService.error('FAILED to rerun request', e)); } @@ -1078,6 +1079,7 @@ export class ChatWidget extends Disposable implements IChatWidget { attachedContext, workingSet, noCommandDetection: options?.noCommandDetection, + hasInstructionAttachments: this.inputPart.hasInstructionAttachments, }); if (result) { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts index 2a5ecdf50485..15bf36b7017f 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts @@ -7,6 +7,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { assert } from '../../../../../../base/common/assert.js'; import { IDynamicVariable } from '../../../common/chatVariables.js'; import { IRange } from '../../../../../../editor/common/core/range.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; import { PromptFileReference } from '../../../common/promptFileReference.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; @@ -22,6 +23,7 @@ export class ChatFileReference extends PromptFileReference implements IDynamicVa */ constructor( public readonly reference: IDynamicVariable, + @ILogService logService: ILogService, @IFileService fileService: IFileService, @IConfigurationService configService: IConfigurationService, ) { @@ -32,7 +34,7 @@ export class ChatFileReference extends PromptFileReference implements IDynamicVa `Variable data must be an URI, got '${data}'.`, ); - super(data, fileService, configService); + super(data, logService, fileService, configService); } /** diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index f90656216375..aac5782c1941 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -925,6 +925,73 @@ have to be updated for changes to the rules above, or to support more deeply nes border-color: var(--vscode-notificationsWarningIcon-foreground); } +/** + * Styles for the `prompt instructions` attachment widget. + */ +.chat-attached-context .chat-prompt-instructions-attachments { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; +} +.chat-attached-context .chat-prompt-instructions-attachment { + display: flex; + gap: 4px; +} +.chat-attached-context .chat-prompt-instructions-attachment .codicon { + color: inherit; + text-decoration: none; +} +.chat-attached-context .chat-prompt-instructions-attachment .monaco-icon-label::before { + color: var(--vscode-notificationsWarningIcon-foreground); +} +.chat-attached-context .chat-prompt-instructions-attachment .chat-implicit-hint { + opacity: 0.7; + font-size: .9em; +} +.chat-attached-context .chat-prompt-instructions-attachment.warning { + color: var(--vscode-notificationsWarningIcon-foreground); +} +.chat-attached-context .chat-prompt-instructions-attachment.error { + color: var(--vscode-notificationsErrorIcon-foreground); +} +.chat-attached-context .chat-prompt-instructions-attachment.disabled { + border-style: dashed; + opacity: 0.75; +} +/* + * This overly specific CSS selector is needed to beat priority of some + * styles applied on the the `.chat-attached-context-attachment` element. + */ +.chat-attached-context .chat-prompt-instructions-attachments .chat-prompt-instructions-attachment.error.implicit, +.chat-attached-context .chat-prompt-instructions-attachments .chat-prompt-instructions-attachment.warning.implicit { + border: 1px solid currentColor; +} +/* + * If in one of the non-normal states, make sure the `main icon` of + * the component has the same color as the component itself + */ +.chat-attached-context .chat-prompt-instructions-attachment.error .monaco-icon-label::before, +.chat-attached-context .chat-prompt-instructions-attachment.warning .monaco-icon-label::before, +.chat-attached-context .chat-prompt-instructions-attachment.disabled .monaco-icon-label::before { + color: inherit; +} +.chat-attached-context .chat-prompt-instructions-attachment.disabled .monaco-icon-label::before { + font-style: italic; +} +.chat-attached-context .chat-prompt-instructions-attachment.disabled:hover { + opacity: 1; +} +.chat-attached-context .chat-prompt-instructions-attachment.disabled .chat-implicit-hint, +.chat-attached-context .chat-prompt-instructions-attachment.disabled .label-name { + font-style: italic; + text-decoration: line-through; +} +.chat-attached-context .chat-prompt-instructions-attachment.disabled:focus { + outline: none; + border-color: var(--vscode-focusBorder); +} + .chat-notification-widget .chat-warning-codicon .codicon-warning, .chat-quota-error-widget .codicon-warning { color: var(--vscode-notificationsWarningIcon-foreground) !important; /* Have to override default styles which apply to all lists */ diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index e9fa7f0ac91a..f7553fe66543 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -29,6 +29,7 @@ export namespace ChatContextKeys { export const inChatInput = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); export const inChatSession = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); export const inChatEditWorkingSet = new RawContextKey('inChatEditWorkingSet', false, { type: 'boolean', description: localize('inChatEditWorkingSet', "True when focus is in the chat edit working set, false otherwise.") }); + export const instructionsAttached = new RawContextKey('chatInstructionsAttached', false, { type: 'boolean', description: localize('chatInstructionsAttachedContextDescription', "True when the chat has a prompt instructions attached.") }); export const supported = ContextKeyExpr.or(IsWebContext.toNegated(), RemoteNameContext.notEqualsTo('')); // supported on desktop and in web only with a remote connection export const enabled = new RawContextKey('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is activated with an implementation.") }); diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index f56ac22858ae..20089cb5315a 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -431,6 +431,11 @@ export interface IChatSendRequestOptions { * The label of the confirmation action that was selected. */ confirmation?: string; + + /** + * Flag to indicate whether a prompt instructions attachment is present. + */ + hasInstructionAttachments?: boolean; } export const IChatService = createDecorator('IChatService'); diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index aad0cd8dab14..6e2741852cf6 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -475,13 +475,21 @@ export class ChatService extends Disposable implements IChatService { 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; } async sendRequest(sessionId: string, request: string, options?: IChatSendRequestOptions): Promise { this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`); - if (!request.trim() && !options?.slashCommand && !options?.agentId) { + + // if text is not provided, but chat input has `prompt instructions` + // attached, use the default prompt text to avoid empty messages + if (!request.trim() && options?.hasInstructionAttachments) { + request = 'Follow these instructions.'; + } + + if (!request.trim() && !options?.slashCommand && !options?.agentId && !options?.hasInstructionAttachments) { this.trace('sendRequest', 'Rejected empty message'); return; } diff --git a/src/vs/workbench/contrib/chat/common/promptFileReference.ts b/src/vs/workbench/contrib/chat/common/promptFileReference.ts index 878c800d7854..e31f7da57225 100644 --- a/src/vs/workbench/contrib/chat/common/promptFileReference.ts +++ b/src/vs/workbench/contrib/chat/common/promptFileReference.ts @@ -8,6 +8,7 @@ import { Emitter } from '../../../../base/common/event.js'; import { extUri } from '../../../../base/common/resources.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { Location } from '../../../../editor/common/languages.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { ChatPromptCodec } from './codecs/chatPromptCodec/chatPromptCodec.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { FileOpenFailed, NonPromptSnippetFile, RecursiveReference } from './promptFileReferenceErrors.js'; @@ -109,6 +110,7 @@ export class PromptFileReference extends Disposable { constructor( private readonly _uri: URI | Location, + @ILogService private readonly logService: ILogService, @IFileService private readonly fileService: IFileService, @IConfigurationService private readonly configService: IConfigurationService, ) { @@ -116,18 +118,17 @@ export class PromptFileReference extends Disposable { this.onFilesChanged = this.onFilesChanged.bind(this); // make sure the variable is updated on file changes - // but only for the prompt snippet files - if (this.isPromptSnippetFile) { - this.addFilesystemListeners(); - } + this.addFilesystemListeners(); } /** * Subscribe to the `onUpdate` event. * @param callback */ - public onUpdate(callback: () => unknown) { + public onUpdate(callback: () => unknown): this { this._register(this._onUpdate.event(callback)); + + return this; } /** @@ -199,10 +200,32 @@ export class PromptFileReference extends Disposable { private onFilesChanged(event: FileChangesEvent) { const fileChanged = event.contains(this.uri, FileChangeType.UPDATED); const fileDeleted = event.contains(this.uri, FileChangeType.DELETED); - if (!fileChanged && !fileDeleted) { + const fileAdded = event.contains(this.uri, FileChangeType.ADDED); + + // if the change does not relate to the current file, nothing to do + if (!fileChanged && !fileDeleted && !fileAdded) { + return; + } + + // handle file changes only for prompt snippet files but in the case a file was + // deleted, it does not matter if it was a prompt - we still need to handle it by + // calling the `resolve()` method, which will set an error condition if the file + // does not exist anymore, or of it is not a prompt snippet file + if (fileChanged && !this.isPromptSnippetFile) { return; } + // if we receive an `add` event, validate that the file was previously deleted, because + // that is the only way we could have end up in this state of the file reference object + if (fileAdded && (!this._errorCondition || !(this._errorCondition instanceof FileOpenFailed))) { + this.logService.warn( + [ + `Received 'add' event for file at '${this.uri.path}', but it was not previously deleted.`, + 'This most likely indicates a bug in our logic, so please report it.', + ].join(' '), + ); + } + // if file is changed or deleted, re-resolve the file reference // in the case when the file is deleted, this should result in // failure to open the file, so the `errorCondition` field will @@ -307,6 +330,7 @@ export class PromptFileReference extends Disposable { const child = new PromptFileReference( childUri, + this.logService, this.fileService, this.configService, ); diff --git a/src/vs/workbench/contrib/chat/test/common/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptFileReference.test.ts index e7256cf7099a..4a3b710e8ceb 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptFileReference.test.ts @@ -60,7 +60,7 @@ class ExpectedReference extends PromptFileReference { nullPolicyService, nullLogService, ); - super(uri, nullFileService, nullConfigService); + super(uri, nullLogService, nullFileService, nullConfigService); this._register(nullFileService); this._register(nullConfigService); @@ -83,6 +83,7 @@ class TestPromptFileReference extends Disposable { private readonly fileStructure: IFolder, private readonly rootFileUri: URI, private readonly expectedReferences: ExpectedReference[], + @ILogService private readonly logService: ILogService, @IFileService private readonly fileService: IFileService, @IConfigurationService private readonly configService: IConfigurationService, ) { @@ -111,6 +112,7 @@ class TestPromptFileReference extends Disposable { // start resolving references for the specified root file const rootReference = this._register(new PromptFileReference( this.rootFileUri, + this.logService, this.fileService, this.configService, )); From a7468b9d9b4b7439ef64420bd371505598a8e03d Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 10 Jan 2025 10:34:50 -0800 Subject: [PATCH 0483/3587] Bump TS version used for building VS Code --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7a0663628aac..5e7d7f307c10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,7 +154,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^5.8.0-dev.20241217", + "typescript": "^5.8.0-dev.20250110", "typescript-eslint": "^8.8.0", "util": "^0.12.4", "webpack": "^5.94.0", @@ -17610,9 +17610,9 @@ "dev": true }, "node_modules/typescript": { - "version": "5.8.0-dev.20241217", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.0-dev.20241217.tgz", - "integrity": "sha512-Q/I+eHfiwN0aWhitenThTT2FcA1lTlUZR1z+6d2WaD/8/wHfdjQjdHynCpYXjAwDkfG8Apf9LdzZ3rLRD3O9iQ==", + "version": "5.8.0-dev.20250110", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.0-dev.20250110.tgz", + "integrity": "sha512-+qwHVEvUm4CeQGtZIvlwE8HmRFcBMV4F/8OPKv+mIyGRGx4Chrj2v0VCsReVJwRdjjs6Dat/lPzkJW1E18+eOg==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 49b8a27df13d..4c27a1523b98 100644 --- a/package.json +++ b/package.json @@ -212,7 +212,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^5.8.0-dev.20241217", + "typescript": "^5.8.0-dev.20250110", "typescript-eslint": "^8.8.0", "util": "^0.12.4", "webpack": "^5.94.0", From 53ea27f0e248b378b3199f2284c4a0ea03cff181 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 10 Jan 2025 19:43:51 +0100 Subject: [PATCH 0484/3587] Dynamic display name for Gutter Indicator Menu Content (#237651) * dynamic displayname * Inline Edits Display Name from extension --- src/vs/editor/common/languages.ts | 3 +++ .../browser/view/inlineEdits/gutterIndicatorMenu.ts | 6 +++--- .../browser/view/inlineEdits/gutterIndicatorView.ts | 10 ++++++++++ src/vs/monaco.d.ts | 2 ++ .../api/browser/mainThreadLanguageFeatures.ts | 3 ++- src/vs/workbench/api/common/extHost.protocol.ts | 2 +- src/vs/workbench/api/common/extHostLanguageFeatures.ts | 2 +- src/vscode-dts/vscode.proposed.inlineEdit.d.ts | 3 +++ 8 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index bef68de579de..cc98d8d2d1e2 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -834,6 +834,8 @@ export interface InlineCompletionsProvider { + displayName?: string; provideInlineEdit(model: model.ITextModel, context: IInlineEditContext, token: CancellationToken): ProviderResult; freeInlineEdit(edit: T): void; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts index 70160ee12b87..afbcc2b58d5c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts @@ -20,6 +20,7 @@ import { ChildNode, FirstFnArg, LiveElement, n } from './utils.js'; export class GutterIndicatorMenuContent { constructor( + private readonly _menuTitle: IObservable, private readonly _selectionOverride: IObservable<'jump' | 'accept' | undefined>, private readonly _close: (focusEditor: boolean) => void, private readonly _extensionCommands: IObservable, @@ -53,8 +54,7 @@ export class GutterIndicatorMenuContent { // TODO make this menu contributable! return hoverContent([ - // TODO: make header dynamic, get from extension - header(localize('inlineEdit', "Inline Edit")), + header(this._menuTitle), option(createOptionArgs({ id: 'jump', title: localize('goto', "Go To"), icon: Codicon.arrowRight, commandId: new JumpToNextInlineEdit().id })), option(createOptionArgs({ id: 'accept', title: localize('accept', "Accept"), icon: Codicon.check, commandId: new AcceptInlineCompletion().id })), option(createOptionArgs({ id: 'reject', title: localize('reject', "Reject"), icon: Codicon.close, commandId: new HideInlineCompletion().id })), @@ -85,7 +85,7 @@ function hoverContent(content: ChildNode) { }, content); } -function header(title: string) { +function header(title: string | IObservable) { return n.div({ class: 'header', style: { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index d0cdbc360b38..847f3a87edeb 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -187,9 +187,19 @@ export class InlineEditsGutterIndicator extends Disposable { return; } + const displayName = derived(this, reader => { + const state = this._model.read(reader)?.inlineEditState; + const item = state?.read(reader); + const completionSource = item?.inlineCompletion?.inlineCompletion.source; + // TODO: expose the provider (typed) and expose the provider the edit belongs totyping and get correct edit + const displayName = (completionSource?.inlineCompletions as any).edits[0]?.provider?.displayName ?? localize('inlineEdit', "Inline Edit"); + return displayName; + }); + const disposableStore = new DisposableStore(); const content = disposableStore.add(this._instantiationService.createInstance( GutterIndicatorMenuContent, + displayName, this._hoverSelectionOverride, (focusEditor) => { if (focusEditor) { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 5db429fff414..461852b5196d 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7320,6 +7320,7 @@ declare namespace monaco.languages { * The current provider is only requested for completions if no provider with a preferred group id returned a result. */ yieldsToGroupIds?: InlineCompletionProviderGroupId[]; + displayName?: string; toString?(): string; } @@ -8181,6 +8182,7 @@ declare namespace monaco.languages { } export interface InlineEditProvider { + displayName?: string; provideInlineEdit(model: editor.ITextModel, context: IInlineEditContext, token: CancellationToken): ProviderResult; freeInlineEdit(edit: T): void; } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 3d2c82050578..98faa3e3814a 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -644,8 +644,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread this._registrations.set(handle, this._languageFeaturesService.inlineCompletionsProvider.register(selector, provider)); } - $registerInlineEditProvider(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier): void { + $registerInlineEditProvider(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void { const provider: languages.InlineEditProvider = { + displayName, provideInlineEdit: async (model: ITextModel, context: languages.IInlineEditContext, token: CancellationToken): Promise => { return this._proxy.$provideInlineEdit(handle, model.uri, context, token); }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 9155c298fc3c..e8de9400531e 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -455,7 +455,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: languages.SemanticTokensLegend): void; $registerCompletionsProvider(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, extensionId: ExtensionIdentifier): void; $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[], supportsHandleDidShowCompletionItem: boolean, extensionId: string, yieldsToExtensionIds: string[]): void; - $registerInlineEditProvider(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier): void; + $registerInlineEditProvider(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; $registerInlayHintsProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean, eventHandle: number | undefined, displayName: string | undefined): void; $emitInlayHintsEvent(eventHandle: number): void; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index ac9576a3b22d..1951cbf1c6fd 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -2729,7 +2729,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF registerInlineEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineEditProvider): vscode.Disposable { const adapter = new InlineEditAdapter(extension, this._documents, provider, this._commands.converter); const handle = this._addNewAdapter(adapter, extension); - this._proxy.$registerInlineEditProvider(handle, this._transformDocumentSelector(selector, extension), extension.identifier); + this._proxy.$registerInlineEditProvider(handle, this._transformDocumentSelector(selector, extension), extension.identifier, provider.displayName || extension.name); return this._createDisposable(handle); } diff --git a/src/vscode-dts/vscode.proposed.inlineEdit.d.ts b/src/vscode-dts/vscode.proposed.inlineEdit.d.ts index 12e87cab4cf2..ad6fe45dbec2 100644 --- a/src/vscode-dts/vscode.proposed.inlineEdit.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineEdit.d.ts @@ -71,6 +71,9 @@ declare module 'vscode' { } export interface InlineEditProvider { + + readonly displayName?: string; + /** * Provide inline edit for the given document. * From 645027c7d953169e8cd556431bcbe512f8e83f47 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 10 Jan 2025 21:20:18 +0100 Subject: [PATCH 0485/3587] Highlight accepted inline edits temporarily (#237682) highlight edit after applying --- .../lib/stylelint/vscode-known-variables.json | 1 + .../browser/model/inlineCompletionsModel.ts | 25 +++++++++++++++---- .../inlineCompletions/browser/utils.ts | 8 ++++-- .../view/inlineEdits/sideBySideDiff.ts | 14 ++++++++++- .../browser/view/inlineEdits/view.css | 4 +++ 5 files changed, 44 insertions(+), 8 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 23979f5686ad..d6eb99d2f398 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -370,6 +370,7 @@ "--vscode-inlineEdit-originalBackground", "--vscode-inlineEdit-originalChangedLineBackground", "--vscode-inlineEdit-originalChangedTextBackground", + "--vscode-inlineEdit-acceptedBackground", "--vscode-input-background", "--vscode-input-border", "--vscode-input-foreground", diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index efb2b8c4bc0e..111013977f67 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { mapFindFirst } from '../../../../../base/common/arraysFind.js'; +import { disposableTimeout } from '../../../../../base/common/async.js'; import { itemsEquals } from '../../../../../base/common/equals.js'; import { BugIndicatingError, onUnexpectedError, onUnexpectedExternalError } from '../../../../../base/common/errors.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { IObservable, IObservableWithChange, IReader, ITransaction, autorun, derived, derivedHandleChanges, derivedOpts, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from '../../../../../base/common/observable.js'; import { commonPrefixLength, firstNonWhitespaceIndex } from '../../../../../base/common/strings.js'; import { isDefined } from '../../../../../base/common/types.js'; @@ -26,11 +27,11 @@ import { TextLength } from '../../../../common/core/textLength.js'; import { ScrollType } from '../../../../common/editorCommon.js'; import { Command, InlineCompletion, InlineCompletionContext, InlineCompletionTriggerKind, PartialAcceptTriggerKind } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; -import { EndOfLinePreference, ITextModel } from '../../../../common/model.js'; +import { EndOfLinePreference, IModelDecorationOptions, ITextModel, TrackedRangeStickiness } from '../../../../common/model.js'; import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js'; import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js'; import { SnippetController2 } from '../../../snippet/browser/snippetController2.js'; -import { addPositions, getEndPositionsAfterApplying, substringPos, subtractPositions } from '../utils.js'; +import { addPositions, getEndPositionsAfterApplying, getModifiedRangesAfterApplying, substringPos, subtractPositions } from '../utils.js'; import { computeGhostText } from './computeGhostText.js'; import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostTextsOrReplacementsEqual } from './ghostText.js'; import { InlineCompletionWithUpdatedRange, InlineCompletionsSource } from './inlineCompletionsSource.js'; @@ -54,6 +55,13 @@ export class InlineCompletionsModel extends Disposable { private readonly _editorObs = observableCodeEditor(this._editor); + private readonly _acceptCompletionDecorationTimer = this._register(new MutableDisposable()); + private readonly _acceptCompletionDecoration: IModelDecorationOptions = { + description: 'inline-completion-accepted', + className: 'inlineCompletionAccepted', + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + }; + private readonly _suggestPreviewEnabled = this._editorObs.getOption(EditorOption.suggest).map(v => v.preview); private readonly _suggestPreviewMode = this._editorObs.getOption(EditorOption.suggest).map(v => v.previewMode); private readonly _inlineSuggestMode = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => v.mode); @@ -573,9 +581,10 @@ export class InlineCompletionsModel extends Disposable { completion.source.addRef(); } + this._acceptCompletionDecorationTimer.clear(); + editor.pushUndoStop(); if (completion.snippetInfo) { - // ... editor.executeEdits( 'inlineSuggestion.accept', [ @@ -587,12 +596,18 @@ export class InlineCompletionsModel extends Disposable { SnippetController2.get(editor)?.insert(completion.snippetInfo.snippet, { undoStopBefore: false }); } else { const edits = state.edits; - const selections = getEndPositionsAfterApplying(edits).map(p => Selection.fromPositions(p)); + const modifiedRanges = getModifiedRangesAfterApplying(edits); + const selections = modifiedRanges.map(r => Selection.fromPositions(r.getEndPosition())); editor.executeEdits('inlineSuggestion.accept', [ ...edits.map(edit => EditOperation.replace(edit.range, edit.text)), ...completion.additionalTextEdits ]); editor.setSelections(selections, 'inlineCompletionAccept'); + + if (state.kind === 'inlineEdit') { + const acceptEditsDecorations = editor.createDecorationsCollection(modifiedRanges.map(r => ({ range: r, options: this._acceptCompletionDecoration }))); + this._acceptCompletionDecorationTimer.value = disposableTimeout(() => acceptEditsDecorations.clear(), 2500); + } } // Reset before invoking the command, as the command might cause a follow up trigger (which we don't want to reset). diff --git a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts index 0f37508a9a29..4541e90d9fad 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts @@ -53,11 +53,15 @@ export function substringPos(text: string, pos: Position): string { return text.substring(offset); } -export function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Position[] { +export function getModifiedRangesAfterApplying(edits: readonly SingleTextEdit[]): Range[] { const sortPerm = Permutation.createSortPermutation(edits, compareBy(e => e.range, Range.compareRangesUsingStarts)); const edit = new TextEdit(sortPerm.apply(edits)); const sortedNewRanges = edit.getNewRanges(); - const newRanges = sortPerm.inverse().apply(sortedNewRanges); + return sortPerm.inverse().apply(sortedNewRanges); +} + +export function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Position[] { + const newRanges = getModifiedRangesAfterApplying(edits); return newRanges.map(range => range.getEndPosition()); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index 2235a94e05bf..67c8dfb38fa5 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -13,7 +13,7 @@ import { MenuId, MenuItemAction } from '../../../../../../platform/actions/commo import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { diffInserted, diffRemoved } from '../../../../../../platform/theme/common/colorRegistry.js'; -import { darken, lighten, registerColor } from '../../../../../../platform/theme/common/colorUtils.js'; +import { darken, lighten, registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; @@ -96,6 +96,18 @@ export const modifiedBorder = registerColor( localize('inlineEdit.modifiedBorder', 'Border color for the modified text in inline edits.') ); +export const acceptedDecorationBackgroundColor = registerColor( + 'inlineEdit.acceptedBackground', + { + light: transparent(modifiedChangedTextOverlayColor, 0.5), + dark: transparent(modifiedChangedTextOverlayColor, 0.5), + hcDark: modifiedChangedTextOverlayColor, + hcLight: modifiedChangedTextOverlayColor + }, + localize('inlineEdit.acceptedBackground', 'Background color for the accepted text after appying an inline edit.'), + true +); + export class InlineEditsSideBySideDiff extends Disposable { private readonly _editorObs = observableCodeEditor(this._editor); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index 0a46a1490424..05fc3ffa137e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -184,3 +184,7 @@ outline-offset: -1px; } } + +.monaco-editor .inlineCompletionAccepted { + background-color: var(--vscode-inlineEdit-acceptedBackground); +} From c8be65a66abe8ea057dc05cc787084c0fb79d2bf Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 10 Jan 2025 21:53:39 +0100 Subject: [PATCH 0486/3587] SCM - add plumbing to support history item ref actions (#237687) * Implement using classes (order is not working) * A better implementation with order working as well * Remove commands for the time being --- extensions/git/src/historyProvider.ts | 4 + .../contrib/scm/browser/scmHistoryViewPane.ts | 97 ++++++++++++------- 2 files changed, 68 insertions(+), 33 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 9512facf774e..6d65c5226b67 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -369,6 +369,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec id: ref.substring('HEAD -> '.length), name: ref.substring('HEAD -> refs/heads/'.length), revision: commit.hash, + category: l10n.t('branches'), icon: new ThemeIcon('target') }); break; @@ -377,6 +378,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec id: ref, name: ref.substring('refs/heads/'.length), revision: commit.hash, + category: l10n.t('branches'), icon: new ThemeIcon('git-branch') }); break; @@ -385,6 +387,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec id: ref, name: ref.substring('refs/remotes/'.length), revision: commit.hash, + category: l10n.t('remote branches'), icon: new ThemeIcon('cloud') }); break; @@ -393,6 +396,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec id: ref.substring('tag: '.length), name: ref.substring('tag: refs/tags/'.length), revision: commit.hash, + category: l10n.t('tags'), icon: new ThemeIcon('tag') }); break; diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 7b9e08f87e15..ad0a2c98f51d 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -15,7 +15,7 @@ import { IAsyncDataSource, ITreeContextMenuEvent, ITreeNode, ITreeRenderer } fro import { fromNow, safeIntl } from '../../../../base/common/date.js'; import { createMatches, FuzzyScore, IMatch } from '../../../../base/common/filters.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { autorun, autorunWithStore, derived, IObservable, observableValue, waitForState, constObservable, latestChangedValue, observableFromEvent, runOnChange, observableSignal } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; @@ -40,11 +40,11 @@ import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/lis import { stripIcons } from '../../../../base/common/iconLabels.js'; import { IWorkbenchLayoutService, Position } from '../../../services/layout/browser/layoutService.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; -import { Action2, IMenuService, MenuId, MenuItemAction, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { Action2, IMenuService, isIMenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { Sequencer, Throttler } from '../../../../base/common/async.js'; import { URI } from '../../../../base/common/uri.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { ActionRunner, IAction, IActionRunner, Separator, SubmenuAction } from '../../../../base/common/actions.js'; +import { ActionRunner, IAction, IActionRunner } from '../../../../base/common/actions.js'; import { delta, groupBy } from '../../../../base/common/arrays.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { IProgressService } from '../../../../platform/progress/common/progress.js'; @@ -1215,6 +1215,8 @@ export class SCMHistoryViewPane extends ViewPane { private readonly _scmCurrentHistoryItemRefHasRemote: IContextKey; private readonly _scmCurrentHistoryItemRefInFilter: IContextKey; + private readonly _contextMenuDisposables = new MutableDisposable(); + constructor( options: IViewPaneOptions, @ICommandService private readonly _commandService: ICommandService, @@ -1577,46 +1579,74 @@ export class SCMHistoryViewPane extends ViewPane { return; } - const historyItemMenuActions = this._menuService.getMenuActions(MenuId.SCMChangesContext, this.scopedContextKeyService, { - arg: element.repository.provider, - shouldForwardArgs: true - }); + this._contextMenuDisposables.value = new DisposableStore(); - const actions = getFlatContextMenuActions(historyItemMenuActions); - if (element.historyItemViewModel.historyItem.references?.length) { - actions.push(new Separator()); - } + const historyItemRefMenuItems = MenuRegistry.getMenuItems(MenuId.SCMHistoryItemRefContext).filter(item => isIMenuItem(item)); - const that = this; - for (const ref of element.historyItemViewModel.historyItem.references ?? []) { - const contextKeyService = this.scopedContextKeyService.createOverlay([ - ['scmHistoryItemRef', ref.id] - ]); - - const historyItemRefMenuActions = this._menuService.getMenuActions(MenuId.SCMHistoryItemRefContext, contextKeyService); - const historyItemRefSubMenuActions = getFlatContextMenuActions(historyItemRefMenuActions) - .map(action => new class extends MenuItemAction { - constructor() { - super( - { id: action.id, title: action.label }, undefined, - { arg: element!.repository.provider, shouldForwardArgs: true }, - undefined, undefined, contextKeyService, that._commandService); - } + // If there are any history item references we have to add a submenu item for each orignal action, + // and a menu item for each history item ref that matches the `when` clause of the original action. + if (historyItemRefMenuItems.length > 0 && element.historyItemViewModel.historyItem.references?.length) { + const submenuIds = new Map(); - override run(): Promise { - return super.run(element.historyItemViewModel.historyItem, ref.id); - } - }); + for (const ref of element.historyItemViewModel.historyItem.references) { + const contextKeyService = this.scopedContextKeyService.createOverlay([ + ['scmHistoryItemRef', ref.id] + ]); + + for (const [, actions] of this._menuService.getMenuActions(MenuId.SCMHistoryItemRefContext, contextKeyService)) { + for (const action of actions) { + let subMenuId = submenuIds.get(action.id); + + if (!subMenuId) { + subMenuId = MenuId.for(action.id); - if (historyItemRefSubMenuActions.length > 0) { - actions.push(new SubmenuAction(`scm.historyItemRef.${ref.id}`, ref.name, historyItemRefSubMenuActions)); + // Get the menu item for the original action so that + // we can create a submenu with the same group, order + const historyItemRefMenuItem = historyItemRefMenuItems + .find(item => item.command.id === action.id); + + // Register the submenu for the original action + this._contextMenuDisposables.value.add(MenuRegistry.appendMenuItem(MenuId.SCMChangesContext, { + title: action.label, + submenu: subMenuId, + group: historyItemRefMenuItem?.group, + order: historyItemRefMenuItem?.order + })); + + submenuIds.set(action.id, subMenuId); + } + + // Register a new action for the history item ref + this._contextMenuDisposables.value.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: `${action.id}.${ref.id}`, + title: ref.name, + menu: { + id: subMenuId!, + group: ref.category + } + }); + } + override run(accessor: ServicesAccessor, ...args: any[]): void { + const commandService = accessor.get(ICommandService); + commandService.executeCommand(action.id, ...args, ref.id); + } + })); + } + } } } + const historyItemMenuActions = this._menuService.getMenuActions(MenuId.SCMChangesContext, this.scopedContextKeyService, { + arg: element.repository.provider, + shouldForwardArgs: true + }); + this.contextMenuService.showContextMenu({ contextKeyService: this.scopedContextKeyService, getAnchor: () => e.anchor, - getActions: () => actions, + getActions: () => getFlatContextMenuActions(historyItemMenuActions), getActionsContext: () => element.historyItemViewModel.historyItem }); } @@ -1649,6 +1679,7 @@ export class SCMHistoryViewPane extends ViewPane { } override dispose(): void { + this._contextMenuDisposables.dispose(); this._visibilityDisposables.dispose(); super.dispose(); } From f5ed61a6d609ee5785950706419358a703822f75 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:56:33 -0800 Subject: [PATCH 0487/3587] resize images for copilot vision (#237689) resizing clean --- .../browser/actions/chatContextActions.ts | 34 ++++++---- .../contrib/chat/browser/chatDragAndDrop.ts | 9 ++- .../chat/browser/chatPasteProviders.ts | 15 +++-- .../contrib/chat/browser/imageUtils.ts | 63 +++++++++++++++++++ 4 files changed, 101 insertions(+), 20 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/imageUtils.ts diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index e74066f28970..320eeaec1ffd 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -22,6 +22,7 @@ import { Action2, IAction2Options, MenuId, registerAction2 } from '../../../../. import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { AnythingQuickAccessProviderRunOptions } from '../../../../../platform/quickinput/common/quickAccess.js'; @@ -52,6 +53,7 @@ import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView, showE import { imageToHash, isImage } from '../chatPasteProviders.js'; import { isQuickChat } from '../chatWidget.js'; import { convertBufferToScreenshotVariable, ScreenshotVariableId } from '../contrib/screenshot.js'; +import { resizeImage } from '../imageUtils.js'; import { CHAT_CATEGORY } from './chatActions.js'; export function registerChatContextActions() { @@ -458,7 +460,7 @@ export class AttachContextAction extends Action2 { `:${item.range.startLineNumber}`); } - private async _attachContext(widget: IChatWidget, quickInputService: IQuickInputService, commandService: ICommandService, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, isInBackground?: boolean, ...picks: IChatContextQuickPickItem[]) { + private async _attachContext(widget: IChatWidget, quickInputService: IQuickInputService, commandService: ICommandService, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, fileService: IFileService, isInBackground?: boolean, ...picks: IChatContextQuickPickItem[]) { const toAttach: IChatRequestVariableEntry[] = []; for (const pick of picks) { if (isISymbolQuickPickItem(pick) && pick.symbol) { @@ -475,14 +477,19 @@ export class AttachContextAction extends Action2 { } 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 - toAttach.push({ - id: pick.resource.toString(), - name: pick.label, - fullName: pick.label, - value: pick.resource, - isDynamic: true, - isImage: true - }); + if (URI.isUri(pick.resource)) { + // read the image and attach a new file context. + const readFile = await fileService.readFile(pick.resource); + const resizedImage = await resizeImage(readFile.value.buffer); + toAttach.push({ + id: pick.resource.toString(), + name: pick.label, + fullName: pick.label, + value: resizedImage, + isDynamic: true, + isImage: true + }); + } } else { // file attachment if (chatEditingService) { @@ -681,6 +688,7 @@ export class AttachContextAction extends Action2 { const viewsService = accessor.get(IViewsService); const hostService = accessor.get(IHostService); const extensionService = accessor.get(IExtensionService); + const fileService = accessor.get(IFileService); const context: { widget?: IChatWidget; showFilesOnly?: boolean; placeholder?: string } | undefined = args[0]; const widget = context?.widget ?? widgetService.lastFocusedWidget; @@ -845,19 +853,19 @@ export class AttachContextAction extends Action2 { const second = extractTextFromIconLabel(b.label).toUpperCase(); return compare(first, second); - }), clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, '', context?.placeholder); + }), clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, '', context?.placeholder); } - 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, query: string = '', placeholder?: string) { + 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, query: string = '', placeholder?: string) { const providerOptions: AnythingQuickAccessProviderRunOptions = { handleAccept: (item: IChatContextQuickPickItem, isBackgroundAccept: boolean) => { if ('prefix' in item) { - this._show(quickInputService, commandService, widget, quickChatService, quickPickItems, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, item.prefix, placeholder); + this._show(quickInputService, commandService, widget, quickChatService, quickPickItems, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, item.prefix, placeholder); } else { if (!clipboardService) { return; } - this._attachContext(widget, quickInputService, commandService, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, isBackgroundAccept, item); + this._attachContext(widget, quickInputService, commandService, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, isBackgroundAccept, item); if (isQuickChat(widget)) { quickChatService.open(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts b/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts index c9194487fe08..8b428308f7ce 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts @@ -26,6 +26,7 @@ import { UntitledTextEditorInput } from '../../../services/untitled/common/untit import { IChatRequestVariableEntry, ISymbolVariableEntry } from '../common/chatModel.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; import { IChatInputStyles } from './chatInputPart.js'; +import { resizeImage } from './imageUtils.js'; enum ChatDragAndDropType { FILE_INTERNAL, @@ -232,7 +233,7 @@ export class ChatDragAndDrop extends Themable { private async resolveAttachContext(editorInput: IDraggedResourceEditorInput): Promise { // Image - const imageContext = getImageAttachContext(editorInput); + const imageContext = await getImageAttachContext(editorInput, this.fileService); if (imageContext) { return this.extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData')) ? imageContext : undefined; } @@ -425,18 +426,20 @@ function getResourceAttachContext(resource: URI, isDirectory: boolean): IChatReq }; } -function getImageAttachContext(editor: EditorInput | IDraggedResourceEditorInput): IChatRequestVariableEntry | undefined { +async function getImageAttachContext(editor: EditorInput | IDraggedResourceEditorInput, fileService: IFileService): Promise { if (!editor.resource) { return undefined; } if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(editor.resource.path)) { const fileName = basename(editor.resource); + const readFile = await fileService.readFile(editor.resource); + const resizedImage = await resizeImage(readFile.value.buffer); return { id: editor.resource.toString(), name: fileName, fullName: editor.resource.path, - value: editor.resource, + value: resizedImage, icon: Codicon.fileMedia, isDynamic: true, isImage: true, diff --git a/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts b/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts index 9d990d093020..5550d3d33b9c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts @@ -21,6 +21,7 @@ import { Mimes } from '../../../../base/common/mime.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { basename } from '../../../../base/common/resources.js'; +import { resizeImage } from './imageUtils.js'; const COPY_MIME_TYPES = 'application/vnd.code.additional-editor-data'; @@ -89,19 +90,25 @@ export class PasteImageProvider implements DocumentPasteEditProvider { tempDisplayName = `${displayName} ${appendValue}`; } - const imageContext = await getImageAttachContext(currClipboard, mimeType, token, tempDisplayName); + const scaledImageData = await resizeImage(currClipboard); + if (token.isCancellationRequested || !scaledImageData) { + return; + } - if (token.isCancellationRequested || !imageContext) { + const scaledImageContext = await getImageAttachContext(scaledImageData, mimeType, token, tempDisplayName); + if (token.isCancellationRequested || !scaledImageContext) { return; } + widget.attachmentModel.addContext(scaledImageContext); + // Make sure to attach only new contexts const currentContextIds = widget.attachmentModel.getAttachmentIDs(); - if (currentContextIds.has(imageContext.id)) { + if (currentContextIds.has(scaledImageContext.id)) { return; } - const edit = createCustomPasteEdit(model, imageContext, mimeType, this.kind, localize('pastedImageAttachment', 'Pasted Image Attachment'), this.chatWidgetService); + const edit = createCustomPasteEdit(model, scaledImageContext, mimeType, this.kind, localize('pastedImageAttachment', 'Pasted Image Attachment'), this.chatWidgetService); return createEditSession(edit); } } diff --git a/src/vs/workbench/contrib/chat/browser/imageUtils.ts b/src/vs/workbench/contrib/chat/browser/imageUtils.ts new file mode 100644 index 000000000000..c176876c5380 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/imageUtils.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +/** + * Resizes an image provided as a UInt8Array string. Resizing is based on Open AI's algorithm for tokenzing images. + * https://platform.openai.com/docs/guides/vision#calculating-costs + * @param data - The UInt8Array string of the image to resize. + * @returns A promise that resolves to the UInt8Array string of the resized image. + */ + +export async function resizeImage(data: Uint8Array): Promise { + const blob = new Blob([data]); + const img = new Image(); + const url = URL.createObjectURL(blob); + img.src = url; + + return new Promise((resolve, reject) => { + img.onload = () => { + URL.revokeObjectURL(url); + let { width, height } = img; + + // Calculate the new dimensions while maintaining the aspect ratio + if (width > 2048 || height > 2048) { + const scaleFactor = 2048 / Math.max(width, height); + width = Math.round(width * scaleFactor); + height = Math.round(height * scaleFactor); + } + + const scaleFactor = 768 / Math.min(width, height); + width = Math.round(width * scaleFactor); + height = Math.round(height * scaleFactor); + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.drawImage(img, 0, 0, width, height); + canvas.toBlob((blob) => { + if (blob) { + const reader = new FileReader(); + reader.onload = () => { + resolve(new Uint8Array(reader.result as ArrayBuffer)); + }; + reader.onerror = (error) => reject(error); + reader.readAsArrayBuffer(blob); + } else { + reject(new Error('Failed to create blob from canvas')); + } + }, 'image/png'); + } else { + reject(new Error('Failed to get canvas context')); + } + }; + img.onerror = (error) => { + URL.revokeObjectURL(url); + reject(error); + }; + }); +} From 25a0bb98b7dfb524e0c71e01832e5b08961bfd00 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 10 Jan 2025 13:46:36 -0800 Subject: [PATCH 0488/3587] chat: allow working set files to be hinted as readonly (#237690) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds a 🔒 icon that can be toggled on files in the working set - When toggled, extensions with the chatReadonlyPromptReference will see the references is `isReadonly` - Currently the 🔒 icon is only shown when an extension that has the proposal is installed. - I'm not a big fan of the name "isReadonly", it's more like "hintReadonly", at least in my current use case. But "isReadonly" is better existing terminology that we already have in vscode API. ![](https://memes.peet.io/img/25-01-71eea854-5670-40d0-92ee-7c5bb8d22cb9.gif) --- package.json | 2 +- .../common/extensionsApiProposals.ts | 3 ++ .../api/common/extHostChatAgents2.ts | 25 ++++++++------- .../api/common/extHostTypeConverters.ts | 9 +++--- .../chat/browser/chatAttachmentModel.ts | 5 +-- .../chatReferencesContentPart.ts | 10 ++++-- .../browser/chatEditing/chatEditingActions.ts | 31 ++++++++++++++++++- .../browser/chatEditing/chatEditingService.ts | 14 ++++++++- .../browser/chatEditing/chatEditingSession.ts | 16 ++++++++++ .../contrib/chat/browser/chatInputPart.ts | 10 ++++-- .../contrib/chat/browser/chatWidget.ts | 10 +++--- .../contrib/chat/browser/media/chat.css | 6 ++++ .../contrib/chat/common/chatEditingService.ts | 9 +++++- .../contrib/chat/common/chatModel.ts | 1 + ....proposed.chatReadonlyPromptReference.d.ts | 17 ++++++++++ 15 files changed, 136 insertions(+), 32 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.chatReadonlyPromptReference.d.ts diff --git a/package.json b/package.json index 4c27a1523b98..e6b539b597af 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.97.0", - "distro": "baf3347105f6082ae1df942fd1c052cd06f7a7f0", + "distro": "d631a7e71cfab757e241fd75ecb1594c6b427920", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index ad633927aeaf..0ca98611fb8b 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -38,6 +38,9 @@ const _allApiProposals = { chatProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', }, + chatReadonlyPromptReference: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatReadonlyPromptReference.d.ts', + }, chatReferenceBinaryData: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts', }, diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 13a6572cba19..45b7fd7fd0b4 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -18,7 +18,7 @@ import { assertType } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { Location } from '../../../editor/common/languages.js'; -import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; +import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { ILogService } from '../../../platform/log/common/log.js'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult, IChatAgentResultTimings, IChatWelcomeMessageContent } from '../../contrib/chat/common/chatAgents.js'; import { ChatAgentVoteDirection, IChatContentReference, IChatFollowup, IChatResponseErrorDetails, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService.js'; @@ -387,15 +387,15 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } async $detectChatParticipant(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { location: ChatAgentLocation; participants?: vscode.ChatParticipantMetadata[] }, token: CancellationToken): Promise { - const { request, location, history } = await this._createRequest(requestDto, context); - const detector = this._participantDetectionProviders.get(handle); if (!detector) { return undefined; } + 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); + const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(detector.extension, 'chatReadonlyPromptReference')); return detector.provider.provideParticipantDetection( extRequest, @@ -405,9 +405,9 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS ); } - private async _createRequest(requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }) { + private async _createRequest(requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, extension: IExtensionDescription) { const request = revive(requestDto); - const convertedHistory = await this.prepareHistoryTurns(request.agentId, context); + const convertedHistory = await this.prepareHistoryTurns(extension, request.agentId, context); // in-place converting for location-data let location: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined; @@ -452,7 +452,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS let stream: ChatAgentResponseStream | undefined; try { - const { request, location, history } = await this._createRequest(requestDto, context); + const { request, location, history } = await this._createRequest(requestDto, context, agent.extension); // Init session disposables let sessionDisposables = this._sessionDisposables.get(request.sessionId); @@ -464,7 +464,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); + const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(agent.extension, 'chatReadonlyPromptReference')); const task = agent.invoke( extRequest, @@ -511,7 +511,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } } - private async prepareHistoryTurns(agentId: string, context: { history: IChatAgentHistoryEntryDto[] }): Promise<(vscode.ChatRequestTurn | vscode.ChatResponseTurn)[]> { + private async prepareHistoryTurns(extension: Readonly, agentId: string, context: { history: IChatAgentHistoryEntryDto[] }): Promise<(vscode.ChatRequestTurn | vscode.ChatResponseTurn)[]> { const res: (vscode.ChatRequestTurn | vscode.ChatResponseTurn)[] = []; for (const h of context.history) { @@ -521,9 +521,10 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS { ...ehResult, metadata: undefined }; // REQUEST turn + const hasReadonlyProposal = isProposedApiEnabled(extension, 'chatReadonlyPromptReference'); const varsWithoutTools = h.request.variables.variables .filter(v => !v.isTool) - .map(typeConvert.ChatPromptReference.to); + .map(v => typeConvert.ChatPromptReference.to(v, hasReadonlyProposal)); const toolReferences = h.request.variables.variables .filter(v => v.isTool) .map(typeConvert.ChatLanguageModelToolReference.to); @@ -549,7 +550,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } const request = revive(requestDto); - const convertedHistory = await this.prepareHistoryTurns(agent.id, context); + const convertedHistory = await this.prepareHistoryTurns(agent.extension, agent.id, context); const ehResult = typeConvert.ChatAgentResult.to(result); return (await agent.provideFollowups(ehResult, { history: convertedHistory }, token)) @@ -642,7 +643,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return; } - const history = await this.prepareHistoryTurns(agent.id, { history: context }); + const history = await this.prepareHistoryTurns(agent.extension, agent.id, { history: context }); return await agent.provideTitle({ history }, token); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index b7f3d7e8b5ec..203ebfae86d3 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2799,7 +2799,7 @@ export namespace ChatResponsePart { } export namespace ChatAgentRequest { - export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat): vscode.ChatRequest { + export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, hasReadonlyProposal: boolean): vscode.ChatRequest { const toolReferences = request.variables.variables.filter(v => v.isTool); const variableReferences = request.variables.variables.filter(v => !v.isTool); return { @@ -2808,7 +2808,7 @@ export namespace ChatAgentRequest { attempt: request.attempt ?? 0, enableCommandDetection: request.enableCommandDetection ?? true, isParticipantDetected: request.isParticipantDetected ?? false, - references: variableReferences.map(ChatPromptReference.to), + references: variableReferences.map(v => ChatPromptReference.to(v, hasReadonlyProposal)), toolReferences: toolReferences.map(ChatLanguageModelToolReference.to), location: ChatLocation.to(request.location), acceptedConfirmationData: request.acceptedConfirmationData, @@ -2852,7 +2852,7 @@ export namespace ChatLocation { } export namespace ChatPromptReference { - export function to(variable: IChatRequestVariableEntry): vscode.ChatPromptReference { + export function to(variable: IChatRequestVariableEntry, hasReadonlyProposal: boolean): vscode.ChatPromptReference { const value = variable.value; if (!value) { throw new Error('Invalid value reference'); @@ -2865,7 +2865,8 @@ export namespace ChatPromptReference { 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)))) : value, - modelDescription: variable.modelDescription + modelDescription: variable.modelDescription, + isReadonly: hasReadonlyProposal ? variable.isMarkedReadonly : undefined, }; } } diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts index 2cb3ffb405a7..739be7f185d4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts @@ -63,13 +63,14 @@ export class ChatAttachmentModel extends Disposable { this.addContext(this.asVariableEntry(uri, range)); } - asVariableEntry(uri: URI, range?: IRange): IChatRequestVariableEntry { + asVariableEntry(uri: URI, range?: IRange, isMarkedReadonly?: boolean): IChatRequestVariableEntry { return { value: range ? { uri, range } : uri, id: uri.toString() + (range?.toString() ?? ''), name: basename(uri), isFile: true, - isDynamic: true + isDynamic: true, + isMarkedReadonly, }; } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts index a06c21d966b0..04ec43faa61a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -37,7 +37,7 @@ import { ResourceContextKey } from '../../../../common/contextkeys.js'; import { SETTINGS_AUTHORITY } from '../../../../services/preferences/common/preferences.js'; import { createFileIconThemableTreeContainerScope } from '../../../files/browser/views/explorerView.js'; import { ExplorerFolderContext } from '../../../files/common/files.js'; -import { chatEditingWidgetFileStateContextKey, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { chatEditingWidgetFileReadonlyContextKey, chatEditingWidgetFileStateContextKey, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { ChatResponseReferencePartStatusKind, IChatContentReference, IChatWarningMessage } from '../../common/chatService.js'; import { IChatVariablesService } from '../../common/chatVariables.js'; import { IChatRendererContent, IChatResponseViewModel } from '../../common/chatViewModel.js'; @@ -52,6 +52,7 @@ export interface IChatReferenceListItem extends IChatContentReference { description?: string; state?: WorkingSetEntryState; excluded?: boolean; + isMarkedReadonly?: boolean; } export type IChatCollapsibleListItem = IChatReferenceListItem | IChatWarningMessage; @@ -435,8 +436,11 @@ class CollapsibleListRenderer implements IListRenderer { + for (const uri of uris) { + currentEditingSession.markIsReadonly(uri); + } + } +}); + registerAction2(class AddFileToWorkingSet extends WorkingSetAction { constructor() { super({ diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts index f93b829cf148..9a2b6ae27eac 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts @@ -29,11 +29,12 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../../pla import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; import { IDecorationData, IDecorationsProvider, IDecorationsService } from '../../../../services/decorations/common/decorations.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from '../../../multiDiffEditor/browser/multiDiffSourceResolverService.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { applyingChatEditsContextKey, applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingMaxFileAssignmentName, chatEditingResourceContextKey, ChatEditingSessionState, decidedChatEditingResourceContextKey, defaultChatEditingMaxFileLimit, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, IChatEditingSessionStream, IChatRelatedFile, IChatRelatedFilesProvider, IModifiedFileEntry, inChatEditingSessionContextKey, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { applyingChatEditsContextKey, applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingAgentSupportsReadonlyReferencesContextKey, chatEditingMaxFileAssignmentName, chatEditingResourceContextKey, ChatEditingSessionState, decidedChatEditingResourceContextKey, defaultChatEditingMaxFileLimit, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, IChatEditingSessionStream, IChatRelatedFile, IChatRelatedFilesProvider, IModifiedFileEntry, inChatEditingSessionContextKey, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel, IChatTextEditGroup } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { ChatEditingSession } from './chatEditingSession.js'; @@ -91,6 +92,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic @IWorkbenchAssignmentService private readonly _workbenchAssignmentService: IWorkbenchAssignmentService, @IStorageService storageService: IStorageService, @ILogService logService: ILogService, + @IExtensionService extensionService: IExtensionService, ) { super(); this._applyingChatEditsFailedContextKey = applyingChatEditsFailedContextKey.bindTo(contextKeyService); @@ -144,6 +146,16 @@ export class ChatEditingService extends Disposable implements IChatEditingServic } })); + // todo@connor4312: temporary until chatReadonlyPromptReference proposal is finalized + const readonlyEnabledContextKey = chatEditingAgentSupportsReadonlyReferencesContextKey.bindTo(contextKeyService); + const setReadonlyFilesEnabled = () => { + const enabled = extensionService.extensions.some(e => e.enabledApiProposals?.includes('chatReadonlyPromptReference')); + readonlyEnabledContextKey.set(enabled); + }; + setReadonlyFilesEnabled(); + this._register(extensionService.onDidRegisterExtensions(setReadonlyFilesEnabled)); + this._register(extensionService.onDidChangeExtensions(setReadonlyFilesEnabled)); + this._register(this.lifecycleService.onWillShutdown((e) => { const session = this._currentSessionObs.get(); if (session) { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 17024a89bdf9..6bb25683e106 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -420,6 +420,22 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); } + markIsReadonly(resource: URI, isReadonly?: boolean): void { + const entry = this._workingSet.get(resource); + if (entry) { + if (entry.state === WorkingSetEntryState.Transient || entry.state === WorkingSetEntryState.Suggested) { + entry.state = WorkingSetEntryState.Attached; + } + entry.isMarkedReadonly = isReadonly ?? !entry.isMarkedReadonly; + } else { + this._workingSet.set(resource, { + state: WorkingSetEntryState.Attached, + isMarkedReadonly: isReadonly ?? true + }); + } + this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); + } + private _assertNotDisposed(): void { if (this._state.get() === ChatEditingSessionState.Disposed) { throw new BugIndicatingError(`Cannot access a disposed editing session`); diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index f61955b5b5dd..78a14c2e3fbe 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -127,6 +127,11 @@ interface IChatInputPartOptions { enableImplicitContext?: boolean; } +export interface IWorkingSetEntry { + uri: URI; + isMarkedReadonly?: boolean; +} + export class ChatInputPart extends Disposable implements IHistoryNavigationWidget { static readonly INPUT_SCHEME = 'chatSessionInput'; private static _counter = 0; @@ -324,7 +329,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge public get attemptedWorkingSetEntriesCount() { return this._attemptedWorkingSetEntriesCount; } - private _combinedChatEditWorkingSetEntries: URI[] = []; + private _combinedChatEditWorkingSetEntries: IWorkingSetEntry[] = []; public get chatEditWorkingSetFiles() { return this._combinedChatEditWorkingSetEntries; } @@ -1167,6 +1172,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge state: metadata.state, description: metadata.description, kind: 'reference', + isMarkedReadonly: metadata.isMarkedReadonly, }); seenEntries.add(file); } @@ -1357,7 +1363,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge list.getHTMLElement().style.height = `${height}px`; list.splice(0, list.length, entries); list.splice(entries.length, 0, excludedEntries); - this._combinedChatEditWorkingSetEntries = coalesce(entries.map((e) => e.kind === 'reference' && URI.isUri(e.reference) ? e.reference : undefined)); + 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')); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index eea04f739842..0da2466b9d9c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1015,13 +1015,13 @@ export class ChatWidget extends Disposable implements IChatWidget { const editingSessionAttachedContext: IChatRequestVariableEntry[] = []; // 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 v of this.inputPart.chatEditWorkingSetFiles) { + for (const { uri, isMarkedReadonly } of this.inputPart.chatEditWorkingSetFiles) { // Skip over any suggested files that haven't been confirmed yet in the working set - if (currentEditingSession?.workingSet.get(v)?.state === WorkingSetEntryState.Suggested) { - unconfirmedSuggestions.add(v); + if (currentEditingSession?.workingSet.get(uri)?.state === WorkingSetEntryState.Suggested) { + unconfirmedSuggestions.add(uri); } else { - uniqueWorkingSetEntries.add(v); - editingSessionAttachedContext.push(this.attachmentModel.asVariableEntry(v)); + uniqueWorkingSetEntries.add(uri); + editingSessionAttachedContext.push(this.attachmentModel.asVariableEntry(uri, undefined, isMarkedReadonly)); } } let maximumFileEntries = this.chatEditingService.editingSessionFileLimit - editingSessionAttachedContext.length; diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index aac5782c1941..a3b4fb8384d6 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -566,6 +566,12 @@ have to be updated for changes to the rules above, or to support more deeply nes display: inherit; } +.interactive-session .chat-editing-session .monaco-list-row .chat-collapsible-list-action-bar .action-label.checked { + color: var(--vscode-inputOption-activeForeground); + background-color: var(--vscode-inputOption-activeBackground); + box-shadow: inset 0 0 0 1px var(--vscode-inputOption-activeBorder); +} + .interactive-session .chat-editing-session .chat-editing-session-container.show-file-icons .monaco-scrollable-element .monaco-list-rows .monaco-list-row { border-radius: 2px; } diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 57e0924ce482..ec99832ce0c1 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -62,7 +62,11 @@ export interface IChatRelatedFilesProvider { provideRelatedFiles(chatRequest: IChatRequestDraft, token: CancellationToken): Promise; } -export interface WorkingSetDisplayMetadata { state: WorkingSetEntryState; description?: string } +export interface WorkingSetDisplayMetadata { + state: WorkingSetEntryState; + description?: string; + isMarkedReadonly?: boolean; +} export interface IChatEditingSession { readonly chatSessionId: string; @@ -75,6 +79,7 @@ export interface IChatEditingSession { addFileToWorkingSet(uri: URI, description?: string, kind?: WorkingSetEntryState.Transient | WorkingSetEntryState.Suggested): void; show(): Promise; remove(reason: WorkingSetEntryRemovalReason, ...uris: URI[]): void; + markIsReadonly(uri: URI, isReadonly?: boolean): void; accept(...uris: URI[]): Promise; reject(...uris: URI[]): Promise; getEntry(uri: URI): IModifiedFileEntry | undefined; @@ -142,6 +147,8 @@ export const enum ChatEditingSessionState { export const CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME = 'chat-editing-multi-diff-source'; export const chatEditingWidgetFileStateContextKey = new RawContextKey('chatEditingWidgetFileState', undefined, localize('chatEditingWidgetFileState', "The current state of the file in the chat editing widget")); +export const chatEditingWidgetFileReadonlyContextKey = new RawContextKey('chatEditingWidgetFileReadonly', undefined, localize('chatEditingWidgetFileReadonly', "Whether the file has been marked as read-only in the chat editing widget")); +export const chatEditingAgentSupportsReadonlyReferencesContextKey = new RawContextKey('chatEditingAgentSupportsReadonlyReferences', undefined, localize('chatEditingAgentSupportsReadonlyReferences', "Whether the chat editing agent supports readonly references (temporary)")); export const decidedChatEditingResourceContextKey = new RawContextKey('decidedChatEditingResource', []); export const chatEditingResourceContextKey = new RawContextKey('chatEditingResource', undefined); export const inChatEditingSessionContextKey = new RawContextKey('inChatEditingSession', undefined); diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 5c0530f7aa32..d79d8c7cd906 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -30,6 +30,7 @@ export interface IBaseChatRequestVariableEntry { fullName?: string; icon?: ThemeIcon; name: string; + isMarkedReadonly?: boolean; modelDescription?: string; range?: IOffsetRange; value: IChatRequestVariableValue; diff --git a/src/vscode-dts/vscode.proposed.chatReadonlyPromptReference.d.ts b/src/vscode-dts/vscode.proposed.chatReadonlyPromptReference.d.ts new file mode 100644 index 000000000000..0d91b3ed4e79 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.chatReadonlyPromptReference.d.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +declare module 'vscode' { + + export interface ChatPromptReference { + /** + * When true, the user has indicated at the reference is informational only. + * The model should avoid changing or suggesting changes to the reference. + */ + readonly isReadonly?: boolean; + } + +} From be8f9a30c06b056ba22296031e830f2f73c87cf6 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 10 Jan 2025 17:13:35 -0600 Subject: [PATCH 0489/3587] scroll list view on keyboard focus (#232644) --- src/vs/base/browser/ui/list/listView.ts | 38 ++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index e305b7de4b9a..4e065bf3539d 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { DataTransfers, IDragAndDropData } from '../../dnd.js'; -import { $, addDisposableListener, animate, Dimension, getContentHeight, getContentWidth, getDocument, getTopLeftOffset, getWindow, isAncestor, isHTMLElement, isSVGElement, scheduleAtNextAnimationFrame } from '../../dom.js'; +import { $, addDisposableListener, animate, Dimension, getActiveElement, getContentHeight, getContentWidth, getDocument, getTopLeftOffset, getWindow, isAncestor, isHTMLElement, isSVGElement, scheduleAtNextAnimationFrame } from '../../dom.js'; import { DomEmitter } from '../../event.js'; import { IMouseWheelEvent } from '../../mouseEvent.js'; import { EventType as TouchEventType, Gesture, GestureEvent } from '../../touch.js'; @@ -324,6 +324,7 @@ export class ListView implements IListView { private onDragLeaveTimeout: IDisposable = Disposable.None; private currentSelectionDisposable: IDisposable = Disposable.None; private currentSelectionBounds: IRange | undefined; + private activeElement: HTMLElement | undefined; private readonly disposables: DisposableStore = new DisposableStore(); @@ -439,9 +440,13 @@ export class ListView implements IListView { this.scrollableElement.onScroll(this.onScroll, this, this.disposables); this.disposables.add(addDisposableListener(this.rowsContainer, TouchEventType.Change, e => this.onTouchChange(e as GestureEvent))); - // Prevent the monaco-scrollable-element from scrolling - // https://github.com/microsoft/vscode/issues/44181 - this.disposables.add(addDisposableListener(this.scrollableElement.getDomNode(), 'scroll', e => (e.target as HTMLElement).scrollTop = 0)); + this.disposables.add(addDisposableListener(this.scrollableElement.getDomNode(), 'scroll', e => { + // Make sure the active element is scrolled into view + const element = (e.target as HTMLElement); + const scrollValue = element.scrollTop; + element.scrollTop = 0; + this.setScrollTop(this.scrollTop + scrollValue); + })); this.disposables.add(addDisposableListener(this.domNode, 'dragover', e => this.onDragOver(this.toDragEvent(e)))); this.disposables.add(addDisposableListener(this.domNode, 'drop', e => this.onDrop(this.toDragEvent(e)))); @@ -460,6 +465,31 @@ export class ListView implements IListView { this.dnd = options.dnd ?? this.disposables.add(DefaultOptions.dnd); this.layout(options.initialSize?.height, options.initialSize?.width); + this._setupFocusObserver(container); + } + + private _setupFocusObserver(container: HTMLElement): void { + this.disposables.add(addDisposableListener(container, 'focus', () => { + const element = getActiveElement() as HTMLElement | null; + if (this.activeElement !== element && element !== null) { + this.activeElement = element; + this._scrollToActiveElement(this.activeElement, container); + } + }, true)); + } + + private _scrollToActiveElement(element: HTMLElement, container: HTMLElement) { + // The scroll event on the list only fires when scrolling down. + // If the active element is above the viewport, we need to scroll up. + const containerRect = container.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + + const topOffset = elementRect.top - containerRect.top; + + if (topOffset < 0) { + // Scroll up + this.setScrollTop(this.scrollTop + topOffset); + } } updateOptions(options: IListViewOptionsUpdate) { From 3501d55910f5c8ec32fd949b550ec41545a22e25 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 10 Jan 2025 15:22:15 -0800 Subject: [PATCH 0490/3587] Use `.js` --- src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts b/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts index d04a15e1c672..33ac5a816c12 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { IMouseWheelEvent } from '../../../../base/browser/mouseEvent.ts'; -import type { WebviewStyles } from './webview.ts'; +import type { IMouseWheelEvent } from '../../../../base/browser/mouseEvent.js'; +import type { WebviewStyles } from './webview.js'; type KeyEvent = { key: string; From e6805d7927dc05d2a38dac3c272788e484197648 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Fri, 10 Jan 2025 19:22:07 -0500 Subject: [PATCH 0491/3587] Expose shell's environment - bash (#237602) * start terminal shell env proposed * fix typo * progress on shellEnvDetectionCapability, mainThreadTerminalSI * update IShellEnvDetectionCapability interface * touch up on $shellEnvChange * adjust IShellEnvDetectionCapability * properly listen to envChangeEvent Co-authored-by: Daniel Imms * Serialize env map, expose on exthost * start adding to zsh script * receive environment variable in extension host, properly escape " Co-authored-by: Daniel Imms * clean up * Add TODO: properly escape double quotes, figure out why JSON parse fails for bash Co-authored-by: Daniel Imms * Fix nonce check, ignore PS1 for now in bash * Add some simple PS1 string tests to deserializeMessage * New approach of sending env entries separately * be able to get EnvSingleVar * few comments * add function signature for start, set, end environment var * implement EnvStart, EnvEntry, EnvEnd for single env entry * deserialize env value for EnvEntry * Remove unncessary comments * only leave pwsh in this PR and exclude other shells * keep exlcuding other shell env - only pwsh should remain * Update src/vs/workbench/api/common/extHostTerminalShellIntegration.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/api/common/extHostTerminalShellIntegration.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/api/common/extHost.protocol.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/capabilities/capabilities.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * add comment for ShellEnvDetection * change envs in shellEnvDetectionCapability to env * Mention escaping character for EnvJSON similar to commandLine * Do not fire env event if env has not changed * add link to CommandLine * follow main branch format so I avoid merge conflict * remove resolved TODO * Update src/vs/workbench/api/browser/mainThreadTerminalShellIntegration.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * use vscode object equals * add zsh update_env for shellIntegration-zsh.sh * add EnvStart, EnvEntry, EnvEnd * why doesnt if [ "$__vsc_stable" = "0" ]; work * add test to check shellPath * stop messing with formatting * try to be more detail in testing * clean up * dont change the format * properly use stable/insider flag so update_env on insiders * modify test after feedback * rename to *EnvironmentSingleVar and make it transactional via _pending env * add docs for *EnvSingle and update bash script to conform to *EnvSingle * Update src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * remove _env.clear from start and make _env non-readonly * Update src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts --------- Co-authored-by: Daniel Imms Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> --- .../terminal.shellIntegration.test.ts | 10 ++++ .../common/capabilities/capabilities.ts | 3 ++ .../shellEnvDetectionCapability.ts | 29 +++++++++++- .../common/xterm/shellIntegrationAddon.ts | 46 +++++++++++++++++++ .../common/scripts/shellIntegration-bash.sh | 19 ++++++++ 5 files changed, 106 insertions(+), 1 deletion(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts index db48c2593e07..726f0ed278f3 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts @@ -87,6 +87,16 @@ import { assertNoRpc } from '../utils'; await closeTerminalAsync(terminal); }); + if (platform() === 'darwin' || platform() === 'linux') { + test('Test if env is set', async () => { + const { shellIntegration } = await createTerminalAndWaitForShellIntegration(); + const env = shellIntegration.env; + ok(env); + ok(env.PATH); + ok(env.PATH.length > 0, 'env.PATH should have a length greater than 0'); + }); + } + test('execution events should fire in order when a command runs', async () => { const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration(); const events: string[] = []; diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index 50f39332a431..ba7597b9d926 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -155,6 +155,9 @@ export interface IShellEnvDetectionCapability { readonly onDidChangeEnv: Event>; get env(): Map; setEnvironment(envs: { [key: string]: string | undefined } | undefined, isTrusted: boolean): void; + startEnvironmentSingleVar(isTrusted: boolean): void; + setEnvironmentSingleVar(key: string, value: string | undefined, isTrusted: boolean): void; + endEnvironmentSingleVar(isTrusted: boolean): void; } export const enum CommandInvalidationReason { diff --git a/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts index 95e1d827dc43..be9577acfe93 100644 --- a/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts @@ -11,7 +11,8 @@ import { equals } from '../../../../base/common/objects.js'; export class ShellEnvDetectionCapability extends Disposable implements IShellEnvDetectionCapability { readonly type = TerminalCapability.ShellEnvDetection; - private readonly _env: Map = new Map(); + private _pendingEnv: Map | undefined; + private _env: Map = new Map(); get env(): Map { return this._env; } private readonly _onDidChangeEnv = this._register(new Emitter>()); @@ -36,4 +37,30 @@ export class ShellEnvDetectionCapability extends Disposable implements IShellEnv // Convert to event and fire event this._onDidChangeEnv.fire(this._env); } + + startEnvironmentSingleVar(isTrusted: boolean): void { + if (!isTrusted) { + return; + } + this._pendingEnv = new Map(); + } + setEnvironmentSingleVar(key: string, value: string | undefined, isTrusted: boolean): void { + if (!isTrusted) { + return; + } + if (key !== undefined && value !== undefined) { + this._pendingEnv?.set(key, value); + } + } + endEnvironmentSingleVar(isTrusted: boolean): void { + if (!isTrusted) { + return; + } + if (!this._pendingEnv) { + return; + } + this._env = this._pendingEnv; + this._pendingEnv = undefined; + this._onDidChangeEnv.fire(this._env); + } } diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index ee7dbff267e5..92b1851c275c 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -239,6 +239,34 @@ const enum VSCodeOscPt { */ EnvJson = 'EnvJson', + /** + * The start of the collecting user's environment variables individually. + * Clears any environment residuals in previous sessions. + * + * Format: `OSC 633 ; EnvSingleStart ; ` + * + * WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script. + */ + EnvSingleStart = 'EnvSingleStart', + + /** + * Sets an entry of single environment variable to transactional pending map of environment variables. + * + * Format: `OSC 633 ; EnvSingleEntry ; ; ; ` + * + * WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script. + */ + EnvSingleEntry = 'EnvSingleEntry', + + /** + * The end of the collecting user's environment variables individually. + * Clears any pending environment variables and fires an event that contains user's environment. + * + * Format: `OSC 633 ; EnvSingleEnd ; ` + * + * WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script. + */ + EnvSingleEnd = 'EnvSingleEnd' } /** @@ -446,6 +474,24 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati } return true; } + case VSCodeOscPt.EnvSingleStart: { + this._createOrGetShellEnvDetection().startEnvironmentSingleVar(args[0] === this._nonce); + return true; + } + case VSCodeOscPt.EnvSingleEntry: { + const arg0 = args[0]; + const arg1 = args[1]; + const arg2 = args[2]; + if (arg0 !== undefined && arg1 !== undefined) { + const env = deserializeMessage(arg1); + this._createOrGetShellEnvDetection().setEnvironmentSingleVar(arg0, env, arg2 === this._nonce); + } + return true; + } + case VSCodeOscPt.EnvSingleEnd: { + this._createOrGetShellEnvDetection().endEnvironmentSingleVar(args[0] === this._nonce); + return true; + } case VSCodeOscPt.RightPromptStart: { this._createOrGetCommandDetection(this._terminal).handleRightPromptStart(); return true; diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index fbeaa22f90a7..3040617a5a6d 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -214,6 +214,17 @@ __vsc_update_cwd() { builtin printf '\e]633;P;Cwd=%s\a' "$(__vsc_escape_value "$__vsc_cwd")" } +__vsc_update_env() { + builtin printf '\e]633;EnvSingleStart;%s;\a' $__vsc_nonce + for var in $(compgen -v); do + if printenv "$var" >/dev/null 2>&1; then + value=$(builtin printf '%s' "${!var}") + builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$var" "$(__vsc_escape_value "$value")" $__vsc_nonce + fi + done + builtin printf '\e]633;EnvSingleEnd;%s;\a' $__vsc_nonce +} + __vsc_command_output_start() { if [[ -z "${__vsc_first_prompt-}" ]]; then builtin return @@ -240,6 +251,10 @@ __vsc_command_complete() { builtin printf '\e]633;D;%s\a' "$__vsc_status" fi __vsc_update_cwd + + if [ "$__vsc_stable" = "0" ]; then + __vsc_update_env + fi } __vsc_update_prompt() { # in command execution @@ -269,6 +284,10 @@ __vsc_precmd() { fi __vsc_first_prompt=1 __vsc_update_prompt + + if [ "$__vsc_stable" = "0" ]; then + __vsc_update_env + fi } __vsc_preexec() { From 9547e6d5c0d223ad684173af8e58477e14129c70 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 10 Jan 2025 19:43:35 -0600 Subject: [PATCH 0492/3587] add details view, fix terminal suggest out of sync bug (#237686) --- .../api/browser/mainThreadTerminalService.ts | 7 +- .../browser/pwshCompletionProviderAddon.ts | 1 + .../browser/terminal.suggest.contribution.ts | 13 + .../browser/terminalCompletionService.ts | 29 +- .../suggest/browser/terminalSuggestAddon.ts | 25 +- .../browser/terminalCompletionService.test.ts | 26 +- .../suggest/browser/media/suggest.css | 137 +++++- .../suggest/browser/simpleCompletionItem.ts | 4 + .../suggest/browser/simpleSuggestWidget.ts | 176 ++++++- .../browser/simpleSuggestWidgetDetails.ts | 446 ++++++++++++++++++ 10 files changed, 807 insertions(+), 57 deletions(-) create mode 100644 src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 57fe7dc25abf..bf7e6953bf0b 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -271,7 +271,12 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._completionProviders.set(id, this._terminalCompletionService.registerTerminalCompletionProvider(extensionIdentifier, id, { id, provideCompletions: async (commandLine, cursorPosition, token) => { - return await this._proxy.$provideTerminalCompletions(id, { commandLine, cursorPosition }, token); + const completions = await this._proxy.$provideTerminalCompletions(id, { commandLine, cursorPosition }, token); + if (Array.isArray(completions)) { + return completions.map(c => ({ ...c, provider: id })); + } else { + return { items: completions?.items.map(c => ({ ...c, provider: id })), resourceRequestConfig: completions?.resourceRequestConfig }; + } } }, ...triggerCharacters)); } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts index 596b6ce452d7..f10c11b7cd68 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts @@ -348,6 +348,7 @@ function rawCompletionToITerminalCompletion(rawCompletion: PwshCompletion, repla return { label, + provider: 'pwsh-script', icon, detail, isFile: rawCompletion.ResultType === 3, diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index c67d6c4cdaf5..89e59369df8a 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -244,6 +244,19 @@ registerActiveInstanceAction({ run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.selectNextSuggestion() }); +registerActiveInstanceAction({ + id: 'terminalSuggestToggleExplainMode', + title: localize2('workbench.action.terminal.suggestToggleExplainMode', 'Suggest Toggle Explain Modes'), + f1: false, + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.focus, TerminalContextKeys.isOpen, TerminalContextKeys.suggestWidgetVisible), + keybinding: { + // Down is bound to other workbench keybindings that this needs to beat + weight: KeybindingWeight.WorkbenchContrib + 1, + primary: KeyMod.CtrlCmd | KeyCode.Slash, + }, + run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.toggleExplainMode() +}); + registerActiveInstanceAction({ id: TerminalSuggestCommandId.SelectNextPageSuggestion, title: localize2('workbench.action.terminal.selectNextPageSuggestion', 'Select the Next Page Suggestion'), diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index aa81849cb8dc..9f874a9fbc45 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -10,7 +10,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; -import { TerminalSettingId, TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; +import { TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; import { ISimpleCompletion } from '../../../../services/suggest/browser/simpleCompletionItem.js'; import { ITerminalSuggestConfiguration, terminalSuggestConfigSection } from '../common/terminalSuggestConfiguration.js'; @@ -164,17 +164,19 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return undefined; } const completionItems = Array.isArray(completions) ? completions : completions.items ?? []; - const itemsWithModifiedLabels = this._addDevModeLabel(completionItems, provider.id); + for (const item of completionItems) { + item.provider = provider.id; + } if (Array.isArray(completions)) { - return itemsWithModifiedLabels; + return completionItems; } if (completions.resourceRequestConfig) { - const resourceCompletions = await this.resolveResources(completions.resourceRequestConfig, promptValue, cursorPosition); + const resourceCompletions = await this.resolveResources(completions.resourceRequestConfig, promptValue, cursorPosition, provider.id); if (resourceCompletions) { - itemsWithModifiedLabels.push(...this._addDevModeLabel(resourceCompletions, provider.id)); + completionItems.push(...resourceCompletions); } - return itemsWithModifiedLabels; + return completionItems; } return; }); @@ -183,19 +185,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return results.filter(result => !!result).flat(); } - private _addDevModeLabel(completions: ITerminalCompletion[], providerId: string): ITerminalCompletion[] { - const devModeEnabled = this._configurationService.getValue(TerminalSettingId.DevMode); - return completions.map(completion => { - // TODO: This providerId check shouldn't be necessary, instead we should ensure this - // function is never called twice - if (devModeEnabled && !completion.detail?.includes(providerId)) { - completion.detail = `(${providerId}) ${completion.detail ?? ''}`; - } - return completion; - }); - } - - async resolveResources(resourceRequestConfig: TerminalResourceRequestConfig, promptValue: string, cursorPosition: number): Promise { + async resolveResources(resourceRequestConfig: TerminalResourceRequestConfig, promptValue: string, cursorPosition: number, providerId: string): Promise { const cwd = URI.revive(resourceRequestConfig.cwd); const foldersRequested = resourceRequestConfig.foldersRequested ?? false; const filesRequested = resourceRequestConfig.filesRequested ?? false; @@ -246,6 +236,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo } resourceCompletions.push({ label, + provider: providerId, kind, isDirectory, isFile: kind === TerminalCompletionItemKind.File, diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 52d7c1b8c37a..dc709289fba6 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -163,15 +163,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest if (enableExtensionCompletions && !doNotRequestExtensionCompletions) { await this._extensionService.activateByEvent('onTerminalCompletionsRequested'); } - - const providedCompletions = await this._terminalCompletionService.provideCompletions(this._promptInputModel.prefix, this._promptInputModel.cursorIndex, this.shellType, token, doNotRequestExtensionCompletions); - if (!providedCompletions?.length || token.isCancellationRequested) { - return; - } - this._onDidReceiveCompletions.fire(); - - this._requestedCompletionsIndex = this._promptInputModel.cursorIndex; - this._currentPromptInputState = { value: this._promptInputModel.value, prefix: this._promptInputModel.prefix, @@ -179,8 +170,17 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest cursorIndex: this._promptInputModel.cursorIndex, ghostTextIndex: this._promptInputModel.ghostTextIndex }; + this._requestedCompletionsIndex = this._currentPromptInputState.cursorIndex; - this._leadingLineContent = this._currentPromptInputState.prefix.substring(0, this._requestedCompletionsIndex + this._cursorIndexDelta); + const providedCompletions = await this._terminalCompletionService.provideCompletions(this._currentPromptInputState.prefix, this._currentPromptInputState.cursorIndex, this.shellType, token, doNotRequestExtensionCompletions); + + if (!providedCompletions?.length || token.isCancellationRequested) { + return; + } + this._onDidReceiveCompletions.fire(); + + this._cursorIndexDelta = this._promptInputModel.cursorIndex - this._requestedCompletionsIndex; + this._leadingLineContent = this._promptInputModel.prefix.substring(0, this._requestedCompletionsIndex + this._cursorIndexDelta); const completions = providedCompletions.flat(); if (!completions?.length) { @@ -198,7 +198,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } this._mostRecentCompletion = undefined; - this._cursorIndexDelta = this._currentPromptInputState.cursorIndex - this._requestedCompletionsIndex; let normalizedLeadingLineContent = this._leadingLineContent; @@ -234,6 +233,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._screen = screen; } + toggleExplainMode(): void { + this._suggestWidget?.toggleExplainMode(); + } + resetWidgetSize(): void { this._suggestWidget?.resetWidgetSize(); } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index ebfa97fe98b3..34f73a15d25c 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -22,6 +22,7 @@ suite('TerminalCompletionService', () => { let childResources: { resource: URI; isFile?: boolean; isDirectory?: boolean }[]; const pathSeparator = isWindows ? '\\' : '/'; let terminalCompletionService: TerminalCompletionService; + const provider: string = 'testProvider'; setup(() => { instantiationService = store.add(new TestInstantiationService()); @@ -48,7 +49,7 @@ suite('TerminalCompletionService', () => { const resourceRequestConfig: TerminalResourceRequestConfig = { pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider); assert(!result); }); @@ -58,7 +59,7 @@ suite('TerminalCompletionService', () => { pathSeparator }; validResources = [URI.parse('file:///test')]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider); assert(!result); }); }); @@ -77,9 +78,10 @@ suite('TerminalCompletionService', () => { foldersRequested: true, pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 1); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 1, provider); assert.deepEqual(result, [{ label: `.${pathSeparator}folder1${pathSeparator}`, + provider, kind: TerminalCompletionItemKind.Folder, isDirectory: true, isFile: false, @@ -93,9 +95,10 @@ suite('TerminalCompletionService', () => { foldersRequested: true, pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '.', 2); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '.', 2, provider); assert.deepEqual(result, [{ label: `.${pathSeparator}folder1${pathSeparator}`, + provider, kind: TerminalCompletionItemKind.Folder, isDirectory: true, isFile: false, @@ -109,9 +112,10 @@ suite('TerminalCompletionService', () => { foldersRequested: true, pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 3); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 3, provider); assert.deepEqual(result, [{ label: `.${pathSeparator}folder1${pathSeparator}`, + provider, kind: TerminalCompletionItemKind.Folder, isDirectory: true, isFile: false, @@ -125,9 +129,10 @@ suite('TerminalCompletionService', () => { foldersRequested: true, pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider); assert.deepEqual(result, [{ label: `.${pathSeparator}folder1${pathSeparator}`, + provider, kind: TerminalCompletionItemKind.Folder, isDirectory: true, isFile: false, @@ -141,9 +146,10 @@ suite('TerminalCompletionService', () => { foldersRequested: true, pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd .', 4); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd .', 4, provider); assert.deepEqual(result, [{ label: `.${pathSeparator}folder1${pathSeparator}`, + provider, kind: TerminalCompletionItemKind.Folder, isDirectory: true, isFile: false, @@ -157,9 +163,10 @@ suite('TerminalCompletionService', () => { foldersRequested: true, pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./', 5); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./', 5, provider); assert.deepEqual(result, [{ label: `.${pathSeparator}folder1${pathSeparator}`, + provider, kind: TerminalCompletionItemKind.Folder, isDirectory: true, isFile: false, @@ -173,9 +180,10 @@ suite('TerminalCompletionService', () => { foldersRequested: true, pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./f', 6); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./f', 6, provider); assert.deepEqual(result, [{ label: `.${pathSeparator}folder1${pathSeparator}`, + provider, kind: TerminalCompletionItemKind.Folder, isDirectory: true, isFile: false, diff --git a/src/vs/workbench/services/suggest/browser/media/suggest.css b/src/vs/workbench/services/suggest/browser/media/suggest.css index b540839efa3c..0f525d83eaf2 100644 --- a/src/vs/workbench/services/suggest/browser/media/suggest.css +++ b/src/vs/workbench/services/suggest/browser/media/suggest.css @@ -12,6 +12,17 @@ position: fixed; left: 0; top: 0; + border-style: solid; + border-width: 1px; + border-color: var(--vscode-editorSuggestWidget-border); + background-color: var(--vscode-editorSuggestWidget-background); +} + +.workbench-suggest-widget .suggest-details { + border-style: solid; + border-width: 1px; + border-color: var(--vscode-editorSuggestWidget-border); + background-color: var(--vscode-editorSuggestWidget-background); } /* Suggest widget*/ @@ -29,7 +40,7 @@ } .workbench-suggest-widget, -.monaco-workbench .workbench-suggest-details { +.monaco-workbench .suggest-details { flex: 0 1 auto; width: 100%; border-style: solid; @@ -39,9 +50,9 @@ } .monaco-workbench.hc-black .workbench-suggest-widget, -.monaco-workbench.hc-black .workbench-suggest-details, +.monaco-workbench.hc-black .suggest-details, .monaco-workbench.hc-light .workbench-suggest-widget, -.monaco-workbench.hc-light .workbench-suggest-details { +.monaco-workbench.hc-light .suggest-details { border-width: 2px; } @@ -270,3 +281,123 @@ height: 18px; visibility: hidden; } + +.workbench-suggest-widget .suggest-details { + display: flex; + flex-direction: column; + cursor: default; + color: var(--vscode-editorSuggestWidget-foreground); +} + +.workbench-suggest-widget .suggest-details:focus { + border-color: var(--vscode-focusBorder); +} + +.workbench-suggest-widget .suggest-details a { + color: var(--vscode-textLink-foreground); +} + +.workbench-suggest-widget .suggest-details a:hover { + color: var(--vscode-textLink-activeForeground); +} + +.workbench-suggest-widget .suggest-details code { + background-color: var(--vscode-textCodeBlock-background); +} + +.workbench-suggest-widget .suggest-details.no-docs { + display: none; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element { + flex: 1; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body { + box-sizing: border-box; + height: 100%; + width: 100%; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .header > .type { + flex: 2; + overflow: hidden; + text-overflow: ellipsis; + opacity: 0.7; + white-space: pre; + margin: 0 24px 0 0; + padding: 4px 0 4px 5px; +} + +.workbench-suggest-widget .suggest-details.detail-and-doc > .monaco-scrollable-element > .body > .header > .type { + padding-bottom: 12px; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .header > .type.auto-wrap { + white-space: normal; + word-break: break-all; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs { + margin: 0; + padding: 4px 5px; + white-space: pre-wrap; +} + +.workbench-suggest-widget .suggest-details.no-type > .monaco-scrollable-element > .body > .docs { + margin-right: 24px; + overflow: hidden; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs.markdown-docs { + padding: 0; + white-space: initial; + min-height: calc(1rem + 8px); +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs.markdown-docs > div, +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs.markdown-docs > span:not(:empty) { + padding: 4px 5px; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs.markdown-docs > div > p:first-child { + margin-top: 0; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs.markdown-docs > div > p:last-child { + margin-bottom: 0; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs.markdown-docs .monaco-tokenized-source { + white-space: pre; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs .code { + white-space: pre-wrap; + word-wrap: break-word; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .docs.markdown-docs .codicon { + vertical-align: sub; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > p:empty { + display: none; +} + +.workbench-suggest-widget .suggest-details code { + border-radius: 3px; + padding: 0 0.4em; +} + +.workbench-suggest-widget .suggest-details ul { + padding-left: 20px; +} + +.workbench-suggest-widget .suggest-details ol { + padding-left: 20px; +} + +.workbench-suggest-widget .suggest-details p code { + font-family: var(--monaco-monospace-font); +} diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts index 06778c860381..466a979d1968 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts @@ -12,6 +12,10 @@ export interface ISimpleCompletion { * The completion's label which appears on the left beside the icon. */ label: string; + /** + * The ID of the provider the completion item came from + */ + provider: string; /** * The completion's icon to show on the left of the suggest widget. */ diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index 5e48a435c41b..69f00e2c463d 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -11,7 +11,7 @@ import { ResizableHTMLElement } from '../../../../base/browser/ui/resizable/resi import { SimpleCompletionItem } from './simpleCompletionItem.js'; import { LineContext, SimpleCompletionModel } from './simpleCompletionModel.js'; import { getAriaId, SimpleSuggestWidgetItemRenderer, type ISimpleSuggestWidgetFontInfo } from './simpleSuggestWidgetRenderer.js'; -import { TimeoutTimer } from '../../../../base/common/async.js'; +import { CancelablePromise, createCancelablePromise, disposableTimeout, TimeoutTimer } from '../../../../base/common/async.js'; import { Emitter, Event, PauseableEmitter } from '../../../../base/common/event.js'; import { MutableDisposable, Disposable } from '../../../../base/common/lifecycle.js'; import { clamp } from '../../../../base/common/numbers.js'; @@ -20,6 +20,8 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { SuggestWidgetStatus } from '../../../../editor/contrib/suggest/browser/suggestWidgetStatus.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { SimpleSuggestDetailsOverlay, SimpleSuggestDetailsWidget } from './simpleSuggestWidgetDetails.js'; const $ = dom.$; @@ -66,9 +68,12 @@ export class SimpleSuggestWidget extends Disposable { private _completionModel?: SimpleCompletionModel; private _cappedHeight?: { wanted: number; capped: number }; private _forceRenderingAbove: boolean = false; + private _explainMode: boolean = false; + private _preference?: WidgetPositionPreference; + private readonly _pendingShowDetails = this._register(new MutableDisposable()); private readonly _pendingLayout = this._register(new MutableDisposable()); - // private _currentSuggestionDetails?: CancelablePromise; + private _currentSuggestionDetails?: CancelablePromise; private _focusedItem?: SimpleCompletionItem; private _ignoreFocusEvents: boolean = false; readonly element: ResizableHTMLElement; @@ -76,6 +81,7 @@ export class SimpleSuggestWidget extends Disposable { private readonly _listElement: HTMLElement; private readonly _list: List; private readonly _status?: SuggestWidgetStatus; + private readonly _details: SimpleSuggestDetailsOverlay; private readonly _showTimeout = this._register(new TimeoutTimer()); @@ -97,6 +103,7 @@ export class SimpleSuggestWidget extends Disposable { options: IWorkbenchSuggestWidgetOptions, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, + @IStorageService private readonly _storageService: IStorageService, ) { super(); @@ -203,6 +210,10 @@ export class SimpleSuggestWidget extends Disposable { this._messageElement = dom.append(this.element.domNode, dom.$('.message')); + const details: SimpleSuggestDetailsWidget = this._register(instantiationService.createInstance(SimpleSuggestDetailsWidget)); + this._register(details.onDidClose(() => this.toggleDetails())); + this._details = this._register(new SimpleSuggestDetailsOverlay(details, this._listElement)); + if (options.statusBarMenuId) { this._status = this._register(instantiationService.createInstance(SuggestWidgetStatus, this.element.domNode, options.statusBarMenuId)); this.element.domNode.classList.toggle('with-status-bar', true); @@ -231,11 +242,11 @@ export class SimpleSuggestWidget extends Disposable { } if (!e.elements.length) { - // if (this._currentSuggestionDetails) { - // this._currentSuggestionDetails.cancel(); - // this._currentSuggestionDetails = undefined; - // this._focusedItem = undefined; - // } + if (this._currentSuggestionDetails) { + this._currentSuggestionDetails.cancel(); + this._currentSuggestionDetails = undefined; + this._focusedItem = undefined; + } this._clearAriaActiveDescendant(); return; } @@ -250,12 +261,13 @@ export class SimpleSuggestWidget extends Disposable { if (item !== this._focusedItem) { - // this._currentSuggestionDetails?.cancel(); - // this._currentSuggestionDetails = undefined; + this._currentSuggestionDetails?.cancel(); + this._currentSuggestionDetails = undefined; this._focusedItem = item; this._list.reveal(index); + const id = getAriaId(index); const node = dom.getActiveWindow().document.activeElement; if (node && id) { @@ -265,6 +277,40 @@ export class SimpleSuggestWidget extends Disposable { } else { this._clearAriaActiveDescendant(); } + + this._currentSuggestionDetails = createCancelablePromise(async token => { + const loading = disposableTimeout(() => { + if (this._isDetailsVisible()) { + this._showDetails(true, false); + } + }, 250); + const sub = token.onCancellationRequested(() => loading.dispose()); + try { + return await Promise.resolve(); + } finally { + loading.dispose(); + sub.dispose(); + } + }); + + this._currentSuggestionDetails.then(() => { + if (index >= this._list.length || item !== this._list.element(index)) { + return; + } + + // item can have extra information, so re-render + this._ignoreFocusEvents = true; + this._list.splice(index, 1, [item]); + this._list.setFocus([index]); + this._ignoreFocusEvents = false; + + if (this._isDetailsVisible()) { + this._showDetails(false, false); + } else { + this.element.domNode.classList.remove('docs-side'); + } + + }).catch(); } // emit an event this._onDidFocus.fire({ item, index, model: this._completionModel }); @@ -343,6 +389,7 @@ export class SimpleSuggestWidget extends Disposable { // Reset focus border // this._details.widget.domNode.classList.remove('focused'); }); + this._afterRender(); } setLineContext(lineContext: LineContext): void { @@ -379,9 +426,9 @@ export class SimpleSuggestWidget extends Disposable { this._showTimeout.cancel(); this.element.domNode.classList.remove('visible'); this._list.splice(0, this._list.length); - // this._focusedItem = undefined; + this._focusedItem = undefined; this._cappedHeight = undefined; - // this._explainMode = false; + this._explainMode = false; break; case State.Loading: this.element.domNode.classList.add('message'); @@ -453,9 +500,80 @@ export class SimpleSuggestWidget extends Disposable { }, 100); } + + toggleDetailsFocus(): void { + if (this._state === State.Details) { + // Should return the focus to the list item. + this._list.setFocus(this._list.getFocus()); + this._setState(State.Open); + } else if (this._state === State.Open) { + this._setState(State.Details); + if (!this._isDetailsVisible()) { + this.toggleDetails(true); + } else { + this._details.widget.focus(); + } + } + } + + toggleDetails(focused: boolean = false): void { + if (this._isDetailsVisible()) { + // hide details widget + this._pendingShowDetails.clear(); + // this._ctxSuggestWidgetDetailsVisible.set(false); + this._setDetailsVisible(false); + this._details.hide(); + this.element.domNode.classList.remove('shows-details'); + + } else if ((this._explainMode) && (this._state === State.Open || this._state === State.Details || this._state === State.Frozen)) { + // show details widget (iff possible) + // this._ctxSuggestWidgetDetailsVisible.set(true); + this._setDetailsVisible(true); + this._showDetails(false, focused); + } + } + + private _showDetails(loading: boolean, focused: boolean): void { + this._pendingShowDetails.value = dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.element.domNode), () => { + this._pendingShowDetails.clear(); + this._details.show(); + let didFocusDetails = false; + if (loading) { + this._details.widget.renderLoading(); + } else { + this._details.widget.renderItem(this._list.getFocusedElements()[0], this._explainMode); + } + if (!this._details.widget.isEmpty) { + this._positionDetails(); + this.element.domNode.classList.add('shows-details'); + if (focused) { + this._details.widget.focus(); + didFocusDetails = true; + } + } else { + this._details.hide(); + } + if (!didFocusDetails) { + // this.editor.focus(); + } + }); + } + + toggleExplainMode(): void { + if (this._list.getFocusedElements()[0]) { + this._explainMode = !this._explainMode; + if (!this._isDetailsVisible()) { + this.toggleDetails(); + } else { + this._showDetails(false, false); + } + } + } + + hide(): void { this._pendingLayout.clear(); - // this._pendingShowDetails.clear(); + this._pendingShowDetails.clear(); // this._loadingTimeout?.dispose(); this._setState(State.Hidden); @@ -572,6 +690,23 @@ export class SimpleSuggestWidget extends Disposable { this._resize(width, height); } + _afterRender() { + // if (position === null) { + // if (this._isDetailsVisible()) { + // this._details.hide(); //todo@jrieken soft-hide + // } + // return; + // } + if (this._state === State.Empty || this._state === State.Loading) { + // no special positioning when widget isn't showing list + return; + } + if (this._isDetailsVisible() && !this._details.widget.isEmpty) { + this._details.show(); + } + this._positionDetails(); + } + private _resize(width: number, height: number): void { const { width: maxWidth, height: maxHeight } = this.element.maxSize; width = Math.min(maxWidth, width); @@ -587,8 +722,13 @@ export class SimpleSuggestWidget extends Disposable { this._listElement.style.height = `${height}px`; this.element.layout(height, width); - // this._positionDetails(); - // TODO: Position based on preference + this._positionDetails(); + } + + private _positionDetails(): void { + if (this._isDetailsVisible()) { + this._details.placeAtAnchor(this.element.domNode); + } } private _getLayoutInfo() { @@ -682,6 +822,14 @@ export class SimpleSuggestWidget extends Disposable { return undefined; } + private _isDetailsVisible(): boolean { + return this._storageService.getBoolean('expandSuggestionDocs', StorageScope.PROFILE, false); + } + + private _setDetailsVisible(value: boolean) { + this._storageService.store('expandSuggestionDocs', value, StorageScope.PROFILE, StorageTarget.USER); + } + forceRenderingAbove() { if (!this._forceRenderingAbove) { this._forceRenderingAbove = true; diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts new file mode 100644 index 000000000000..595afceb0430 --- /dev/null +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts @@ -0,0 +1,446 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { ResizableHTMLElement } from '../../../../base/browser/ui/resizable/resizable.js'; +import * as nls from '../../../../nls.js'; +import { SimpleCompletionItem } from './simpleCompletionItem.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; + +export function canExpandCompletionItem(item: SimpleCompletionItem | undefined): boolean { + return !!item && Boolean(item.completion.detail && item.completion.detail !== item.completion.label); +} + +export class SimpleSuggestDetailsWidget { + + readonly domNode: HTMLDivElement; + + private readonly _onDidClose = new Emitter(); + readonly onDidClose: Event = this._onDidClose.event; + + private readonly _onDidChangeContents = new Emitter(); + readonly onDidChangeContents: Event = this._onDidChangeContents.event; + + private readonly _close: HTMLElement; + private readonly _scrollbar: DomScrollableElement; + private readonly _body: HTMLElement; + private readonly _header: HTMLElement; + private readonly _type: HTMLElement; + private readonly _docs: HTMLElement; + private readonly _disposables = new DisposableStore(); + + private readonly _markdownRenderer: MarkdownRenderer; + + private readonly _renderDisposeable = this._disposables.add(new DisposableStore()); + private _borderWidth: number = 1; + private _size = new dom.Dimension(330, 0); + + constructor( + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IInstantiationService instaService: IInstantiationService, + ) { + this.domNode = dom.$('.suggest-details'); + this.domNode.classList.add('no-docs'); + + this._markdownRenderer = instaService.createInstance(MarkdownRenderer, {}); + + this._body = dom.$('.body'); + + this._scrollbar = new DomScrollableElement(this._body, { + alwaysConsumeMouseWheel: true, + }); + dom.append(this.domNode, this._scrollbar.getDomNode()); + this._disposables.add(this._scrollbar); + + this._header = dom.append(this._body, dom.$('.header')); + this._close = dom.append(this._header, dom.$('span' + ThemeIcon.asCSSSelector(Codicon.close))); + this._close.title = nls.localize('details.close', "Close"); + this._close.role = 'button'; + this._close.tabIndex = -1; + this._type = dom.append(this._header, dom.$('p.type')); + + this._docs = dom.append(this._body, dom.$('p.docs')); + } + + dispose(): void { + this._disposables.dispose(); + this._onDidClose.dispose(); + this._onDidChangeContents.dispose(); + } + + getLayoutInfo() { + const lineHeight = this._configurationService.getValue('editor.lineHeight'); + const borderWidth = this._borderWidth; + const borderHeight = borderWidth * 2; + return { + lineHeight, + borderWidth, + borderHeight, + verticalPadding: 22, + horizontalPadding: 14 + }; + } + + renderLoading(): void { + this._type.textContent = nls.localize('loading', "Loading..."); + this._docs.textContent = ''; + this.domNode.classList.remove('no-docs', 'no-type'); + this.layout(this.size.width, this.getLayoutInfo().lineHeight * 2); + this._onDidChangeContents.fire(this); + } + + renderItem(item: SimpleCompletionItem, explainMode: boolean): void { + this._renderDisposeable.clear(); + + let { detail } = item.completion; + + let md = ''; + let documentation; + if (explainMode) { + md += `score: ${item.score[0]}\n`; + md += `prefix: ${item.word ?? '(no prefix)'}\n`; + md += `replacementIndex: ${item.completion.replacementIndex}\n`; + md += `replacementLength: ${item.completion.replacementLength}\n`; + md += `index: ${item.idx}\n`; + detail = `Provider: ${item.completion.provider}`; + documentation = new MarkdownString().appendCodeblock('empty', md); + } + + if (!explainMode && !canExpandCompletionItem(item)) { + this.clearContents(); + return; + } + + this.domNode.classList.remove('no-docs', 'no-type'); + + // --- details + + if (detail) { + const cappedDetail = detail.length > 100000 ? `${detail.substr(0, 100000)}…` : detail; + this._type.textContent = cappedDetail; + this._type.title = cappedDetail; + dom.show(this._type); + this._type.classList.toggle('auto-wrap', !/\r?\n^\s+/gmi.test(cappedDetail)); + } else { + dom.clearNode(this._type); + this._type.title = ''; + dom.hide(this._type); + this.domNode.classList.add('no-type'); + } + + // // --- documentation + + dom.clearNode(this._docs); + if (typeof documentation === 'string') { + this._docs.classList.remove('markdown-docs'); + this._docs.textContent = documentation; + + } else if (documentation) { + this._docs.classList.add('markdown-docs'); + dom.clearNode(this._docs); + const renderedContents = this._markdownRenderer.render(documentation, { + asyncRenderCallback: () => { + this.layout(this._size.width, this._type.clientHeight + this._docs.clientHeight); + this._onDidChangeContents.fire(this); + } + }); + this._docs.appendChild(renderedContents.element); + this._renderDisposeable.add(renderedContents); + } + + // this.domNode.classList.toggle('detail-and-doc', !!documentation); + + this.domNode.style.userSelect = 'text'; + this.domNode.tabIndex = -1; + + this._close.onmousedown = e => { + e.preventDefault(); + e.stopPropagation(); + }; + this._close.onclick = e => { + e.preventDefault(); + e.stopPropagation(); + this._onDidClose.fire(); + }; + + this._body.scrollTop = 0; + + this.layout(this._size.width, this._type.clientHeight + this._docs.clientHeight); + this._onDidChangeContents.fire(this); + } + + clearContents() { + this.domNode.classList.add('no-docs'); + this._type.textContent = ''; + this._docs.textContent = ''; + } + + get isEmpty(): boolean { + return this.domNode.classList.contains('no-docs'); + } + + get size() { + return this._size; + } + + layout(width: number, height: number): void { + const newSize = new dom.Dimension(width, height); + if (!dom.Dimension.equals(newSize, this._size)) { + this._size = newSize; + dom.size(this.domNode, width, height); + } + this._scrollbar.scanDomNode(); + } + + scrollDown(much = 8): void { + this._body.scrollTop += much; + } + + scrollUp(much = 8): void { + this._body.scrollTop -= much; + } + + scrollTop(): void { + this._body.scrollTop = 0; + } + + scrollBottom(): void { + this._body.scrollTop = this._body.scrollHeight; + } + + pageDown(): void { + this.scrollDown(80); + } + + pageUp(): void { + this.scrollUp(80); + } + + set borderWidth(width: number) { + this._borderWidth = width; + } + + get borderWidth() { + return this._borderWidth; + } + + focus() { + this.domNode.focus(); + } +} + +export class SimpleSuggestDetailsOverlay { + + private readonly _disposables = new DisposableStore(); + private readonly _resizable: ResizableHTMLElement; + + private _added: boolean = false; + private _anchorBox?: dom.IDomNodePagePosition; + // private _preferAlignAtTop: boolean = true; + private _userSize?: dom.Dimension; + // private _topLeft?: TopLeftPosition; + + constructor( + readonly widget: SimpleSuggestDetailsWidget, + private _container: HTMLElement, + ) { + + this._resizable = this._disposables.add(new ResizableHTMLElement()); + this._resizable.domNode.classList.add('suggest-details-container'); + this._resizable.domNode.appendChild(widget.domNode); + this._resizable.enableSashes(false, true, true, false); + + // let topLeftNow: TopLeftPosition | undefined; + // let sizeNow: dom.Dimension | undefined; + // let deltaTop: number = 0; + // let deltaLeft: number = 0; + // this._disposables.add(this._resizable.onDidWillResize(() => { + // topLeftNow = this._topLeft; + // sizeNow = this._resizable.size; + // })); + + // this._disposables.add(this._resizable.onDidResize(e => { + // if (topLeftNow && sizeNow) { + // this.widget.layout(e.dimension.width, e.dimension.height); + + // let updateTopLeft = false; + // if (e.west) { + // deltaLeft = sizeNow.width - e.dimension.width; + // updateTopLeft = true; + // } + // if (e.north) { + // deltaTop = sizeNow.height - e.dimension.height; + // updateTopLeft = true; + // } + // if (updateTopLeft) { + // this._applyTopLeft({ + // top: topLeftNow.top + deltaTop, + // left: topLeftNow.left + deltaLeft, + // }); + // } + // } + // if (e.done) { + // topLeftNow = undefined; + // sizeNow = undefined; + // deltaTop = 0; + // deltaLeft = 0; + // this._userSize = e.dimension; + // } + // })); + + this._disposables.add(this.widget.onDidChangeContents(() => { + if (this._anchorBox) { + this._placeAtAnchor(this._anchorBox, this._userSize ?? this.widget.size); + } + })); + } + + dispose(): void { + this.widget.dispose(); + this._disposables.dispose(); + this.hide(); + } + + getId(): string { + return 'suggest.details'; + } + + getDomNode(): HTMLElement { + return this._resizable.domNode; + } + + show(): void { + if (!this._added) { + this._container.appendChild(this._resizable.domNode); + this._added = true; + } + } + + hide(sessionEnded: boolean = false): void { + this._resizable.clearSashHoverState(); + + if (this._added) { + this._container.removeChild(this._resizable.domNode); + this._added = false; + this._anchorBox = undefined; + // this._topLeft = undefined; + } + if (sessionEnded) { + this._userSize = undefined; + this.widget.clearContents(); + } + } + + placeAtAnchor(anchor: HTMLElement) { + const anchorBox = anchor.getBoundingClientRect(); + this._anchorBox = anchorBox; + this.widget.layout(this._resizable.size.width, this._resizable.size.height); + this._placeAtAnchor(this._anchorBox, this._userSize ?? this.widget.size); + } + + _placeAtAnchor(anchorBox: dom.IDomNodePagePosition, size: dom.Dimension) { + const bodyBox = dom.getClientArea(this.getDomNode().ownerDocument.body); + + const info = this.widget.getLayoutInfo(); + + const defaultMinSize = new dom.Dimension(220, 2 * info.lineHeight); + const defaultTop = anchorBox.top; + + type Placement = { top: number; left: number; fit: number; maxSizeTop: dom.Dimension; maxSizeBottom: dom.Dimension; minSize: dom.Dimension }; + + // EAST + const eastPlacement: Placement = (function () { + const width = bodyBox.width - (anchorBox.left + anchorBox.width + info.borderWidth + info.horizontalPadding); + const left = -info.borderWidth + anchorBox.left + anchorBox.width; + const maxSizeTop = new dom.Dimension(width, bodyBox.height - anchorBox.top - info.borderHeight - info.verticalPadding); + const maxSizeBottom = maxSizeTop.with(undefined, anchorBox.top + anchorBox.height - info.borderHeight - info.verticalPadding); + return { top: defaultTop, left, fit: width - size.width, maxSizeTop, maxSizeBottom, minSize: defaultMinSize.with(Math.min(width, defaultMinSize.width)) }; + })(); + + // WEST + const westPlacement: Placement = (function () { + const width = anchorBox.left - info.borderWidth - info.horizontalPadding; + const left = Math.max(info.horizontalPadding, anchorBox.left - size.width - info.borderWidth); + const maxSizeTop = new dom.Dimension(width, bodyBox.height - anchorBox.top - info.borderHeight - info.verticalPadding); + const maxSizeBottom = maxSizeTop.with(undefined, anchorBox.top + anchorBox.height - info.borderHeight - info.verticalPadding); + return { top: defaultTop, left, fit: width - size.width, maxSizeTop, maxSizeBottom, minSize: defaultMinSize.with(Math.min(width, defaultMinSize.width)) }; + })(); + + // SOUTH + const southPacement: Placement = (function () { + const left = anchorBox.left; + const top = -info.borderWidth + anchorBox.top + anchorBox.height; + const maxSizeBottom = new dom.Dimension(anchorBox.width - info.borderHeight, bodyBox.height - anchorBox.top - anchorBox.height - info.verticalPadding); + return { top, left, fit: maxSizeBottom.height - size.height, maxSizeBottom, maxSizeTop: maxSizeBottom, minSize: defaultMinSize.with(maxSizeBottom.width) }; + })(); + + // take first placement that fits or the first with "least bad" fit + const placements = [eastPlacement, westPlacement, southPacement]; + const placement = placements.find(p => p.fit >= 0) ?? placements.sort((a, b) => b.fit - a.fit)[0]; + + // top/bottom placement + const bottom = anchorBox.top + anchorBox.height - info.borderHeight; + let alignAtTop: boolean; + let height = size.height; + const maxHeight = Math.max(placement.maxSizeTop.height, placement.maxSizeBottom.height); + if (height > maxHeight) { + height = maxHeight; + } + let maxSize: dom.Dimension; + // if (preferAlignAtTop) { + if (height <= placement.maxSizeTop.height) { + alignAtTop = true; + maxSize = placement.maxSizeTop; + } else { + alignAtTop = false; + maxSize = placement.maxSizeBottom; + } + // } else { + // if (height <= placement.maxSizeBottom.height) { + // alignAtTop = false; + // maxSize = placement.maxSizeBottom; + // } else { + // alignAtTop = true; + // maxSize = placement.maxSizeTop; + // } + // } + + let { top, left } = placement; + if (!alignAtTop && height > anchorBox.height) { + top = bottom - height; + } + const editorDomNode = this._container; + if (editorDomNode) { + // get bounding rectangle of the suggest widget relative to the editor + const editorBoundingBox = editorDomNode.getBoundingClientRect(); + top -= editorBoundingBox.top; + left -= editorBoundingBox.left; + } + this._applyTopLeft({ left, top }); + + this._resizable.enableSashes(!alignAtTop, placement === eastPlacement, alignAtTop, placement !== eastPlacement); + + this._resizable.minSize = placement.minSize; + this._resizable.maxSize = maxSize; + this._resizable.layout(height, Math.min(maxSize.width, size.width)); + this.widget.layout(this._resizable.size.width, this._resizable.size.height); + } + + private _applyTopLeft(topLeft: { left: number; top: number }): void { + // this._topLeft = topLeft; + // this._editor.layoutOverlayWidget(this); + this._resizable.domNode.style.top = `${topLeft.top}px`; + this._resizable.domNode.style.left = `${topLeft.left}px`; + this._resizable.domNode.style.position = 'absolute'; + } +} From d665c1b0aa499f351d5dbfa9831b43cadb2c14c6 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Fri, 10 Jan 2025 20:46:57 -0800 Subject: [PATCH 0493/3587] support `markdown links` syntax in prompt files (#237698) * [markdown links]: add II of `MarkdownDecoder` * [markdown links]: add `Colon` and `Hash` simple tokens, implement the new logic inside `ChatPromptDecoder` * [markdown links]: add unit tests for the `MarkdownDecoder`, update other unit tests to account for the new MD links * [markdown links]: refactor and improve docs * [markdown links]: improve unit tests of the `MarkdownDecoder` * [markdown links]: fix recursion issue caused by `##` in `ChatPromptDecoder` and improve unit tests * [markdown links]: improve docs --- src/vs/editor/common/codecs/baseToken.ts | 5 + .../linesCodec/tokens/carriageReturn.ts | 7 + .../codecs/linesCodec/tokens/newLine.ts | 7 + .../codecs/markdownCodec/markdownDecoder.ts | 301 ++++++++++++++++ .../markdownCodec/tokens/markdownLink.ts | 101 ++++++ .../markdownCodec/tokens/markdownToken.ts | 12 + .../common/codecs/simpleCodec/parserBase.ts | 73 ++++ .../codecs/simpleCodec/simpleDecoder.ts | 67 ++-- .../codecs/simpleCodec/tokens/brackets.ts | 99 ++++++ .../common/codecs/simpleCodec/tokens/colon.ts | 54 +++ .../codecs/simpleCodec/tokens/formFeed.ts | 7 + .../common/codecs/simpleCodec/tokens/hash.ts | 54 +++ .../codecs/simpleCodec/tokens/parentheses.ts | 99 ++++++ .../common/codecs/simpleCodec/tokens/space.ts | 7 + .../common/codecs/simpleCodec/tokens/tab.ts | 7 + .../codecs/simpleCodec/tokens/verticalTab.ts | 7 + .../common/codecs/markdownDecoder.test.ts | 332 ++++++++++++++++++ .../test/common/codecs/simpleDecoder.test.ts | 23 +- .../editor/test/common/utils/testDecoder.ts | 14 +- .../browser/contrib/chatDynamicVariables.ts | 2 +- .../chatPromptCodec/chatPromptDecoder.ts | 245 ++++++++++++- .../chatPromptCodec/tokens/fileReference.ts | 8 +- .../common/codecs/chatPromptDecoder.test.ts | 25 +- .../test/common/promptFileReference.test.ts | 11 +- 24 files changed, 1500 insertions(+), 67 deletions(-) create mode 100644 src/vs/editor/common/codecs/markdownCodec/markdownDecoder.ts create mode 100644 src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts create mode 100644 src/vs/editor/common/codecs/markdownCodec/tokens/markdownToken.ts create mode 100644 src/vs/editor/common/codecs/simpleCodec/parserBase.ts create mode 100644 src/vs/editor/common/codecs/simpleCodec/tokens/brackets.ts create mode 100644 src/vs/editor/common/codecs/simpleCodec/tokens/colon.ts create mode 100644 src/vs/editor/common/codecs/simpleCodec/tokens/hash.ts create mode 100644 src/vs/editor/common/codecs/simpleCodec/tokens/parentheses.ts create mode 100644 src/vs/editor/test/common/codecs/markdownDecoder.test.ts diff --git a/src/vs/editor/common/codecs/baseToken.ts b/src/vs/editor/common/codecs/baseToken.ts index 9ebe3ad8abc3..6430ffb61a5e 100644 --- a/src/vs/editor/common/codecs/baseToken.ts +++ b/src/vs/editor/common/codecs/baseToken.ts @@ -18,6 +18,11 @@ export abstract class BaseToken { return this._range; } + /** + * Return text representation of the token. + */ + public abstract get text(): string; + /** * Check if this token has the same range as another one. */ diff --git a/src/vs/editor/common/codecs/linesCodec/tokens/carriageReturn.ts b/src/vs/editor/common/codecs/linesCodec/tokens/carriageReturn.ts index 5120f4ac322b..a509940bc4e8 100644 --- a/src/vs/editor/common/codecs/linesCodec/tokens/carriageReturn.ts +++ b/src/vs/editor/common/codecs/linesCodec/tokens/carriageReturn.ts @@ -31,6 +31,13 @@ export class CarriageReturn extends BaseToken { return CarriageReturn.byte; } + /** + * Return text representation of the token. + */ + public get text(): string { + return CarriageReturn.symbol; + } + /** * Create new `CarriageReturn` token with range inside * the given `Line` at the given `column number`. diff --git a/src/vs/editor/common/codecs/linesCodec/tokens/newLine.ts b/src/vs/editor/common/codecs/linesCodec/tokens/newLine.ts index 19b80dd88a3c..fb826b759ca2 100644 --- a/src/vs/editor/common/codecs/linesCodec/tokens/newLine.ts +++ b/src/vs/editor/common/codecs/linesCodec/tokens/newLine.ts @@ -24,6 +24,13 @@ export class NewLine extends BaseToken { */ public static readonly byte = VSBuffer.fromString(NewLine.symbol); + /** + * Return text representation of the token. + */ + public get text(): string { + return NewLine.symbol; + } + /** * The byte representation of the token. */ diff --git a/src/vs/editor/common/codecs/markdownCodec/markdownDecoder.ts b/src/vs/editor/common/codecs/markdownCodec/markdownDecoder.ts new file mode 100644 index 000000000000..3703fa1df29c --- /dev/null +++ b/src/vs/editor/common/codecs/markdownCodec/markdownDecoder.ts @@ -0,0 +1,301 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownLink } from './tokens/markdownLink.js'; +import { NewLine } from '../linesCodec/tokens/newLine.js'; +import { assert } from '../../../../base/common/assert.js'; +import { FormFeed } from '../simpleCodec/tokens/formFeed.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { VerticalTab } from '../simpleCodec/tokens/verticalTab.js'; +import { ReadableStream } from '../../../../base/common/stream.js'; +import { CarriageReturn } from '../linesCodec/tokens/carriageReturn.js'; +import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; +import { LeftBracket, RightBracket } from '../simpleCodec/tokens/brackets.js'; +import { SimpleDecoder, TSimpleToken } from '../simpleCodec/simpleDecoder.js'; +import { ParserBase, TAcceptTokenResult } from '../simpleCodec/parserBase.js'; +import { LeftParenthesis, RightParenthesis } from '../simpleCodec/tokens/parentheses.js'; + +/** + * Tokens handled by this decoder. + */ +export type TMarkdownToken = MarkdownLink | TSimpleToken; + +/** + * List of characters that stop a markdown link sequence. + */ +const MARKDOWN_LINK_STOP_CHARACTERS: readonly string[] = [CarriageReturn, NewLine, VerticalTab, FormFeed] + .map((token) => { return token.symbol; }); + +/** + * The parser responsible for parsing a `markdown link caption` part of a markdown + * link (e.g., the `[caption text]` part of the `[caption text](./some/path)` link). + * + * The parsing process starts with single `[` token and collects all tokens until + * the first `]` token is encountered. In this successful case, the parser transitions + * into the {@linkcode MarkdownLinkCaption} parser type which continues the general + * parsing process of the markdown link. + * + * Otherwise, if one of the stop characters defined in the {@linkcode MARKDOWN_LINK_STOP_CHARACTERS} + * is encountered before the `]` token, the parsing process is aborted which is communicated to + * the caller by returning a `failure` result. In this case, the caller is assumed to be responsible + * for re-emitting the {@link tokens} accumulated so far as standalone entities since they are no + * longer represent a coherent token entity of a larger size. + */ +class PartialMarkdownLinkCaption extends ParserBase { + constructor(token: LeftBracket) { + super([token]); + } + + public accept(token: TSimpleToken): TAcceptTokenResult { + // any of stop characters is are breaking a markdown link caption sequence + if (MARKDOWN_LINK_STOP_CHARACTERS.includes(token.text)) { + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + // the `]` character ends the caption of a markdown link + if (token instanceof RightBracket) { + return { + result: 'success', + nextParser: new MarkdownLinkCaption([...this.tokens, token]), + wasTokenConsumed: true, + }; + } + + // otherwise, include the token in the sequence + // and keep the current parser object instance + this.currentTokens.push(token); + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } +} + +/** + * The parser responsible for transitioning from a {@linkcode PartialMarkdownLinkCaption} + * parser to the {@link PartialMarkdownLink} one, therefore serves a parser glue between + * the `[caption]` and the `(./some/path)` parts of the `[caption](./some/path)` link. + * + * The only successful case of this parser is the `(` token that initiated the process + * of parsing the `reference` part of a markdown link and in this case the parser + * transitions into the `PartialMarkdownLink` parser type. + * + * Any other character is considered a failure result. In this case, the caller is assumed + * to be responsible for re-emitting the {@link tokens} accumulated so far as standalone + * entities since they are no longer represent a coherent token entity of a larger size. + */ +class MarkdownLinkCaption extends ParserBase { + public accept(token: TSimpleToken): TAcceptTokenResult { + // the `(` character starts the link part of a markdown link + // that is the only character that can follow the caption + if (token instanceof LeftParenthesis) { + return { + result: 'success', + wasTokenConsumed: true, + nextParser: new PartialMarkdownLink([...this.tokens], token), + }; + } + + return { + result: 'failure', + wasTokenConsumed: false, + }; + } +} + +/** + * The parser responsible for parsing a `link reference` part of a markdown link + * (e.g., the `(./some/path)` part of the `[caption text](./some/path)` link). + * + * The parsing process starts with tokens that represent the `[caption]` part of a markdown + * link, followed by the `(` token. The parser collects all subsequent tokens until final closing + * parenthesis (`)`) is encountered (*\*see [1] below*). In this successful case, the parser object + * transitions into the {@linkcode MarkdownLink} token type which signifies the end of the entire + * parsing process of the link text. + * + * Otherwise, if one of the stop characters defined in the {@linkcode MARKDOWN_LINK_STOP_CHARACTERS} + * is encountered before the final `)` token, the parsing process is aborted which is communicated to + * the caller by returning a `failure` result. In this case, the caller is assumed to be responsible + * for re-emitting the {@link tokens} accumulated so far as standalone entities since they are no + * longer represent a coherent token entity of a larger size. + * + * `[1]` The `reference` part of the markdown link can contain any number of nested parenthesis, e.g., + * `[caption](/some/p(th/file.md)` is a valid markdown link and a valid folder name, hence number + * of open parenthesis must match the number of closing ones and the path sequence is considered + * to be complete as soon as this requirement is met. Therefore the `final` word is used in + * the description comments above to highlight this important detail. + */ +class PartialMarkdownLink extends ParserBase { + /** + * Number of open parenthesis in the sequence. + * See comment in the {@linkcode accept} method for more details. + */ + private openParensCount: number = 1; + + constructor( + protected readonly captionTokens: TSimpleToken[], + token: LeftParenthesis, + ) { + super([token]); + } + + public override get tokens(): readonly TSimpleToken[] { + return [...this.captionTokens, ...this.currentTokens]; + } + + public accept(token: TSimpleToken): TAcceptTokenResult { + // markdown links allow for nested parenthesis inside the link reference part, but + // the number of open parenthesis must match the number of closing parenthesis, e.g.: + // - `[caption](/some/p()th/file.md)` is a valid markdown link + // - `[caption](/some/p(th/file.md)` is an invalid markdown link + // hence we use the `openParensCount` variable to keep track of the number of open + // parenthesis encountered so far; then upon encountering a closing parenthesis we + // decrement the `openParensCount` and if it reaches 0 - we consider the link reference + // to be complete + + if (token instanceof LeftParenthesis) { + this.openParensCount += 1; + } + + if (token instanceof RightParenthesis) { + this.openParensCount -= 1; + + // sanity check! this must alway hold true because we return a complete markdown + // link as soon as we encounter matching number of closing parenthesis, hence + // we must never have `openParensCount` that is less than 0 + assert( + this.openParensCount >= 0, + `Unexpected right parenthesis token encountered: '${token}'.`, + ); + + // the markdown link is complete as soon as we get the same number of closing parenthesis + if (this.openParensCount === 0) { + const { startLineNumber, startColumn } = this.captionTokens[0].range; + + // create link caption string + const caption = this.captionTokens + .map((token) => { return token.text; }) + .join(''); + + // create link reference string + this.currentTokens.push(token); + const reference = this.currentTokens + .map((token) => { return token.text; }).join(''); + + // return complete markdown link object + return { + result: 'success', + wasTokenConsumed: true, + nextParser: new MarkdownLink( + startLineNumber, + startColumn, + caption, + reference, + ), + }; + } + } + + // any of stop characters is are breaking a markdown link reference sequence + if (MARKDOWN_LINK_STOP_CHARACTERS.includes(token.text)) { + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + // the rest of the tokens can be included in the sequence + this.currentTokens.push(token); + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } +} + +/** + * Decoder capable of parsing markdown entities (e.g., links) from a sequence of simplier tokens. + */ +export class MarkdownDecoder extends BaseDecoder { + /** + * Current parser object that is responsible for parsing a sequence of tokens + * into some markdown entity. + */ + private current?: PartialMarkdownLinkCaption | MarkdownLinkCaption | PartialMarkdownLink; + + constructor( + stream: ReadableStream, + ) { + super(new SimpleDecoder(stream)); + } + + protected override onStreamData(token: TSimpleToken): void { + // markdown links start with `[` character, so here we can + // initiate the process of parsing a markdown link + if (token instanceof LeftBracket && !this.current) { + this.current = new PartialMarkdownLinkCaption(token); + + return; + } + + // if current parser was not initiated before, - we are not inside a + // sequence of tokens we care about, therefore re-emit the token + // immediately and continue to the next one + if (!this.current) { + this._onData.fire(token); + return; + } + + // if there is a current parser object, submit the token to it + // so it can progress with parsing the tokens sequence + const parseResult = this.current.accept(token); + if (parseResult.result === 'success') { + // if got a parsed out `MarkdownLink` back, emit it + // then reset the current parser object + if (parseResult.nextParser instanceof MarkdownLink) { + this._onData.fire(parseResult.nextParser); + delete this.current; + } else { + // otherwise, update the current parser object + this.current = parseResult.nextParser; + } + } else { + // if failed to parse a sequence of a tokens as a single markdown + // entity (e.g., a link), re-emit the tokens accumulated so far + // then reset the current parser object + for (const token of this.current.tokens) { + this._onData.fire(token); + delete this.current; + } + } + + // if token was not consumed by the parser, call `onStreamData` again + // so the token is properly handled by the decoder in the case when a + // new sequence starts with this token + if (!parseResult.wasTokenConsumed) { + this.onStreamData(token); + } + } + + protected override onStreamEnd(): void { + // if the stream has ended and there is a current incomplete parser + // object present, then re-emit its tokens as standalone entities + if (this.current) { + const { tokens } = this.current; + delete this.current; + + for (const token of [...tokens]) { + this._onData.fire(token); + } + } + + super.onStreamEnd(); + } +} diff --git a/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts b/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts new file mode 100644 index 000000000000..174365c45599 --- /dev/null +++ b/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { Range } from '../../../core/range.js'; +import { MarkdownToken } from './markdownToken.js'; +import { assert } from '../../../../../base/common/assert.js'; + +/** + * A token that represent a `markdown link` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class MarkdownLink extends MarkdownToken { + constructor( + /** + * The starting line number of the link (1-based indexing). + */ + lineNumber: number, + /** + * The starting column number of the link (1-based indexing). + */ + columnNumber: number, + /** + * The caprtion of the link, including the square brackets. + */ + private readonly caption: string, + /** + * The reference of the link, including the parentheses. + */ + private readonly reference: string, + ) { + assert( + !isNaN(lineNumber), + `The line number must not be a NaN.`, + ); + + assert( + lineNumber > 0, + `The line number must be >= 1, got "${lineNumber}".`, + ); + + assert( + columnNumber > 0, + `The column number must be >= 1, got "${columnNumber}".`, + ); + + assert( + caption[0] === '[' && caption[caption.length - 1] === ']', + `The caption must be enclosed in square brackets, got "${caption}".`, + ); + + assert( + reference[0] === '(' && reference[reference.length - 1] === ')', + `The reference must be enclosed in parentheses, got "${reference}".`, + ); + + super( + new Range( + lineNumber, + columnNumber, + lineNumber, + columnNumber + caption.length + reference.length, + ), + ); + } + + public override get text(): string { + return `${this.caption}${this.reference}`; + } + + /** + * Returns the `reference` part of the link without enclosing parentheses. + */ + public get path(): string { + return this.reference.slice(1, this.reference.length - 1); + } + + /** + * Check if this token is equal to another one. + */ + public override equals(other: T): boolean { + if (!super.sameRange(other.range)) { + return false; + } + + if (!(other instanceof MarkdownLink)) { + return false; + } + + return this.text === other.text; + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `md-link("${this.text}")${this.range}`; + } +} diff --git a/src/vs/editor/common/codecs/markdownCodec/tokens/markdownToken.ts b/src/vs/editor/common/codecs/markdownCodec/tokens/markdownToken.ts new file mode 100644 index 000000000000..fc1935d081bf --- /dev/null +++ b/src/vs/editor/common/codecs/markdownCodec/tokens/markdownToken.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; + +/** + * Common base token that all `markdown` tokens should + * inherit from. + */ +export abstract class MarkdownToken extends BaseToken { } diff --git a/src/vs/editor/common/codecs/simpleCodec/parserBase.ts b/src/vs/editor/common/codecs/simpleCodec/parserBase.ts new file mode 100644 index 000000000000..9e864177f9fd --- /dev/null +++ b/src/vs/editor/common/codecs/simpleCodec/parserBase.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../baseToken.js'; + +/** + * Common interface for a result of accepting a next token + * in a sequence. + */ +export interface IAcceptTokenResult { + /** + * The result type of accepting a next token in a sequence. + */ + result: 'success' | 'failure'; + + /** + * Whether the token to accept was consumed by the parser + * during the accept operation. + */ + wasTokenConsumed: boolean; +} + +/** + * Successful result of accepting a next token in a sequence. + */ +export interface IAcceptTokenSuccess extends IAcceptTokenResult { + result: 'success'; + nextParser: T; +} + +/** + * Failure result of accepting a next token in a sequence. + */ +export interface IAcceptTokenFailure extends IAcceptTokenResult { + result: 'failure'; +} + +/** + * The result of operation of accepting a next token in a sequence. + */ +export type TAcceptTokenResult = IAcceptTokenSuccess | IAcceptTokenFailure; + +/** + * An abstract parser class that is able to parse a sequence of + * tokens into a new single entity. + */ +export abstract class ParserBase { + constructor( + /** + * Set of tokens that were accumulated so far. + */ + protected readonly currentTokens: TToken[] = [], + ) { } + + /** + * Get the tokens that were accumulated so far. + */ + public get tokens(): readonly TToken[] { + return this.currentTokens; + } + + /** + * Accept a new token returning parsing result: + * - successful result must include the next parser object or a fully parsed out token + * - failure result must indicate that the token was not consumed + * + * @param token The token to accept. + * @returns The parsing result. + */ + public abstract accept(token: TToken): TAcceptTokenResult; +} diff --git a/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts b/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts index 64173eceabda..88ad12985017 100644 --- a/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts +++ b/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Hash } from './tokens/hash.js'; +import { Colon } from './tokens/colon.js'; import { FormFeed } from './tokens/formFeed.js'; import { Tab } from '../simpleCodec/tokens/tab.js'; import { Word } from '../simpleCodec/tokens/word.js'; @@ -10,22 +12,39 @@ import { VerticalTab } from './tokens/verticalTab.js'; import { Space } from '../simpleCodec/tokens/space.js'; import { NewLine } from '../linesCodec/tokens/newLine.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; +import { LeftBracket, RightBracket } from './tokens/brackets.js'; import { ReadableStream } from '../../../../base/common/stream.js'; import { CarriageReturn } from '../linesCodec/tokens/carriageReturn.js'; import { LinesDecoder, TLineToken } from '../linesCodec/linesDecoder.js'; import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; +import { LeftParenthesis, RightParenthesis } from './tokens/parentheses.js'; /** * A token type that this decoder can handle. */ -export type TSimpleToken = Word | Space | Tab | VerticalTab | NewLine | FormFeed | CarriageReturn; +export type TSimpleToken = Word | Space | Tab | VerticalTab | NewLine | FormFeed | CarriageReturn | LeftBracket + | RightBracket | LeftParenthesis | RightParenthesis | Colon | Hash; + +/** + * List of well-known distinct tokens that this decoder emits (excluding + * the word stop characters defined below). Everything else is considered + * an arbitrary "text" sequence and is emitted as a single `Word` token. + */ +const WELL_KNOWN_TOKENS = [ + Space, Tab, VerticalTab, FormFeed, LeftBracket, RightBracket, + LeftParenthesis, RightParenthesis, Colon, Hash, +]; /** * Characters that stop a "word" sequence. * Note! the `\r` and `\n` are excluded from the list because this decoder based on `LinesDecoder` which * already handles the `carriagereturn`/`newline` cases and emits lines that don't contain them. */ -const STOP_CHARACTERS = [Space.symbol, Tab.symbol, VerticalTab.symbol, FormFeed.symbol]; +const WORD_STOP_CHARACTERS = [ + Space.symbol, Tab.symbol, VerticalTab.symbol, FormFeed.symbol, + LeftBracket.symbol, RightBracket.symbol, LeftParenthesis.symbol, + RightParenthesis.symbol, Colon.symbol, Hash.symbol, +]; /** * A decoder that can decode a stream of `Line`s into a stream @@ -39,7 +58,7 @@ export class SimpleDecoder extends BaseDecoder { } protected override onStreamData(token: TLineToken): void { - // re-emit new line tokens + // re-emit new line tokens immediately if (token instanceof CarriageReturn || token instanceof NewLine) { this._onData.fire(token); @@ -52,46 +71,30 @@ export class SimpleDecoder extends BaseDecoder { // index is 0-based, but column numbers are 1-based const columnNumber = i + 1; - // if a space character, emit a `Space` token and continue - if (token.text[i] === Space.symbol) { - this._onData.fire(Space.newOnLine(token, columnNumber)); - - i++; - continue; - } - - // if a tab character, emit a `Tab` token and continue - if (token.text[i] === Tab.symbol) { - this._onData.fire(Tab.newOnLine(token, columnNumber)); - - i++; - continue; - } - - // if a vertical tab character, emit a `VerticalTab` token and continue - if (token.text[i] === VerticalTab.symbol) { - this._onData.fire(VerticalTab.newOnLine(token, columnNumber)); - - i++; - continue; - } + // check if the current character is a well-known token + const tokenConstructor = WELL_KNOWN_TOKENS + .find((wellKnownToken) => { + return wellKnownToken.symbol === token.text[i]; + }); - // if a form feed character, emit a `FormFeed` token and continue - if (token.text[i] === FormFeed.symbol) { - this._onData.fire(FormFeed.newOnLine(token, columnNumber)); + // if it is a well-known token, emit it and continue to the next one + if (tokenConstructor) { + this._onData.fire(tokenConstructor.newOnLine(token, columnNumber)); i++; continue; } - // if a non-space character, parse out the whole word and - // emit it, then continue from the last word character position + // otherwise, it is an arbitrary "text" sequence of characters, + // that needs to be collected into a single `Word` token, hence + // read all the characters until a stop character is encountered let word = ''; - while (i < token.text.length && !(STOP_CHARACTERS.includes(token.text[i]))) { + while (i < token.text.length && !(WORD_STOP_CHARACTERS.includes(token.text[i]))) { word += token.text[i]; i++; } + // emit a "text" sequence of characters as a single `Word` token this._onData.fire( Word.newOnLine(word, token, columnNumber), ); diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/brackets.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/brackets.ts new file mode 100644 index 000000000000..5c6c1e46a5d3 --- /dev/null +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/brackets.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { Range } from '../../../core/range.js'; +import { Position } from '../../../core/position.js'; +import { Line } from '../../linesCodec/tokens/line.js'; + +/** + * A token that represent a `[` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class LeftBracket extends BaseToken { + /** + * The underlying symbol of the `LeftBracket` token. + */ + public static readonly symbol: string = '['; + + /** + * Return text representation of the token. + */ + public get text(): string { + return LeftBracket.symbol; + } + + /** + * Create new `LeftBracket` token with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + ): LeftBracket { + const { range } = line; + + const startPosition = new Position(range.startLineNumber, atColumnNumber); + // the tab token length is 1, hence `+ 1` + const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); + + return new LeftBracket(Range.fromPositions( + startPosition, + endPosition, + )); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `left-bracket${this.range}`; + } +} + +/** + * A token that represent a `]` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class RightBracket extends BaseToken { + /** + * The underlying symbol of the `RightBracket` token. + */ + public static readonly symbol: string = ']'; + + /** + * Return text representation of the token. + */ + public get text(): string { + return RightBracket.symbol; + } + + /** + * Create new `RightBracket` token with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + ): RightBracket { + const { range } = line; + + const startPosition = new Position(range.startLineNumber, atColumnNumber); + // the tab token length is 1, hence `+ 1` + const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); + + return new RightBracket(Range.fromPositions( + startPosition, + endPosition, + )); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `right-bracket${this.range}`; + } +} diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/colon.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/colon.ts new file mode 100644 index 000000000000..2c4b89d9ce52 --- /dev/null +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/colon.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { Range } from '../../../core/range.js'; +import { Position } from '../../../core/position.js'; +import { Line } from '../../linesCodec/tokens/line.js'; + +/** + * A token that represent a `:` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class Colon extends BaseToken { + /** + * The underlying symbol of the `LeftBracket` token. + */ + public static readonly symbol: string = ':'; + + /** + * Return text representation of the token. + */ + public get text(): string { + return Colon.symbol; + } + + /** + * Create new `LeftBracket` token with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + ): Colon { + const { range } = line; + + const startPosition = new Position(range.startLineNumber, atColumnNumber); + // the tab token length is 1, hence `+ 1` + const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); + + return new Colon(Range.fromPositions( + startPosition, + endPosition, + )); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `colon${this.range}`; + } +} diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/formFeed.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/formFeed.ts index ab40192f459e..35f55dd8a2ab 100644 --- a/src/vs/editor/common/codecs/simpleCodec/tokens/formFeed.ts +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/formFeed.ts @@ -18,6 +18,13 @@ export class FormFeed extends BaseToken { */ public static readonly symbol: string = '\f'; + /** + * Return text representation of the token. + */ + public get text(): string { + return FormFeed.symbol; + } + /** * Create new `FormFeed` token with range inside * the given `Line` at the given `column number`. diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/hash.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/hash.ts new file mode 100644 index 000000000000..372e0b2ee3d9 --- /dev/null +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/hash.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { Range } from '../../../core/range.js'; +import { Position } from '../../../core/position.js'; +import { Line } from '../../linesCodec/tokens/line.js'; + +/** + * A token that represent a `#` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class Hash extends BaseToken { + /** + * The underlying symbol of the `LeftBracket` token. + */ + public static readonly symbol: string = '#'; + + /** + * Return text representation of the token. + */ + public get text(): string { + return Hash.symbol; + } + + /** + * Create new `LeftBracket` token with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + ): Hash { + const { range } = line; + + const startPosition = new Position(range.startLineNumber, atColumnNumber); + // the tab token length is 1, hence `+ 1` + const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); + + return new Hash(Range.fromPositions( + startPosition, + endPosition, + )); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `hash${this.range}`; + } +} diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/parentheses.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/parentheses.ts new file mode 100644 index 000000000000..b67f4e10f5c7 --- /dev/null +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/parentheses.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { Range } from '../../../core/range.js'; +import { Position } from '../../../core/position.js'; +import { Line } from '../../linesCodec/tokens/line.js'; + +/** + * A token that represent a `(` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class LeftParenthesis extends BaseToken { + /** + * The underlying symbol of the `LeftParenthesis` token. + */ + public static readonly symbol: string = '('; + + /** + * Return text representation of the token. + */ + public get text(): string { + return LeftParenthesis.symbol; + } + + /** + * Create new `LeftParenthesis` token with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + ): LeftParenthesis { + const { range } = line; + + const startPosition = new Position(range.startLineNumber, atColumnNumber); + // the tab token length is 1, hence `+ 1` + const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); + + return new LeftParenthesis(Range.fromPositions( + startPosition, + endPosition, + )); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `left-parenthesis${this.range}`; + } +} + +/** + * A token that represent a `)` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class RightParenthesis extends BaseToken { + /** + * The underlying symbol of the `RightParenthesis` token. + */ + public static readonly symbol: string = ')'; + + /** + * Return text representation of the token. + */ + public get text(): string { + return RightParenthesis.symbol; + } + + /** + * Create new `RightParenthesis` token with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + ): RightParenthesis { + const { range } = line; + + const startPosition = new Position(range.startLineNumber, atColumnNumber); + // the tab token length is 1, hence `+ 1` + const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); + + return new RightParenthesis(Range.fromPositions( + startPosition, + endPosition, + )); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `right-parenthesis${this.range}`; + } +} diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/space.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/space.ts index 9961c38ece9c..18a5dff4a0a9 100644 --- a/src/vs/editor/common/codecs/simpleCodec/tokens/space.ts +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/space.ts @@ -18,6 +18,13 @@ export class Space extends BaseToken { */ public static readonly symbol: string = ' '; + /** + * Return text representation of the token. + */ + public get text(): string { + return Space.symbol; + } + /** * Create new `Space` token with range inside * the given `Line` at the given `column number`. diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/tab.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/tab.ts index aab11327bc15..7f511c2626bf 100644 --- a/src/vs/editor/common/codecs/simpleCodec/tokens/tab.ts +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/tab.ts @@ -18,6 +18,13 @@ export class Tab extends BaseToken { */ public static readonly symbol: string = '\t'; + /** + * Return text representation of the token. + */ + public get text(): string { + return Tab.symbol; + } + /** * Create new `Tab` token with range inside * the given `Line` at the given `column number`. diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/verticalTab.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/verticalTab.ts index 11e5ca6efabf..c6b87db0e37b 100644 --- a/src/vs/editor/common/codecs/simpleCodec/tokens/verticalTab.ts +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/verticalTab.ts @@ -18,6 +18,13 @@ export class VerticalTab extends BaseToken { */ public static readonly symbol: string = '\v'; + /** + * Return text representation of the token. + */ + public get text(): string { + return VerticalTab.symbol; + } + /** * Create new `VerticalTab` token with range inside * the given `Line` at the given `column number`. diff --git a/src/vs/editor/test/common/codecs/markdownDecoder.test.ts b/src/vs/editor/test/common/codecs/markdownDecoder.test.ts new file mode 100644 index 000000000000..bff4b428ae1d --- /dev/null +++ b/src/vs/editor/test/common/codecs/markdownDecoder.test.ts @@ -0,0 +1,332 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TestDecoder } from '../utils/testDecoder.js'; +import { Range } from '../../../common/core/range.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { newWriteableStream } from '../../../../base/common/stream.js'; +import { Tab } from '../../../common/codecs/simpleCodec/tokens/tab.js'; +import { Word } from '../../../common/codecs/simpleCodec/tokens/word.js'; +import { Space } from '../../../common/codecs/simpleCodec/tokens/space.js'; +import { NewLine } from '../../../common/codecs/linesCodec/tokens/newLine.js'; +import { VerticalTab } from '../../../common/codecs/simpleCodec/tokens/verticalTab.js'; +import { MarkdownLink } from '../../../common/codecs/markdownCodec/tokens/markdownLink.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { MarkdownDecoder, TMarkdownToken } from '../../../common/codecs/markdownCodec/markdownDecoder.js'; +import { FormFeed } from '../../../common/codecs/simpleCodec/tokens/formFeed.js'; +import { LeftParenthesis, RightParenthesis } from '../../../common/codecs/simpleCodec/tokens/parentheses.js'; +import { LeftBracket, RightBracket } from '../../../common/codecs/simpleCodec/tokens/brackets.js'; +import { CarriageReturn } from '../../../common/codecs/linesCodec/tokens/carriageReturn.js'; +import assert from 'assert'; + +/** + * A reusable test utility that asserts that a `TestMarkdownDecoder` instance + * correctly decodes `inputData` into a stream of `TMarkdownToken` tokens. + * + * ## Examples + * + * ```typescript + * // create a new test utility instance + * const test = testDisposables.add(new TestMarkdownDecoder()); + * + * // run the test + * await test.run( + * ' hello [world](/etc/hosts)!', + * [ + * new Space(new Range(1, 1, 1, 2)), + * new Word(new Range(1, 2, 1, 7), 'hello'), + * new Space(new Range(1, 7, 1, 8)), + * new MarkdownLink(1, 8, '[world]', '(/etc/hosts)'), + * new Word(new Range(1, 27, 1, 28), '!'), + * new NewLine(new Range(1, 28, 1, 29)), + * ], + * ); + */ +export class TestMarkdownDecoder extends TestDecoder { + constructor() { + const stream = newWriteableStream(null); + + super(stream, new MarkdownDecoder(stream)); + } +} + +suite('MarkdownDecoder', () => { + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + + test('produces expected tokens', async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + await test.run( + ' hello world\nhow are\t you [caption text](./some/file/path/refer🎨nce.md)?\v\n\n[(example)](another/path/with[-and-]-chars/folder)\t \n\t[#file:something.txt](/absolute/path/to/something.txt)', + [ + // first line + new Space(new Range(1, 1, 1, 2)), + new Word(new Range(1, 2, 1, 7), 'hello'), + new Space(new Range(1, 7, 1, 8)), + new Word(new Range(1, 8, 1, 13), 'world'), + new NewLine(new Range(1, 13, 1, 14)), + // second line + new Word(new Range(2, 1, 2, 4), 'how'), + new Space(new Range(2, 4, 2, 5)), + new Word(new Range(2, 5, 2, 8), 'are'), + new Tab(new Range(2, 8, 2, 9)), + new Space(new Range(2, 9, 2, 10)), + new Word(new Range(2, 10, 2, 13), 'you'), + new Space(new Range(2, 13, 2, 14)), + new MarkdownLink(2, 14, '[caption text]', '(./some/file/path/refer🎨nce.md)'), + new Word(new Range(2, 60, 2, 61), '?'), + new VerticalTab(new Range(2, 61, 2, 62)), + new NewLine(new Range(2, 62, 2, 63)), + // third line + new NewLine(new Range(3, 1, 3, 2)), + // fourth line + new MarkdownLink(4, 1, '[(example)]', '(another/path/with[-and-]-chars/folder)'), + new Tab(new Range(4, 51, 4, 52)), + new Space(new Range(4, 52, 4, 53)), + new NewLine(new Range(4, 53, 4, 54)), + // fifth line + new Tab(new Range(5, 1, 5, 2)), + new MarkdownLink(5, 2, '[#file:something.txt]', '(/absolute/path/to/something.txt)'), + ], + ); + }); + + test('handles complex cases', async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + const inputLines = [ + // tests that the link caption contain a chat prompt `#file:` reference, while + // the file path can contain other `graphical characters` + '\v\t[#file:./another/path/to/file.txt](./real/filepath/file◆name.md)', + // tests that the link file path contain a chat prompt `#file:` reference, + // `spaces`, `emojies`, and other `graphical characters` + ' [reference ∘ label](/absolute/pa th/to-#file:file.txt/f🥸⚡️le.md)', + // tests that link caption and file path can contain `parentheses`, `spaces`, and + // `emojies` + '\f[!(hello)!](./w(())rld/nice-🦚-filen(a)me.git))\n\t', + // tests that the link caption can be empty, while the file path can contain `square brackets` + '[](./s[]me/pa[h!) ', + ]; + + await test.run( + inputLines, + [ + // `1st` line + new VerticalTab(new Range(1, 1, 1, 2)), + new Tab(new Range(1, 2, 1, 3)), + new MarkdownLink(1, 3, '[#file:./another/path/to/file.txt]', '(./real/filepath/file◆name.md)'), + new NewLine(new Range(1, 67, 1, 68)), + // `2nd` line + new Space(new Range(2, 1, 2, 2)), + new MarkdownLink(2, 2, '[reference ∘ label]', '(/absolute/pa th/to-#file:file.txt/f🥸⚡️le.md)'), + new NewLine(new Range(2, 67, 2, 68)), + // `3rd` line + new FormFeed(new Range(3, 1, 3, 2)), + new MarkdownLink(3, 2, '[!(hello)!]', '(./w(())rld/nice-🦚-filen(a)me.git)'), + new RightParenthesis(new Range(3, 48, 3, 49)), + new NewLine(new Range(3, 49, 3, 50)), + // `4th` line + new Tab(new Range(4, 1, 4, 2)), + new NewLine(new Range(4, 2, 4, 3)), + // `5th` line + new MarkdownLink(5, 1, '[]', '(./s[]me/pa[h!)'), + new Space(new Range(5, 18, 5, 19)), + ], + ); + }); + + suite('broken links', () => { + test('incomplete/invalid links', async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + const inputLines = [ + // incomplete link reference with empty caption + '[ ](./real/file path/file⇧name.md', + // space between caption and reference is disallowed + '[link text] (./file path/name.txt)', + ]; + + await test.run( + inputLines, + [ + // `1st` line + new LeftBracket(new Range(1, 1, 1, 2)), + new Space(new Range(1, 2, 1, 3)), + new RightBracket(new Range(1, 3, 1, 4)), + new LeftParenthesis(new Range(1, 4, 1, 5)), + new Word(new Range(1, 5, 1, 5 + 11), './real/file'), + new Space(new Range(1, 16, 1, 17)), + new Word(new Range(1, 17, 1, 17 + 17), 'path/file⇧name.md'), + new NewLine(new Range(1, 34, 1, 35)), + // `2nd` line + new LeftBracket(new Range(2, 1, 2, 2)), + new Word(new Range(2, 2, 2, 2 + 4), 'link'), + new Space(new Range(2, 6, 2, 7)), + new Word(new Range(2, 7, 2, 7 + 4), 'text'), + new RightBracket(new Range(2, 11, 2, 12)), + new Space(new Range(2, 12, 2, 13)), + new LeftParenthesis(new Range(2, 13, 2, 14)), + new Word(new Range(2, 14, 2, 14 + 6), './file'), + new Space(new Range(2, 20, 2, 21)), + new Word(new Range(2, 21, 2, 21 + 13), 'path/name.txt'), + new RightParenthesis(new Range(2, 34, 2, 35)), + ], + ); + }); + + suite('stop characters inside caption/reference (new lines)', () => { + for (const stopCharacter of [CarriageReturn, NewLine]) { + let characterName = ''; + + if (stopCharacter === CarriageReturn) { + characterName = '\\r'; + } + if (stopCharacter === NewLine) { + characterName = '\\n'; + } + + assert( + characterName !== '', + 'The "characterName" must be set, got "empty line".', + ); + + test(`stop character - "${characterName}"`, async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + const inputLines = [ + // stop character inside link caption + `[haa${stopCharacter.symbol}loů](./real/💁/name.txt)`, + // stop character inside link reference + `[ref text](/etc/pat${stopCharacter.symbol}h/to/file.md)`, + // stop character between line caption and link reference is disallowed + `[text]${stopCharacter.symbol}(/etc/ path/file.md)`, + ]; + + + await test.run( + inputLines, + [ + // `1st` input line + new LeftBracket(new Range(1, 1, 1, 2)), + new Word(new Range(1, 2, 1, 2 + 3), 'haa'), + new stopCharacter(new Range(1, 5, 1, 6)), // <- stop character + new Word(new Range(2, 1, 2, 1 + 3), 'loů'), + new RightBracket(new Range(2, 4, 2, 5)), + new LeftParenthesis(new Range(2, 5, 2, 6)), + new Word(new Range(2, 6, 2, 6 + 18), './real/💁/name.txt'), + new RightParenthesis(new Range(2, 24, 2, 25)), + new NewLine(new Range(2, 25, 2, 26)), + // `2nd` input line + new LeftBracket(new Range(3, 1, 3, 2)), + new Word(new Range(3, 2, 3, 2 + 3), 'ref'), + new Space(new Range(3, 5, 3, 6)), + new Word(new Range(3, 6, 3, 6 + 4), 'text'), + new RightBracket(new Range(3, 10, 3, 11)), + new LeftParenthesis(new Range(3, 11, 3, 12)), + new Word(new Range(3, 12, 3, 12 + 8), '/etc/pat'), + new stopCharacter(new Range(3, 20, 3, 21)), // <- stop character + new Word(new Range(4, 1, 4, 1 + 12), 'h/to/file.md'), + new RightParenthesis(new Range(4, 13, 4, 14)), + new NewLine(new Range(4, 14, 4, 15)), + // `3nd` input line + new LeftBracket(new Range(5, 1, 5, 2)), + new Word(new Range(5, 2, 5, 2 + 4), 'text'), + new RightBracket(new Range(5, 6, 5, 7)), + new stopCharacter(new Range(5, 7, 5, 8)), // <- stop character + new LeftParenthesis(new Range(6, 1, 6, 2)), + new Word(new Range(6, 2, 6, 2 + 5), '/etc/'), + new Space(new Range(6, 7, 6, 8)), + new Word(new Range(6, 8, 6, 8 + 12), 'path/file.md'), + new RightParenthesis(new Range(6, 20, 6, 21)), + ], + ); + }); + } + }); + + /** + * Same as above but these stop characters do not move the caret to the next line. + */ + suite('stop characters inside caption/reference (same line)', () => { + for (const stopCharacter of [VerticalTab, FormFeed]) { + let characterName = ''; + + if (stopCharacter === VerticalTab) { + characterName = '\\v'; + } + if (stopCharacter === FormFeed) { + characterName = '\\f'; + } + + assert( + characterName !== '', + 'The "characterName" must be set, got "empty line".', + ); + + test(`stop character - "${characterName}"`, async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + const inputLines = [ + // stop character inside link caption + `[haa${stopCharacter.symbol}loů](./real/💁/name.txt)`, + // stop character inside link reference + `[ref text](/etc/pat${stopCharacter.symbol}h/to/file.md)`, + // stop character between line caption and link reference is disallowed + `[text]${stopCharacter.symbol}(/etc/ path/file.md)`, + ]; + + + await test.run( + inputLines, + [ + // `1st` input line + new LeftBracket(new Range(1, 1, 1, 2)), + new Word(new Range(1, 2, 1, 2 + 3), 'haa'), + new stopCharacter(new Range(1, 5, 1, 6)), // <- stop character + new Word(new Range(1, 6, 1, 6 + 3), 'loů'), + new RightBracket(new Range(1, 9, 1, 10)), + new LeftParenthesis(new Range(1, 10, 1, 11)), + new Word(new Range(1, 11, 1, 11 + 18), './real/💁/name.txt'), + new RightParenthesis(new Range(1, 29, 1, 30)), + new NewLine(new Range(1, 30, 1, 31)), + // `2nd` input line + new LeftBracket(new Range(2, 1, 2, 2)), + new Word(new Range(2, 2, 2, 2 + 3), 'ref'), + new Space(new Range(2, 5, 2, 6)), + new Word(new Range(2, 6, 2, 6 + 4), 'text'), + new RightBracket(new Range(2, 10, 2, 11)), + new LeftParenthesis(new Range(2, 11, 2, 12)), + new Word(new Range(2, 12, 2, 12 + 8), '/etc/pat'), + new stopCharacter(new Range(2, 20, 2, 21)), // <- stop character + new Word(new Range(2, 21, 2, 21 + 12), 'h/to/file.md'), + new RightParenthesis(new Range(2, 33, 2, 34)), + new NewLine(new Range(2, 34, 2, 35)), + // `3nd` input line + new LeftBracket(new Range(3, 1, 3, 2)), + new Word(new Range(3, 2, 3, 2 + 4), 'text'), + new RightBracket(new Range(3, 6, 3, 7)), + new stopCharacter(new Range(3, 7, 3, 8)), // <- stop character + new LeftParenthesis(new Range(3, 8, 3, 9)), + new Word(new Range(3, 9, 3, 9 + 5), '/etc/'), + new Space(new Range(3, 14, 3, 15)), + new Word(new Range(3, 15, 3, 15 + 12), 'path/file.md'), + new RightParenthesis(new Range(3, 27, 3, 28)), + ], + ); + }); + } + }); + }); +}); diff --git a/src/vs/editor/test/common/codecs/simpleDecoder.test.ts b/src/vs/editor/test/common/codecs/simpleDecoder.test.ts index 2e57a1c8219b..b0804a2fe5fa 100644 --- a/src/vs/editor/test/common/codecs/simpleDecoder.test.ts +++ b/src/vs/editor/test/common/codecs/simpleDecoder.test.ts @@ -8,6 +8,7 @@ import { Range } from '../../../common/core/range.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { newWriteableStream } from '../../../../base/common/stream.js'; import { Tab } from '../../../common/codecs/simpleCodec/tokens/tab.js'; +import { Hash } from '../../../common/codecs/simpleCodec/tokens/hash.js'; import { Word } from '../../../common/codecs/simpleCodec/tokens/word.js'; import { Space } from '../../../common/codecs/simpleCodec/tokens/space.js'; import { NewLine } from '../../../common/codecs/linesCodec/tokens/newLine.js'; @@ -16,6 +17,8 @@ import { VerticalTab } from '../../../common/codecs/simpleCodec/tokens/verticalT import { CarriageReturn } from '../../../common/codecs/linesCodec/tokens/carriageReturn.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { SimpleDecoder, TSimpleToken } from '../../../common/codecs/simpleCodec/simpleDecoder.js'; +import { LeftBracket, RightBracket } from '../../../common/codecs/simpleCodec/tokens/brackets.js'; +import { LeftParenthesis, RightParenthesis } from '../../../common/codecs/simpleCodec/tokens/parentheses.js'; /** * A reusable test utility that asserts that a `SimpleDecoder` instance @@ -57,7 +60,7 @@ suite('SimpleDecoder', () => { ); await test.run( - ' hello world\nhow are\t you?\v\n\n (test) [!@#$%^&*_+=]\f \n\t\t🤗❤ \t\n hey\vthere\r\n\r\n', + ' hello world\nhow are\t you?\v\n\n (test) [!@#$%^🦄&*_+=]\f \n\t\t🤗❤ \t\n hey\vthere\r\n\r\n', [ // first line new Space(new Range(1, 1, 1, 2)), @@ -80,14 +83,20 @@ suite('SimpleDecoder', () => { new Space(new Range(4, 1, 4, 2)), new Space(new Range(4, 2, 4, 3)), new Space(new Range(4, 3, 4, 4)), - new Word(new Range(4, 4, 4, 10), '(test)'), + new LeftParenthesis(new Range(4, 4, 4, 5)), + new Word(new Range(4, 5, 4, 5 + 4), 'test'), + new RightParenthesis(new Range(4, 9, 4, 10)), new Space(new Range(4, 10, 4, 11)), new Space(new Range(4, 11, 4, 12)), - new Word(new Range(4, 12, 4, 25), '[!@#$%^&*_+=]'), - new FormFeed(new Range(4, 25, 4, 26)), - new Space(new Range(4, 26, 4, 27)), - new Space(new Range(4, 27, 4, 28)), - new NewLine(new Range(4, 28, 4, 29)), + new LeftBracket(new Range(4, 12, 4, 13)), + new Word(new Range(4, 13, 4, 13 + 2), '!@'), + new Hash(new Range(4, 15, 4, 16)), + new Word(new Range(4, 16, 4, 16 + 10), '$%^🦄&*_+='), + new RightBracket(new Range(4, 26, 4, 27)), + new FormFeed(new Range(4, 27, 4, 28)), + new Space(new Range(4, 28, 4, 29)), + new Space(new Range(4, 29, 4, 30)), + new NewLine(new Range(4, 30, 4, 31)), // fifth line new Tab(new Range(5, 1, 5, 2)), new Tab(new Range(5, 2, 5, 3)), diff --git a/src/vs/editor/test/common/utils/testDecoder.ts b/src/vs/editor/test/common/utils/testDecoder.ts index e9ee9ce1067e..bd0a7a07e6cc 100644 --- a/src/vs/editor/test/common/utils/testDecoder.ts +++ b/src/vs/editor/test/common/utils/testDecoder.ts @@ -7,6 +7,7 @@ import assert from 'assert'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { randomInt } from '../../../../base/common/numbers.js'; import { BaseToken } from '../../../common/codecs/baseToken.js'; +import { assertDefined } from '../../../../base/common/types.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { WriteableStream } from '../../../../base/common/stream.js'; import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; @@ -61,9 +62,15 @@ export class TestDecoder> extends * that the decoder produces the `expectedTokens` sequence of tokens. */ public async run( - inputData: string, + inputData: string | string[], expectedTokens: readonly T[], ): Promise { + // if input data was passed as an array of lines, + // join them into a single string with newlines + if (Array.isArray(inputData)) { + inputData = inputData.join('\n'); + } + // write the data to the stream after a short delay to ensure // that the the data is sent after the reading loop below setTimeout(() => { @@ -100,6 +107,11 @@ export class TestDecoder> extends const expectedToken = expectedTokens[i]; const receivedtoken = receivedTokens[i]; + assertDefined( + receivedtoken, + `Expected token '${i}' to be '${expectedToken}', got 'undefined'.`, + ); + assert( receivedtoken.equals(expectedToken), `Expected token '${i}' to be '${expectedToken}', got '${receivedtoken}'.`, diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index f4a6d525ce8e..8a557e382a92 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -140,7 +140,7 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC this.widget.refreshParsedInput(); // if the `prompt snippets` feature is enabled, and file is a `prompt snippet`, - // start resolving nested file references immediatelly and subscribe to updates + // start resolving nested file references immediately and subscribe to updates if (variable instanceof ChatFileReference && variable.isPromptSnippetFile) { // subscribe to variable changes variable.onUpdate(() => { diff --git a/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/chatPromptDecoder.ts b/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/chatPromptDecoder.ts index 57b7f0955b82..06d8e1620ec6 100644 --- a/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/chatPromptDecoder.ts +++ b/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/chatPromptDecoder.ts @@ -5,41 +5,258 @@ import { FileReference } from './tokens/fileReference.js'; import { VSBuffer } from '../../../../../../base/common/buffer.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; import { ReadableStream } from '../../../../../../base/common/stream.js'; import { BaseDecoder } from '../../../../../../base/common/codecs/baseDecoder.js'; +import { Tab } from '../../../../../../editor/common/codecs/simpleCodec/tokens/tab.js'; import { Word } from '../../../../../../editor/common/codecs/simpleCodec/tokens/word.js'; -import { SimpleDecoder, TSimpleToken } from '../../../../../../editor/common/codecs/simpleCodec/simpleDecoder.js'; +import { Hash } from '../../../../../../editor/common/codecs/simpleCodec/tokens/hash.js'; +import { Space } from '../../../../../../editor/common/codecs/simpleCodec/tokens/space.js'; +import { Colon } from '../../../../../../editor/common/codecs/simpleCodec/tokens/colon.js'; +import { NewLine } from '../../../../../../editor/common/codecs/linesCodec/tokens/newLine.js'; +import { FormFeed } from '../../../../../../editor/common/codecs/simpleCodec/tokens/formFeed.js'; +import { VerticalTab } from '../../../../../../editor/common/codecs/simpleCodec/tokens/verticalTab.js'; +import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; +import { CarriageReturn } from '../../../../../../editor/common/codecs/linesCodec/tokens/carriageReturn.js'; +import { ParserBase, TAcceptTokenResult } from '../../../../../../editor/common/codecs/simpleCodec/parserBase.js'; +import { MarkdownDecoder, TMarkdownToken } from '../../../../../../editor/common/codecs/markdownCodec/markdownDecoder.js'; /** - * Tokens handled by the `ChatPromptDecoder` decoder. + * Tokens produced by this decoder. */ -export type TChatPromptToken = FileReference; +export type TChatPromptToken = MarkdownLink | FileReference; + +/** + * The Parser responsible for processing a `prompt variable name` syntax from + * a sequence of tokens (e.g., `#variable:`). + * + * The parsing process starts with single `#` token, then can accept `file` word, + * followed by the `:` token, resulting in the tokens sequence equivalent to + * the `#file:` text sequence. In this successful case, the parser transitions into + * the {@linkcode PartialPromptFileReference} parser to continue the parsing process. + */ +class PartialPromptVariableName extends ParserBase { + constructor(token: Hash) { + super([token]); + } + + public accept(token: TMarkdownToken): TAcceptTokenResult { + // given we currently hold the `#` token, if we receive a `file` word, + // we can successfully proceed to the next token in the sequence + if (token instanceof Word) { + if (token.text === 'file') { + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + // if we receive the `:` token, we can successfully proceed to the next + // token in the sequence `only if` the previous token was a `file` word + // therefore for currently tokens sequence equivalent to the `#file` text + if (token instanceof Colon) { + const lastToken = this.currentTokens[this.currentTokens.length - 1]; + + if (lastToken instanceof Word) { + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: new PartialPromptFileReference(this.currentTokens), + wasTokenConsumed: true, + }; + } + } + + // all other cases are failures and we don't consume the offending token + return { + result: 'failure', + wasTokenConsumed: false, + }; + } +} + +/** + * List of characters that stop a prompt variable sequence. + */ +const PROMPT_FILE_REFERENCE_STOP_CHARACTERS: readonly string[] = [Space, Tab, CarriageReturn, NewLine, VerticalTab, FormFeed] + .map((token) => { return token.symbol; }); + +/** + * Parser responsible for processing the `file reference` syntax part from + * a sequence of tokens (e.g., #variable:`./some/file/path.md`). + * + * The parsing process starts with the sequence of `#`, `file`, and `:` tokens, + * then can accept a sequence of tokens until one of the tokens defined in + * the {@linkcode PROMPT_FILE_REFERENCE_STOP_CHARACTERS} list is encountered. + * This sequence of tokens is treated as a `file path` part of the `#file:` variable, + * and in the successful case, the parser transitions into the {@linkcode FileReference} + * token which signifies the end of the file reference text parsing process. + */ +class PartialPromptFileReference extends ParserBase { + /** + * Set of tokens that were accumulated so far. + */ + private readonly fileReferenceTokens: (Hash | Word | Colon)[]; + + constructor(tokens: (Hash | Word | Colon)[]) { + super([]); + + this.fileReferenceTokens = tokens; + } + + /** + * List of tokens that were accumulated so far. + */ + public override get tokens(): readonly (Hash | Word | Colon)[] { + return [...this.fileReferenceTokens, ...this.currentTokens]; + } + + /** + * Return the `FileReference` instance created from the current object. + */ + public asFileReference(): FileReference { + // use only tokens in the `currentTokens` list to + // create the path component of the file reference + const path = this.currentTokens + .map((token) => { return token.text; }) + .join(''); + + const firstToken = this.tokens[0]; + + const range = new Range( + firstToken.range.startLineNumber, + firstToken.range.startColumn, + firstToken.range.startLineNumber, + firstToken.range.startColumn + FileReference.TOKEN_START.length + path.length, + ); + + return new FileReference(range, path); + } + + public accept(token: TMarkdownToken): TAcceptTokenResult { + // any of stop characters is are breaking a prompt variable sequence + if (PROMPT_FILE_REFERENCE_STOP_CHARACTERS.includes(token.text)) { + return { + result: 'success', + wasTokenConsumed: false, + nextParser: this.asFileReference(), + }; + } + + // any other token can be included in the sequence so accumulate + // it and continue with using the current parser instance + this.currentTokens.push(token); + return { + result: 'success', + wasTokenConsumed: true, + nextParser: this, + }; + } +} /** * Decoder for the common chatbot prompt message syntax. * For instance, the file references `#file:./path/file.md` are handled by this decoder. */ -export class ChatPromptDecoder extends BaseDecoder { +export class ChatPromptDecoder extends BaseDecoder { + /** + * Currently active parser object that is used to parse a well-known equence of + * tokens, for instance, a `file reference` that consists of `hash`, `word`, and + * `colon` tokens sequence plus following file path part. + */ + private current?: PartialPromptVariableName; + constructor( stream: ReadableStream, ) { - super(new SimpleDecoder(stream)); + super(new MarkdownDecoder(stream)); } - protected override onStreamData(simpleToken: TSimpleToken): void { - // handle the word tokens only - if (!(simpleToken instanceof Word)) { + protected override onStreamData(token: TMarkdownToken): void { + // prompt variables always start with the `#` character, hence + // initiate a parser object if we encounter respective token and + /// there is no active parser object present at the moment + if (token instanceof Hash && !this.current) { + this.current = new PartialPromptVariableName(token); + return; } - // handle file references only for now - const { text } = simpleToken; - if (!text.startsWith(FileReference.TOKEN_START)) { + // if current parser was not yet initiated, - we are in the general + // "text" mode, therefore re-emit the token immediately and return + if (!this.current) { + // at the moment, the decoder outputs only specific markdown tokens, like + // the `markdown link` one, so re-emit only these tokens ignoring the rest + // + // note! to make the decoder consistent with others we would need to: + // - re-emit all tokens here + // - collect all "text" sequences of tokens and emit them as a single + // "text" sequence token + if (token instanceof MarkdownLink) { + this._onData.fire(token); + } + return; } - this._onData.fire( - FileReference.fromWord(simpleToken), - ); + // if there is a current parser object, submit the token to it + // so it can progress with parsing the tokens sequence + const parseResult = this.current.accept(token); + + // process the parse result next + switch (parseResult.result) { + // in the case of success there might be 2 cases: + // 1) parsing fully completed and an parsed entity is returned back, in this case, + // emit the parsed token (e.g., a `link`) and reset current parser object + // 2) parsing is still in progress and the next parser object is returned, hence + // we need to update the current praser object with a new one and continue + case 'success': { + if (parseResult.nextParser instanceof FileReference) { + this._onData.fire(parseResult.nextParser); + delete this.current; + } else { + this.current = parseResult.nextParser; + } + + break; + } + // in the case of failure, reset the current parser object + case 'failure': { + delete this.current; + + // note! when this decoder becomes consistent with other ones and hence starts emitting + // all token types, not just links, we would need to re-emit all the tokens that + // the parser object has accumulated so far + break; + } + } + + // if token was not consumed by the parser, call `onStreamData` again + // so the token is properly handled by the decoder in the case when a + // new sequence starts with this token + if (!parseResult.wasTokenConsumed) { + this.onStreamData(token); + } + } + + protected override onStreamEnd(): void { + // if the stream has ended and there is a current `PartialPromptFileReference` + // parser object, then the file reference was terminated by the end of the stream + if (this.current && this.current instanceof PartialPromptFileReference) { + this._onData.fire(this.current.asFileReference()); + delete this.current; + } + + super.onStreamEnd(); } } diff --git a/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/tokens/fileReference.ts b/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/tokens/fileReference.ts index 5a68344a7575..224e0086851c 100644 --- a/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/tokens/fileReference.ts +++ b/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/tokens/fileReference.ts @@ -8,14 +8,18 @@ import { Range } from '../../../../../../../editor/common/core/range.js'; import { BaseToken } from '../../../../../../../editor/common/codecs/baseToken.js'; import { Word } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/word.js'; -// Start sequence for a file reference token in a prompt. +/** + * Start sequence for a file reference token in a prompt. + */ const TOKEN_START: string = '#file:'; /** * Object represents a file reference token inside a chatbot prompt. */ export class FileReference extends BaseToken { - // Start sequence for a file reference token in a prompt. + /** + * Start sequence for a file reference token in a prompt. + */ public static readonly TOKEN_START = TOKEN_START; constructor( diff --git a/src/vs/workbench/contrib/chat/test/common/codecs/chatPromptDecoder.test.ts b/src/vs/workbench/contrib/chat/test/common/codecs/chatPromptDecoder.test.ts index 2ebadb798f18..56f4e30059cd 100644 --- a/src/vs/workbench/contrib/chat/test/common/codecs/chatPromptDecoder.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/codecs/chatPromptDecoder.test.ts @@ -9,6 +9,7 @@ import { newWriteableStream } from '../../../../../../base/common/stream.js'; import { TestDecoder } from '../../../../../../editor/test/common/utils/testDecoder.js'; import { FileReference } from '../../../common/codecs/chatPromptCodec/tokens/fileReference.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; import { ChatPromptDecoder, TChatPromptToken } from '../../../common/codecs/chatPromptCodec/chatPromptDecoder.js'; /** @@ -50,23 +51,39 @@ suite('ChatPromptDecoder', () => { new TestChatPromptDecoder(), ); + const contents = [ + '', + 'haalo!', + ' message 👾 message #file:./path/to/file1.md', + '', + '## Heading Title', + ' \t#file:a/b/c/filename2.md\t🖖\t#file:other-file.md', + ' [#file:reference.md](./reference.md)some text #file:/some/file/with/absolute/path.md', + ]; + await test.run( - '\nhaalo!\n message 👾 message #file:./path/to/file1.md \n\n \t#file:a/b/c/filename2.md\t🖖\t#file:other-file.md\nsome text #file:/some/file/with/absolute/path.md\t', + contents, [ new FileReference( new Range(3, 21, 3, 21 + 24), './path/to/file1.md', ), new FileReference( - new Range(5, 3, 5, 3 + 24), + new Range(6, 3, 6, 3 + 24), 'a/b/c/filename2.md', ), new FileReference( - new Range(5, 31, 5, 31 + 19), + new Range(6, 31, 6, 31 + 19), 'other-file.md', ), + new MarkdownLink( + 7, + 2, + '[#file:reference.md]', + '(./reference.md)', + ), new FileReference( - new Range(6, 11, 6, 11 + 38), + new Range(7, 48, 7, 48 + 38), '/some/file/with/absolute/path.md', ), ], diff --git a/src/vs/workbench/contrib/chat/test/common/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptFileReference.test.ts index 4a3b710e8ceb..e281ff697fc2 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptFileReference.test.ts @@ -118,8 +118,7 @@ class TestPromptFileReference extends Disposable { )); // resolve the root file reference including all nested references - const resolvedReferences = (await rootReference.resolve(true)) - .flatten(); + const resolvedReferences = (await rootReference.resolve(true)).flatten(); assert.strictEqual( resolvedReferences.length, @@ -237,7 +236,7 @@ suite('PromptFileReference (Unix)', function () { }, { name: 'file2.prompt.md', - contents: '## Files\n\t- this file #file:folder1/file3.prompt.md \n\t- also this #file:./folder1/some-other-folder/file4.prompt.md please!\n ', + contents: '## Files\n\t- this file #file:folder1/file3.prompt.md \n\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!\n ', }, { name: 'folder1', @@ -262,7 +261,7 @@ suite('PromptFileReference (Unix)', function () { children: [ { name: 'another-file.prompt.md', - contents: 'another-file.prompt.md contents\t #file:../file.txt', + contents: 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)', }, { name: 'one_more_file_just_in_case.prompt.md', @@ -353,14 +352,14 @@ suite('PromptFileReference (Unix)', function () { }, { name: 'file2.prompt.md', - contents: `## Files\n\t- this file #file:folder1/file3.prompt.md \n\t- also this #file:./folder1/some-other-folder/file4.prompt.md\n\n#file:${rootFolder}/folder1/some-other-folder/file5.prompt.md\t please!\n\t#file:./file1.md `, + contents: `## Files\n\t- this file #file:folder1/file3.prompt.md \n\t- also this #file:./folder1/some-other-folder/file4.prompt.md\n\n#file:${rootFolder}/folder1/some-other-folder/file5.prompt.md\t please!\n\t[some (snippet!) #name))](./file1.md)`, }, { name: 'folder1', 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\n\t- some seemingly random [another-file.prompt.md](${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md) contents\n some more\t content`, }, { name: 'some-other-folder', From 9409b89b3c943759748564ee2004026139e0b2e6 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 10 Jan 2025 21:59:19 -0800 Subject: [PATCH 0494/3587] chat: fix working set readonly state being lost after sending (#237702) --- .../contrib/chat/browser/chatEditing/chatEditingSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 6bb25683e106..4a1a7d6bf893 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -284,7 +284,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio if (requestId) { for (const [uri, data] of this._workingSet) { if (data.state !== WorkingSetEntryState.Suggested) { - this._workingSet.set(uri, { state: WorkingSetEntryState.Sent }); + this._workingSet.set(uri, { state: WorkingSetEntryState.Sent, isMarkedReadonly: data.isMarkedReadonly }); } } const linearHistory = this._linearHistory.get(); From 37249cba4aaffc7ea0ec2a9e7c2ae2ccb595a485 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 11 Jan 2025 08:31:51 +0100 Subject: [PATCH 0495/3587] edits - better fix #236984 (#237685) Restore the semantics of how it used to be in before the change in https://github.com/microsoft/vscode/pull/144890 --- .../contrib/bulkEdit/browser/bulkFileEdits.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index 858d20cd458e..ce82d4983b77 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -255,15 +255,18 @@ class DeleteOperation implements IFileOperation { // read file contents for undo operation. when a file is too large it won't be restored let fileContent: IFileContent | undefined; - const isSizeLimitExceeded = typeof edit.options.maxSize === 'number' && fileStat.size > edit.options.maxSize; - if (!edit.undoesCreate && !edit.options.folder && !isSizeLimitExceeded) { - try { - fileContent = await this._fileService.readFile(edit.oldUri); - } catch (err) { - this._logService.error(err); + let fileContentExceedsMaxSize = false; + if (!edit.undoesCreate && !edit.options.folder) { + fileContentExceedsMaxSize = typeof edit.options.maxSize === 'number' && fileStat.size > edit.options.maxSize; + if (!fileContentExceedsMaxSize) { + try { + fileContent = await this._fileService.readFile(edit.oldUri); + } catch (err) { + this._logService.error(err); + } } } - if (!fileContent || !isSizeLimitExceeded) { + if (!fileContentExceedsMaxSize) { undoes.push(new CreateEdit(edit.oldUri, edit.options, fileContent?.value)); } } From 0a353173315b5e3b3e063f40c625b0a686e88fee Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 12 Jan 2025 07:48:27 +0100 Subject: [PATCH 0496/3587] Add chat setup view for all users out of the box to secondary sidebar (fix microsoft/vscode-copilot#11195) --- .../browser/chatParticipant.contribution.ts | 2 +- .../contrib/chat/browser/chatSetup.ts | 22 +++++++++---------- .../contrib/chat/common/chatContextKeys.ts | 6 ++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts index 6949c7318038..04c182bff6b2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts @@ -312,7 +312,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { }, ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Panel }]), when: ContextKeyExpr.or( - ChatContextKeys.Setup.triggered, + ChatContextKeys.Setup.hidden.negate(), ChatContextKeys.Setup.installed, ChatContextKeys.panelParticipantRegistered, ChatContextKeys.extensionInvalid diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 0ad1f26e379b..aa4a94686c4b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -169,7 +169,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr const configurationService = accessor.get(IConfigurationService); const layoutService = accessor.get(IWorkbenchLayoutService); - await that.context.update({ triggered: true }); + await that.context.update({ hidden: false }); showCopilotView(viewsService, layoutService); ensureSideBarChatViewSize(400, viewDescriptorService, layoutService); @@ -282,7 +282,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr async function hideSetupView(viewsDescriptorService: IViewDescriptorService, layoutService: IWorkbenchLayoutService): Promise { const location = viewsDescriptorService.getViewLocationById(ChatViewId); - await that.context.update({ triggered: false }); + await that.context.update({ hidden: true }); if (location === ViewContainerLocation.AuxiliaryBar) { const activeContainers = viewsDescriptorService.getViewContainersByLocation(location).filter(container => viewsDescriptorService.getViewContainerModel(container).activeViewDescriptors.length > 0); @@ -1020,7 +1020,7 @@ class ChatSetupWelcomeContent extends Disposable { interface IChatSetupContextState { entitlement: ChatEntitlement; - triggered?: boolean; + hidden?: boolean; installed?: boolean; registered?: boolean; } @@ -1033,7 +1033,7 @@ class ChatSetupContext extends Disposable { private readonly signedOutContextKey = ChatContextKeys.Setup.signedOut.bindTo(this.contextKeyService); private readonly limitedContextKey = ChatContextKeys.Setup.limited.bindTo(this.contextKeyService); private readonly proContextKey = ChatContextKeys.Setup.pro.bindTo(this.contextKeyService); - private readonly triggeredContext = ChatContextKeys.Setup.triggered.bindTo(this.contextKeyService); + private readonly hiddenContext = ChatContextKeys.Setup.hidden.bindTo(this.contextKeyService); private readonly installedContext = ChatContextKeys.Setup.installed.bindTo(this.contextKeyService); private _state: IChatSetupContextState = this.storageService.getObject(ChatSetupContext.CHAT_SETUP_CONTEXT_STORAGE_KEY, StorageScope.PROFILE) ?? { entitlement: ChatEntitlement.Unknown }; @@ -1078,21 +1078,21 @@ class ChatSetupContext extends Disposable { } update(context: { installed: boolean }): Promise; - update(context: { triggered: boolean }): Promise; + update(context: { hidden: boolean }): Promise; update(context: { entitlement: ChatEntitlement }): Promise; - update(context: { installed?: boolean; triggered?: boolean; entitlement?: ChatEntitlement }): Promise { + update(context: { installed?: boolean; hidden?: boolean; entitlement?: ChatEntitlement }): Promise { this.logService.trace(`[chat setup] update(): ${JSON.stringify(context)}`); if (typeof context.installed === 'boolean') { this._state.installed = context.installed; if (context.installed) { - context.triggered = true; // allows to fallback to setup view if the extension is uninstalled + context.hidden = false; // allows to fallback to setup view if the extension is uninstalled } } - if (typeof context.triggered === 'boolean') { - this._state.triggered = context.triggered; + if (typeof context.hidden === 'boolean') { + this._state.hidden = context.hidden; } if (typeof context.entitlement === 'number') { @@ -1119,7 +1119,7 @@ class ChatSetupContext extends Disposable { private updateContextSync(): void { this.logService.trace(`[chat setup] updateContext(): ${JSON.stringify(this._state)}`); - if (this._state.triggered && !this._state.installed) { + if (!this._state.hidden && !this._state.installed) { // this is ugly but fixes flicker from a previous chat install this.storageService.remove('chat.welcomeMessageContent.panel', StorageScope.APPLICATION); this.storageService.remove('interactive.sessions', this.workspaceContextService.getWorkspace().folders.length ? StorageScope.WORKSPACE : StorageScope.APPLICATION); @@ -1129,7 +1129,7 @@ class ChatSetupContext extends Disposable { this.canSignUpContextKey.set(this._state.entitlement === ChatEntitlement.Available); this.limitedContextKey.set(this._state.entitlement === ChatEntitlement.Limited); this.proContextKey.set(this._state.entitlement === ChatEntitlement.Pro); - this.triggeredContext.set(!!this._state.triggered); + this.hiddenContext.set(!!this._state.hidden); this.installedContext.set(!!this._state.installed); this._onDidChange.fire(); diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index f7553fe66543..1b52263721a1 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -51,7 +51,7 @@ export namespace ChatContextKeys { // State signedOut: new RawContextKey('chatSetupSignedOut', false, true), // True when user is signed out. - triggered: new RawContextKey('chatSetupTriggered', false, true), // True when chat setup is triggered. + hidden: new RawContextKey('chatSetupHidden', false, true), // True when chat setup is explicitly hidden. installed: new RawContextKey('chatSetupInstalled', false, true), // True when the chat extension is installed. // Plans @@ -60,10 +60,10 @@ export namespace ChatContextKeys { pro: new RawContextKey('chatPlanPro', false, true) // True when user is a chat pro user. }; - export const SetupViewKeys = new Set([ChatContextKeys.Setup.triggered.key, ChatContextKeys.Setup.installed.key, ChatContextKeys.Setup.signedOut.key, ChatContextKeys.Setup.canSignUp.key]); + export const SetupViewKeys = new Set([ChatContextKeys.Setup.hidden.key, ChatContextKeys.Setup.installed.key, ChatContextKeys.Setup.signedOut.key, ChatContextKeys.Setup.canSignUp.key]); export const SetupViewCondition = ContextKeyExpr.or( ContextKeyExpr.and( - ChatContextKeys.Setup.triggered, + ChatContextKeys.Setup.hidden.negate(), ChatContextKeys.Setup.installed.negate() ), ContextKeyExpr.and( From 259bfbadc13a785d53c3eea1b6e64ea6c179eada Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 12 Jan 2025 11:50:23 +0100 Subject: [PATCH 0497/3587] setup - tweak default window dimensions to better account for secondary sidebar --- .../auxiliaryWindowsMainService.ts | 2 +- .../platform/window/electron-main/window.ts | 8 +++---- .../electron-main/windowsStateHandler.test.ts | 6 +++--- .../browser/actions/chatGettingStarted.ts | 2 +- src/vs/workbench/contrib/chat/browser/chat.ts | 6 +++--- .../contrib/chat/browser/chatSetup.ts | 21 +++++++++++-------- .../chat/browser/media/chatViewSetup.css | 2 ++ .../browser/auxiliaryWindowService.ts | 2 +- 8 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts index ea7abedf6f02..3ef1cb281086 100644 --- a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts +++ b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts @@ -98,7 +98,7 @@ export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliar const windowState: IWindowState = {}; const overrides: IDefaultBrowserWindowOptionsOverrides = {}; - const features = details.features.split(','); // for example: popup=yes,left=270,top=14.5,width=800,height=600 + const features = details.features.split(','); // for example: popup=yes,left=270,top=14.5,width=1024,height=768 for (const feature of features) { const [key, value] = feature.split('='); switch (key) { diff --git a/src/vs/platform/window/electron-main/window.ts b/src/vs/platform/window/electron-main/window.ts index 91a2eb1339a8..7f2413d6370b 100644 --- a/src/vs/platform/window/electron-main/window.ts +++ b/src/vs/platform/window/electron-main/window.ts @@ -139,8 +139,8 @@ export interface IWindowState { export const defaultWindowState = function (mode = WindowMode.Normal): IWindowState { return { - width: 1024, - height: 768, + width: 1200, + height: 800, mode }; }; @@ -154,8 +154,8 @@ export const defaultAuxWindowState = function (): IWindowState { // we need to set not only width and height but also x and y to // a good location on the primary display. - const width = 800; - const height = 600; + const width = 1024; + const height = 768; const workArea = electron.screen.getPrimaryDisplay().workArea; const x = Math.max(workArea.x + (workArea.width / 2) - (width / 2), 0); const y = Math.max(workArea.y + (workArea.height / 2) - (height / 2), 0); diff --git a/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts index a1ed6bd27947..68bcfe4e00f6 100644 --- a/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts @@ -183,8 +183,8 @@ suite('Windows State Storing', () => { "mode": 1, "x": 768, "y": 336, - "width": 1024, - "height": 768 + "width": 1200, + "height": 800 } } }`; @@ -194,7 +194,7 @@ suite('Windows State Storing', () => { openedWindows: [], lastActiveWindow: { backupPath: '/home/user/.config/code-oss-dev/Backups/1549539668998', - uiState: { mode: WindowMode.Normal, x: 768, y: 336, width: 1024, height: 768 } + uiState: { mode: WindowMode.Normal, x: 768, y: 336, width: 1200, height: 800 } } }; assertEqualWindowsState(expected, windowsState, 'v1_32_empty_window'); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts b/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts index ea35e44e0169..9b7e8b707d56 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts @@ -77,7 +77,7 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb // Open and configure chat view await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID); - ensureSideBarChatViewSize(400, this.viewDescriptorService, this.layoutService); + ensureSideBarChatViewSize(this.viewDescriptorService, this.layoutService); // Only do this once this.storageService.store(ChatGettingStartedContribution.hideWelcomeView, true, StorageScope.APPLICATION, StorageTarget.MACHINE); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 43d623df320d..15318bd0e457 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -52,7 +52,7 @@ export async function showEditsView(viewsService: IViewsService): Promise(EditsViewId))?.widget; } -export function ensureSideBarChatViewSize(width: number, viewDescriptorService: IViewDescriptorService, layoutService: IWorkbenchLayoutService): void { +export function ensureSideBarChatViewSize(viewDescriptorService: IViewDescriptorService, layoutService: IWorkbenchLayoutService): void { const location = viewDescriptorService.getViewLocationById(ChatViewId); if (location === ViewContainerLocation.Panel) { return; // panel is typically very wide @@ -60,8 +60,8 @@ export function ensureSideBarChatViewSize(width: number, viewDescriptorService: const viewPart = location === ViewContainerLocation.Sidebar ? Parts.SIDEBAR_PART : Parts.AUXILIARYBAR_PART; const partSize = layoutService.getSize(viewPart); - if (partSize.width < width) { - layoutService.setSize(viewPart, { width: width, height: partSize.height }); + if (partSize.width < 300) { + layoutService.setSize(viewPart, { width: 300, height: partSize.height }); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index aa4a94686c4b..99154d7af917 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -172,7 +172,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr await that.context.update({ hidden: false }); showCopilotView(viewsService, layoutService); - ensureSideBarChatViewSize(400, viewDescriptorService, layoutService); + ensureSideBarChatViewSize(viewDescriptorService, layoutService); if (startSetup === true) { that.controller.value.setup(); @@ -911,26 +911,29 @@ class ChatSetupWelcomeContent extends Disposable { const header = localize({ key: 'header', comment: ['{Locked="[Copilot]({0})"}'] }, "[Copilot]({0}) is your AI pair programmer.", this.context.state.installed ? 'command:github.copilot.open.walkthrough' : defaultChat.documentationUrl); this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(header, { isTrusted: true }))).element); - const features = this.element.appendChild($('div.chat-features-container')); - this.element.appendChild(features); + const featuresParent = this.element.appendChild($('div.chat-features-container')); + this.element.appendChild(featuresParent); - const featureChatContainer = features.appendChild($('div.chat-feature-container')); + const featuresContainer = this.element.appendChild($('div')); + featuresParent.appendChild(featuresContainer); + + const featureChatContainer = featuresContainer.appendChild($('div.chat-feature-container')); featureChatContainer.appendChild(renderIcon(Codicon.code)); const featureChatLabel = featureChatContainer.appendChild($('span')); - featureChatLabel.textContent = localize('featureChat', "Code faster with completions and Inline Chat"); + featureChatLabel.textContent = localize('featureChat', "Code faster with Completions"); - const featureEditsContainer = features.appendChild($('div.chat-feature-container')); + const featureEditsContainer = featuresContainer.appendChild($('div.chat-feature-container')); featureEditsContainer.appendChild(renderIcon(Codicon.editSession)); const featureEditsLabel = featureEditsContainer.appendChild($('span')); - featureEditsLabel.textContent = localize('featureEdits', "Build features and resolve bugs with Copilot Edits"); + featureEditsLabel.textContent = localize('featureEdits', "Build features with Copilot Edits"); - const featureExploreContainer = features.appendChild($('div.chat-feature-container')); + const featureExploreContainer = featuresContainer.appendChild($('div.chat-feature-container')); featureExploreContainer.appendChild(renderIcon(Codicon.commentDiscussion)); const featureExploreLabel = featureExploreContainer.appendChild($('span')); - featureExploreLabel.textContent = localize('featureExplore', "Explore your codebase with chat"); + featureExploreLabel.textContent = localize('featureExplore', "Explore your codebase with Chat"); } // Limited SKU diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css b/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css index 8b3a81f428e7..e2d82625ecdb 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css @@ -11,6 +11,8 @@ } .chat-features-container { + display: flex; + justify-content: center; text-align: initial; border-radius: 2px; border: 1px solid var(--vscode-chat-requestBorder); diff --git a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts index ee504955a5a2..e716f2af8d8a 100644 --- a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts @@ -225,7 +225,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili declare readonly _serviceBrand: undefined; - private static readonly DEFAULT_SIZE = { width: 800, height: 600 }; + private static readonly DEFAULT_SIZE = { width: 1024, height: 768 }; private static WINDOW_IDS = getWindowId(mainWindow) + 1; // start from the main window ID + 1 From a2ea5144208a6f7a458452aff811263a97861264 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 12 Jan 2025 12:39:31 +0100 Subject: [PATCH 0498/3587] setup - restore preferred 400px width --- src/vs/workbench/contrib/chat/browser/chat.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 15318bd0e457..47540018a2de 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -60,8 +60,8 @@ export function ensureSideBarChatViewSize(viewDescriptorService: IViewDescriptor const viewPart = location === ViewContainerLocation.Sidebar ? Parts.SIDEBAR_PART : Parts.AUXILIARYBAR_PART; const partSize = layoutService.getSize(viewPart); - if (partSize.width < 300) { - layoutService.setSize(viewPart, { width: 300, height: partSize.height }); + if (partSize.width < 400) { + layoutService.setSize(viewPart, { width: 400, height: partSize.height }); } } From c9efcc2cd0958e959caeca40095b5aceeb192c4e Mon Sep 17 00:00:00 2001 From: Jules Bertholet Date: Sun, 12 Jan 2025 10:16:29 -0500 Subject: [PATCH 0499/3587] Support back and forward keys in default shortcuts (#237701) Support back and forward keys in default shortcuts These keys can be found on certain keyboards, and on custom layouts like [Extend](https://dreymar.colemak.org/layers-extend.html). --- .../workbench/browser/parts/editor/editorActions.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 9d9766f4eb50..fc9b8ec03e0e 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -1420,9 +1420,9 @@ export class NavigateForwardAction extends Action2 { precondition: ContextKeyExpr.has('canNavigateForward'), keybinding: { weight: KeybindingWeight.WorkbenchContrib, - win: { primary: KeyMod.Alt | KeyCode.RightArrow }, - mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Minus }, - linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Minus } + win: { primary: KeyMod.Alt | KeyCode.RightArrow, secondary: [KeyCode.BrowserForward] }, + mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Minus, secondary: [KeyCode.BrowserForward] }, + linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Minus, secondary: [KeyCode.BrowserForward] } }, menu: [ { id: MenuId.MenubarGoMenu, group: '1_history_nav', order: 2 }, @@ -1455,9 +1455,9 @@ export class NavigateBackwardsAction extends Action2 { icon: Codicon.arrowLeft, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - win: { primary: KeyMod.Alt | KeyCode.LeftArrow }, - mac: { primary: KeyMod.WinCtrl | KeyCode.Minus }, - linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Minus } + win: { primary: KeyMod.Alt | KeyCode.LeftArrow, secondary: [KeyCode.BrowserBack] }, + mac: { primary: KeyMod.WinCtrl | KeyCode.Minus, secondary: [KeyCode.BrowserBack] }, + linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Minus, secondary: [KeyCode.BrowserBack] } }, menu: [ { id: MenuId.MenubarGoMenu, group: '1_history_nav', order: 1 }, From d6fd2c075bad1d7dad1e60a75455e7415fd0181b Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Sun, 12 Jan 2025 19:51:10 +0100 Subject: [PATCH 0500/3587] Git - Add "Checkout" action to graph (#237734) --- extensions/git/package.json | 24 +++++++++++++++--------- extensions/git/src/commands.ts | 21 ++++++++++++++++++--- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 66e8dd4981d7..4c3ea11c81f3 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1997,24 +1997,24 @@ ], "scm/historyItem/context": [ { - "command": "git.createTag", + "command": "git.checkoutRefDetached", "when": "scmProvider == git", - "group": "1_create@1" + "group": "1_checkout@2" }, { - "command": "git.branch", + "command": "git.createTag", "when": "scmProvider == git", - "group": "1_create@2" + "group": "2_create@1" }, { - "command": "git.cherryPickRef", + "command": "git.branch", "when": "scmProvider == git", - "group": "2_modify@1" + "group": "2_create@2" }, { - "command": "git.checkoutRefDetached", + "command": "git.cherryPickRef", "when": "scmProvider == git", - "group": "2_modify@2" + "group": "3_modify@1" }, { "command": "git.copyCommitId", @@ -2027,7 +2027,13 @@ "group": "9_copy@2" } ], - "scm/historyItemRef/context": [], + "scm/historyItemRef/context": [ + { + "command": "git.checkoutRef", + "when": "scmProvider == git", + "group": "1_checkout@1" + } + ], "editor/title": [ { "command": "git.openFile", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index d3e852ceab86..46e3f116c21d 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -2545,13 +2545,28 @@ export class CommandCenter { } @command('git.checkoutRef', { repository: true }) - async checkoutRef(repository: Repository, historyItem?: SourceControlHistoryItem, historyItemRefId?: string): Promise { + async checkoutRef(repository: Repository, historyItem?: SourceControlHistoryItem, historyItemRefId?: string): Promise { const historyItemRef = historyItem?.references?.find(r => r.id === historyItemRefId); if (!historyItemRef) { - return false; + return; } - return this._checkout(repository, { treeish: historyItemRefId }); + const config = workspace.getConfiguration('git', Uri.file(repository.root)); + const pullBeforeCheckout = config.get('pullBeforeCheckout', false) === true; + + // Branch, tag + if (historyItemRef.id.startsWith('refs/heads/') || historyItemRef.id.startsWith('refs/tags/')) { + await repository.checkout(historyItemRef.name, { pullBeforeCheckout }); + return; + } + + // Remote branch + const branches = await repository.findTrackingBranches(historyItemRef.name); + if (branches.length > 0) { + await repository.checkout(branches[0].name!, { pullBeforeCheckout }); + } else { + await repository.checkoutTracking(historyItemRef.name); + } } @command('git.checkoutRefDetached', { repository: true }) From 5e1f4570bec320ea644743164cca7e1fa0bc45fa Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Sun, 12 Jan 2025 21:15:15 +0100 Subject: [PATCH 0501/3587] =?UTF-8?q?SCM=20-=20=F0=9F=92=84=20cleanup=20gr?= =?UTF-8?q?aph=20submenu/menu=20code=20(#237745)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contrib/scm/browser/scmHistoryViewPane.ts | 79 ++++++++++--------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index ad0a2c98f51d..039b2d3b68a7 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -1586,54 +1586,59 @@ export class SCMHistoryViewPane extends ViewPane { // If there are any history item references we have to add a submenu item for each orignal action, // and a menu item for each history item ref that matches the `when` clause of the original action. if (historyItemRefMenuItems.length > 0 && element.historyItemViewModel.historyItem.references?.length) { - const submenuIds = new Map(); + const historyItemRefActions = new Map(); for (const ref of element.historyItemViewModel.historyItem.references) { const contextKeyService = this.scopedContextKeyService.createOverlay([ ['scmHistoryItemRef', ref.id] ]); - for (const [, actions] of this._menuService.getMenuActions(MenuId.SCMHistoryItemRefContext, contextKeyService)) { - for (const action of actions) { - let subMenuId = submenuIds.get(action.id); + const menuActions = this._menuService.getMenuActions( + MenuId.SCMHistoryItemRefContext, contextKeyService); - if (!subMenuId) { - subMenuId = MenuId.for(action.id); + for (const action of menuActions.flatMap(a => a[1])) { + if (!historyItemRefActions.has(action.id)) { + historyItemRefActions.set(action.id, []); + } - // Get the menu item for the original action so that - // we can create a submenu with the same group, order - const historyItemRefMenuItem = historyItemRefMenuItems - .find(item => item.command.id === action.id); + historyItemRefActions.get(action.id)!.push(ref); + } + } - // Register the submenu for the original action - this._contextMenuDisposables.value.add(MenuRegistry.appendMenuItem(MenuId.SCMChangesContext, { - title: action.label, - submenu: subMenuId, - group: historyItemRefMenuItem?.group, - order: historyItemRefMenuItem?.order - })); + // Register submenu, menu items + for (const historyItemRefMenuItem of historyItemRefMenuItems) { + const actionId = historyItemRefMenuItem.command.id; - submenuIds.set(action.id, subMenuId); - } + if (!historyItemRefActions.has(actionId)) { + continue; + } - // Register a new action for the history item ref - this._contextMenuDisposables.value.add(registerAction2(class extends Action2 { - constructor() { - super({ - id: `${action.id}.${ref.id}`, - title: ref.name, - menu: { - id: subMenuId!, - group: ref.category - } - }); - } - override run(accessor: ServicesAccessor, ...args: any[]): void { - const commandService = accessor.get(ICommandService); - commandService.executeCommand(action.id, ...args, ref.id); - } - })); - } + // Register the submenu for the original action + this._contextMenuDisposables.value.add(MenuRegistry.appendMenuItem(MenuId.SCMChangesContext, { + title: historyItemRefMenuItem.command.title, + submenu: MenuId.for(actionId), + group: historyItemRefMenuItem?.group, + order: historyItemRefMenuItem?.order + })); + + // Register the action for the history item ref + for (const historyItemRef of historyItemRefActions.get(actionId) ?? []) { + this._contextMenuDisposables.value.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: `${actionId}.${historyItemRef.id}`, + title: historyItemRef.name, + menu: { + id: MenuId.for(actionId), + group: historyItemRef.category + } + }); + } + override run(accessor: ServicesAccessor, ...args: any[]): void { + const commandService = accessor.get(ICommandService); + commandService.executeCommand(actionId, ...args, historyItemRef.id); + } + })); } } } From aa838183e5244f6683e4cdd5bf0e6ff6d6fb9872 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sun, 12 Jan 2025 19:54:13 -0800 Subject: [PATCH 0502/3587] Tweaks to editFile tool (#237753) * Tweaks to editFile tool * Fix newlines in json --- .../contrib/chat/browser/tools/tools.ts | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/tools.ts b/src/vs/workbench/contrib/chat/browser/tools/tools.ts index 53320370c5a0..ec2de00db632 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/tools.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/tools.ts @@ -50,13 +50,10 @@ Avoid repeating existing code, instead use comments to represent regions of unch // ...existing code... Here is an example of how you should format an edit to an existing Person class: -class Person { - // ...existing code... - age: number; - // ...existing code... - getAge() { - return this.age; - } +{ + "explanation": "Add an age property to the Person class", + "filePath": "/folder/person.ts", + "code": "// ...existing code...\\n class Person {\\n // ...existing code...\\n age: number;\\n // ...existing code...\\n getAge() {\\n return this.age;\\n }\n // ...existing code...\n }" } `; @@ -64,7 +61,7 @@ class EditTool implements IToolData, IToolImpl { readonly id = 'vscode_editFile'; readonly tags = ['vscode_editing']; readonly displayName = localize('chat.tools.editFile', "Edit File"); - readonly modelDescription = `Edit a file in the workspace. Use this tool once per file that needs to be modified, even if there are multiple changes for a file. ${codeInstructions}`; + readonly modelDescription = `Edit a file in the workspace. Use this tool once per file that needs to be modified, even if there are multiple changes for a file. Generate the "explanation" property first. ${codeInstructions}`; readonly inputSchema: IJSONSchema; constructor( @@ -75,20 +72,20 @@ class EditTool implements IToolData, IToolImpl { this.inputSchema = { type: 'object', properties: { - filePath: { - type: 'string', - description: 'An absolute path to the file to edit', - }, explanation: { type: 'string', description: 'A short explanation of the edit being made. Can be the same as the explanation you showed to the user.', }, + filePath: { + type: 'string', + description: 'An absolute path to the file to edit', + }, code: { type: 'string', description: 'The code change to apply to the file. ' + codeInstructions } }, - required: ['filePath', 'explanation', 'code'] + required: ['explanation', 'filePath', 'code'] }; } From b6ca732710394538853985f9f501e2ad90c358e7 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sun, 12 Jan 2025 22:10:59 -0800 Subject: [PATCH 0503/3587] Tweak edit file tool description (#237761) Try to keep it from printing json blocks with the tool args --- src/vs/workbench/contrib/chat/browser/tools/tools.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/tools.ts b/src/vs/workbench/contrib/chat/browser/tools/tools.ts index ec2de00db632..cbd33bfce930 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/tools.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/tools.ts @@ -49,7 +49,7 @@ Avoid repeating existing code, instead use comments to represent regions of unch { changed code } // ...existing code... -Here is an example of how you should format an edit to an existing Person class: +Here is an example of how you should use vscode_editFile to edit an existing Person class: { "explanation": "Add an age property to the Person class", "filePath": "/folder/person.ts", From 964233731a98e6c92ccf377b0415a746e9695b2e Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sun, 12 Jan 2025 22:18:58 -0800 Subject: [PATCH 0504/3587] Fix multiple chat rendering issues (#237762) - Fix margin css in edits - Fix flickering while lists stream- from rendering the first item as a header, and a line-height discrepency - Fix extra margin in request in Edits - Briefly rendering empty codeblock in Edits --- src/vs/base/browser/markdownRenderer.ts | 16 ++++++++++++++ .../test/browser/markdownRenderer.test.ts | 21 +++++++++++++++++++ .../chatAttachmentsContentPart.ts | 6 ++++-- .../chatMarkdownContentPart.ts | 7 ++++++- .../contrib/chat/browser/chatListRenderer.ts | 5 ++++- .../contrib/chat/browser/codeBlockPart.css | 2 +- .../contrib/chat/browser/media/chat.css | 15 ++++--------- 7 files changed, 56 insertions(+), 16 deletions(-) diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 5d58485ddd85..71af77b8f254 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -772,9 +772,25 @@ function completeListItemPattern(list: marked.Tokens.List): marked.Tokens.List | codespan */ + const listEndsInHeading = (list: marked.Tokens.List): boolean => { + // A list item can be rendered as a heading for some reason when it has a subitem where we haven't rendered the text yet like this: + // 1. list item + // - + const lastItem = list.items.at(-1); + const lastToken = lastItem?.tokens.at(-1); + return lastToken?.type === 'heading' || lastToken?.type === 'list' && listEndsInHeading(lastToken as marked.Tokens.List); + }; + let newToken: marked.Token | undefined; if (lastListSubToken?.type === 'text' && !('inRawBlock' in lastListItem)) { // Why does Tag have a type of 'text' newToken = completeSingleLinePattern(lastListSubToken as marked.Tokens.Text); + } else if (listEndsInHeading(list)) { + const newList = marked.lexer(list.raw.trim() + '  ')[0] as marked.Tokens.List; + if (newList.type !== 'list') { + // Something went wrong + return; + } + return newList; } if (!newToken || newToken.type !== 'paragraph') { // 'text' item inside the list item turns into paragraph diff --git a/src/vs/base/test/browser/markdownRenderer.test.ts b/src/vs/base/test/browser/markdownRenderer.test.ts index 6ec296beb4a5..3263774598ed 100644 --- a/src/vs/base/test/browser/markdownRenderer.test.ts +++ b/src/vs/base/test/browser/markdownRenderer.test.ts @@ -692,6 +692,27 @@ suite('MarkdownRenderer', () => { const completeTokens = marked.marked.lexer(incomplete + '\`](https://microsoft.com)'); assert.deepStrictEqual(newTokens, completeTokens); }); + + test('list with incomplete subitem', () => { + const incomplete = `1. list item one + - `; + const tokens = marked.marked.lexer(incomplete); + const newTokens = fillInIncompleteTokens(tokens); + + const completeTokens = marked.marked.lexer(incomplete + ' '); + assert.deepStrictEqual(newTokens, completeTokens); + }); + + test('list with incomplete nested subitem', () => { + const incomplete = `1. list item one + - item 2 + - `; + const tokens = marked.marked.lexer(incomplete); + const newTokens = fillInIncompleteTokens(tokens); + + const completeTokens = marked.marked.lexer(incomplete + ' '); + assert.deepStrictEqual(newTokens, completeTokens); + }); }); suite('codespan', () => { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index 6bd5ad400231..591413c9501a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -55,7 +55,7 @@ export class ChatAttachmentsContentPart extends Disposable { private readonly variables: IChatRequestVariableEntry[], private readonly contentReferences: ReadonlyArray = [], private readonly workingSet: ReadonlyArray = [], - public readonly domNode: HTMLElement = dom.$('.chat-attached-context'), + public readonly domNode: HTMLElement | undefined = dom.$('.chat-attached-context'), @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IOpenerService private readonly openerService: IOpenerService, @@ -68,12 +68,14 @@ export class ChatAttachmentsContentPart extends Disposable { super(); this.initAttachedContext(domNode); + if (!domNode.childElementCount) { + this.domNode = undefined; + } } private initAttachedContext(container: HTMLElement) { dom.clearNode(container); this.attachedContextDisposables.clear(); - dom.setVisibility(Boolean(this.variables.length), this.domNode); const hoverDelegate = this.attachedContextDisposables.add(createInstantHoverDelegate()); this.variables.forEach(async (attachment) => { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index 44c0c0dbb6b4..8d7654540df9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -84,7 +84,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const result = this._register(renderer.render(markdown.content, { fillInIncompleteTokens, codeBlockRendererSync: (languageId, text, raw) => { - const isCodeBlockComplete = !isResponseVM(context.element) || context.element.isComplete || !raw || raw?.trim().endsWith('```'); + const isCodeBlockComplete = !isResponseVM(context.element) || context.element.isComplete || !raw || codeblockHasClosingBackticks(raw); if ((!text || (text.startsWith('') && !text.includes('\n'))) && !isCodeBlockComplete && rendererOptions.renderCodeBlockPills) { const hideEmptyCodeblock = $('div'); hideEmptyCodeblock.style.display = 'none'; @@ -278,6 +278,11 @@ export class EditorPool extends Disposable { } } +function codeblockHasClosingBackticks(str: string): boolean { + str = str.trim(); + return !!str.match(/\n```+$/); +} + class CollapsedCodeBlock extends Disposable { public readonly element: HTMLElement; diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 2bc16470629c..e66dde9f6c93 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -559,7 +559,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer :last-child, .interactive-item-container .value > :last-child.rendered-markdown > :last-child { margin-bottom: 0px; } @@ -291,8 +292,8 @@ margin: 16px 0 8px 0; } -.interactive-item-container.editing-session .value .rendered-markdown p { - margin: 0; +.interactive-item-container.editing-session .value .rendered-markdown p:has(+ [data-code] > .chat-codeblock-pill-widget) { + margin-bottom: 8px; } .interactive-item-container.editing-session .value .rendered-markdown h3 { @@ -301,10 +302,6 @@ font-weight: unset; } -.interactive-item-container.editing-session .value .rendered-markdown [data-code] { - margin: 8px 0 16px 0; -} - .interactive-item-container .value .rendered-markdown { /* Codicons next to text need to be aligned with the text */ .codicon { @@ -334,7 +331,7 @@ } } -.interactive-item-container .value .rendered-markdown p { +.interactive-item-container .value .rendered-markdown { line-height: 1.5em; } @@ -387,10 +384,6 @@ have to be updated for changes to the rules above, or to support more deeply nes /* #endregion list indent rules */ -.interactive-item-container .value .rendered-markdown li { - line-height: 1.3rem; -} - .interactive-item-container .value .rendered-markdown img { max-width: 100%; } From 3ff1dceedf606ee5cc60ffab6c1132b91ce67228 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 13 Jan 2025 09:47:32 +0100 Subject: [PATCH 0505/3587] Enable edit context (#237497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "Revert Enablement of EditContext on Insiders (#235062)" This reverts commit 45385e1c6f93a89a7992c0baacac62792434adac. * adding product import * adding som code * removing console logs * make sure editor selection is observed after keybinding dispatches Co-authored-with: Aiday Mar * removing empty lines --------- Co-authored-by: João Moreno --- src/typings/editContext.d.ts | 4 +- src/vs/editor/common/config/editorOptions.ts | 3 +- .../services/driver/browser/driver.ts | 60 +++++++++++++++---- .../services/driver/common/driver.ts | 1 + test/automation/src/code.ts | 13 +++- test/automation/src/debug.ts | 9 ++- test/automation/src/editor.ts | 20 +++++-- test/automation/src/editors.ts | 3 +- test/automation/src/extensions.ts | 3 +- test/automation/src/notebook.ts | 7 ++- test/automation/src/playwrightDriver.ts | 4 ++ test/automation/src/scm.ts | 16 +++-- test/automation/src/settings.ts | 17 ++++-- 13 files changed, 120 insertions(+), 40 deletions(-) diff --git a/src/typings/editContext.d.ts b/src/typings/editContext.d.ts index 5b5da0ac7e95..095858486671 100644 --- a/src/typings/editContext.d.ts +++ b/src/typings/editContext.d.ts @@ -58,8 +58,8 @@ interface EditContextEventHandlersEventMap { type EventHandler = (event: TEvent) => void; -interface TextUpdateEvent extends Event { - new(type: DOMString, options?: TextUpdateEventInit): TextUpdateEvent; +declare class TextUpdateEvent extends Event { + constructor(type: DOMString, options?: TextUpdateEventInit); readonly updateRangeStart: number; readonly updateRangeEnd: number; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 908dcac26bd6..3b85ec2f45af 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -16,6 +16,7 @@ import { USUAL_WORD_SEPARATORS } from '../core/wordHelper.js'; import * as nls from '../../../nls.js'; import { AccessibilitySupport } from '../../../platform/accessibility/common/accessibility.js'; import { IConfigurationPropertySchema } from '../../../platform/configuration/common/configurationRegistry.js'; +import product from '../../../platform/product/common/product.js'; //#region typed options @@ -5814,7 +5815,7 @@ export const EditorOptions = { emptySelectionClipboard: register(new EditorEmptySelectionClipboard()), dropIntoEditor: register(new EditorDropIntoEditor()), experimentalEditContextEnabled: register(new EditorBooleanOption( - EditorOption.experimentalEditContextEnabled, 'experimentalEditContextEnabled', false, + EditorOption.experimentalEditContextEnabled, 'experimentalEditContextEnabled', product.quality !== 'stable', { description: nls.localize('experimentalEditContextEnabled', "Sets whether the new experimental edit context should be used instead of the text area."), included: platform.isChrome || platform.isEdge || platform.isNative diff --git a/src/vs/workbench/services/driver/browser/driver.ts b/src/vs/workbench/services/driver/browser/driver.ts index d78e55aa9737..bb11f37a6413 100644 --- a/src/vs/workbench/services/driver/browser/driver.ts +++ b/src/vs/workbench/services/driver/browser/driver.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getClientArea, getTopLeftOffset } from '../../../../base/browser/dom.js'; +import { getClientArea, getTopLeftOffset, isHTMLDivElement, isHTMLTextAreaElement } from '../../../../base/browser/dom.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { language, locale } from '../../../../base/common/platform.js'; @@ -133,18 +133,54 @@ export class BrowserWindowDriver implements IWindowDriver { if (!element) { throw new Error(`Editor not found: ${selector}`); } + if (isHTMLDivElement(element)) { + // Edit context is enabled + const editContext = element.editContext; + if (!editContext) { + throw new Error(`Edit context not found: ${selector}`); + } + const selectionStart = editContext.selectionStart; + const selectionEnd = editContext.selectionEnd; + const event = new TextUpdateEvent('textupdate', { + updateRangeStart: selectionStart, + updateRangeEnd: selectionEnd, + text, + selectionStart: selectionStart + text.length, + selectionEnd: selectionStart + text.length, + compositionStart: 0, + compositionEnd: 0 + }); + editContext.dispatchEvent(event); + } else if (isHTMLTextAreaElement(element)) { + const start = element.selectionStart; + const newStart = start + text.length; + const value = element.value; + const newValue = value.substr(0, start) + text + value.substr(start); + + element.value = newValue; + element.setSelectionRange(newStart, newStart); + + const event = new Event('input', { 'bubbles': true, 'cancelable': true }); + element.dispatchEvent(event); + } + } - const textarea = element as HTMLTextAreaElement; - const start = textarea.selectionStart; - const newStart = start + text.length; - const value = textarea.value; - const newValue = value.substr(0, start) + text + value.substr(start); - - textarea.value = newValue; - textarea.setSelectionRange(newStart, newStart); - - const event = new Event('input', { 'bubbles': true, 'cancelable': true }); - textarea.dispatchEvent(event); + async getEditorSelection(selector: string): Promise<{ selectionStart: number; selectionEnd: number }> { + const element = mainWindow.document.querySelector(selector); + if (!element) { + throw new Error(`Editor not found: ${selector}`); + } + if (isHTMLDivElement(element)) { + const editContext = element.editContext; + if (!editContext) { + throw new Error(`Edit context not found: ${selector}`); + } + return { selectionStart: editContext.selectionStart, selectionEnd: editContext.selectionEnd }; + } else if (isHTMLTextAreaElement(element)) { + return { selectionStart: element.selectionStart, selectionEnd: element.selectionEnd }; + } else { + throw new Error(`Unknown type of element: ${selector}`); + } } async getTerminalBuffer(selector: string): Promise { diff --git a/src/vs/workbench/services/driver/common/driver.ts b/src/vs/workbench/services/driver/common/driver.ts index 5a00a201414a..4cc5952df5d6 100644 --- a/src/vs/workbench/services/driver/common/driver.ts +++ b/src/vs/workbench/services/driver/common/driver.ts @@ -38,6 +38,7 @@ export interface IWindowDriver { getElements(selector: string, recursive: boolean): Promise; getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number }>; typeInEditor(selector: string, text: string): Promise; + getEditorSelection(selector: string): Promise<{ selectionStart: number; selectionEnd: number }>; getTerminalBuffer(selector: string): Promise; writeInTerminal(selector: string, text: string): Promise; getLocaleInfo(): Promise; diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index a925cdd65bc9..6009e7c51f54 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -12,6 +12,7 @@ import { launch as launchPlaywrightBrowser } from './playwrightBrowser'; import { PlaywrightDriver } from './playwrightDriver'; import { launch as launchPlaywrightElectron } from './playwrightElectron'; import { teardown } from './processes'; +import { Quality } from './application'; export interface LaunchOptions { codePath?: string; @@ -28,6 +29,7 @@ export interface LaunchOptions { readonly tracing?: boolean; readonly headless?: boolean; readonly browser?: 'chromium' | 'webkit' | 'firefox'; + readonly quality: Quality; } interface ICodeInstance { @@ -77,7 +79,7 @@ export async function launch(options: LaunchOptions): Promise { const { serverProcess, driver } = await measureAndLog(() => launchPlaywrightBrowser(options), 'launch playwright (browser)', options.logger); registerInstance(serverProcess, options.logger, 'server'); - return new Code(driver, options.logger, serverProcess); + return new Code(driver, options.logger, serverProcess, options.quality); } // Electron smoke tests (playwright) @@ -85,7 +87,7 @@ export async function launch(options: LaunchOptions): Promise { const { electronProcess, driver } = await measureAndLog(() => launchPlaywrightElectron(options), 'launch playwright (electron)', options.logger); registerInstance(electronProcess, options.logger, 'electron'); - return new Code(driver, options.logger, electronProcess); + return new Code(driver, options.logger, electronProcess, options.quality); } } @@ -96,7 +98,8 @@ export class Code { constructor( driver: PlaywrightDriver, readonly logger: Logger, - private readonly mainProcess: cp.ChildProcess + private readonly mainProcess: cp.ChildProcess, + readonly quality: Quality ) { this.driver = new Proxy(driver, { get(target, prop) { @@ -242,6 +245,10 @@ export class Code { await this.poll(() => this.driver.typeInEditor(selector, text), () => true, `type in editor '${selector}'`); } + async waitForEditorSelection(selector: string, accept: (selection: { selectionStart: number; selectionEnd: number }) => boolean): Promise { + await this.poll(() => this.driver.getEditorSelection(selector), accept, `get editor selection '${selector}'`); + } + async waitForTerminalBuffer(selector: string, accept: (result: string[]) => boolean): Promise { await this.poll(() => this.driver.getTerminalBuffer(selector), accept, `get terminal buffer '${selector}'`); } diff --git a/test/automation/src/debug.ts b/test/automation/src/debug.ts index b7b7d427f4b0..e2e227fc35e1 100644 --- a/test/automation/src/debug.ts +++ b/test/automation/src/debug.ts @@ -9,6 +9,7 @@ import { Code, findElement } from './code'; import { Editors } from './editors'; import { Editor } from './editor'; import { IElement } from './driver'; +import { Quality } from './application'; const VIEWLET = 'div[id="workbench.view.debug"]'; const DEBUG_VIEW = `${VIEWLET}`; @@ -31,7 +32,8 @@ const CONSOLE_OUTPUT = `.repl .output.expression .value`; const CONSOLE_EVALUATION_RESULT = `.repl .evaluation-result.expression .value`; const CONSOLE_LINK = `.repl .value a.link`; -const REPL_FOCUSED = '.repl-input-wrapper .monaco-editor textarea'; +const REPL_FOCUSED_NATIVE_EDIT_CONTEXT = '.repl-input-wrapper .monaco-editor .native-edit-context'; +const REPL_FOCUSED_TEXTAREA = '.repl-input-wrapper .monaco-editor textarea'; export interface IStackFrame { name: string; @@ -127,8 +129,9 @@ export class Debug extends Viewlet { async waitForReplCommand(text: string, accept: (result: string) => boolean): Promise { await this.commands.runCommand('Debug: Focus on Debug Console View'); - await this.code.waitForActiveElement(REPL_FOCUSED); - await this.code.waitForSetValue(REPL_FOCUSED, text); + const selector = this.code.quality === Quality.Stable ? REPL_FOCUSED_TEXTAREA : REPL_FOCUSED_NATIVE_EDIT_CONTEXT; + await this.code.waitForActiveElement(selector); + await this.code.waitForSetValue(selector, text); // Wait for the keys to be picked up by the editor model such that repl evaluates what just got typed await this.editor.waitForEditorContents('debug:replinput', s => s.indexOf(text) >= 0); diff --git a/test/automation/src/editor.ts b/test/automation/src/editor.ts index 538866bfc060..9f5a9c901708 100644 --- a/test/automation/src/editor.ts +++ b/test/automation/src/editor.ts @@ -6,6 +6,7 @@ import { References } from './peek'; import { Commands } from './workbench'; import { Code } from './code'; +import { Quality } from './application'; const RENAME_BOX = '.monaco-editor .monaco-editor.rename-box'; const RENAME_INPUT = `${RENAME_BOX} .rename-input`; @@ -78,10 +79,10 @@ export class Editor { async waitForEditorFocus(filename: string, lineNumber: number, selectorPrefix = ''): Promise { const editor = [selectorPrefix || '', EDITOR(filename)].join(' '); const line = `${editor} .view-lines > .view-line:nth-child(${lineNumber})`; - const textarea = `${editor} textarea`; + const editContext = `${editor} ${this._editContextSelector()}`; await this.code.waitAndClick(line, 1, 1); - await this.code.waitForActiveElement(textarea); + await this.code.waitForActiveElement(editContext); } async waitForTypeInEditor(filename: string, text: string, selectorPrefix = ''): Promise { @@ -92,14 +93,23 @@ export class Editor { await this.code.waitForElement(editor); - const textarea = `${editor} textarea`; - await this.code.waitForActiveElement(textarea); + const editContext = `${editor} ${this._editContextSelector()}`; + await this.code.waitForActiveElement(editContext); - await this.code.waitForTypeInEditor(textarea, text); + await this.code.waitForTypeInEditor(editContext, text); await this.waitForEditorContents(filename, c => c.indexOf(text) > -1, selectorPrefix); } + async waitForEditorSelection(filename: string, accept: (selection: { selectionStart: number; selectionEnd: number }) => boolean): Promise { + const selector = `${EDITOR(filename)} ${this._editContextSelector()}`; + await this.code.waitForEditorSelection(selector, accept); + } + + private _editContextSelector() { + return this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'; + } + async waitForEditorContents(filename: string, accept: (contents: string) => boolean, selectorPrefix = ''): Promise { const selector = [selectorPrefix || '', `${EDITOR(filename)} .view-lines`].join(' '); return this.code.waitForTextContent(selector, undefined, c => accept(c.replace(/\u00a0/g, ' '))); diff --git a/test/automation/src/editors.ts b/test/automation/src/editors.ts index b3a914ffff02..472385c8534d 100644 --- a/test/automation/src/editors.ts +++ b/test/automation/src/editors.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Quality } from './application'; import { Code } from './code'; export class Editors { @@ -53,7 +54,7 @@ export class Editors { } async waitForActiveEditor(fileName: string, retryCount?: number): Promise { - const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] textarea`; + const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`; return this.code.waitForActiveElement(selector, retryCount); } diff --git a/test/automation/src/extensions.ts b/test/automation/src/extensions.ts index 2a481f9fe766..c881e4fd8dc6 100644 --- a/test/automation/src/extensions.ts +++ b/test/automation/src/extensions.ts @@ -8,6 +8,7 @@ import { Code } from './code'; import { ncp } from 'ncp'; import { promisify } from 'util'; import { Commands } from './workbench'; +import { Quality } from './application'; import path = require('path'); import fs = require('fs'); @@ -20,7 +21,7 @@ export class Extensions extends Viewlet { async searchForExtension(id: string): Promise { await this.commands.runCommand('Extensions: Focus on Extensions View', { exactLabelMatch: true }); - await this.code.waitForTypeInEditor('div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor textarea', `@id:${id}`); + await this.code.waitForTypeInEditor(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`, `@id:${id}`); await this.code.waitForTextContent(`div.part.sidebar div.composite.title h2`, 'Extensions: Marketplace'); let retrials = 1; diff --git a/test/automation/src/notebook.ts b/test/automation/src/notebook.ts index dff250027db7..cd46cbdb0dd4 100644 --- a/test/automation/src/notebook.ts +++ b/test/automation/src/notebook.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Quality } from './application'; import { Code } from './code'; import { QuickAccess } from './quickaccess'; import { QuickInput } from './quickinput'; @@ -46,10 +47,10 @@ export class Notebook { await this.code.waitForElement(editor); - const textarea = `${editor} textarea`; - await this.code.waitForActiveElement(textarea); + const editContext = `${editor} ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`; + await this.code.waitForActiveElement(editContext); - await this.code.waitForTypeInEditor(textarea, text); + await this.code.waitForTypeInEditor(editContext, text); await this._waitForActiveCellEditorContents(c => c.indexOf(text) > -1); } diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index 681ca4ef5408..dbde9955766e 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -288,6 +288,10 @@ export class PlaywrightDriver { return this.page.evaluate(([driver, selector, text]) => driver.typeInEditor(selector, text), [await this.getDriverHandle(), selector, text] as const); } + async getEditorSelection(selector: string) { + return this.page.evaluate(([driver, selector]) => driver.getEditorSelection(selector), [await this.getDriverHandle(), selector] as const); + } + async getTerminalBuffer(selector: string) { return this.page.evaluate(([driver, selector]) => driver.getTerminalBuffer(selector), [await this.getDriverHandle(), selector] as const); } diff --git a/test/automation/src/scm.ts b/test/automation/src/scm.ts index 9f950f2b16a7..6489badbe8a4 100644 --- a/test/automation/src/scm.ts +++ b/test/automation/src/scm.ts @@ -6,9 +6,11 @@ import { Viewlet } from './viewlet'; import { IElement } from './driver'; import { findElement, findElements, Code } from './code'; +import { Quality } from './application'; const VIEWLET = 'div[id="workbench.view.scm"]'; -const SCM_INPUT = `${VIEWLET} .scm-editor textarea`; +const SCM_INPUT_NATIVE_EDIT_CONTEXT = `${VIEWLET} .scm-editor .native-edit-context`; +const SCM_INPUT_TEXTAREA = `${VIEWLET} .scm-editor textarea`; const SCM_RESOURCE = `${VIEWLET} .monaco-list-row .resource`; const REFRESH_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Refresh"]`; const COMMIT_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Commit"]`; @@ -44,7 +46,7 @@ export class SCM extends Viewlet { async openSCMViewlet(): Promise { await this.code.dispatchKeybinding('ctrl+shift+g'); - await this.code.waitForElement(SCM_INPUT); + await this.code.waitForElement(this._editContextSelector()); } async waitForChange(name: string, type?: string): Promise { @@ -71,9 +73,13 @@ export class SCM extends Viewlet { } async commit(message: string): Promise { - await this.code.waitAndClick(SCM_INPUT); - await this.code.waitForActiveElement(SCM_INPUT); - await this.code.waitForSetValue(SCM_INPUT, message); + await this.code.waitAndClick(this._editContextSelector()); + await this.code.waitForActiveElement(this._editContextSelector()); + await this.code.waitForSetValue(this._editContextSelector(), message); await this.code.waitAndClick(COMMIT_COMMAND); } + + private _editContextSelector(): string { + return this.code.quality === Quality.Stable ? SCM_INPUT_TEXTAREA : SCM_INPUT_NATIVE_EDIT_CONTEXT; + } } diff --git a/test/automation/src/settings.ts b/test/automation/src/settings.ts index 68401eb0edaa..a7a40ece4311 100644 --- a/test/automation/src/settings.ts +++ b/test/automation/src/settings.ts @@ -7,8 +7,10 @@ import { Editor } from './editor'; import { Editors } from './editors'; import { Code } from './code'; import { QuickAccess } from './quickaccess'; +import { Quality } from './application'; -const SEARCH_BOX = '.settings-editor .suggest-input-container .monaco-editor textarea'; +const SEARCH_BOX_NATIVE_EDIT_CONTEXT = '.settings-editor .suggest-input-container .monaco-editor .native-edit-context'; +const SEARCH_BOX_TEXTAREA = '.settings-editor .suggest-input-container .monaco-editor textarea'; export class SettingsEditor { constructor(private code: Code, private editors: Editors, private editor: Editor, private quickaccess: QuickAccess) { } @@ -23,6 +25,7 @@ export class SettingsEditor { await this.openUserSettingsFile(); await this.code.dispatchKeybinding('right'); + await this.editor.waitForEditorSelection('settings.json', s => s.selectionStart === 1 && s.selectionEnd === 1); await this.editor.waitForTypeInEditor('settings.json', `"${setting}": ${value},`); await this.editors.saveOpenedFile(); } @@ -37,6 +40,7 @@ export class SettingsEditor { await this.openUserSettingsFile(); await this.code.dispatchKeybinding('right'); + await this.editor.waitForEditorSelection('settings.json', s => s.selectionStart === 1 && s.selectionEnd === 1); await this.editor.waitForTypeInEditor('settings.json', settings.map(v => `"${v[0]}": ${v[1]},`).join('')); await this.editors.saveOpenedFile(); } @@ -45,6 +49,7 @@ export class SettingsEditor { await this.openUserSettingsFile(); await this.quickaccess.runCommand('editor.action.selectAll'); await this.code.dispatchKeybinding('Delete'); + await this.editor.waitForEditorContents('settings.json', contents => contents === ''); await this.editor.waitForTypeInEditor('settings.json', `{`); // will auto close } await this.editors.saveOpenedFile(); await this.quickaccess.runCommand('workbench.action.closeActiveEditor'); @@ -57,13 +62,13 @@ export class SettingsEditor { async openUserSettingsUI(): Promise { await this.quickaccess.runCommand('workbench.action.openSettings2'); - await this.code.waitForActiveElement(SEARCH_BOX); + await this.code.waitForActiveElement(this._editContextSelector()); } async searchSettingsUI(query: string): Promise { await this.openUserSettingsUI(); - await this.code.waitAndClick(SEARCH_BOX); + await this.code.waitAndClick(this._editContextSelector()); if (process.platform === 'darwin') { await this.code.dispatchKeybinding('cmd+a'); } else { @@ -71,7 +76,11 @@ export class SettingsEditor { } await this.code.dispatchKeybinding('Delete'); await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => !results || (results?.length === 1 && !results[0].textContent)); - await this.code.waitForTypeInEditor('.settings-editor .suggest-input-container .monaco-editor textarea', query); + await this.code.waitForTypeInEditor(this._editContextSelector(), query); await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => results?.length === 1 && results[0].textContent.includes('Found')); } + + private _editContextSelector() { + return this.code.quality === Quality.Stable ? SEARCH_BOX_TEXTAREA : SEARCH_BOX_NATIVE_EDIT_CONTEXT; + } } From 69d97b0773575a75736850370918e1171cfde5c6 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 13 Jan 2025 10:54:20 +0100 Subject: [PATCH 0506/3587] Revert "Expose shell's environment - bash " (#237791) Revert "Expose shell's environment - bash (#237602)" This reverts commit e6805d7927dc05d2a38dac3c272788e484197648. --- .../terminal.shellIntegration.test.ts | 10 ---- .../common/capabilities/capabilities.ts | 3 -- .../shellEnvDetectionCapability.ts | 29 +----------- .../common/xterm/shellIntegrationAddon.ts | 46 ------------------- .../common/scripts/shellIntegration-bash.sh | 19 -------- 5 files changed, 1 insertion(+), 106 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts index 726f0ed278f3..db48c2593e07 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts @@ -87,16 +87,6 @@ import { assertNoRpc } from '../utils'; await closeTerminalAsync(terminal); }); - if (platform() === 'darwin' || platform() === 'linux') { - test('Test if env is set', async () => { - const { shellIntegration } = await createTerminalAndWaitForShellIntegration(); - const env = shellIntegration.env; - ok(env); - ok(env.PATH); - ok(env.PATH.length > 0, 'env.PATH should have a length greater than 0'); - }); - } - test('execution events should fire in order when a command runs', async () => { const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration(); const events: string[] = []; diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index ba7597b9d926..50f39332a431 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -155,9 +155,6 @@ export interface IShellEnvDetectionCapability { readonly onDidChangeEnv: Event>; get env(): Map; setEnvironment(envs: { [key: string]: string | undefined } | undefined, isTrusted: boolean): void; - startEnvironmentSingleVar(isTrusted: boolean): void; - setEnvironmentSingleVar(key: string, value: string | undefined, isTrusted: boolean): void; - endEnvironmentSingleVar(isTrusted: boolean): void; } export const enum CommandInvalidationReason { diff --git a/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts index be9577acfe93..95e1d827dc43 100644 --- a/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts @@ -11,8 +11,7 @@ import { equals } from '../../../../base/common/objects.js'; export class ShellEnvDetectionCapability extends Disposable implements IShellEnvDetectionCapability { readonly type = TerminalCapability.ShellEnvDetection; - private _pendingEnv: Map | undefined; - private _env: Map = new Map(); + private readonly _env: Map = new Map(); get env(): Map { return this._env; } private readonly _onDidChangeEnv = this._register(new Emitter>()); @@ -37,30 +36,4 @@ export class ShellEnvDetectionCapability extends Disposable implements IShellEnv // Convert to event and fire event this._onDidChangeEnv.fire(this._env); } - - startEnvironmentSingleVar(isTrusted: boolean): void { - if (!isTrusted) { - return; - } - this._pendingEnv = new Map(); - } - setEnvironmentSingleVar(key: string, value: string | undefined, isTrusted: boolean): void { - if (!isTrusted) { - return; - } - if (key !== undefined && value !== undefined) { - this._pendingEnv?.set(key, value); - } - } - endEnvironmentSingleVar(isTrusted: boolean): void { - if (!isTrusted) { - return; - } - if (!this._pendingEnv) { - return; - } - this._env = this._pendingEnv; - this._pendingEnv = undefined; - this._onDidChangeEnv.fire(this._env); - } } diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index 92b1851c275c..ee7dbff267e5 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -239,34 +239,6 @@ const enum VSCodeOscPt { */ EnvJson = 'EnvJson', - /** - * The start of the collecting user's environment variables individually. - * Clears any environment residuals in previous sessions. - * - * Format: `OSC 633 ; EnvSingleStart ; ` - * - * WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script. - */ - EnvSingleStart = 'EnvSingleStart', - - /** - * Sets an entry of single environment variable to transactional pending map of environment variables. - * - * Format: `OSC 633 ; EnvSingleEntry ; ; ; ` - * - * WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script. - */ - EnvSingleEntry = 'EnvSingleEntry', - - /** - * The end of the collecting user's environment variables individually. - * Clears any pending environment variables and fires an event that contains user's environment. - * - * Format: `OSC 633 ; EnvSingleEnd ; ` - * - * WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script. - */ - EnvSingleEnd = 'EnvSingleEnd' } /** @@ -474,24 +446,6 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati } return true; } - case VSCodeOscPt.EnvSingleStart: { - this._createOrGetShellEnvDetection().startEnvironmentSingleVar(args[0] === this._nonce); - return true; - } - case VSCodeOscPt.EnvSingleEntry: { - const arg0 = args[0]; - const arg1 = args[1]; - const arg2 = args[2]; - if (arg0 !== undefined && arg1 !== undefined) { - const env = deserializeMessage(arg1); - this._createOrGetShellEnvDetection().setEnvironmentSingleVar(arg0, env, arg2 === this._nonce); - } - return true; - } - case VSCodeOscPt.EnvSingleEnd: { - this._createOrGetShellEnvDetection().endEnvironmentSingleVar(args[0] === this._nonce); - return true; - } case VSCodeOscPt.RightPromptStart: { this._createOrGetCommandDetection(this._terminal).handleRightPromptStart(); return true; diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index 3040617a5a6d..fbeaa22f90a7 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -214,17 +214,6 @@ __vsc_update_cwd() { builtin printf '\e]633;P;Cwd=%s\a' "$(__vsc_escape_value "$__vsc_cwd")" } -__vsc_update_env() { - builtin printf '\e]633;EnvSingleStart;%s;\a' $__vsc_nonce - for var in $(compgen -v); do - if printenv "$var" >/dev/null 2>&1; then - value=$(builtin printf '%s' "${!var}") - builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$var" "$(__vsc_escape_value "$value")" $__vsc_nonce - fi - done - builtin printf '\e]633;EnvSingleEnd;%s;\a' $__vsc_nonce -} - __vsc_command_output_start() { if [[ -z "${__vsc_first_prompt-}" ]]; then builtin return @@ -251,10 +240,6 @@ __vsc_command_complete() { builtin printf '\e]633;D;%s\a' "$__vsc_status" fi __vsc_update_cwd - - if [ "$__vsc_stable" = "0" ]; then - __vsc_update_env - fi } __vsc_update_prompt() { # in command execution @@ -284,10 +269,6 @@ __vsc_precmd() { fi __vsc_first_prompt=1 __vsc_update_prompt - - if [ "$__vsc_stable" = "0" ]; then - __vsc_update_env - fi } __vsc_preexec() { From c636d93f53b5f695d91a6648b37f472669721212 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:47:18 +0100 Subject: [PATCH 0507/3587] Git - cleanup command names (#237790) --- extensions/git/package.json | 16 ++++++++-------- extensions/git/package.nls.json | 4 ++-- extensions/git/src/commands.ts | 18 +++++++++--------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 4c3ea11c81f3..63dcda18c769 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -454,14 +454,14 @@ "enablement": "!operationInProgress" }, { - "command": "git.checkoutRef", - "title": "%command.checkoutRef%", + "command": "git.graph.checkout", + "title": "%command.graphCheckout%", "category": "Git", "enablement": "!operationInProgress" }, { - "command": "git.checkoutRefDetached", - "title": "%command.checkoutRefDetached%", + "command": "git.graph.checkoutDetached", + "title": "%command.graphCheckoutDetached%", "category": "Git", "enablement": "!operationInProgress" }, @@ -1466,11 +1466,11 @@ "when": "false" }, { - "command": "git.checkoutRef", + "command": "git.graph.checkout", "when": "false" }, { - "command": "git.checkoutRefDetached", + "command": "git.graph.checkoutDetached", "when": "false" }, { @@ -1997,7 +1997,7 @@ ], "scm/historyItem/context": [ { - "command": "git.checkoutRefDetached", + "command": "git.graph.checkoutDetached", "when": "scmProvider == git", "group": "1_checkout@2" }, @@ -2029,7 +2029,7 @@ ], "scm/historyItemRef/context": [ { - "command": "git.checkoutRef", + "command": "git.graph.checkout", "when": "scmProvider == git", "group": "1_checkout@1" } diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 5233f764f131..e27b66875d0b 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -62,8 +62,8 @@ "command.undoCommit": "Undo Last Commit", "command.checkout": "Checkout to...", "command.checkoutDetached": "Checkout to (Detached)...", - "command.checkoutRef": "Checkout", - "command.checkoutRefDetached": "Checkout (Detached)", + "command.graphCheckout": "Checkout", + "command.graphCheckoutDetached": "Checkout (Detached)", "command.branch": "Create Branch...", "command.branchFrom": "Create Branch From...", "command.deleteBranch": "Delete Branch...", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 46e3f116c21d..584eec49422e 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -2539,13 +2539,8 @@ export class CommandCenter { return this._checkout(repository, { treeish }); } - @command('git.checkoutDetached', { repository: true }) - async checkoutDetached(repository: Repository, treeish?: string): Promise { - return this._checkout(repository, { detached: true, treeish }); - } - - @command('git.checkoutRef', { repository: true }) - async checkoutRef(repository: Repository, historyItem?: SourceControlHistoryItem, historyItemRefId?: string): Promise { + @command('git.graph.checkout', { repository: true }) + async checkout2(repository: Repository, historyItem?: SourceControlHistoryItem, historyItemRefId?: string): Promise { const historyItemRef = historyItem?.references?.find(r => r.id === historyItemRefId); if (!historyItemRef) { return; @@ -2569,8 +2564,13 @@ export class CommandCenter { } } - @command('git.checkoutRefDetached', { repository: true }) - async checkoutRefDetached(repository: Repository, historyItem?: SourceControlHistoryItem): Promise { + @command('git.checkoutDetached', { repository: true }) + async checkoutDetached(repository: Repository, treeish?: string): Promise { + return this._checkout(repository, { detached: true, treeish }); + } + + @command('git.graph.checkoutDetached', { repository: true }) + async checkoutDetached2(repository: Repository, historyItem?: SourceControlHistoryItem): Promise { if (!historyItem) { return false; } From 567f6e35bf7b2fd089ec4bec513e5f3d03f0efaa Mon Sep 17 00:00:00 2001 From: John Murray Date: Mon, 13 Jan 2025 11:09:34 +0000 Subject: [PATCH 0508/3587] Add a Configure option to overflow menu of Open Editors view (#237678) * Add a Configure option to overflow menu of Open Editors view * Use `@feature:explorer openEditors` for the query --- .../files/browser/views/openEditorsView.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index d21998d3bb69..dbc04e88561b 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -56,6 +56,7 @@ import { mainWindow } from '../../../../../base/browser/window.js'; import { EditorGroupView } from '../../../../browser/parts/editor/editorGroupView.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IPreferencesService } from '../../../../services/preferences/common/preferences.js'; const $ = dom.$; @@ -943,3 +944,24 @@ registerAction2(class extends Action2 { await commandService.executeCommand(NEW_UNTITLED_FILE_COMMAND_ID); } }); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'openEditors.configure', + title: nls.localize('configureOpenEditorsView', 'Configure \'{0}\'', OpenEditorsView.NAME.value), + f1: false, + icon: Codicon.gear, + menu: { + id: MenuId.ViewTitle, + group: '9_configure', + when: ContextKeyExpr.equals('view', OpenEditorsView.ID), + order: 10 + } + }); + } + async run(accessor: ServicesAccessor): Promise { + const preferencesService = accessor.get(IPreferencesService); + preferencesService.openSettings({ jsonEditor: false, query: '@feature:explorer openEditors' }); + } +}); From 06c07d24efd3aa76412a55b0bd4f3833c901a00e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 13 Jan 2025 13:04:40 +0100 Subject: [PATCH 0509/3587] Asymmetry in Memento API's JSON encoding/decoding (fix #209479) (#237800) --- .../src/singlefolder-tests/state.test.ts | 39 ++++++++++++++++++- src/vs/workbench/api/common/extHostMemento.ts | 10 ++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts index f10edc51b3b0..cbe58948d866 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import 'mocha'; -import { ExtensionContext, extensions } from 'vscode'; +import { ExtensionContext, extensions, Uri } from 'vscode'; suite('vscode API - globalState / workspaceState', () => { @@ -16,7 +16,7 @@ suite('vscode API - globalState / workspaceState', () => { extensionContext = (global as any).testExtensionContext; }); - test('state', async () => { + test('state basics', async () => { for (const state of [extensionContext.globalState, extensionContext.workspaceState]) { let keys = state.keys(); assert.strictEqual(keys.length, 0); @@ -42,4 +42,39 @@ suite('vscode API - globalState / workspaceState', () => { assert.strictEqual(res, 'default'); } }); + + test('state - handling of objects', async () => { + for (const state of [extensionContext.globalState, extensionContext.workspaceState]) { + const keys = state.keys(); + assert.strictEqual(keys.length, 0); + + state.update('state.test.date', new Date()); + const date = state.get('state.test.date'); + assert.ok(typeof date === 'string'); + + state.update('state.test.regex', /foo/); + const regex = state.get('state.test.regex'); + assert.ok(typeof regex === 'object' && !(regex instanceof RegExp)); + + class Foo { } + state.update('state.test.class', new Foo()); + const clazz = state.get('state.test.class'); + assert.ok(typeof clazz === 'object' && !(clazz instanceof Foo)); + + const cycle: any = { self: null }; + cycle.self = cycle; + assert.throws(() => state.update('state.test.cycle', cycle)); + + const uriIn = Uri.parse('/foo/bar'); + state.update('state.test.uri', uriIn); + const uriOut = state.get('state.test.uri') as Uri; + assert.ok(uriIn.toString() === Uri.from(uriOut).toString()); + + state.update('state.test.null', null); + assert.strictEqual(state.get('state.test.null'), null); + + state.update('state.test.undefined', undefined); + assert.strictEqual(state.get('state.test.undefined'), undefined); + } + }); }); diff --git a/src/vs/workbench/api/common/extHostMemento.ts b/src/vs/workbench/api/common/extHostMemento.ts index e1dc4f279478..baad57cfd679 100644 --- a/src/vs/workbench/api/common/extHostMemento.ts +++ b/src/vs/workbench/api/common/extHostMemento.ts @@ -76,7 +76,15 @@ export class ExtensionMemento implements vscode.Memento { } update(key: string, value: any): Promise { - this._value![key] = value; + if (value !== null && typeof value === 'object') { + // Prevent the value from being as-is for until we have + // received the change event from the main side by emulating + // the treatment of values via JSON parsing and stringifying. + // (https://github.com/microsoft/vscode/issues/209479) + this._value![key] = JSON.parse(JSON.stringify(value)); + } else { + this._value![key] = value; + } const record = this._deferredPromises.get(key); if (record !== undefined) { From d3147d00546462818b97bc3dffa1cbbda7708c4c Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 13 Jan 2025 15:07:09 +0100 Subject: [PATCH 0510/3587] improve diff for /edits (#237801) * improve diff for /edits * no double-diff for chat modified entries * align whitespace property * overflow logic for overlay widget * fix compiler --- .../chatEditingModifiedFileEntry.ts | 17 ++- .../chatEditingModifiedNotebookEntry.ts | 4 +- .../chat/browser/chatEditorController.ts | 100 +++++++++++------- .../contrib/chat/browser/chatEditorOverlay.ts | 20 +++- .../chat/browser/media/chatEditorOverlay.css | 1 + 5 files changed, 96 insertions(+), 46 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 37a6b9a2b68d..e022f402c79c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -7,7 +7,7 @@ import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Disposable, IReference, toDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; -import { IObservable, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { autorun, IObservable, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; import { themeColorFromId } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { EditOperation, ISingleEditOperation } from '../../../../../editor/common/core/editOperation.js'; @@ -26,7 +26,9 @@ import { IModelService } from '../../../../../editor/common/services/model.js'; import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { IModelContentChangedEvent } from '../../../../../editor/common/textModelEvents.js'; import { localize } from '../../../../../nls.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; +import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; import { editorSelectionBackground } from '../../../../../platform/theme/common/colorRegistry.js'; import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js'; import { SaveReason } from '../../../../common/editor.js'; @@ -130,6 +132,8 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie return this._telemetryInfo.requestId; } + private readonly _diffTrimWhitespace: IObservable; + constructor( resourceRef: IReference, private readonly _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void }, @@ -139,6 +143,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie @IModelService modelService: IModelService, @ITextModelService textModelService: ITextModelService, @ILanguageService languageService: ILanguageService, + @IConfigurationService configService: IConfigurationService, @IChatService private readonly _chatService: IChatService, @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, @@ -186,6 +191,12 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie this._register(toDisposable(() => { this._clearCurrentEditLineDecoration(); })); + + this._diffTrimWhitespace = observableConfigValue('diffEditor.ignoreTrimWhitespace', true, configService); + this._register(autorun(r => { + this._diffTrimWhitespace.read(r); + this._updateDiffInfoSeq(); + })); } private _clearCurrentEditLineDecoration() { @@ -413,10 +424,12 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie const docVersionNow = this.doc.getVersionId(); const snapshotVersionNow = this.docSnapshot.getVersionId(); + const ignoreTrimWhitespace = this._diffTrimWhitespace.get(); + const diff = await this._editorWorkerService.computeDiff( this.docSnapshot.uri, this.doc.uri, - { computeMoves: true, ignoreTrimWhitespace: false, maxComputationTimeMs: 3000 }, + { ignoreTrimWhitespace, computeMoves: false, maxComputationTimeMs: 3000 }, 'advanced' ); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts index d6cc236478aa..4e2854a7b373 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts @@ -9,6 +9,7 @@ import { ILanguageService } from '../../../../../editor/common/languages/languag import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js'; import { SaveReason } from '../../../../common/editor.js'; @@ -33,8 +34,9 @@ export class ChatEditingModifiedNotebookEntry extends ChatEditingModifiedFileEnt @IEditorWorkerService _editorWorkerService: IEditorWorkerService, @IUndoRedoService _undoRedoService: IUndoRedoService, @IFileService _fileService: IFileService, + @IConfigurationService configService: IConfigurationService ) { - super(resourceRef, _multiDiffEntryDelegate, _telemetryInfo, kind, initialContent, modelService, textModelService, languageService, _chatService, _editorWorkerService, _undoRedoService, _fileService); + super(resourceRef, _multiDiffEntryDelegate, _telemetryInfo, kind, initialContent, modelService, textModelService, languageService, configService, _chatService, _editorWorkerService, _undoRedoService, _fileService); this.resolveTextFileEditorModel = resourceRef.object as IResolvedTextFileEditorModel; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index cfe1214f7d86..78a198389f66 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -6,7 +6,7 @@ import './media/chatEditorController.css'; import { addStandardDisposableListener, getTotalWidth } from '../../../../base/browser/dom.js'; import { Disposable, DisposableStore, dispose, toDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, autorunWithStore, derived, IObservable, observableFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; +import { autorun, autorunWithStore, derived, IObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { themeColorFromId } from '../../../../base/common/themables.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, IViewZone, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; import { LineSource, renderLines, RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; @@ -80,9 +80,10 @@ export class ChatEditorController extends Disposable implements IEditorContribut this._ctxHasEditorModification = ctxHasEditorModification.bindTo(contextKeyService); this._ctxRequestInProgress = ctxHasRequestInProgress.bindTo(contextKeyService); - const fontInfoObs = observableCodeEditor(this._editor).getOption(EditorOption.fontInfo); - const lineHeightObs = observableCodeEditor(this._editor).getOption(EditorOption.lineHeight); - const modelObs = observableCodeEditor(this._editor).model; + const editorObs = observableCodeEditor(this._editor); + const fontInfoObs = editorObs.getOption(EditorOption.fontInfo); + const lineHeightObs = editorObs.getOption(EditorOption.lineHeight); + const modelObs = editorObs.model; this._store.add(autorun(r => { @@ -112,13 +113,13 @@ export class ChatEditorController extends Disposable implements IEditorContribut const currentEditorEntry = entryForEditor.read(r); if (!currentEditorEntry) { - this._clearRendering(); + this._clear(); didReval = false; return; } if (this._editor.getOption(EditorOption.inDiffEditor) && !_instantiationService.invokeFunction(isDiffEditorForEntry, currentEditorEntry.entry, this._editor)) { - this._clearRendering(); + this._clear(); return; } @@ -145,7 +146,16 @@ export class ChatEditorController extends Disposable implements IEditorContribut lineHeightObs.read(r); const diff = entry?.diffInfo.read(r); - this._updateWithDiff(entry, diff); + + // Add line decorations (just markers, no UI) for diff navigation + this._updateDiffLineDecorations(diff); + + // Add diff decoration to the UI (unless in diff editor) + if (!this._editor.getOption(EditorOption.inDiffEditor)) { + this._updateDiffRendering(entry, diff); + } else { + this._clearDiffRendering(); + } if (!didReval && !diff.identical) { didReval = true; @@ -199,11 +209,19 @@ export class ChatEditorController extends Disposable implements IEditorContribut } override dispose(): void { - this._clearRendering(); + this._clear(); super.dispose(); } - private _clearRendering() { + private _clear() { + this._clearDiffRendering(); + this._diffLineDecorations.clear(); + this._currentChangeIndex.set(undefined, undefined); + this._currentEntryIndex.set(undefined, undefined); + this._ctxHasEditorModification.reset(); + } + + private _clearDiffRendering() { this._editor.changeViewZones((viewZoneChangeAccessor) => { for (const id of this._viewZones) { viewZoneChangeAccessor.removeZone(id); @@ -212,18 +230,11 @@ export class ChatEditorController extends Disposable implements IEditorContribut this._viewZones = []; this._diffHunksRenderStore.clear(); this._diffVisualDecorations.clear(); - this._diffLineDecorations.clear(); - this._ctxHasEditorModification.reset(); - transaction(tx => { - this._currentEntryIndex.set(undefined, tx); - this._currentChangeIndex.set(undefined, tx); - }); this._scrollLock = false; } - private _updateWithDiff(entry: IModifiedFileEntry, diff: IDocumentDiff): void { + private _updateDiffRendering(entry: IModifiedFileEntry, diff: IDocumentDiff): void { - this._ctxHasEditorModification.set(!diff.identical); const originalModel = entry.originalModel; const chatDiffAddDecoration = ModelDecorationOptions.createDynamic({ @@ -255,7 +266,6 @@ export class ChatEditorController extends Disposable implements IEditorContribut } this._viewZones = []; const modifiedVisualDecorations: IModelDeltaDecoration[] = []; - const modifiedLineDecorations: IModelDeltaDecoration[] = []; const mightContainNonBasicASCII = originalModel.mightContainNonBasicASCII(); const mightContainRTL = originalModel.mightContainRTL(); const renderOptions = RenderOptions.fromEditor(this._editor); @@ -344,16 +354,9 @@ export class ChatEditorController extends Disposable implements IEditorContribut stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges } }); - - // Add line decorations for diff navigation - modifiedLineDecorations.push({ - range: diffEntry.modified.toInclusiveRange() ?? new Range(diffEntry.modified.startLineNumber, 1, diffEntry.modified.startLineNumber, Number.MAX_SAFE_INTEGER), - options: ChatEditorController._diffLineDecorationData - }); } this._diffVisualDecorations.set(modifiedVisualDecorations); - this._diffLineDecorations.set(modifiedLineDecorations); }); const diffHunkDecoCollection = this._editor.createDecorationsCollection(diffHunkDecorations); @@ -428,6 +431,20 @@ export class ChatEditorController extends Disposable implements IEditorContribut })); } + private _updateDiffLineDecorations(diff: IDocumentDiff): void { + this._ctxHasEditorModification.set(!diff.identical); + + const modifiedLineDecorations: IModelDeltaDecoration[] = []; + + for (const diffEntry of diff.changes) { + modifiedLineDecorations.push({ + range: diffEntry.modified.toInclusiveRange() ?? new Range(diffEntry.modified.startLineNumber, 1, diffEntry.modified.startLineNumber, Number.MAX_SAFE_INTEGER), + options: ChatEditorController._diffLineDecorationData + }); + } + this._diffLineDecorations.set(modifiedLineDecorations); + } + unlockScroll(): void { this._scrollLock = false; } @@ -533,6 +550,12 @@ export class ChatEditorController extends Disposable implements IEditorContribut if (!this._editor.hasModel()) { return; } + + const entry = this._chatEditingService.currentEditingSessionObs.get()?.getEntry(this._editor.getModel().uri); + if (!entry) { + return; + } + const lineRelativeTop = this._editor.getTopForLineNumber(this._editor.getPosition().lineNumber) - this._editor.getScrollTop(); let closestDistance = Number.MAX_VALUE; @@ -549,33 +572,30 @@ export class ChatEditorController extends Disposable implements IEditorContribut } } - if (!(widget instanceof DiffHunkWidget)) { - return; - } - - - const lineNumber = widget.getStartLineNumber(); - const position = lineNumber ? new Position(lineNumber, 1) : undefined; let selection = this._editor.getSelection(); - if (position && !selection.containsPosition(position)) { - selection = Selection.fromPositions(position); + if (widget instanceof DiffHunkWidget) { + const lineNumber = widget.getStartLineNumber(); + const position = lineNumber ? new Position(lineNumber, 1) : undefined; + if (position && !selection.containsPosition(position)) { + selection = Selection.fromPositions(position); + } } const isDiffEditor = this._editor.getOption(EditorOption.inDiffEditor); if (isDiffEditor) { // normal EDITOR - await this._editorService.openEditor({ resource: widget.entry.modifiedURI }); + await this._editorService.openEditor({ resource: entry.modifiedURI }); } else { // DIFF editor const defaultAgentName = this._chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession)?.fullName; const diffEditor = await this._editorService.openEditor({ - original: { resource: widget.entry.originalURI, options: { selection: undefined } }, - modified: { resource: widget.entry.modifiedURI, options: { selection } }, + original: { resource: entry.originalURI, options: { selection: undefined } }, + modified: { resource: entry.modifiedURI, options: { selection } }, label: defaultAgentName - ? localize('diff.agent', '{0} (changes from {1})', basename(widget.entry.modifiedURI), defaultAgentName) - : localize('diff.generic', '{0} (changes from chat)', basename(widget.entry.modifiedURI)) + ? localize('diff.agent', '{0} (changes from {1})', basename(entry.modifiedURI), defaultAgentName) + : localize('diff.generic', '{0} (changes from chat)', basename(entry.modifiedURI)) }); if (diffEditor && diffEditor.input) { @@ -586,7 +606,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut // close diff editor when entry is decided const d = autorun(r => { - const state = widget.entry.state.read(r); + const state = entry.state.read(r); if (state === WorkingSetEntryState.Accepted || state === WorkingSetEntryState.Rejected) { d.dispose(); this._editorService.closeEditor(editorIdent); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index 6306e2361ce6..f0c1ca0a91a7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -28,10 +28,11 @@ import { ChatEditorController } from './chatEditorController.js'; import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; import { isDiffEditorForEntry } from './chatEditing/chatEditing.js'; import './media/chatEditorOverlay.css'; +import { findDiffEditorContainingCodeEditor } from '../../../../editor/browser/widget/diffEditor/commands.js'; class ChatEditorOverlayWidget implements IOverlayWidget { - readonly allowEditorOverflow = false; + readonly allowEditorOverflow = true; private readonly _domNode: HTMLElement; private readonly _progressNode: HTMLElement; @@ -47,7 +48,7 @@ class ChatEditorOverlayWidget implements IOverlayWidget { constructor( private readonly _editor: ICodeEditor, @IEditorService editorService: IEditorService, - @IInstantiationService instaService: IInstantiationService, + @IInstantiationService private readonly _instaService: IInstantiationService, ) { this._domNode = document.createElement('div'); this._domNode.classList.add('chat-editor-overlay-widget'); @@ -62,7 +63,7 @@ class ChatEditorOverlayWidget implements IOverlayWidget { toolbarNode.classList.add('chat-editor-overlay-toolbar'); this._domNode.appendChild(toolbarNode); - this._toolbar = instaService.createInstance(MenuWorkbenchToolBar, toolbarNode, MenuId.ChatEditingEditorContent, { + this._toolbar = _instaService.createInstance(MenuWorkbenchToolBar, toolbarNode, MenuId.ChatEditingEditorContent, { telemetrySource: 'chatEditor.overlayToolbar', hiddenItemStrategy: HiddenItemStrategy.Ignore, toolbarOptions: { @@ -228,6 +229,19 @@ class ChatEditorOverlayWidget implements IOverlayWidget { this._navigationBearings.set({ changeCount: changes, activeIdx, entriesCount: entries.length }, undefined); })); + + const editorWithObs = observableFromEvent(this._editor.onDidLayoutChange, () => { + const diffEditor = this._instaService.invokeFunction(findDiffEditorContainingCodeEditor, this._editor); + return diffEditor + ? diffEditor.getOriginalEditor().getLayoutInfo().contentWidth + diffEditor.getModifiedEditor().getLayoutInfo().contentWidth + : this._editor.getLayoutInfo().contentWidth; + }); + + this._showStore.add(autorun(r => { + const width = editorWithObs.read(r); + this._domNode.style.maxWidth = `${width - 20}px`; + })); + if (!this._isAdded) { this._editor.addOverlayWidget(this); this._isAdded = true; diff --git a/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css index b7a54ada1a2a..153da41fe6ee 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css @@ -13,6 +13,7 @@ align-items: center; z-index: 10; box-shadow: 0 2px 8px var(--vscode-widget-shadow); + overflow: hidden; } @keyframes pulse { From 65c84fd4dec025025dfc16ad5bde8331a37e4137 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 13 Jan 2025 15:30:45 +0100 Subject: [PATCH 0511/3587] Status bar item misses "Manage Extension" action in context menu (fix #237804) (#237805) --- .../api/browser/statusBarExtensionPoint.ts | 2 +- .../parts/statusbar/statusbarActions.ts | 15 +++++ .../browser/parts/statusbar/statusbarModel.ts | 1 + .../browser/parts/statusbar/statusbarPart.ts | 6 +- .../services/statusbar/browser/statusbar.ts | 6 ++ .../parts/statusbar/statusbarModel.test.ts | 58 +++++++++---------- 6 files changed, 57 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/api/browser/statusBarExtensionPoint.ts b/src/vs/workbench/api/browser/statusBarExtensionPoint.ts index bae2812c5f3e..8b2c415ab0b4 100644 --- a/src/vs/workbench/api/browser/statusBarExtensionPoint.ts +++ b/src/vs/workbench/api/browser/statusBarExtensionPoint.ts @@ -101,7 +101,7 @@ class ExtensionStatusBarItemService implements IExtensionStatusBarItemService { color = undefined; backgroundColor = undefined; } - const entry: IStatusbarEntry = { name, text, tooltip, command, color, backgroundColor, ariaLabel, role, kind }; + const entry: IStatusbarEntry = { name, text, tooltip, command, color, backgroundColor, ariaLabel, role, kind, extensionId }; if (typeof priority === 'undefined') { priority = 0; diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarActions.ts b/src/vs/workbench/browser/parts/statusbar/statusbarActions.ts index 55117c9fee2a..08794f0900d4 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarActions.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarActions.ts @@ -16,6 +16,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { StatusbarViewModel } from './statusbarModel.js'; import { StatusBarFocused } from '../../../common/contextkeys.js'; import { getActiveWindow } from '../../../../base/browser/dom.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; export class ToggleStatusbarEntryVisibilityAction extends Action { @@ -45,6 +46,20 @@ export class HideStatusbarEntryAction extends Action { } } +export class ManageExtensionAction extends Action { + + constructor( + private readonly extensionId: string, + @ICommandService private readonly commandService: ICommandService + ) { + super('statusbar.manage.extension', localize('manageExtension', "Manage Extension")); + } + + override run(): Promise { + return this.commandService.executeCommand('_extensions.manage', this.extensionId); + } +} + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.statusBar.focusPrevious', weight: KeybindingWeight.WorkbenchContrib, diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts index 30b01a7a4bb7..82db371b426c 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts @@ -11,6 +11,7 @@ import { Emitter } from '../../../../base/common/event.js'; export interface IStatusbarViewModelEntry { readonly id: string; + readonly extensionId: string | undefined; readonly name: string; readonly hasCommand: boolean; readonly alignment: StatusbarAlignment; diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index efc5e92e03f4..6aee246ef2c4 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -29,7 +29,7 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { isHighContrast } from '../../../../platform/theme/common/theme.js'; import { hash } from '../../../../base/common/hash.js'; import { WorkbenchHoverDelegate } from '../../../../platform/hover/browser/hover.js'; -import { HideStatusbarEntryAction, ToggleStatusbarEntryVisibilityAction } from './statusbarActions.js'; +import { HideStatusbarEntryAction, ManageExtensionAction, ToggleStatusbarEntryVisibilityAction } from './statusbarActions.js'; import { IStatusbarViewModelEntry, StatusbarViewModel } from './statusbarModel.js'; import { StatusbarEntryItem } from './statusbarItem.js'; import { StatusBarFocused } from '../../../common/contextkeys.js'; @@ -259,6 +259,7 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { // View model entry const viewModelEntry: IStatusbarViewModelEntry = new class implements IStatusbarViewModelEntry { readonly id = id; + readonly extensionId = entry.extensionId; readonly alignment = alignment; readonly priority = priority; readonly container = itemContainer; @@ -600,6 +601,9 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { if (statusEntryUnderMouse) { actions.push(new Separator()); + if (statusEntryUnderMouse.extensionId) { + actions.push(this.instantiationService.createInstance(ManageExtensionAction, statusEntryUnderMouse.extensionId)); + } actions.push(new HideStatusbarEntryAction(statusEntryUnderMouse.id, statusEntryUnderMouse.name, this.viewModel)); } diff --git a/src/vs/workbench/services/statusbar/browser/statusbar.ts b/src/vs/workbench/services/statusbar/browser/statusbar.ts index 7c81333e4162..61cc9f3029e9 100644 --- a/src/vs/workbench/services/statusbar/browser/statusbar.ts +++ b/src/vs/workbench/services/statusbar/browser/statusbar.ts @@ -204,6 +204,12 @@ export interface IStatusbarEntry { * the entry to new auxiliary windows opening. */ readonly showInAllWindows?: boolean; + + /** + * If provided, signals what extension is providing the status bar entry. This allows for + * more actions to manage the extension from the status bar entry. + */ + readonly extensionId?: string; } export interface IStatusbarEntryAccessor extends IDisposable { diff --git a/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts b/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts index c0a79706de56..426ef2e55e16 100644 --- a/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts +++ b/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { StatusbarViewModel } from '../../../../browser/parts/statusbar/statusbarModel.js'; +import { IStatusbarViewModelEntry, StatusbarViewModel } from '../../../../browser/parts/statusbar/statusbarModel.js'; import { TestStorageService } from '../../../common/workbenchTestServices.js'; import { StatusbarAlignment } from '../../../../services/statusbar/browser/statusbar.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; @@ -24,13 +24,13 @@ suite('Workbench status bar model', () => { assert.strictEqual(model.entries.length, 0); - const entry1 = { id: '3', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: 3, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + const entry1: IStatusbarViewModelEntry = { id: '3', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: 3, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry1); - const entry2 = { id: '2', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + const entry2: IStatusbarViewModelEntry = { id: '2', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry2); - const entry3 = { id: '1', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + const entry3: IStatusbarViewModelEntry = { id: '1', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry3); - const entry4 = { id: '1-right', alignment: StatusbarAlignment.RIGHT, name: '1-right', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + const entry4: IStatusbarViewModelEntry = { id: '1-right', alignment: StatusbarAlignment.RIGHT, name: '1-right', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry4); assert.strictEqual(model.entries.length, 4); @@ -84,9 +84,9 @@ suite('Workbench status bar model', () => { assert.strictEqual(model.entries.length, 0); - model.add({ id: '1', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: '2', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 2 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: '3', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: 1, secondary: 3 }, container, labelContainer: container, hasCommand: false }); + model.add({ id: '1', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: '2', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 2 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: '3', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: 1, secondary: 3 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); const entries = model.entries; assert.strictEqual(entries[0].id, '3'); @@ -100,9 +100,9 @@ suite('Workbench status bar model', () => { assert.strictEqual(model.entries.length, 0); - model.add({ id: '1', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: '2', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: '3', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); + model.add({ id: '1', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: '2', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: '3', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); const entries = model.entries; assert.strictEqual(entries[0].id, '1'); @@ -115,10 +115,10 @@ suite('Workbench status bar model', () => { const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); // Existing reference, Alignment: left - model.add({ id: 'a', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: 'b', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); + model.add({ id: 'a', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: 'b', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); - let entry = { id: 'c', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: { id: 'a', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + let entry = { id: 'c', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: { id: 'a', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry); let entries = model.entries; @@ -130,7 +130,7 @@ suite('Workbench status bar model', () => { model.remove(entry); // Existing reference, Alignment: right - entry = { id: 'c', alignment: StatusbarAlignment.RIGHT, name: '3', priority: { primary: { id: 'a', alignment: StatusbarAlignment.RIGHT }, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + entry = { id: 'c', alignment: StatusbarAlignment.RIGHT, name: '3', priority: { primary: { id: 'a', alignment: StatusbarAlignment.RIGHT }, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry); entries = model.entries; @@ -145,10 +145,10 @@ suite('Workbench status bar model', () => { const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); // Nonexistent reference, Alignment: left - model.add({ id: 'a', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: 'b', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); + model.add({ id: 'a', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: 'b', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); - let entry = { id: 'c', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: { id: 'not-existing', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + let entry = { id: 'c', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: { id: 'not-existing', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry); let entries = model.entries; @@ -160,7 +160,7 @@ suite('Workbench status bar model', () => { model.remove(entry); // Nonexistent reference, Alignment: right - entry = { id: 'c', alignment: StatusbarAlignment.RIGHT, name: '3', priority: { primary: { id: 'not-existing', alignment: StatusbarAlignment.RIGHT }, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + entry = { id: 'c', alignment: StatusbarAlignment.RIGHT, name: '3', priority: { primary: { id: 'not-existing', alignment: StatusbarAlignment.RIGHT }, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry); entries = model.entries; @@ -174,9 +174,9 @@ suite('Workbench status bar model', () => { const container = document.createElement('div'); const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); - model.add({ id: 'a', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: 'b', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: 'c', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: { id: 'not-existing', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false }); + model.add({ id: 'a', alignment: StatusbarAlignment.LEFT, name: '1', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: 'b', alignment: StatusbarAlignment.LEFT, name: '2', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: 'c', alignment: StatusbarAlignment.LEFT, name: '3', priority: { primary: { id: 'not-existing', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); let entries = model.entries; assert.strictEqual(entries.length, 3); @@ -184,7 +184,7 @@ suite('Workbench status bar model', () => { assert.strictEqual(entries[1].id, 'b'); assert.strictEqual(entries[2].id, 'c'); - const entry = { id: 'not-existing', alignment: StatusbarAlignment.LEFT, name: 'not-existing', priority: { primary: 3, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + const entry = { id: 'not-existing', alignment: StatusbarAlignment.LEFT, name: 'not-existing', priority: { primary: 3, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(entry); entries = model.entries; @@ -207,16 +207,16 @@ suite('Workbench status bar model', () => { const container = document.createElement('div'); const model = disposables.add(new StatusbarViewModel(disposables.add(new TestStorageService()))); - model.add({ id: '1-left', alignment: StatusbarAlignment.LEFT, name: '1-left', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: '2-left', alignment: StatusbarAlignment.LEFT, name: '2-left', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); + model.add({ id: '1-left', alignment: StatusbarAlignment.LEFT, name: '1-left', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: '2-left', alignment: StatusbarAlignment.LEFT, name: '2-left', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); - model.add({ id: '1-right', alignment: StatusbarAlignment.RIGHT, name: '1-right', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false }); - model.add({ id: '2-right', alignment: StatusbarAlignment.RIGHT, name: '2-right', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false }); + model.add({ id: '1-right', alignment: StatusbarAlignment.RIGHT, name: '1-right', priority: { primary: 2, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); + model.add({ id: '2-right', alignment: StatusbarAlignment.RIGHT, name: '2-right', priority: { primary: 1, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }); assert.strictEqual(model.getEntries(StatusbarAlignment.LEFT).length, 2); assert.strictEqual(model.getEntries(StatusbarAlignment.RIGHT).length, 2); - const relativeEntryLeft = { id: 'relative', alignment: StatusbarAlignment.LEFT, name: 'relative', priority: { primary: { id: '1-right', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + const relativeEntryLeft = { id: 'relative', alignment: StatusbarAlignment.LEFT, name: 'relative', priority: { primary: { id: '1-right', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(relativeEntryLeft); assert.strictEqual(model.getEntries(StatusbarAlignment.LEFT).length, 3); @@ -225,7 +225,7 @@ suite('Workbench status bar model', () => { model.remove(relativeEntryLeft); - const relativeEntryRight = { id: 'relative', alignment: StatusbarAlignment.RIGHT, name: 'relative', priority: { primary: { id: '1-right', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false }; + const relativeEntryRight = { id: 'relative', alignment: StatusbarAlignment.RIGHT, name: 'relative', priority: { primary: { id: '1-right', alignment: StatusbarAlignment.LEFT }, secondary: 1 }, container, labelContainer: container, hasCommand: false, extensionId: undefined }; model.add(relativeEntryRight); assert.strictEqual(model.getEntries(StatusbarAlignment.LEFT).length, 2); From de44151376b31f700a3f0a7f877766d61ce95ad7 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 13 Jan 2025 15:38:53 +0100 Subject: [PATCH 0512/3587] use the util (#237806) --- .../extensions/browser/extensionEditor.ts | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index a47806e80f40..8542167a79b3 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -87,7 +87,7 @@ import { IViewsService } from '../../../services/views/common/viewsService.js'; import { VIEW_ID as EXPLORER_VIEW_ID } from '../../files/common/files.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; +import { ByteSize, IFileService } from '../../../../platform/files/common/files.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; @@ -95,19 +95,6 @@ function toDateString(date: Date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}, ${date.toLocaleTimeString(language, { hourCycle: 'h23' })}`; } -function toMemoryString(bytes: number) { - if (bytes < 1024) { - return `${bytes} B`; - } - if (bytes < 1024 * 1024) { - return `${(bytes / 1024).toFixed(1)} KB`; - } - if (bytes < 1024 * 1024 * 1024) { - return `${(bytes / 1024 / 1024).toFixed(1)} MB`; - } - return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`; -} - class NavBar extends Disposable { private _onChange = this._register(new Emitter<{ id: string | null; focus: boolean }>()); @@ -1188,7 +1175,7 @@ class AdditionalDetailsWidget extends Disposable { } } if (extension.size) { - const element = $('div', undefined, toMemoryString(extension.size)); + const element = $('div', undefined, ByteSize.formatSize(extension.size)); append(installInfo, $('.more-info-entry', undefined, $('div.more-info-entry-name', { title: localize('size when installed', "Size when installed") }, localize('size', "Size")), @@ -1209,7 +1196,7 @@ class AdditionalDetailsWidget extends Disposable { if (!cacheSize) { return; } - const element = $('div', undefined, toMemoryString(cacheSize)); + const element = $('div', undefined, ByteSize.formatSize(cacheSize)); append(installInfo, $('.more-info-entry', undefined, $('div.more-info-entry-name', { title: localize('disk space used', "Cache size") }, localize('cache size', "Cache")), From 4375521987d80f0eecc8062325119fcb310f60ea Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 13 Jan 2025 15:47:49 +0100 Subject: [PATCH 0513/3587] clean up (#237807) --- .../browser/parts/compositeBarActions.ts | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index 726b76aeffd8..136b41c9800c 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -514,26 +514,11 @@ export class CompositeOverflowActivityActionViewItem extends CompositeBarActionV } } -class ManageExtensionAction extends Action { - - constructor( - @ICommandService private readonly commandService: ICommandService - ) { - super('activitybar.manage.extension', localize('manageExtension', "Manage Extension")); - } - - override run(id: string): Promise { - return this.commandService.executeCommand('_extensions.manage', id); - } -} - export class CompositeActionViewItem extends CompositeBarActionViewItem { - private static manageExtensionAction: ManageExtensionAction; - constructor( options: ICompositeBarActionViewItemOptions, - private readonly compositeActivityAction: CompositeBarAction, + compositeActivityAction: CompositeBarAction, private readonly toggleCompositePinnedAction: IAction, private readonly toggleCompositeBadgeAction: IAction, private readonly compositeContextMenuActionsProvider: (compositeId: string) => IAction[], @@ -557,10 +542,6 @@ export class CompositeActionViewItem extends CompositeBarActionViewItem { configurationService, keybindingService ); - - if (!CompositeActionViewItem.manageExtensionAction) { - CompositeActionViewItem.manageExtensionAction = instantiationService.createInstance(ManageExtensionAction); - } } override render(container: HTMLElement): void { @@ -669,11 +650,6 @@ export class CompositeActionViewItem extends CompositeBarActionViewItem { actions.push(...compositeContextMenuActions); } - if ((this.compositeActivityAction.compositeBarActionItem).extensionId) { - actions.push(new Separator()); - actions.push(CompositeActionViewItem.manageExtensionAction); - } - const isPinned = this.compositeBar.isPinned(this.compositeBarActionItem.id); if (isPinned) { this.toggleCompositePinnedAction.label = localize('hide', "Hide '{0}'", this.compositeBarActionItem.name); From a4ba0d716a4873a641df653cf004d07d3e25ea6f Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:50:10 +0100 Subject: [PATCH 0514/3587] Improve inline ghost text styling with border (#237808) Green Inline ghost text with border --- .../browser/model/inlineCompletionsModel.ts | 2 +- .../browser/view/inlineEdits/inlineDiffView.ts | 2 +- .../inlineCompletions/browser/view/inlineEdits/view.css | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index 111013977f67..d1e6cbd30045 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -58,7 +58,7 @@ export class InlineCompletionsModel extends Disposable { private readonly _acceptCompletionDecorationTimer = this._register(new MutableDisposable()); private readonly _acceptCompletionDecoration: IModelDecorationOptions = { description: 'inline-completion-accepted', - className: 'inlineCompletionAccepted', + className: 'inline-completion-accepted', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts index 38343311a3e2..095a75807498 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts @@ -214,7 +214,7 @@ export class OriginalEditorInlineDiffView extends Disposable { description: 'inserted-text', before: { content: insertedText, - inlineClassName: diff.mode === 'ghostText' ? 'ghost-text-decoration' : 'inlineCompletions-char-insert', + inlineClassName: diff.mode === 'ghostText' ? 'inlineCompletions-char-insert inline' : 'inlineCompletions-char-insert', }, zIndex: 2, showIfCollapsed: true, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index 05fc3ffa137e..7f47c69720ba 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -164,6 +164,13 @@ .inlineCompletions-char-insert.diff-range-empty { border-left: solid var(--vscode-inlineEdit-modifiedChangedTextBackground) 3px; } + + .inlineCompletions-char-delete.inline, + .inlineCompletions-char-insert.inline { + border-radius: 4px; + border: 1px solid var(--vscode-editorHoverWidget-border); + padding: 2px; + } } .monaco-menu-option { @@ -185,6 +192,6 @@ } } -.monaco-editor .inlineCompletionAccepted { +.monaco-editor .inline-completion-accepted { background-color: var(--vscode-inlineEdit-acceptedBackground); } From 78f24d856a93eff2bb7abccba972df226dafb2fa Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 13 Jan 2025 16:51:03 +0100 Subject: [PATCH 0515/3587] close all diff editors on accept/discard (#237809) --- .../chat/browser/chatEditorController.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index 78a198389f66..f265c823433a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -32,9 +32,9 @@ import { observableCodeEditor } from '../../../../editor/browser/observableCodeE import { minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../../scm/common/quickDiff.js'; import { DetailedLineRangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; import { isDiffEditorForEntry } from './chatEditing/chatEditing.js'; -import { basename } from '../../../../base/common/resources.js'; +import { basename, isEqual } from '../../../../base/common/resources.js'; import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; -import { IEditorIdentifier } from '../../../common/editor.js'; +import { EditorsOrder, IEditorIdentifier, isDiffEditorInput } from '../../../common/editor.js'; export const ctxHasEditorModification = new RawContextKey('chat.hasEditorModifications', undefined, localize('chat.hasEditorModifications', "The current editor contains chat modifications")); export const ctxHasRequestInProgress = new RawContextKey('chat.ctxHasRequestInProgress', false, localize('chat.ctxHasRequestInProgress', "The current editor shows a file from an edit session which is still in progress")); @@ -599,7 +599,6 @@ export class ChatEditorController extends Disposable implements IEditorContribut }); if (diffEditor && diffEditor.input) { - const editorIdent: IEditorIdentifier = { editor: diffEditor.input, groupId: diffEditor.group.id }; // this is needed, passing the selection doesn't seem to work diffEditor.getControl()?.setSelection(selection); @@ -609,7 +608,18 @@ export class ChatEditorController extends Disposable implements IEditorContribut const state = entry.state.read(r); if (state === WorkingSetEntryState.Accepted || state === WorkingSetEntryState.Rejected) { d.dispose(); - this._editorService.closeEditor(editorIdent); + + const editorIdents: IEditorIdentifier[] = []; + for (const candidate of this._editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { + if (isDiffEditorInput(candidate.editor) + && isEqual(candidate.editor.original.resource, entry.originalURI) + && isEqual(candidate.editor.modified.resource, entry.modifiedURI) + ) { + editorIdents.push(candidate); + } + } + + this._editorService.closeEditors(editorIdents); } }); } From eaa1502b23c52fbdf933bad1a179bb82c7ae0697 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 13 Jan 2025 17:20:46 +0100 Subject: [PATCH 0516/3587] Git - add more commands to the graph context menu (#237811) * Git - add "Delete Branch" and "Delete Tag" actions * Git - update the graph cherry pick command --- extensions/git/package.json | 56 +++++++++++++++++++++++++-------- extensions/git/package.nls.json | 8 +++-- extensions/git/src/commands.ts | 34 +++++++++++++++++--- 3 files changed, 77 insertions(+), 21 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 63dcda18c769..db47053cd32c 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -448,14 +448,14 @@ "enablement": "!operationInProgress" }, { - "command": "git.checkoutDetached", - "title": "%command.checkoutDetached%", + "command": "git.graph.checkout", + "title": "%command.graphCheckout%", "category": "Git", "enablement": "!operationInProgress" }, { - "command": "git.graph.checkout", - "title": "%command.graphCheckout%", + "command": "git.checkoutDetached", + "title": "%command.checkoutDetached%", "category": "Git", "enablement": "!operationInProgress" }, @@ -483,6 +483,12 @@ "category": "Git", "enablement": "!operationInProgress" }, + { + "command": "git.graph.deleteBranch", + "title": "%command.graphDeleteBranch%", + "category": "Git", + "enablement": "!operationInProgress" + }, { "command": "git.renameBranch", "title": "%command.renameBranch%", @@ -519,6 +525,12 @@ "category": "Git", "enablement": "!operationInProgress" }, + { + "command": "git.graph.deleteTag", + "title": "%command.graphDeleteTag%", + "category": "Git", + "enablement": "!operationInProgress" + }, { "command": "git.deleteRemoteTag", "title": "%command.deleteRemoteTag%", @@ -632,8 +644,8 @@ "enablement": "!operationInProgress" }, { - "command": "git.cherryPickRef", - "title": "%command.cherryPickRef%", + "command": "git.graph.cherryPick", + "title": "%command.graphCherryPick%", "category": "Git", "enablement": "!operationInProgress" }, @@ -1474,7 +1486,15 @@ "when": "false" }, { - "command": "git.cherryPickRef", + "command": "git.graph.deleteBranch", + "when": "false" + }, + { + "command": "git.graph.deleteTag", + "when": "false" + }, + { + "command": "git.graph.cherryPick", "when": "false" }, { @@ -2002,19 +2022,19 @@ "group": "1_checkout@2" }, { - "command": "git.createTag", + "command": "git.branch", "when": "scmProvider == git", - "group": "2_create@1" + "group": "2_branch@2" }, { - "command": "git.branch", + "command": "git.createTag", "when": "scmProvider == git", - "group": "2_create@2" + "group": "3_tag@1" }, { - "command": "git.cherryPickRef", + "command": "git.graph.cherryPick", "when": "scmProvider == git", - "group": "3_modify@1" + "group": "4_modify@1" }, { "command": "git.copyCommitId", @@ -2032,6 +2052,16 @@ "command": "git.graph.checkout", "when": "scmProvider == git", "group": "1_checkout@1" + }, + { + "command": "git.graph.deleteBranch", + "when": "scmProvider == git && scmHistoryItemRef =~ /^refs\\/heads\\//", + "group": "2_branch@2" + }, + { + "command": "git.graph.deleteTag", + "when": "scmProvider == git && scmHistoryItemRef =~ /^refs\\/tags\\//", + "group": "3_tag@2" } ], "editor/title": [ diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index e27b66875d0b..61168a79c344 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -62,14 +62,11 @@ "command.undoCommit": "Undo Last Commit", "command.checkout": "Checkout to...", "command.checkoutDetached": "Checkout to (Detached)...", - "command.graphCheckout": "Checkout", - "command.graphCheckoutDetached": "Checkout (Detached)", "command.branch": "Create Branch...", "command.branchFrom": "Create Branch From...", "command.deleteBranch": "Delete Branch...", "command.renameBranch": "Rename Branch...", "command.cherryPick": "Cherry Pick...", - "command.cherryPickRef": "Cherry Pick", "command.cherryPickAbort": "Abort Cherry Pick", "command.merge": "Merge...", "command.mergeAbort": "Abort Merge", @@ -126,6 +123,11 @@ "command.viewStagedChanges": "Open Staged Changes", "command.viewUntrackedChanges": "Open Untracked Changes", "command.viewCommit": "Open Commit", + "command.graphCheckout": "Checkout", + "command.graphCheckoutDetached": "Checkout (Detached)", + "command.graphCherryPick": "Cherry Pick", + "command.graphDeleteBranch": "Delete Branch", + "command.graphDeleteTag": "Delete Tag", "command.api.getRepositories": "Get Repositories", "command.api.getRepositoryState": "Get Repository State", "command.api.getRemoteSources": "Get Remote Sources", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 584eec49422e..26d0a1a1c3da 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -2875,10 +2875,24 @@ export class CommandCenter { } @command('git.deleteBranch', { repository: true }) - async deleteBranch(repository: Repository, name: string, force?: boolean): Promise { + async deleteBranch(repository: Repository, name: string | undefined, force?: boolean): Promise { + await this._deleteBranch(repository, name, force); + } + + @command('git.graph.deleteBranch', { repository: true }) + async deleteBranch2(repository: Repository, historyItem?: SourceControlHistoryItem, historyItemRefId?: string): Promise { + const historyItemRef = historyItem?.references?.find(r => r.id === historyItemRefId); + if (!historyItemRef) { + return; + } + + await this._deleteBranch(repository, historyItemRef.name); + } + + private async _deleteBranch(repository: Repository, name: string | undefined, force?: boolean): Promise { let run: (force?: boolean) => Promise; if (typeof name === 'string') { - run = force => repository.deleteBranch(name, force); + run = force => repository.deleteBranch(name!, force); } else { const getBranchPicks = async () => { const refs = await repository.getRefs({ pattern: 'refs/heads' }); @@ -3014,12 +3028,21 @@ export class CommandCenter { const placeHolder = l10n.t('Select a tag to delete'); const choice = await this.pickRef(tagPicks(), placeHolder); - if (choice instanceof TagDeleteItem) { await choice.run(repository); } } + @command('git.graph.deleteTag', { repository: true }) + async deleteTag2(repository: Repository, historyItem?: SourceControlHistoryItem, historyItemRefId?: string): Promise { + const historyItemRef = historyItem?.references?.find(r => r.id === historyItemRefId); + if (!historyItemRef) { + return; + } + + await repository.deleteTag(historyItemRef.name); + } + @command('git.deleteRemoteTag', { repository: true }) async deleteRemoteTag(repository: Repository): Promise { const remotePicks = repository.remotes @@ -3378,11 +3401,12 @@ export class CommandCenter { await repository.cherryPick(hash); } - @command('git.cherryPickRef', { repository: true }) - async cherryPickRef(repository: Repository, historyItem?: SourceControlHistoryItem): Promise { + @command('git.graph.cherryPick', { repository: true }) + async cherryPick2(repository: Repository, historyItem?: SourceControlHistoryItem): Promise { if (!historyItem) { return; } + await repository.cherryPick(historyItem.id); } From 10afa670b5be1487f778342efaf7cc371113be8e Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 13 Jan 2025 17:37:10 +0100 Subject: [PATCH 0517/3587] refactor ouput channel model to support model with multiple files (#237814) * refactor ouput channel model to support model with multiple files * do not start polling immediately --- .../output/common/outputChannelModel.ts | 303 ++++++++++-------- 1 file changed, 174 insertions(+), 129 deletions(-) diff --git a/src/vs/workbench/contrib/output/common/outputChannelModel.ts b/src/vs/workbench/contrib/output/common/outputChannelModel.ts index 8d8134011130..f4416c250bde 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModel.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModel.ts @@ -9,11 +9,11 @@ import { ITextModel } from '../../../../editor/common/model.js'; import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { URI } from '../../../../base/common/uri.js'; -import { Promises, ThrottledDelayer } from '../../../../base/common/async.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; +import { ThrottledDelayer } from '../../../../base/common/async.js'; +import { FileOperationResult, IFileService, toFileOperationResult } from '../../../../platform/files/common/files.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { ILanguageSelection } from '../../../../editor/common/languages/language.js'; -import { Disposable, toDisposable, IDisposable, dispose, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, toDisposable, IDisposable, MutableDisposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { isNumber } from '../../../../base/common/types.js'; import { EditOperation, ISingleEditOperation } from '../../../../editor/common/core/editOperation.js'; import { Position } from '../../../../editor/common/core/position.js'; @@ -33,31 +33,63 @@ export interface IOutputChannelModel extends IDisposable { replace(value: string): void; } -class OutputFileListener extends Disposable { +interface IContentProvider { + readonly onDidAppend: Event; + readonly onDidReset: Event; + reset(): void; + watch(): IDisposable; + getContent(): Promise<{ readonly content: string; readonly consume: () => void }>; +} + +class FileContentProvider extends Disposable implements IContentProvider { - private readonly _onDidContentChange = new Emitter(); - readonly onDidContentChange: Event = this._onDidContentChange.event; + private readonly _onDidAppend = new Emitter(); + readonly onDidAppend = this._onDidAppend.event; + + private readonly _onDidReset = new Emitter(); + readonly onDidReset = this._onDidReset.event; private watching: boolean = false; private syncDelayer: ThrottledDelayer; - private etag: string | undefined; + private etag: string | undefined = ''; + + private startOffset: number = 0; + private endOffset: number = 0; constructor( private readonly file: URI, - private readonly fileService: IFileService, - private readonly logService: ILogService + @IFileService private readonly fileService: IFileService, + @ILogService private readonly logService: ILogService, ) { super(); + this.syncDelayer = new ThrottledDelayer(500); + this._register(toDisposable(() => this.unwatch())); + } + + reset(offset?: number): void { + this.endOffset = this.startOffset = offset ?? this.startOffset; } - watch(eTag: string | undefined): void { + resetToEnd(): void { + this.startOffset = this.endOffset; + } + + watch(): IDisposable { if (!this.watching) { - this.etag = eTag; - this.poll(); this.logService.trace('Started polling', this.file.toString()); + this.poll(); this.watching = true; } + return toDisposable(() => this.unwatch()); + } + + private unwatch(): void { + if (this.watching) { + this.syncDelayer.cancel(); + this.watching = false; + this.logService.trace('Stopped polling', this.file.toString()); + } } private poll(): void { @@ -70,118 +102,106 @@ class OutputFileListener extends Disposable { } private async doWatch(): Promise { - const stat = await this.fileService.stat(this.file); - if (stat.etag !== this.etag) { - this.etag = stat.etag; - this._onDidContentChange.fire(stat.size); + try { + const stat = await this.fileService.stat(this.file); + if (stat.etag !== this.etag) { + this.etag = stat.etag; + if (isNumber(stat.size) && this.endOffset > stat.size) { + this.reset(0); + this._onDidReset.fire(); + } else { + this._onDidAppend.fire(); + } + } + } catch (error) { + if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { + throw error; + } } } - unwatch(): void { - if (this.watching) { - this.syncDelayer.cancel(); - this.watching = false; - this.logService.trace('Stopped polling', this.file.toString()); + async getContent(): Promise<{ readonly content: string; readonly consume: () => void }> { + try { + const content = await this.fileService.readFile(this.file, { position: this.endOffset }); + let consumed = false; + return { + content: content.value.toString(), + consume: () => { + if (!consumed) { + consumed = true; + this.endOffset += content.value.byteLength; + this.etag = content.etag; + } + } + }; + } catch (error) { + if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { + throw error; + } + return { + content: '', + consume: () => { /* No Op */ } + }; } } - - override dispose(): void { - this.unwatch(); - super.dispose(); - } } -export class FileOutputChannelModel extends Disposable implements IOutputChannelModel { +export abstract class AbstractFileOutputChannelModel extends Disposable implements IOutputChannelModel { private readonly _onDispose = this._register(new Emitter()); readonly onDispose: Event = this._onDispose.event; - private readonly fileHandler: OutputFileListener; - private etag: string | undefined = ''; - - private loadModelPromise: Promise | null = null; - private model: ITextModel | null = null; + private readonly modelDisposable = this._register(new MutableDisposable()); + protected model: ITextModel | null = null; private modelUpdateInProgress: boolean = false; private readonly modelUpdateCancellationSource = this._register(new MutableDisposable()); private readonly appendThrottler = this._register(new ThrottledDelayer(300)); private replacePromise: Promise | undefined; - private startOffset: number = 0; - private endOffset: number = 0; - constructor( private readonly modelUri: URI, private readonly language: ILanguageSelection, - private readonly file: URI, - @IFileService private readonly fileService: IFileService, - @IModelService private readonly modelService: IModelService, - @ILogService logService: ILogService, + private readonly outputContentProvider: IContentProvider, + @IModelService protected readonly modelService: IModelService, @IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService, ) { super(); - - this.fileHandler = this._register(new OutputFileListener(this.file, this.fileService, logService)); - this._register(this.fileHandler.onDidContentChange(size => this.onDidContentChange(size))); - this._register(toDisposable(() => this.fileHandler.unwatch())); - } - - append(message: string): void { - throw new Error('Not supported'); } - replace(message: string): void { - throw new Error('Not supported'); - } - - clear(): void { - this.update(OutputChannelUpdateMode.Clear, this.endOffset, true); - } - - update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void { - const loadModelPromise: Promise = this.loadModelPromise ? this.loadModelPromise : Promise.resolve(); - loadModelPromise.then(() => this.doUpdate(mode, till, immediate)); - } - - loadModel(): Promise { - this.loadModelPromise = Promises.withAsyncBody(async (c, e) => { - try { - let content = ''; - if (await this.fileService.exists(this.file)) { - const fileContent = await this.fileService.readFile(this.file, { position: this.startOffset }); - this.endOffset = this.startOffset + fileContent.value.byteLength; - this.etag = fileContent.etag; - content = fileContent.value.toString(); - } else { - this.startOffset = 0; - this.endOffset = 0; - } - c(this.createModel(content)); - } catch (error) { - e(error); - } - }); - return this.loadModelPromise; - } - - private createModel(content: string): ITextModel { - if (this.model) { - this.model.setValue(content); - } else { - this.model = this.modelService.createModel(content, this.language, this.modelUri); - this.fileHandler.watch(this.etag); - const disposable = this.model.onWillDispose(() => { + async loadModel(): Promise { + if (!this.model) { + this.modelDisposable.value = new DisposableStore(); + this.model = this.modelService.createModel('', this.language, this.modelUri); + this.outputContentProvider.getContent() + .then(({ content, consume }) => { + if (!this.model || !this.modelDisposable.value) { + return; + } + this.doAppendContent(this.model, content); + consume(); + this.modelDisposable.value.add(this.outputContentProvider.onDidReset(() => this.onDidContentChange(true, true))); + this.modelDisposable.value.add(this.outputContentProvider.onDidAppend(() => this.onDidContentChange(false, false))); + this.modelDisposable.value.add(this.outputContentProvider.watch()); + }); + this.modelDisposable.value.add(this.model.onWillDispose(() => { + this.outputContentProvider.reset(); + this.modelDisposable.value = undefined; this.cancelModelUpdate(); - this.fileHandler.unwatch(); this.model = null; - dispose(disposable); - }); + })); } return this.model; } - private doUpdate(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void { + private onDidContentChange(reset: boolean, appendImmediately: boolean): void { + if (reset && !this.modelUpdateInProgress) { + this.doUpdate(OutputChannelUpdateMode.Clear, true); + } + this.doUpdate(OutputChannelUpdateMode.Append, appendImmediately); + } + + protected doUpdate(mode: OutputChannelUpdateMode, immediate: boolean): void { if (mode === OutputChannelUpdateMode.Clear || mode === OutputChannelUpdateMode.Replace) { - this.startOffset = this.endOffset = isNumber(till) ? till : this.endOffset; this.cancelModelUpdate(); } if (!this.model) { @@ -208,7 +228,8 @@ export class FileOutputChannelModel extends Disposable implements IOutputChannel } private clearContent(model: ITextModel): void { - this.doUpdateModel(model, [EditOperation.delete(model.getFullModelRange())], VSBuffer.fromString('')); + model.applyEdits([EditOperation.delete(model.getFullModelRange())]); + this.modelUpdateInProgress = false; } private appendContent(model: ITextModel, immediate: boolean, token: CancellationToken): void { @@ -228,17 +249,16 @@ export class FileOutputChannelModel extends Disposable implements IOutputChannel } /* Get content to append */ - const contentToAppend = await this.getContentToUpdate(); + const { content, consume } = await this.outputContentProvider.getContent(); /* Abort if operation is cancelled */ if (token.isCancellationRequested) { return; } /* Appned Content */ - const lastLine = model.getLineCount(); - const lastLineMaxColumn = model.getLineMaxColumn(lastLine); - const edits = [EditOperation.insert(new Position(lastLine, lastLineMaxColumn), contentToAppend.toString())]; - this.doUpdateModel(model, edits, contentToAppend); + this.doAppendContent(model, content); + consume(); + this.modelUpdateInProgress = false; }, immediate ? 0 : undefined).catch(error => { if (!isCancellationError(error)) { throw error; @@ -246,23 +266,33 @@ export class FileOutputChannelModel extends Disposable implements IOutputChannel }); } + private doAppendContent(model: ITextModel, content: string): void { + const lastLine = model.getLineCount(); + const lastLineMaxColumn = model.getLineMaxColumn(lastLine); + model.applyEdits([EditOperation.insert(new Position(lastLine, lastLineMaxColumn), content)]); + } + private async replaceContent(model: ITextModel, token: CancellationToken): Promise { /* Get content to replace */ - const contentToReplace = await this.getContentToUpdate(); + const { content, consume } = await this.outputContentProvider.getContent(); /* Abort if operation is cancelled */ if (token.isCancellationRequested) { return; } /* Compute Edits */ - const edits = await this.getReplaceEdits(model, contentToReplace.toString()); + const edits = await this.getReplaceEdits(model, content.toString()); /* Abort if operation is cancelled */ if (token.isCancellationRequested) { return; } - /* Apply Edits */ - this.doUpdateModel(model, edits, contentToReplace); + if (edits.length) { + /* Apply Edits */ + model.applyEdits(edits); + } + consume(); + this.modelUpdateInProgress = false; } private async getReplaceEdits(model: ITextModel, contentToReplace: string): Promise { @@ -278,14 +308,6 @@ export class FileOutputChannelModel extends Disposable implements IOutputChannel return []; } - private doUpdateModel(model: ITextModel, edits: ISingleEditOperation[], content: VSBuffer): void { - if (edits.length) { - model.applyEdits(edits); - } - this.endOffset = this.endOffset + content.byteLength; - this.modelUpdateInProgress = false; - } - protected cancelModelUpdate(): void { this.modelUpdateCancellationSource.value?.cancel(); this.modelUpdateCancellationSource.value = undefined; @@ -294,24 +316,6 @@ export class FileOutputChannelModel extends Disposable implements IOutputChannel this.modelUpdateInProgress = false; } - private async getContentToUpdate(): Promise { - const content = await this.fileService.readFile(this.file, { position: this.endOffset }); - this.etag = content.etag; - return content.value; - } - - private onDidContentChange(size: number | undefined): void { - if (this.model) { - if (!this.modelUpdateInProgress) { - if (isNumber(size) && this.endOffset > size) { - // Reset - Content is removed - this.update(OutputChannelUpdateMode.Clear, 0, true); - } - } - this.update(OutputChannelUpdateMode.Append, undefined, false /* Not needed to update immediately. Wait to collect more changes and update. */); - } - } - protected isVisible(): boolean { return !!this.model; } @@ -320,6 +324,47 @@ export class FileOutputChannelModel extends Disposable implements IOutputChannel this._onDispose.fire(); super.dispose(); } + + append(message: string): void { throw new Error('Not supported'); } + replace(message: string): void { throw new Error('Not supported'); } + + abstract clear(): void; + abstract update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void; +} + +export class FileOutputChannelModel extends AbstractFileOutputChannelModel implements IOutputChannelModel { + + private readonly fileOutput: FileContentProvider; + + constructor( + modelUri: URI, + language: ILanguageSelection, + file: URI, + @IFileService fileService: IFileService, + @IModelService modelService: IModelService, + @ILogService logService: ILogService, + @IEditorWorkerService editorWorkerService: IEditorWorkerService, + ) { + const fileOutput = new FileContentProvider(file, fileService, logService); + super(modelUri, language, fileOutput, modelService, editorWorkerService); + this.fileOutput = this._register(fileOutput); + } + + override clear(): void { + this.update(OutputChannelUpdateMode.Clear, undefined, true); + } + + override update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void { + if (mode === OutputChannelUpdateMode.Clear || mode === OutputChannelUpdateMode.Replace) { + if (isNumber(till)) { + this.fileOutput.reset(till); + } else { + this.fileOutput.resetToEnd(); + } + } + this.doUpdate(mode, immediate); + } + } class OutputChannelBackedByFile extends FileOutputChannelModel implements IOutputChannelModel { From 56f135e366811556623a96a81f1419b77b9a2091 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 13 Jan 2025 17:42:16 +0100 Subject: [PATCH 0518/3587] debt - let `ChatEditorController` also control the chat overlay, remove `ChatEditorOverlayController` (#237816) --- .../contrib/chat/browser/chat.contribution.ts | 2 - .../chat/browser/chatEditorController.ts | 31 ++++++-- .../contrib/chat/browser/chatEditorOverlay.ts | 75 +------------------ 3 files changed, 27 insertions(+), 81 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index a43f6b50cacf..88fb0c5ba8e5 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -78,7 +78,6 @@ import { ChatViewsWelcomeHandler } from './viewsWelcome/chatViewsWelcomeHandler. import { ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService } from '../common/ignoredFiles.js'; import { ChatGettingStartedContribution } from './actions/chatGettingStarted.js'; import { Extensions, IConfigurationMigrationRegistry } from '../../../common/configuration.js'; -import { ChatEditorOverlayController } from './chatEditorOverlay.js'; import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesContrib.js'; import { ChatQuotasService, ChatQuotasStatusBarEntry, IChatQuotasService } from './chatQuotasService.js'; import { BuiltinToolsContribution } from './tools/tools.js'; @@ -340,7 +339,6 @@ registerChatEditorActions(); registerEditorFeature(ChatPasteProvidersFeature); registerEditorContribution(ChatEditorController.ID, ChatEditorController, EditorContributionInstantiation.Eventually); -registerEditorContribution(ChatEditorOverlayController.ID, ChatEditorOverlayController, EditorContributionInstantiation.Eventually); registerSingleton(IChatService, ChatService, InstantiationType.Delayed); registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index f265c823433a..03ea41e0ba40 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -35,6 +35,7 @@ import { isDiffEditorForEntry } from './chatEditing/chatEditing.js'; import { basename, isEqual } from '../../../../base/common/resources.js'; import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; import { EditorsOrder, IEditorIdentifier, isDiffEditorInput } from '../../../common/editor.js'; +import { ChatEditorOverlayWidget } from './chatEditorOverlay.js'; export const ctxHasEditorModification = new RawContextKey('chat.hasEditorModifications', undefined, localize('chat.hasEditorModifications', "The current editor contains chat modifications")); export const ctxHasRequestInProgress = new RawContextKey('chat.ctxHasRequestInProgress', false, localize('chat.ctxHasRequestInProgress', "The current editor shows a file from an edit session which is still in progress")); @@ -51,6 +52,9 @@ export class ChatEditorController extends Disposable implements IEditorContribut private readonly _diffHunkWidgets: DiffHunkWidget[] = []; private _viewZones: string[] = []; + + private readonly _overlayWidget: ChatEditorOverlayWidget; + private readonly _ctxHasEditorModification: IContextKey; private readonly _ctxRequestInProgress: IContextKey; @@ -77,6 +81,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut ) { super(); + this._overlayWidget = _instantiationService.createInstance(ChatEditorOverlayWidget, _editor); this._ctxHasEditorModification = ctxHasEditorModification.bindTo(contextKeyService); this._ctxRequestInProgress = ctxHasRequestInProgress.bindTo(contextKeyService); @@ -98,11 +103,16 @@ export class ChatEditorController extends Disposable implements IEditorContribut if (!session) { return undefined; } - const entry = model?.uri ? session.readEntry(model.uri, r) : undefined; - if (!entry || entry.state.read(r) !== WorkingSetEntryState.Modified) { + + const entries = session.entries.read(r); + const idx = model?.uri + ? entries.findIndex(e => isEqual(e.modifiedURI, model.uri)) + : -1; + + if (idx < 0) { return undefined; } - return { session, entry }; + return { session, entry: entries[idx], entries, idx }; }); @@ -123,11 +133,19 @@ export class ChatEditorController extends Disposable implements IEditorContribut return; } - const { session, entry } = currentEditorEntry; + const { session, entries, idx, entry } = currentEditorEntry; - const entryIndex = session.entries.read(r).indexOf(entry); - this._currentEntryIndex.set(entryIndex, undefined); + // context + this._currentEntryIndex.set(idx, undefined); + + // overlay widget + if (entry.state.read(r) === WorkingSetEntryState.Accepted || entry.state.read(r) === WorkingSetEntryState.Rejected) { + this._overlayWidget.hide(); + } else { + this._overlayWidget.show(session, entry, entries[(idx + 1) % entries.length]); + } + // scrolling logic if (entry.isCurrentlyBeingModified.read(r)) { // while modified: scroll along unless locked if (!this._scrollLock) { @@ -215,6 +233,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut private _clear() { this._clearDiffRendering(); + this._overlayWidget.hide(); this._diffLineDecorations.clear(); this._currentChangeIndex.set(undefined, undefined); this._currentEntryIndex.set(undefined, undefined); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index f0c1ca0a91a7..304a02f01cb0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -5,12 +5,10 @@ import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { autorun, observableFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; -import { isEqual } from '../../../../base/common/resources.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js'; -import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar, WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ChatEditingSessionState, IChatEditingService, IChatEditingSession, IModifiedFileEntry, WorkingSetEntryState } from '../common/chatEditingService.js'; +import { IChatEditingSession, IModifiedFileEntry } from '../common/chatEditingService.js'; import { MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js'; @@ -25,12 +23,10 @@ import { localize } from '../../../../nls.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { AcceptAction, RejectAction } from './chatEditorActions.js'; import { ChatEditorController } from './chatEditorController.js'; -import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; -import { isDiffEditorForEntry } from './chatEditing/chatEditing.js'; import './media/chatEditorOverlay.css'; import { findDiffEditorContainingCodeEditor } from '../../../../editor/browser/widget/diffEditor/commands.js'; -class ChatEditorOverlayWidget implements IOverlayWidget { +export class ChatEditorOverlayWidget implements IOverlayWidget { readonly allowEditorOverflow = true; @@ -274,70 +270,3 @@ MenuRegistry.appendMenuItem(MenuId.ChatEditingEditorContent, { group: 'navigate', order: -1 }); - -export class ChatEditorOverlayController implements IEditorContribution { - - static readonly ID = 'editor.contrib.chatOverlayController'; - - private readonly _store = new DisposableStore(); - - static get(editor: ICodeEditor) { - return editor.getContribution(ChatEditorOverlayController.ID); - } - - constructor( - private readonly _editor: ICodeEditor, - @IChatEditingService chatEditingService: IChatEditingService, - @IInstantiationService instaService: IInstantiationService - ) { - const modelObs = observableFromEvent(this._editor.onDidChangeModel, () => this._editor.getModel()); - const widget = this._store.add(instaService.createInstance(ChatEditorOverlayWidget, this._editor)); - - - this._store.add(autorun(r => { - const model = modelObs.read(r); - const session = chatEditingService.currentEditingSessionObs.read(r); - if (!session || !model) { - widget.hide(); - return; - } - - const state = session.state.read(r); - if (state === ChatEditingSessionState.Disposed) { - widget.hide(); - return; - } - - const entries = session.entries.read(r); - const idx = entries.findIndex(e => isEqual(e.modifiedURI, model.uri)); - if (idx < 0) { - widget.hide(); - return; - } - - const entry = entries[idx]; - - if (this._editor.getOption(EditorOption.inDiffEditor) && !instaService.invokeFunction(isDiffEditorForEntry, entry, this._editor)) { - widget.hide(); - return; - } - - const isModifyingOrModified = entries.some(e => e.state.read(r) === WorkingSetEntryState.Modified || e.isCurrentlyBeingModified.read(r)); - if (!isModifyingOrModified) { - widget.hide(); - return; - } - - if (entry.state.read(r) === WorkingSetEntryState.Accepted || entry.state.read(r) === WorkingSetEntryState.Rejected) { - widget.hide(); - return; - } - widget.show(session, entry, entries[(idx + 1) % entries.length]); - - })); - } - - dispose() { - this._store.dispose(); - } -} From 99bcf08774784dedbb5e19b5ee332e7169a7159d Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 13 Jan 2025 10:44:26 -0600 Subject: [PATCH 0519/3587] rm setting provider as it's already set (#237817) --- .../suggest/browser/terminalCompletionService.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 9f874a9fbc45..c7af21695a13 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -164,9 +164,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return undefined; } const completionItems = Array.isArray(completions) ? completions : completions.items ?? []; - for (const item of completionItems) { - item.provider = provider.id; - } if (Array.isArray(completions)) { return completionItems; @@ -185,7 +182,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return results.filter(result => !!result).flat(); } - async resolveResources(resourceRequestConfig: TerminalResourceRequestConfig, promptValue: string, cursorPosition: number, providerId: string): Promise { + async resolveResources(resourceRequestConfig: TerminalResourceRequestConfig, promptValue: string, cursorPosition: number, provider: string): Promise { const cwd = URI.revive(resourceRequestConfig.cwd); const foldersRequested = resourceRequestConfig.foldersRequested ?? false; const filesRequested = resourceRequestConfig.filesRequested ?? false; @@ -236,7 +233,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo } resourceCompletions.push({ label, - provider: providerId, + provider, kind, isDirectory, isFile: kind === TerminalCompletionItemKind.File, From c9209bef9df0a5e1bd9d961139e1f6b348dca713 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 13 Jan 2025 09:42:20 -0800 Subject: [PATCH 0520/3587] Support opacity in inline decorations Fixes #233990 --- .../browser/gpu/css/decorationStyleCache.ts | 13 +++++++++++-- .../browser/gpu/fullFileRenderStrategy.ts | 19 ++++++++++++++++++- .../browser/gpu/raster/glyphRasterizer.ts | 7 +++++++ src/vs/editor/browser/gpu/viewGpuContext.ts | 1 + 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts index 5e023e840784..879b72470f14 100644 --- a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts +++ b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts @@ -12,6 +12,10 @@ export interface IDecorationStyleSet { * Whether the text should be rendered in bold. */ bold: boolean | undefined; + /** + * A number between 0 and 1 representing the opacity of the text. + */ + opacity: number | undefined; } export interface IDecorationStyleCacheEntry extends IDecorationStyleSet { @@ -27,8 +31,12 @@ export class DecorationStyleCache { private readonly _cache = new Map(); - getOrCreateEntry(color: number | undefined, bold: boolean | undefined): number { - if (color === undefined && bold === undefined) { + getOrCreateEntry( + color: number | undefined, + bold: boolean | undefined, + opacity: number | undefined + ): number { + if (color === undefined && bold === undefined && opacity === undefined) { return 0; } const id = this._nextId++; @@ -36,6 +44,7 @@ export class DecorationStyleCache { id, color, bold, + opacity, }; this._cache.set(id, entry); return id; diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 0d597760bc87..9fb4a441e193 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -258,6 +258,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend let decorationStyleSetBold: boolean | undefined; let decorationStyleSetColor: number | undefined; + let decorationStyleSetOpacity: number | undefined; let lineData: ViewLineRenderingData; let decoration: InlineDecoration; @@ -363,6 +364,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend chars = content.charAt(x); decorationStyleSetColor = undefined; decorationStyleSetBold = undefined; + decorationStyleSetOpacity = undefined; // Apply supported inline decoration styles to the cell metadata for (decoration of lineData.inlineDecorations) { @@ -402,6 +404,11 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } break; } + case 'opacity': { + const parsedValue = parseCssOpacity(value); + decorationStyleSetOpacity = parsedValue; + break; + } default: throw new BugIndicatingError('Unexpected inline decoration style'); } } @@ -419,7 +426,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend continue; } - const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold); + const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity); glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer.value, chars, tokenMetadata, decorationStyleSetId); // TODO: Support non-standard character widths @@ -509,3 +516,13 @@ function parseCssFontWeight(value: string) { } return parseInt(value); } + +function parseCssOpacity(value: string): number { + if (value.endsWith('%')) { + return parseFloat(value.substring(0, value.length - 1)) / 100; + } + if (value.match(/^\d+(?:\.\d*)/)) { + return parseFloat(value); + } + return 1; +} diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 0375d305f155..d7f39647d82a 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -106,6 +106,8 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._canvas.height = canvasDim; } + this._ctx.save(); + const decorationStyleSet = ViewGpuContext.decorationStyleCache.getStyleSet(decorationStyleSetId); // TODO: Support workbench.fontAliasing @@ -139,7 +141,12 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { } this._ctx.textBaseline = 'top'; + if (decorationStyleSet?.opacity !== undefined) { + this._ctx.globalAlpha = decorationStyleSet.opacity; + } + this._ctx.fillText(chars, originX, originY); + this._ctx.restore(); const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); this._findGlyphBoundingBox(imageData, this._workGlyph.boundingBox); diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 7c0985a376b2..f33b9ee6fa11 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -268,6 +268,7 @@ export class ViewGpuContext extends Disposable { const gpuSupportedDecorationCssRules = [ 'color', 'font-weight', + 'opacity', ]; function supportsCssRule(rule: string, style: CSSStyleDeclaration) { From d6b4380df5e3526aa965e641a660e3c136e91494 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 13 Jan 2025 09:52:14 -0800 Subject: [PATCH 0521/3587] Fix exception when rendering lines > gpu cap Fixes #237823 --- src/vs/editor/browser/gpu/fullFileRenderStrategy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 0d597760bc87..fa696796d1d4 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -279,7 +279,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend const lineIndexCount = this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell; const upToDateLines = this._upToDateLines[this._activeDoubleBufferIndex]; - let dirtyLineStart = Number.MAX_SAFE_INTEGER; + let dirtyLineStart = 3000; let dirtyLineEnd = 0; // Handle any queued buffer updates @@ -458,6 +458,8 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend const visibleObjectCount = (viewportData.endLineNumber - viewportData.startLineNumber + 1) * lineIndexCount; // Only write when there is changed data + dirtyLineStart = Math.min(dirtyLineStart, this._viewGpuContext.maxGpuLines); + dirtyLineEnd = Math.min(dirtyLineEnd, this._viewGpuContext.maxGpuLines); if (dirtyLineStart <= dirtyLineEnd) { // Write buffer and swap it out to unblock writes this._device.queue.writeBuffer( From 2e0fcd7614a96b2aedfe259e19de6708b1cdc61c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 13 Jan 2025 10:18:23 -0800 Subject: [PATCH 0522/3587] Support basic cache hits in deco style cache Part of #234473 --- .../editor/browser/gpu/css/decorationStyleCache.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts index 879b72470f14..9da4147e7ba9 100644 --- a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts +++ b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ThreeKeyMap } from '../../../../base/common/map.js'; + export interface IDecorationStyleSet { /** * A 24-bit number representing `color`. @@ -29,7 +31,8 @@ export class DecorationStyleCache { private _nextId = 1; - private readonly _cache = new Map(); + private readonly _cacheById = new Map(); + private readonly _cacheByStyle = new ThreeKeyMap(); getOrCreateEntry( color: number | undefined, @@ -39,6 +42,10 @@ export class DecorationStyleCache { if (color === undefined && bold === undefined && opacity === undefined) { return 0; } + const result = this._cacheByStyle.get(color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2)); + if (result) { + return result.id; + } const id = this._nextId++; const entry = { id, @@ -46,7 +53,8 @@ export class DecorationStyleCache { bold, opacity, }; - this._cache.set(id, entry); + this._cacheById.set(id, entry); + this._cacheByStyle.set(color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2), entry); return id; } @@ -54,6 +62,6 @@ export class DecorationStyleCache { if (id === 0) { return undefined; } - return this._cache.get(id); + return this._cacheById.get(id); } } From 8ac2694a171d3a468b2b8cc16f77be54d66a44d2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 13 Jan 2025 10:48:06 -0800 Subject: [PATCH 0523/3587] Implement NKeyMap and remove 2, 3, 4 variants --- src/vs/base/common/map.ts | 117 ++++++++---------- src/vs/base/test/common/map.test.ts | 116 ++++++----------- src/vs/editor/browser/gpu/atlas/atlas.ts | 9 +- .../editor/browser/gpu/atlas/textureAtlas.ts | 8 +- .../browser/gpu/atlas/textureAtlasPage.ts | 6 +- .../gpu/atlas/textureAtlasSlabAllocator.ts | 6 +- .../browser/gpu/css/decorationStyleCache.ts | 6 +- 7 files changed, 109 insertions(+), 159 deletions(-) diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index bc06aafaec23..c8f9de67964e 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -877,91 +877,76 @@ export function mapsStrictEqualIgnoreOrder(a: Map, b: Map { - private _data: { [key: string | number]: { [key: string | number]: TValue | undefined } | undefined } = {}; +export class NKeyMap { + private _data: Map = new Map(); - public set(first: TFirst, second: TSecond, value: TValue): void { - if (!this._data[first]) { - this._data[first] = {}; - } - this._data[first as string | number]![second] = value; - } - - public get(first: TFirst, second: TSecond): TValue | undefined { - return this._data[first as string | number]?.[second]; - } - - public clear(): void { - this._data = {}; - } - - public *values(): IterableIterator { - for (const first in this._data) { - for (const second in this._data[first]) { - const value = this._data[first]![second]; - if (value) { - yield value; - } + /** + * Sets a value on the map. Note that unlike a standard `Map`, the first argument is the value. + * This is because the spread operator is used for the keys and must be last.. + * @param value The value to set. + * @param keys The keys for the value. + */ + public set(value: TValue, ...keys: [...TKeys]): void { + let currentMap = this._data; + for (let i = 0; i < keys.length - 1; i++) { + if (!currentMap.has(keys[i])) { + currentMap.set(keys[i], new Map()); } + currentMap = currentMap.get(keys[i]); } + currentMap.set(keys[keys.length - 1], value); } -} -/** - * A map that is addressable with 3 separate keys. This is useful in high performance scenarios - * where creating a composite key whenever the data is accessed is too expensive. - */ -export class ThreeKeyMap { - private _data: { [key: string | number]: TwoKeyMap | undefined } = {}; - - public set(first: TFirst, second: TSecond, third: TThird, value: TValue): void { - if (!this._data[first]) { - this._data[first] = new TwoKeyMap(); + public get(...keys: [...TKeys]): TValue | undefined { + let currentMap = this._data; + for (let i = 0; i < keys.length - 1; i++) { + if (!currentMap.has(keys[i])) { + return undefined; + } + currentMap = currentMap.get(keys[i]); } - this._data[first as string | number]!.set(second, third, value); - } - - public get(first: TFirst, second: TSecond, third: TThird): TValue | undefined { - return this._data[first as string | number]?.get(second, third); + return currentMap.get(keys[keys.length - 1]); } public clear(): void { - this._data = {}; + this._data.clear(); } public *values(): IterableIterator { - for (const first in this._data) { - for (const value of this._data[first]!.values()) { - if (value) { + function* iterate(map: Map): IterableIterator { + for (const value of map.values()) { + if (value instanceof Map) { + yield* iterate(value); + } else { yield value; } } } - } -} - -/** - * A map that is addressable with 4 separate keys. This is useful in high performance scenarios - * where creating a composite key whenever the data is accessed is too expensive. - */ -export class FourKeyMap { - private _data: TwoKeyMap> = new TwoKeyMap(); - - public set(first: TFirst, second: TSecond, third: TThird, fourth: TFourth, value: TValue): void { - if (!this._data.get(first, second)) { - this._data.set(first, second, new TwoKeyMap()); - } - this._data.get(first, second)!.set(third, fourth, value); + yield* iterate(this._data); } - public get(first: TFirst, second: TSecond, third: TThird, fourth: TFourth): TValue | undefined { - return this._data.get(first, second)?.get(third, fourth); - } + /** + * Get a textual representation of the map for debugging purposes. + */ + public toString(): string { + const printMap = (map: Map, depth: number): string => { + let result = ''; + for (const [key, value] of map) { + result += `${' '.repeat(depth)}${key}: `; + if (value instanceof Map) { + result += '\n' + printMap(value, depth + 1); + } else { + result += `${value}\n`; + } + } + return result; + }; - public clear(): void { - this._data.clear(); + return printMap(this._data, 0); } } diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index aa4240238855..895726ab312d 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { BidirectionalMap, FourKeyMap, LinkedMap, LRUCache, mapsStrictEqualIgnoreOrder, MRUCache, ResourceMap, SetMap, ThreeKeyMap, Touch, TwoKeyMap } from '../../common/map.js'; +import { BidirectionalMap, LinkedMap, LRUCache, mapsStrictEqualIgnoreOrder, MRUCache, NKeyMap, ResourceMap, SetMap, Touch } from '../../common/map.js'; import { extUriIgnorePathCase } from '../../common/resources.js'; import { URI } from '../../common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; @@ -683,82 +683,14 @@ suite('SetMap', () => { }); }); -suite('TwoKeyMap', () => { +suite('NKeyMap', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('set and get', () => { - const map = new TwoKeyMap(); - map.set('a', 'b', 1); - map.set('a', 'c', 2); - map.set('b', 'c', 3); - assert.strictEqual(map.get('a', 'b'), 1); - assert.strictEqual(map.get('a', 'c'), 2); - assert.strictEqual(map.get('b', 'c'), 3); - assert.strictEqual(map.get('a', 'd'), undefined); - }); - - test('clear', () => { - const map = new TwoKeyMap(); - map.set('a', 'b', 1); - map.set('a', 'c', 2); - map.set('b', 'c', 3); - map.clear(); - assert.strictEqual(map.get('a', 'b'), undefined); - assert.strictEqual(map.get('a', 'c'), undefined); - assert.strictEqual(map.get('b', 'c'), undefined); - }); - - test('values', () => { - const map = new TwoKeyMap(); - map.set('a', 'b', 1); - map.set('a', 'c', 2); - map.set('b', 'c', 3); - assert.deepStrictEqual(Array.from(map.values()), [1, 2, 3]); - }); -}); - -suite('ThreeKeyMap', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - test('set and get', () => { - const map = new ThreeKeyMap(); - map.set('a', 'b', 'c', 1); - map.set('a', 'c', 'd', 2); - map.set('b', 'c', 'e', 3); - assert.strictEqual(map.get('a', 'b', 'c'), 1); - assert.strictEqual(map.get('a', 'c', 'd'), 2); - assert.strictEqual(map.get('b', 'c', 'e'), 3); - assert.strictEqual(map.get('a', 'd', 'e'), undefined); - }); - - test('clear', () => { - const map = new ThreeKeyMap(); - map.set('a', 'b', 'c', 1); - map.set('a', 'c', 'd', 2); - map.set('b', 'c', 'e', 3); - map.clear(); - assert.strictEqual(map.get('a', 'b', 'c'), undefined); - assert.strictEqual(map.get('a', 'c', 'd'), undefined); - assert.strictEqual(map.get('b', 'c', 'e'), undefined); - }); - - test('values', () => { - const map = new ThreeKeyMap(); - map.set('a', 'b', 'c', 1); - map.set('a', 'c', 'd', 2); - map.set('b', 'c', 'e', 3); - assert.deepStrictEqual(Array.from(map.values()), [1, 2, 3]); - }); -}); - -suite('FourKeyMap', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - test('set and get', () => { - const map = new FourKeyMap(); - map.set('a', 'b', 'c', 'd', 1); - map.set('a', 'c', 'c', 'd', 2); - map.set('b', 'e', 'f', 'g', 3); + const map = new NKeyMap(); + map.set(1, 'a', 'b', 'c', 'd'); + map.set(2, 'a', 'c', 'c', 'd'); + map.set(3, 'b', 'e', 'f', 'g'); assert.strictEqual(map.get('a', 'b', 'c', 'd'), 1); assert.strictEqual(map.get('a', 'c', 'c', 'd'), 2); assert.strictEqual(map.get('b', 'e', 'f', 'g'), 3); @@ -766,13 +698,41 @@ suite('FourKeyMap', () => { }); test('clear', () => { - const map = new FourKeyMap(); - map.set('a', 'b', 'c', 'd', 1); - map.set('a', 'c', 'c', 'd', 2); - map.set('b', 'e', 'f', 'g', 3); + const map = new NKeyMap(); + map.set(1, 'a', 'b', 'c', 'd'); + map.set(2, 'a', 'c', 'c', 'd'); + map.set(3, 'b', 'e', 'f', 'g'); map.clear(); assert.strictEqual(map.get('a', 'b', 'c', 'd'), undefined); assert.strictEqual(map.get('a', 'c', 'c', 'd'), undefined); assert.strictEqual(map.get('b', 'e', 'f', 'g'), undefined); }); + + test('values', () => { + const map = new NKeyMap(); + map.set(1, 'a', 'b', 'c', 'd'); + map.set(2, 'a', 'c', 'c', 'd'); + map.set(3, 'b', 'e', 'f', 'g'); + assert.deepStrictEqual(Array.from(map.values()), [1, 2, 3]); + }); + + test('toString', () => { + const map = new NKeyMap(); + map.set(1, 'f', 'o', 'o'); + map.set(2, 'b', 'a', 'r'); + map.set(3, 'b', 'a', 'z'); + map.set(3, 'b', 'o', 'o'); + assert.strictEqual(map.toString(), [ + 'f: ', + ' o: ', + ' o: 1', + 'b: ', + ' a: ', + ' r: 2', + ' z: 3', + ' o: ', + ' o: 3', + '', + ].join('\n')); + }); }); diff --git a/src/vs/editor/browser/gpu/atlas/atlas.ts b/src/vs/editor/browser/gpu/atlas/atlas.ts index 42d1eacaa891..17900d5273c8 100644 --- a/src/vs/editor/browser/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/gpu/atlas/atlas.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { FourKeyMap } from '../../../../base/common/map.js'; +import type { NKeyMap } from '../../../../base/common/map.js'; import type { IBoundingBox, IRasterizedGlyph } from '../raster/raster.js'; /** @@ -106,4 +106,9 @@ export const enum UsagePreviewColors { Restricted = '#FF000088', } -export type GlyphMap = FourKeyMap; +export type GlyphMap = NKeyMap; diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts index 472f39e67098..32987628de34 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts @@ -8,7 +8,7 @@ import { CharCode } from '../../../../base/common/charCode.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, dispose, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { FourKeyMap } from '../../../../base/common/map.js'; +import { NKeyMap } from '../../../../base/common/map.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { MetadataConsts } from '../../../common/encodedTokenAttributes.js'; @@ -50,7 +50,7 @@ export class TextureAtlas extends Disposable { * so it is not guaranteed to be the actual page the glyph is on. But it is guaranteed that all * pages with a lower index do not contain the glyph. */ - private readonly _glyphPageIndex: GlyphMap = new FourKeyMap(); + private readonly _glyphPageIndex: GlyphMap = new NKeyMap(); private readonly _onDidDeleteGlyphs = this._register(new Emitter()); readonly onDidDeleteGlyphs = this._onDidDeleteGlyphs.event; @@ -126,7 +126,7 @@ export class TextureAtlas extends Disposable { } private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly { - this._glyphPageIndex.set(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey, pageIndex); + this._glyphPageIndex.set(pageIndex, chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey); return ( this._pages[pageIndex].getGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId) ?? (pageIndex + 1 < this._pages.length @@ -141,7 +141,7 @@ export class TextureAtlas extends Disposable { throw new Error(`Attempt to create a texture atlas page past the limit ${TextureAtlas.maximumPageCount}`); } this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, this._pages.length, this.pageSize, this._allocatorType)); - this._glyphPageIndex.set(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey, this._pages.length - 1); + this._glyphPageIndex.set(this._pages.length - 1, chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey); return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId)!; } diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts index 4c48a3f70e22..1548f772e88b 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { FourKeyMap } from '../../../../base/common/map.js'; +import { NKeyMap } from '../../../../base/common/map.js'; import { ILogService, LogLevel } from '../../../../platform/log/common/log.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import type { IBoundingBox, IGlyphRasterizer } from '../raster/raster.js'; @@ -31,7 +31,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla private readonly _canvas: OffscreenCanvas; get source(): OffscreenCanvas { return this._canvas; } - private readonly _glyphMap: GlyphMap = new FourKeyMap(); + private readonly _glyphMap: GlyphMap = new NKeyMap(); private readonly _glyphInOrderSet: Set = new Set(); get glyphs(): IterableIterator { return this._glyphInOrderSet.values(); @@ -89,7 +89,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla } // Save the glyph - this._glyphMap.set(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey, glyph); + this._glyphMap.set(glyph, chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey); this._glyphInOrderSet.add(glyph); // Update page version and it's tracked used area diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts index f9fa20dcfdb2..e6340cac982d 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts @@ -5,7 +5,7 @@ import { getActiveWindow } from '../../../../base/browser/dom.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; -import { TwoKeyMap } from '../../../../base/common/map.js'; +import { NKeyMap } from '../../../../base/common/map.js'; import { ensureNonNullable } from '../gpuUtils.js'; import type { IRasterizedGlyph } from '../raster/raster.js'; import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from './atlas.js'; @@ -29,7 +29,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { private readonly _ctx: OffscreenCanvasRenderingContext2D; private readonly _slabs: ITextureAtlasSlab[] = []; - private readonly _activeSlabsByDims: TwoKeyMap = new TwoKeyMap(); + private readonly _activeSlabsByDims: NKeyMap = new NKeyMap(); private readonly _unusedRects: ITextureAtlasSlabUnusedRect[] = []; @@ -243,7 +243,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { }); } this._slabs.push(slab); - this._activeSlabsByDims.set(desiredSlabSize.w, desiredSlabSize.h, slab); + this._activeSlabsByDims.set(slab, desiredSlabSize.w, desiredSlabSize.h); } const glyphsPerRow = Math.floor(this._slabW / slab.entryW); diff --git a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts index 9da4147e7ba9..1b1c07df1630 100644 --- a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts +++ b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ThreeKeyMap } from '../../../../base/common/map.js'; +import { NKeyMap } from '../../../../base/common/map.js'; export interface IDecorationStyleSet { /** @@ -32,7 +32,7 @@ export class DecorationStyleCache { private _nextId = 1; private readonly _cacheById = new Map(); - private readonly _cacheByStyle = new ThreeKeyMap(); + private readonly _cacheByStyle = new NKeyMap(); getOrCreateEntry( color: number | undefined, @@ -54,7 +54,7 @@ export class DecorationStyleCache { opacity, }; this._cacheById.set(id, entry); - this._cacheByStyle.set(color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2), entry); + this._cacheByStyle.set(entry, color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2)); return id; } From c594d55bae90276d174cea4ddf2901694d4ebb3e Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 13 Jan 2025 15:13:26 -0600 Subject: [PATCH 0524/3587] add a setting to determine which terminal suggest providers are activated (#237826) --- .../browser/pwshCompletionProviderAddon.ts | 4 ++-- .../browser/terminal.suggest.contribution.ts | 7 ++++++- .../browser/terminalCompletionService.ts | 20 +++++++++++++++++-- .../common/terminalSuggestConfiguration.ts | 18 ++++++++++++++++- .../terminalSuggestAddon.integrationTest.ts | 4 ++++ 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts index f10c11b7cd68..53a0eb48bccc 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts @@ -56,7 +56,7 @@ export class PwshCompletionProviderAddon extends Disposable implements ITerminal id: string = PwshCompletionProviderAddon.ID; triggerCharacters?: string[] | undefined; isBuiltin?: boolean = true; - static readonly ID = 'terminal.pwshCompletionProvider'; + static readonly ID = 'pwsh-shell-integration'; static cachedPwshCommands: Set; readonly shellTypes = [GeneralShellType.PowerShell]; private _codeCompletionsRequested: boolean = false; @@ -348,7 +348,7 @@ function rawCompletionToITerminalCompletion(rawCompletion: PwshCompletion, repla return { label, - provider: 'pwsh-script', + provider: PwshCompletionProviderAddon.ID, icon, detail, isFile: rawCompletion.ResultType === 3, diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 89e59369df8a..ddaed199002a 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -112,7 +112,7 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo this._ctx.instance.focus(); this._ctx.instance.sendText(text, false); })); - this.add(this._terminalCompletionService.registerTerminalCompletionProvider('builtinPwsh', 'pwsh', pwshCompletionProviderAddon)); + this.add(this._terminalCompletionService.registerTerminalCompletionProvider('builtinPwsh', pwshCompletionProviderAddon.id, pwshCompletionProviderAddon)); // If completions are requested, pause and queue input events until completions are // received. This fixing some problems in PowerShell, particularly enter not executing // when typing quickly and some characters being printed twice. On Windows this isn't @@ -178,6 +178,11 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo return; } addon.shellType = this._ctx.instance.shellType; + if (!this._ctx.instance.xterm?.raw) { + return; + } + // Relies on shell type being set + this._loadPwshCompletionAddon(this._ctx.instance.xterm.raw); } } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index c7af21695a13..fac59f23a364 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -12,7 +12,7 @@ import { IFileService } from '../../../../../platform/files/common/files.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; import { ISimpleCompletion } from '../../../../services/suggest/browser/simpleCompletionItem.js'; -import { ITerminalSuggestConfiguration, terminalSuggestConfigSection } from '../common/terminalSuggestConfiguration.js'; +import { ITerminalSuggestConfiguration, terminalSuggestConfigSection, TerminalSuggestSettingId } from '../common/terminalSuggestConfiguration.js'; export const ITerminalCompletionService = createDecorator('terminalCompletionService'); @@ -149,6 +149,17 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo if (!extensionCompletionsEnabled || skipExtensionCompletions) { providers = providers.filter(p => p.isBuiltin); + return this._collectCompletions(providers, shellType, promptValue, cursorPosition, token); + } + + const providerConfig: { [key: string]: boolean } = this._configurationService.getValue(TerminalSuggestSettingId.Providers); + providers = providers.filter(p => { + const providerId = p.id; + return providerId && providerId in providerConfig && providerConfig[providerId]; + }); + + if (!providers.length) { + return; } return this._collectCompletions(providers, shellType, promptValue, cursorPosition, token); @@ -164,7 +175,12 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return undefined; } const completionItems = Array.isArray(completions) ? completions : completions.items ?? []; - + if (provider.isBuiltin) { + //TODO: why is this needed? + for (const item of completionItems) { + item.provider = provider.id; + } + } if (Array.isArray(completions)) { return completionItems; } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts index 860bb28015ac..ac7e94ead6a7 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts @@ -15,6 +15,7 @@ export const enum TerminalSuggestSettingId { RunOnEnter = 'terminal.integrated.suggest.runOnEnter', BuiltinCompletions = 'terminal.integrated.suggest.builtinCompletions', EnableExtensionCompletions = 'terminal.integrated.suggest.enableExtensionCompletions', + Providers = 'terminal.integrated.suggest.providers', } export const terminalSuggestConfigSection = 'terminal.integrated.suggest'; @@ -28,6 +29,10 @@ export interface ITerminalSuggestConfiguration { 'pwshCode': boolean; 'pwshGit': boolean; }; + providers: { + 'terminal-suggest': boolean; + 'pwsh-shell-integration': boolean; + }; enableExtensionCompletions: boolean; } @@ -39,6 +44,17 @@ export const terminalSuggestConfiguration: IStringDictionary { pwshCode: true, pwshGit: true }, + providers: { + 'terminal-suggest': true, + 'pwsh-shell-integration': true, + }, enableExtensionCompletions: false } satisfies ITerminalSuggestConfiguration } From 31090a0521aa83e1eadbc0a266c5b35a28a44fc3 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 13 Jan 2025 17:15:03 -0800 Subject: [PATCH 0525/3587] Fix bad code action conversation From #237857 For a simple `CodeAction` that has a `title` and `command` , looks like we were mistakenly treating it as a `command` instead of as a `CodeAction` One side effect was that a lot of extensions were reported as returning commands for code actions when they were actually not: #237857 --- .../api/common/extHostLanguageFeatures.ts | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 1951cbf1c6fd..1594dc6f749d 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -31,7 +31,7 @@ import { ExtHostDiagnostics } from './extHostDiagnostics.js'; import { ExtHostDocuments } from './extHostDocuments.js'; import { ExtHostTelemetry, IExtHostTelemetry } from './extHostTelemetry.js'; import * as typeConvert from './extHostTypeConverters.js'; -import { CodeActionKind, CompletionList, Disposable, DocumentDropOrPasteEditKind, DocumentSymbol, InlineCompletionTriggerKind, InlineEditTriggerKind, InternalDataTransferItem, Location, NewSymbolNameTriggerKind, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType } from './extHostTypes.js'; +import { CodeAction, CodeActionKind, CompletionList, Disposable, DocumentDropOrPasteEditKind, DocumentSymbol, InlineCompletionTriggerKind, InlineEditTriggerKind, InternalDataTransferItem, Location, NewSymbolNameTriggerKind, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType } from './extHostTypes.js'; import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import type * as vscode from 'vscode'; import { Cache } from './cache.js'; @@ -501,7 +501,8 @@ class CodeActionAdapter { if (!candidate) { continue; } - if (CodeActionAdapter._isCommand(candidate)) { + + if (CodeActionAdapter._isCommand(candidate) && !(candidate instanceof CodeAction)) { // old school: synthetic code action this._apiDeprecation.report('CodeActionProvider.provideCodeActions - return commands', this._extension, `Return 'CodeAction' instances instead.`); @@ -512,29 +513,31 @@ class CodeActionAdapter { command: this._commands.toInternal(candidate, disposables), }); } else { + const toConvert = candidate as vscode.CodeAction; + + // new school: convert code action if (codeActionContext.only) { - if (!candidate.kind) { + if (!toConvert.kind) { this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value}' requested but returned code action does not have a 'kind'. Code action will be dropped. Please set 'CodeAction.kind'.`); - } else if (!codeActionContext.only.contains(candidate.kind)) { - this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value}' requested but returned code action is of kind '${candidate.kind.value}'. Code action will be dropped. Please check 'CodeActionContext.only' to only return requested code actions.`); + } else if (!codeActionContext.only.contains(toConvert.kind)) { + this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value}' requested but returned code action is of kind '${toConvert.kind.value}'. Code action will be dropped. Please check 'CodeActionContext.only' to only return requested code actions.`); } } // Ensures that this is either a Range[] or an empty array so we don't get Array - const range = candidate.ranges ?? []; + const range = toConvert.ranges ?? []; - // new school: convert code action actions.push({ cacheId: [cacheId, i], - title: candidate.title, - command: candidate.command && this._commands.toInternal(candidate.command, disposables), - diagnostics: candidate.diagnostics && candidate.diagnostics.map(typeConvert.Diagnostic.from), - edit: candidate.edit && typeConvert.WorkspaceEdit.from(candidate.edit, undefined), - kind: candidate.kind && candidate.kind.value, - isPreferred: candidate.isPreferred, - isAI: isProposedApiEnabled(this._extension, 'codeActionAI') ? candidate.isAI : false, + title: toConvert.title, + command: toConvert.command && this._commands.toInternal(toConvert.command, disposables), + diagnostics: toConvert.diagnostics && toConvert.diagnostics.map(typeConvert.Diagnostic.from), + edit: toConvert.edit && typeConvert.WorkspaceEdit.from(toConvert.edit, undefined), + kind: toConvert.kind && toConvert.kind.value, + isPreferred: toConvert.isPreferred, + isAI: isProposedApiEnabled(this._extension, 'codeActionAI') ? toConvert.isAI : false, ranges: isProposedApiEnabled(this._extension, 'codeActionRanges') ? coalesce(range.map(typeConvert.Range.from)) : undefined, - disabled: candidate.disabled?.reason + disabled: toConvert.disabled?.reason }); } } From d8ea024868c9a487036bfbacca6471417320527b Mon Sep 17 00:00:00 2001 From: aslezar <97354675+aslezar@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:49:27 +0530 Subject: [PATCH 0526/3587] chore: used same message decription for TS and JS --- extensions/typescript-language-features/package.json | 4 ++-- extensions/typescript-language-features/package.nls.json | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 24c672ce7c46..b47af81f94aa 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -528,13 +528,13 @@ "typescript.format.indentSwitchCase": { "type": "boolean", "default": true, - "description": "%typescript.format.indentSwitchCase%", + "description": "%format.indentSwitchCase%", "scope": "resource" }, "javascript.format.indentSwitchCase": { "type": "boolean", "default": true, - "description": "%javascript.format.indentSwitchCase%", + "description": "%format.indentSwitchCase%", "scope": "resource" }, "javascript.validate.enable": { diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index d5ea70614987..6a7dc26a24d6 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -40,8 +40,7 @@ "format.semicolons.ignore": "Don't insert or remove any semicolons.", "format.semicolons.insert": "Insert semicolons at statement ends.", "format.semicolons.remove": "Remove unnecessary semicolons.", - "typescript.format.indentSwitchCase": "Indent case clauses in switch statements in TypeScript Files. Requires using TypeScript 5.1+ in the workspace.", - "javascript.format.indentSwitchCase": "Indent case clauses in switch statements in JavaScript Files. Requires using TypeScript 5.1+ in the workspace.", + "format.indentSwitchCase": "Indent case clauses in switch statements. Requires using TypeScript 5.1+ in the workspace.", "javascript.validate.enable": "Enable/disable JavaScript validation.", "javascript.goToProjectConfig.title": "Go to Project Configuration (jsconfig / tsconfig)", "typescript.goToProjectConfig.title": "Go to Project Configuration (tsconfig)", From ef520e4d79ec41fe4d1d2f225c43170046d9a74e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 14 Jan 2025 11:11:56 +0100 Subject: [PATCH 0527/3587] history - drop `label` if it matches path (#237876) --- .../platform/workspaces/common/workspaces.ts | 29 +++++++++++++++++-- .../workspacesHistoryStorage.test.ts | 18 ++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index 6097076c43f4..7dddb7f3764c 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -364,16 +364,39 @@ export function restoreRecentlyOpened(data: RecentlyOpenedStorageData | undefine export function toStoreData(recents: IRecentlyOpened): RecentlyOpenedStorageData { const serialized: ISerializedRecentlyOpened = { entries: [] }; + const storeLabel = (label: string | undefined, uri: URI) => { + // Only store the label if it is provided + // and only if it differs from the path + // This gives us a chance to render the + // path better, e.g. use `~` for home. + return label && label !== uri.fsPath && label !== uri.path; + }; + for (const recent of recents.workspaces) { if (isRecentFolder(recent)) { - serialized.entries.push({ folderUri: recent.folderUri.toString(), label: recent.label, remoteAuthority: recent.remoteAuthority }); + serialized.entries.push({ + folderUri: recent.folderUri.toString(), + label: storeLabel(recent.label, recent.folderUri) ? recent.label : undefined, + remoteAuthority: recent.remoteAuthority + }); } else { - serialized.entries.push({ workspace: { id: recent.workspace.id, configPath: recent.workspace.configPath.toString() }, label: recent.label, remoteAuthority: recent.remoteAuthority }); + serialized.entries.push({ + workspace: { + id: recent.workspace.id, + configPath: recent.workspace.configPath.toString() + }, + label: storeLabel(recent.label, recent.workspace.configPath) ? recent.label : undefined, + remoteAuthority: recent.remoteAuthority + }); } } for (const recent of recents.files) { - serialized.entries.push({ fileUri: recent.fileUri.toString(), label: recent.label, remoteAuthority: recent.remoteAuthority }); + serialized.entries.push({ + fileUri: recent.fileUri.toString(), + label: storeLabel(recent.label, recent.fileUri) ? recent.label : undefined, + remoteAuthority: recent.remoteAuthority + }); } return serialized; diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts index 1c86fda94426..787b8784d9a5 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts @@ -145,5 +145,23 @@ suite('History Storage', () => { assertEqualRecentlyOpened(windowsState, expected, 'v1_33'); }); + test('toStoreData drops label if it matches path', () => { + const actual = toStoreData({ + workspaces: [], + files: [{ + fileUri: URI.parse('file:///foo/bar/test.txt'), + label: '/foo/bar/test.txt', + remoteAuthority: undefined + }] + }); + assert.deepStrictEqual(actual, { + entries: [{ + fileUri: 'file:///foo/bar/test.txt', + label: undefined, + remoteAuthority: undefined + }] + }); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); From 9ee0345b6303b6d6cd8984bd4848a25b5a7f385f Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:45:03 +0100 Subject: [PATCH 0528/3587] Git - fix "Open on GitHub" command (#237832) --- extensions/git/src/repository.ts | 2 +- extensions/github/src/commands.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 9b70bef793cb..9ad8c2916c20 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1518,7 +1518,7 @@ export class Repository implements Disposable { async getBranches(query: BranchQuery = {}, cancellationToken?: CancellationToken): Promise { return await this.run(Operation.GetBranches, async () => { const refs = await this.getRefs(query, cancellationToken); - return refs.filter(value => (value.type === RefType.Head || value.type === RefType.RemoteHead) && (query.remote || !value.remote)); + return refs.filter(value => value.type === RefType.Head || (value.type === RefType.RemoteHead && query.remote)); }); } diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 0889628cf0d0..8630d2ef0fc4 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -73,7 +73,7 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { } // Get the unique remotes that contain the commit - const branches = await apiRepository.getBranches({ contains: historyItem.id }); + const branches = await apiRepository.getBranches({ contains: historyItem.id, remote: true }); const remoteNames = new Set(branches.filter(b => b.type === RefType.RemoteHead && b.remote).map(b => b.remote!)); // GitHub remotes that contain the commit From 814ea92f3379fa4cb1fb2ee85f7e1d340ac50712 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:54:40 +0100 Subject: [PATCH 0529/3587] Enhance inline edit rendering (#237841) * Only allow same replacements * better alignment * inline insertion and deletion rendering --- .../view/inlineEdits/gutterIndicatorView.ts | 2 +- .../view/inlineEdits/inlineDiffView.ts | 9 +++++-- .../browser/view/inlineEdits/view.css | 24 ++++++++++++++--- .../browser/view/inlineEdits/view.ts | 27 ++++++++++++++++--- 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index 847f3a87edeb..5d6dd535722f 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -293,7 +293,7 @@ export class InlineEditsGutterIndicator extends Disposable { transition: 'rotate 0.2s ease-in-out', } }, [ - renderIcon(Codicon.arrowRight) + renderIcon(Codicon.arrowRight) // TODO: allow setting css here, is this already supported? ]) ]), ])).keepUpdated(this._store); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts index 095a75807498..38f9be9092ae 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts @@ -191,9 +191,11 @@ export class OriginalEditorInlineDiffView extends Disposable { shouldFillLineOnLineBreak: false, className: classNames( 'inlineCompletions-char-delete', + i.originalRange.isSingleLine() && diff.mode === 'ghostText' && 'single-line-inline', + i.originalRange.isEmpty() && 'empty', (i.originalRange.isEmpty() && showEmptyDecorations && !useInlineDiff) && 'diff-range-empty' ), - inlineClassName: useInlineDiff ? 'strike-through' : null, + inlineClassName: useInlineDiff ? classNames('strike-through', 'inlineCompletions') : null, zIndex: 1 } }); @@ -214,7 +216,10 @@ export class OriginalEditorInlineDiffView extends Disposable { description: 'inserted-text', before: { content: insertedText, - inlineClassName: diff.mode === 'ghostText' ? 'inlineCompletions-char-insert inline' : 'inlineCompletions-char-insert', + inlineClassName: classNames( + 'inlineCompletions-char-insert', + i.modifiedRange.isSingleLine() && diff.mode === 'ghostText' && 'single-line-inline' + ), }, zIndex: 2, showIfCollapsed: true, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index 7f47c69720ba..46ef6e893476 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -164,13 +164,27 @@ .inlineCompletions-char-insert.diff-range-empty { border-left: solid var(--vscode-inlineEdit-modifiedChangedTextBackground) 3px; } - - .inlineCompletions-char-delete.inline, - .inlineCompletions-char-insert.inline { + + .inlineCompletions-char-delete.single-line-inline, + .inlineCompletions-char-insert.single-line-inline { border-radius: 4px; border: 1px solid var(--vscode-editorHoverWidget-border); padding: 2px; } + + .inlineCompletions-char-delete.single-line-inline.empty, + .inlineCompletions-char-insert.single-line-inline.empty { + display: none; + } + + /* Adjustments due to being a decoration */ + .inlineCompletions-char-delete.single-line-inline { + margin: -2px 0 0 -2px; + } + + .inlineCompletions.strike-through { + text-decoration-thickness: 1px; + } } .monaco-menu-option { @@ -195,3 +209,7 @@ .monaco-editor .inline-completion-accepted { background-color: var(--vscode-inlineEdit-acceptedBackground); } + +.inline-edits-view-gutter-indicator .codicon{ + margin-top: 1px; /* TODO: Move into gutter DOM initialization */ +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 8e01c35c5bca..b9ae59af7088 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -168,23 +168,29 @@ export class InlineEditsView extends Disposable { if ( this._useMixedLinesDiff.read(reader) === 'forStableInsertions' - && isInsertionAfterPosition(diff, edit.cursorPosition) + && isSingleLineInsertionAfterPosition(diff, edit.cursorPosition) + || isSingleLineDeletion(diff) ) { return { kind: 'ghostText' as const }; } if (diff.length === 1 && diff[0].original.length === 1 && diff[0].modified.length === 1) { const inner = diff.flatMap(d => d.innerChanges!); + const originalText = edit.originalText.getValueOfRange(inner[0].originalRange); + const modifiedText = newText.getValueOfRange(inner[0].modifiedRange); if (inner.every( m => (m.originalRange.isEmpty() && this._useWordInsertionView.read(reader) === 'whenPossible' || !m.originalRange.isEmpty() && this._useWordReplacementView.read(reader) === 'whenPossible') && TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100 + // If multiple word replacements, check that they are all the same + && edit.originalText.getValueOfRange(m.originalRange) === originalText + && newText.getValueOfRange(m.modifiedRange) === modifiedText )) { return { kind: 'wordReplacements' as const, replacements: inner.map(i => - new SingleTextEdit(i.originalRange, newText.getValueOfRange(i.modifiedRange)) + new SingleTextEdit(i.originalRange, modifiedText) ) }; } @@ -205,7 +211,7 @@ export class InlineEditsView extends Disposable { } } -function isInsertionAfterPosition(diff: DetailedLineRangeMapping[], position: Position | null) { +function isSingleLineInsertionAfterPosition(diff: DetailedLineRangeMapping[], position: Position | null) { if (!position) { return false; } @@ -231,3 +237,18 @@ function isInsertionAfterPosition(diff: DetailedLineRangeMapping[], position: Po return false; } } + +function isSingleLineDeletion(diff: DetailedLineRangeMapping[]): boolean { + return diff.every(m => m.innerChanges!.every(r => isDeletion(r))); + + function isDeletion(r: RangeMapping) { + if (!r.modifiedRange.isEmpty()) { + return false; + } + const isDeletionWithinLine = r.originalRange.startLineNumber === r.originalRange.endLineNumber; + if (!isDeletionWithinLine) { + return false; + } + return true; + } +} From bb4308eb6a754c25fab8e1aedde0e8138a3b1549 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 14 Jan 2025 12:33:50 +0100 Subject: [PATCH 0530/3587] Support compound log - initial impl (#237830) * Support compound log - initial impl #237829 * fix integration tests --- .../api/browser/mainThreadOutputService.ts | 2 +- .../contrib/logs/common/logs.contribution.ts | 6 +- .../contrib/logs/common/logsActions.ts | 8 +- .../output/browser/output.contribution.ts | 168 +++++++++++-- .../contrib/output/browser/outputServices.ts | 95 ++++++- .../contrib/output/browser/outputView.ts | 14 +- .../output/common/outputChannelModel.ts | 235 +++++++++++++++--- .../common/outputChannelModelService.ts | 11 +- .../electron-sandbox/output.contribution.ts | 4 +- .../services/output/common/output.ts | 33 ++- 10 files changed, 482 insertions(+), 94 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadOutputService.ts b/src/vs/workbench/api/browser/mainThreadOutputService.ts index 4fff96c51503..0bb3d04b1354 100644 --- a/src/vs/workbench/api/browser/mainThreadOutputService.ts +++ b/src/vs/workbench/api/browser/mainThreadOutputService.ts @@ -59,7 +59,7 @@ export class MainThreadOutputService extends Disposable implements MainThreadOut const id = `extension-output-${extensionId}-#${idCounter}-${label}`; const resource = URI.revive(file); - Registry.as(Extensions.OutputChannels).registerChannel({ id, label, file: resource, log: false, languageId, extensionId }); + Registry.as(Extensions.OutputChannels).registerChannel({ id, label, files: [resource], log: false, languageId, extensionId }); this._register(toDisposable(() => this.$dispose(id))); return id; } diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 4901650bec94..11c7aa061f24 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -143,7 +143,7 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { private registerLogChannel(logger: ILoggerResource): void { const channel = this.outputChannelRegistry.getChannel(logger.id); - if (channel && this.uriIdentityService.extUri.isEqual(channel.file, logger.resource)) { + if (channel?.files?.length === 1 && this.uriIdentityService.extUri.isEqual(channel.files[0], logger.resource)) { return; } const disposables = new DisposableStore(); @@ -152,14 +152,14 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { try { await this.whenFileExists(logger.resource, 1, token); const existingChannel = this.outputChannelRegistry.getChannel(logger.id); - const remoteLogger = existingChannel?.file?.scheme === Schemas.vscodeRemote ? this.loggerService.getRegisteredLogger(existingChannel.file) : undefined; + const remoteLogger = existingChannel?.files?.[0].scheme === Schemas.vscodeRemote ? this.loggerService.getRegisteredLogger(existingChannel.files[0]) : undefined; if (remoteLogger) { this.deregisterLogChannel(remoteLogger); } const hasToAppendRemote = existingChannel && logger.resource.scheme === Schemas.vscodeRemote; const id = hasToAppendRemote ? `${logger.id}.remote` : logger.id; const label = hasToAppendRemote ? nls.localize('remote name', "{0} (Remote)", logger.name ?? logger.id) : logger.name ?? logger.id; - this.outputChannelRegistry.registerChannel({ id, label, file: logger.resource, log: true, extensionId: logger.extensionId }); + this.outputChannelRegistry.registerChannel({ id, label, files: [logger.resource], log: true, extensionId: logger.extensionId }); disposables.add(toDisposable(() => this.outputChannelRegistry.removeChannel(id))); if (remoteLogger) { this.registerLogChannel(remoteLogger); diff --git a/src/vs/workbench/contrib/logs/common/logsActions.ts b/src/vs/workbench/contrib/logs/common/logsActions.ts index c28c6ca91406..26514a1cb5b4 100644 --- a/src/vs/workbench/contrib/logs/common/logsActions.ts +++ b/src/vs/workbench/contrib/logs/common/logsActions.ts @@ -52,11 +52,11 @@ export class SetLogLevelAction extends Action { const extensionLogs: LogChannelQuickPickItem[] = [], logs: LogChannelQuickPickItem[] = []; const logLevel = this.loggerService.getLogLevel(); for (const channel of this.outputService.getChannelDescriptors()) { - if (!SetLogLevelAction.isLevelSettable(channel) || !channel.file) { + if (!SetLogLevelAction.isLevelSettable(channel) || channel.files?.length !== 1) { continue; } - const channelLogLevel = this.loggerService.getLogLevel(channel.file) ?? logLevel; - const item: LogChannelQuickPickItem = { id: channel.id, resource: channel.file, label: channel.label, description: channelLogLevel !== logLevel ? this.getLabel(channelLogLevel) : undefined, extensionId: channel.extensionId }; + const channelLogLevel = this.loggerService.getLogLevel(channel.files[0]) ?? logLevel; + const item: LogChannelQuickPickItem = { id: channel.id, resource: channel.files[0], label: channel.label, description: channelLogLevel !== logLevel ? this.getLabel(channelLogLevel) : undefined, extensionId: channel.extensionId }; if (channel.extensionId) { extensionLogs.push(item); } else { @@ -97,7 +97,7 @@ export class SetLogLevelAction extends Action { } static isLevelSettable(channel: IOutputChannelDescriptor): boolean { - return channel.log && channel.file !== undefined && channel.id !== telemetryLogId && channel.id !== extensionTelemetryLogChannelId; + return channel.log && channel.files?.length === 1 && channel.id !== telemetryLogId && channel.id !== extensionTelemetryLogChannelId; } private async setLogLevelForChannel(logChannel: LogChannelQuickPickItem): Promise { diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 0066f704b1fd..ee82400c9c49 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -10,7 +10,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuId, registerAction2, Action2, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { OutputService } from './outputServices.js'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, OUTPUT_FILTER_FOCUS_CONTEXT } from '../../../services/output/common/output.js'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, OUTPUT_FILTER_FOCUS_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT } from '../../../services/output/common/output.js'; import { OutputViewPane } from './outputView.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; @@ -40,6 +40,7 @@ import { FocusedViewContext } from '../../../common/contextkeys.js'; import { localize, localize2 } from '../../../../nls.js'; import { viewFilterSubmenu } from '../../../browser/parts/views/viewFilter.js'; import { ViewAction } from '../../../browser/parts/views/viewPane.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; // Register Service registerSingleton(IOutputService, OutputService, InstantiationType.Delayed); @@ -107,10 +108,12 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { this.registerToggleAutoScrollAction(); this.registerOpenActiveOutputFileAction(); this.registerOpenActiveOutputFileInAuxWindowAction(); + this.registerSaveActiveOutputAsAction(); this.registerShowLogsAction(); this.registerOpenLogFileAction(); this.registerConfigureActiveOutputLogLevelAction(); this.registerFilterActions(); + this.registerSaveLogsAsAction(); } private registerSwitchOutputAction(): void { @@ -141,7 +144,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { const registerOutputChannels = (channels: IOutputChannelDescriptor[]) => { for (const channel of channels) { const title = channel.label; - const group = channel.extensionId ? '0_ext_outputchannels' : '1_core_outputchannels'; + const group = channel.files && channel.files.length > 1 ? '2_compound_logs' : channel.extensionId ? '0_ext_outputchannels' : '1_core_outputchannels'; registeredChannels.set(channel.id, registerAction2(class extends Action2 { constructor() { super({ @@ -169,8 +172,74 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { } })); this._register(outputChannelRegistry.onDidRemoveChannel(e => { - registeredChannels.get(e)?.dispose(); - registeredChannels.delete(e); + registeredChannels.get(e.id)?.dispose(); + registeredChannels.delete(e.id); + })); + + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.output.addCompoundLog', + title: nls.localize2('addCompoundLog', "Add Compound Log..."), + category: Categories.Developer, + f1: true + }); + } + async run(accessor: ServicesAccessor): Promise { + const outputService = accessor.get(IOutputService); + const quickInputService = accessor.get(IQuickInputService); + + const extensionLogs: IFileOutputChannelDescriptor[] = [], logs: IFileOutputChannelDescriptor[] = []; + for (const channel of outputService.getChannelDescriptors()) { + if (channel.log && channel.files?.length === 1) { + const fileChannel = channel; + if (channel.extensionId) { + extensionLogs.push(fileChannel); + } else { + logs.push(fileChannel); + } + } + } + const entries: Array = []; + for (const log of logs.sort((a, b) => a.label.localeCompare(b.label))) { + entries.push(log); + } + if (extensionLogs.length && logs.length) { + entries.push({ type: 'separator', label: nls.localize('extensionLogs', "Extension Logs") }); + } + for (const log of extensionLogs.sort((a, b) => a.label.localeCompare(b.label))) { + entries.push(log); + } + const result = await quickInputService.pick(entries, { placeHolder: nls.localize('selectlog', "Select Log"), canPickMany: true }); + if (result?.length) { + outputService.showChannel(outputService.registerCompoundLogChannel(result)); + } + } + })); + + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.output.removeCompoundLog', + title: nls.localize2('removeCompoundLog', "Remove Compound Log..."), + category: Categories.Developer, + f1: true + }); + } + async run(accessor: ServicesAccessor): Promise { + const outputService = accessor.get(IOutputService); + const quickInputService = accessor.get(IQuickInputService); + const notificationService = accessor.get(INotificationService); + const entries: Array = outputService.getChannelDescriptors().filter(channel => channel.files && channel.files?.length > 1); + if (entries.length === 0) { + notificationService.info(nls.localize('noCompoundLogs', "No compound logs to remove.")); + return; + } + const result = await quickInputService.pick(entries, { placeHolder: nls.localize('selectlog', "Select Log"), canPickMany: true }); + for (const channel of result ?? []) { + outputChannelRegistry.removeChannel(channel.id); + } + } })); } @@ -293,7 +362,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { }); } async run(): Promise { - that.openActiveOutoutFile(); + that.openActiveOutputFile(); } })); } @@ -317,17 +386,41 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { }); } async run(): Promise { - that.openActiveOutoutFile(AUX_WINDOW_GROUP); + that.openActiveOutputFile(AUX_WINDOW_GROUP); } })); } - private async openActiveOutoutFile(group?: AUX_WINDOW_GROUP_TYPE): Promise { + private registerSaveActiveOutputAsAction(): void { + const that = this; + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.action.saveActiveLogOutputAs`, + title: nls.localize2('saveActiveOutputAs', "Save Output As..."), + menu: [{ + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), + group: 'export', + }], + }); + } + async run(accessor: ServicesAccessor): Promise { + const outputService = accessor.get(IOutputService); + const fileOutputChannelDescriptor = that.getFileOutputChannelDescriptor(); + if (fileOutputChannelDescriptor) { + await outputService.saveOutputAs(fileOutputChannelDescriptor); + } + } + })); + } + + private async openActiveOutputFile(group?: AUX_WINDOW_GROUP_TYPE): Promise { const fileOutputChannelDescriptor = this.getFileOutputChannelDescriptor(); if (fileOutputChannelDescriptor) { - await this.fileConfigurationService.updateReadonly(fileOutputChannelDescriptor.file, true); + await this.fileConfigurationService.updateReadonly(fileOutputChannelDescriptor.files[0], true); await this.editorService.openEditor({ - resource: fileOutputChannelDescriptor.file, + resource: fileOutputChannelDescriptor.files[0], options: { pinned: true, }, @@ -339,7 +432,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { const channel = this.outputService.getActiveChannel(); if (channel) { const descriptor = this.outputService.getChannelDescriptors().filter(c => c.id === channel.id)[0]; - if (descriptor?.file) { + if (descriptor?.files) { return descriptor; } } @@ -377,8 +470,8 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { const channel = that.outputService.getActiveChannel(); if (channel) { const channelDescriptor = that.outputService.getChannelDescriptor(channel.id); - if (channelDescriptor?.log && channelDescriptor.file) { - return accessor.get(ILoggerService).setLogLevel(channelDescriptor.file, logLevel); + if (channelDescriptor?.log && channelDescriptor.files?.length === 1) { + return accessor.get(ILoggerService).setLogLevel(channelDescriptor.files[0], logLevel); } } } @@ -409,8 +502,8 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { const channel = that.outputService.getActiveChannel(); if (channel) { const channelDescriptor = that.outputService.getChannelDescriptor(channel.id); - if (channelDescriptor?.log && channelDescriptor.file) { - const logLevel = accessor.get(ILoggerService).getLogLevel(channelDescriptor.file); + if (channelDescriptor?.log && channelDescriptor.files?.length === 1) { + const logLevel = accessor.get(ILoggerService).getLogLevel(channelDescriptor.files[0]); return await accessor.get(IDefaultLogLevelsService).setDefaultLogLevel(logLevel, channelDescriptor.extensionId); } } @@ -497,7 +590,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { const extensionChannels: IOutputChannelQuickPickItem[] = []; const coreChannels: IOutputChannelQuickPickItem[] = []; for (const c of outputService.getChannelDescriptors()) { - if (c.file && c.log) { + if (c.files?.length === 1 && c.log) { const e = { id: c.id, label: c.label, channel: c }; if (c.extensionId) { extensionChannels.push(e); @@ -518,7 +611,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { entry = await quickInputService.pick(entries, { placeHolder: nls.localize('selectlogFile', "Select Log File") }); } if (entry) { - const resource = assertIsDefined(entry.channel.file); + const resource = assertIsDefined(entry.channel.files?.[0]); await fileConfigurationService.updateReadonly(resource, true); await editorService.openEditor({ resource, @@ -546,7 +639,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { menu: { id: viewFilterSubmenu, group: '1_filter', - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), CONTEXT_ACTIVE_LOG_FILE_OUTPUT), order: order++ }, viewId: OUTPUT_VIEW_ID @@ -602,6 +695,47 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { })); } + private registerSaveLogsAsAction(): void { + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.action.saveLogsAs`, + title: nls.localize2('saveLogsAs', "Save Logs As..."), + f1: true, + category: Categories.Developer, + }); + } + async run(accessor: ServicesAccessor): Promise { + const outputService = accessor.get(IOutputService); + const quickInputService = accessor.get(IQuickInputService); + const extensionLogs: IFileOutputChannelDescriptor[] = [], logs: IFileOutputChannelDescriptor[] = []; + for (const channel of outputService.getChannelDescriptors()) { + if (channel.log && channel.files?.length === 1) { + const fileChannel = channel; + if (channel.extensionId) { + extensionLogs.push(fileChannel); + } else { + logs.push(fileChannel); + } + } + } + const entries: Array = []; + for (const log of logs.sort((a, b) => a.label.localeCompare(b.label))) { + entries.push(log); + } + if (extensionLogs.length && logs.length) { + entries.push({ type: 'separator', label: nls.localize('extensionLogs', "Extension Logs") }); + } + for (const log of extensionLogs.sort((a, b) => a.label.localeCompare(b.label))) { + entries.push(log); + } + const result = await quickInputService.pick(entries, { placeHolder: nls.localize('selectlog', "Select Log"), canPickMany: true }); + if (result?.length) { + await outputService.saveOutputAs(...result); + } + } + })); + } } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(OutputContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 299491940ce0..37a028ad4c54 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -10,7 +10,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; -import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, IOutputViewFilters, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT } from '../../../services/output/common/output.js'; +import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, IOutputViewFilters, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT, IFileOutputChannelDescriptor } from '../../../services/output/common/output.js'; import { OutputLinkProvider } from './outputLinkProvider.js'; import { ITextModelService, ITextModelContentProvider } from '../../../../editor/common/services/resolverService.js'; import { ITextModel } from '../../../../editor/common/model.js'; @@ -24,6 +24,11 @@ import { ILanguageService } from '../../../../editor/common/languages/language.j import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { SetLogLevelAction } from '../../logs/common/logsActions.js'; import { IDefaultLogLevelsService } from '../../logs/common/defaultLogLevels.js'; +import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { localize } from '../../../../nls.js'; +import { joinPath } from '../../../../base/common/resources.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel'; @@ -44,7 +49,12 @@ class OutputChannel extends Disposable implements IOutputChannel { this.id = outputChannelDescriptor.id; this.label = outputChannelDescriptor.label; this.uri = URI.from({ scheme: Schemas.outputChannel, path: this.id }); - this.model = this._register(outputChannelModelService.createOutputChannelModel(this.id, this.uri, outputChannelDescriptor.languageId ? languageService.createById(outputChannelDescriptor.languageId) : languageService.createByMimeType(outputChannelDescriptor.log ? LOG_MIME : OUTPUT_MIME), outputChannelDescriptor.file)); + this.model = this._register(outputChannelModelService.createOutputChannelModel( + this.id, + this.uri, + outputChannelDescriptor.languageId ? languageService.createById(outputChannelDescriptor.languageId) : languageService.createByMimeType(outputChannelDescriptor.log ? LOG_MIME : OUTPUT_MIME), + outputChannelDescriptor.files ? outputChannelDescriptor.files.map((file, index) => ({ file, name: outputChannelDescriptor.fileNames?.[index] ?? '' })) : undefined) + ); } append(output: string): void { @@ -175,6 +185,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo private readonly activeOutputChannelContext: IContextKey; private readonly activeFileOutputChannelContext: IContextKey; + private readonly activeLogOutputChannelContext: IContextKey; private readonly activeOutputChannelLevelSettableContext: IContextKey; private readonly activeOutputChannelLevelContext: IContextKey; private readonly activeOutputChannelLevelIsDefaultContext: IContextKey; @@ -184,13 +195,15 @@ export class OutputService extends Disposable implements IOutputService, ITextMo constructor( @IStorageService private readonly storageService: IStorageService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @ITextModelService textModelResolverService: ITextModelService, + @ITextModelService private readonly textModelService: ITextModelService, @ILogService private readonly logService: ILogService, @ILoggerService private readonly loggerService: ILoggerService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IViewsService private readonly viewsService: IViewsService, @IContextKeyService contextKeyService: IContextKeyService, - @IDefaultLogLevelsService private readonly defaultLogLevelsService: IDefaultLogLevelsService + @IDefaultLogLevelsService private readonly defaultLogLevelsService: IDefaultLogLevelsService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IFileService private readonly fileService: IFileService, ) { super(); this.activeChannelIdInStorage = this.storageService.get(OUTPUT_ACTIVE_CHANNEL_KEY, StorageScope.WORKSPACE, ''); @@ -199,12 +212,13 @@ export class OutputService extends Disposable implements IOutputService, ITextMo this._register(this.onActiveOutputChannel(channel => this.activeOutputChannelContext.set(channel))); this.activeFileOutputChannelContext = CONTEXT_ACTIVE_FILE_OUTPUT.bindTo(contextKeyService); + this.activeLogOutputChannelContext = CONTEXT_ACTIVE_LOG_FILE_OUTPUT.bindTo(contextKeyService); this.activeOutputChannelLevelSettableContext = CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE.bindTo(contextKeyService); this.activeOutputChannelLevelContext = CONTEXT_ACTIVE_OUTPUT_LEVEL.bindTo(contextKeyService); this.activeOutputChannelLevelIsDefaultContext = CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT.bindTo(contextKeyService); // Register as text model content provider for output - this._register(textModelResolverService.registerTextModelContentProvider(Schemas.outputChannel, this)); + this._register(textModelService.registerTextModelContentProvider(Schemas.outputChannel, this)); this._register(instantiationService.createInstance(OutputLinkProvider)); // Create output channels for already registered channels @@ -212,7 +226,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo for (const channelIdentifier of registry.getChannels()) { this.onDidRegisterChannel(channelIdentifier.id); } - this._register(registry.onDidRegisterChannel(this.onDidRegisterChannel, this)); + this._register(registry.onDidRegisterChannel(id => this.onDidRegisterChannel(id))); // Set active channel to first channel if not set if (!this.activeChannel) { @@ -282,6 +296,66 @@ export class OutputService extends Disposable implements IOutputService, ITextMo return this.activeChannel; } + registerCompoundLogChannel(channels: IFileOutputChannelDescriptor[]): string { + const outputChannelRegistry = Registry.as(Extensions.OutputChannels); + channels.sort((a, b) => a.label.localeCompare(b.label)); + const id = channels.map(r => r.id.toLowerCase()).join('-'); + if (!outputChannelRegistry.getChannel(id)) { + outputChannelRegistry.registerChannel({ + id, + label: channels.map(r => r.label).join(', '), + log: channels.some(r => r.log), + files: channels.map(r => r.files[0]), + fileNames: channels.map(r => r.label) + }); + } + return id; + } + + async saveOutputAs(...channels: IFileOutputChannelDescriptor[]): Promise { + let channel: IOutputChannel | undefined; + if (channels.length > 1) { + const compoundChannelId = this.registerCompoundLogChannel(channels); + channel = this.getChannel(compoundChannelId); + } else { + channel = this.getChannel(channels[0].id); + } + + if (!channel) { + return; + } + + try { + const name = channels.length > 1 ? 'output' : channels[0].label; + const uri = await this.fileDialogService.showSaveDialog({ + title: localize('saveLog.dialogTitle', "Save Output As"), + availableFileSystems: [Schemas.file], + defaultUri: joinPath(await this.fileDialogService.defaultFilePath(), `${name}.log`), + filters: [{ + name, + extensions: ['log'] + }] + }); + + if (!uri) { + return; + } + + const modelRef = await this.textModelService.createModelReference(channel.uri); + try { + await this.fileService.writeFile(uri, VSBuffer.fromString(modelRef.object.textEditorModel.getValue())); + } finally { + modelRef.dispose(); + } + return; + } + finally { + if (channels.length > 1) { + Registry.as(Extensions.OutputChannels).removeChannel(channel.id); + } + } + } + private async onDidRegisterChannel(channelId: string): Promise { const channel = this.createChannel(channelId); this.channels.set(channelId, channel); @@ -322,14 +396,14 @@ export class OutputService extends Disposable implements IOutputService, ITextMo private setLevelContext(): void { const descriptor = this.activeChannel?.outputChannelDescriptor; - const channelLogLevel = descriptor?.log ? this.loggerService.getLogLevel(descriptor.file) : undefined; + const channelLogLevel = descriptor?.log && descriptor.files?.length === 1 ? this.loggerService.getLogLevel(descriptor.files[0]) : undefined; this.activeOutputChannelLevelContext.set(channelLogLevel !== undefined ? LogLevelToString(channelLogLevel) : ''); } private async setLevelIsDefaultContext(): Promise { const descriptor = this.activeChannel?.outputChannelDescriptor; - if (descriptor?.log) { - const channelLogLevel = this.loggerService.getLogLevel(descriptor.file); + if (descriptor?.log && descriptor.files?.length === 1) { + const channelLogLevel = this.loggerService.getLogLevel(descriptor.files[0]); const channelDefaultLogLevel = await this.defaultLogLevelsService.getDefaultLogLevel(descriptor.extensionId); this.activeOutputChannelLevelIsDefaultContext.set(channelDefaultLogLevel === channelLogLevel); } else { @@ -340,7 +414,8 @@ export class OutputService extends Disposable implements IOutputService, ITextMo private setActiveChannel(channel: OutputChannel | undefined): void { this.activeChannel = channel; const descriptor = channel?.outputChannelDescriptor; - this.activeFileOutputChannelContext.set(!!descriptor?.file); + this.activeFileOutputChannelContext.set(descriptor?.files?.length === 1); + this.activeLogOutputChannelContext.set(!!descriptor?.log); this.activeOutputChannelLevelSettableContext.set(descriptor !== undefined && SetLogLevelAction.isLevelSettable(descriptor)); this.setLevelIsDefaultContext(); this.setLevelContext(); diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index c32d92d14b6d..c6d759974add 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -13,7 +13,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { IContextKeyService, IContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IEditorOpenContext } from '../../../common/editor.js'; import { AbstractTextResourceEditor } from '../../../browser/parts/editor/textResourceEditor.js'; -import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT } from '../../../services/output/common/output.js'; +import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT, LOG_ENTRY_REGEX } from '../../../services/output/common/output.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; @@ -346,8 +346,6 @@ interface ILogEntry { readonly lineRange: [number, number]; } -const logEntryRegex = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \[(info|trace|debug|error|warn)\]/; - export class FilterController extends Disposable implements IEditorContribution { public static readonly ID = 'output.editor.contrib.filterController'; @@ -406,7 +404,7 @@ export class FilterController extends Disposable implements IEditorContribution private computeLogEntries(model: ITextModel): void { this.logEntries = undefined; const firstLine = model.getLineContent(1); - if (!logEntryRegex.test(firstLine)) { + if (!LOG_ENTRY_REGEX.test(firstLine)) { return; } @@ -422,15 +420,15 @@ export class FilterController extends Disposable implements IEditorContribution const lineCount = model.getLineCount(); for (let lineNumber = fromLine; lineNumber <= lineCount; lineNumber++) { const lineContent = model.getLineContent(lineNumber); - const match = logEntryRegex.exec(lineContent); + const match = LOG_ENTRY_REGEX.exec(lineContent); if (match) { - const logLevel = this.parseLogLevel(match[2]); + const logLevel = this.parseLogLevel(match[3]); const startLine = lineNumber; let endLine = lineNumber; while (endLine < lineCount) { const nextLineContent = model.getLineContent(endLine + 1); - if (model.getLineFirstNonWhitespaceColumn(endLine + 1) === 0 || logEntryRegex.test(nextLineContent)) { + if (model.getLineFirstNonWhitespaceColumn(endLine + 1) === 0 || LOG_ENTRY_REGEX.test(nextLineContent)) { break; } endLine++; @@ -520,7 +518,7 @@ export class FilterController extends Disposable implements IEditorContribution return LogLevel.Debug; case 'info': return LogLevel.Info; - case 'warn': + case 'warning': return LogLevel.Warning; case 'error': return LogLevel.Error; diff --git a/src/vs/workbench/contrib/output/common/outputChannelModel.ts b/src/vs/workbench/contrib/output/common/outputChannelModel.ts index f4416c250bde..a942ccaf3c16 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModel.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModel.ts @@ -9,11 +9,11 @@ import { ITextModel } from '../../../../editor/common/model.js'; import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { URI } from '../../../../base/common/uri.js'; -import { ThrottledDelayer } from '../../../../base/common/async.js'; +import { Promises, ThrottledDelayer } from '../../../../base/common/async.js'; import { FileOperationResult, IFileService, toFileOperationResult } from '../../../../platform/files/common/files.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { ILanguageSelection } from '../../../../editor/common/languages/language.js'; -import { Disposable, toDisposable, IDisposable, MutableDisposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { Disposable, toDisposable, IDisposable, MutableDisposable, DisposableStore, combinedDisposable } from '../../../../base/common/lifecycle.js'; import { isNumber } from '../../../../base/common/types.js'; import { EditOperation, ISingleEditOperation } from '../../../../editor/common/core/editOperation.js'; import { Position } from '../../../../editor/common/core/position.js'; @@ -21,8 +21,9 @@ import { Range } from '../../../../editor/common/core/range.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { ILogger, ILoggerService, ILogService } from '../../../../platform/log/common/log.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { OutputChannelUpdateMode } from '../../../services/output/common/output.js'; +import { LOG_ENTRY_REGEX, OutputChannelUpdateMode } from '../../../services/output/common/output.js'; import { isCancellationError } from '../../../../base/common/errors.js'; +import { binarySearch } from '../../../../base/common/arrays.js'; export interface IOutputChannelModel extends IDisposable { readonly onDispose: Event; @@ -33,6 +34,11 @@ export interface IOutputChannelModel extends IDisposable { replace(value: string): void; } +export interface IOutputChannelFileInfo { + readonly name: string; + readonly file: URI; +} + interface IContentProvider { readonly onDidAppend: Event; readonly onDidReset: Event; @@ -56,13 +62,18 @@ class FileContentProvider extends Disposable implements IContentProvider { private startOffset: number = 0; private endOffset: number = 0; + private readonly file: URI; + private readonly name: string; + constructor( - private readonly file: URI, + { name, file }: IOutputChannelFileInfo, @IFileService private readonly fileService: IFileService, @ILogService private readonly logService: ILogService, ) { super(); + this.name = name; + this.file = file; this.syncDelayer = new ThrottledDelayer(500); this._register(toDisposable(() => this.unwatch())); } @@ -120,11 +131,12 @@ class FileContentProvider extends Disposable implements IContentProvider { } } - async getContent(): Promise<{ readonly content: string; readonly consume: () => void }> { + async getContent(): Promise<{ readonly name: string; readonly content: string; readonly consume: () => void }> { try { const content = await this.fileService.readFile(this.file, { position: this.endOffset }); let consumed = false; return { + name: this.name, content: content.value.toString(), consume: () => { if (!consumed) { @@ -139,6 +151,7 @@ class FileContentProvider extends Disposable implements IContentProvider { throw error; } return { + name: this.name, content: '', consume: () => { /* No Op */ } }; @@ -146,11 +159,56 @@ class FileContentProvider extends Disposable implements IContentProvider { } } +class MultiFileContentProvider extends Disposable implements IContentProvider { + + readonly onDidAppend: Event; + readonly onDidReset = Event.None; + + private readonly fileOutputs: ReadonlyArray = []; + + constructor( + filesInfos: IOutputChannelFileInfo[], + @IFileService fileService: IFileService, + @ILogService logService: ILogService, + ) { + super(); + this.fileOutputs = filesInfos.map(file => this._register(new FileContentProvider(file, fileService, logService))); + this.onDidAppend = Event.any(...this.fileOutputs.map(output => output.onDidAppend)); + } + + watch(): IDisposable { + return combinedDisposable(...this.fileOutputs.map(output => output.watch())); + } + + reset(): void { + for (const output of this.fileOutputs) { + output.reset(); + } + } + + resetToEnd(): void { + for (const output of this.fileOutputs) { + output.resetToEnd(); + } + } + + async getContent(): Promise<{ readonly content: string; readonly consume: () => void }> { + const outputs = await Promise.all(this.fileOutputs.map(output => output.getContent())); + const content = combineLogEntries(outputs); + return { + content, + consume: () => outputs.forEach(({ consume }) => consume()) + }; + } +} + export abstract class AbstractFileOutputChannelModel extends Disposable implements IOutputChannelModel { private readonly _onDispose = this._register(new Emitter()); readonly onDispose: Event = this._onDispose.event; + protected loadModelPromise: Promise | null = null; + private readonly modelDisposable = this._register(new MutableDisposable()); protected model: ITextModel | null = null; private modelUpdateInProgress: boolean = false; @@ -169,28 +227,28 @@ export abstract class AbstractFileOutputChannelModel extends Disposable implemen } async loadModel(): Promise { - if (!this.model) { - this.modelDisposable.value = new DisposableStore(); - this.model = this.modelService.createModel('', this.language, this.modelUri); - this.outputContentProvider.getContent() - .then(({ content, consume }) => { - if (!this.model || !this.modelDisposable.value) { - return; - } - this.doAppendContent(this.model, content); - consume(); - this.modelDisposable.value.add(this.outputContentProvider.onDidReset(() => this.onDidContentChange(true, true))); - this.modelDisposable.value.add(this.outputContentProvider.onDidAppend(() => this.onDidContentChange(false, false))); - this.modelDisposable.value.add(this.outputContentProvider.watch()); - }); - this.modelDisposable.value.add(this.model.onWillDispose(() => { - this.outputContentProvider.reset(); - this.modelDisposable.value = undefined; - this.cancelModelUpdate(); - this.model = null; - })); - } - return this.model; + this.loadModelPromise = Promises.withAsyncBody(async (c, e) => { + try { + this.modelDisposable.value = new DisposableStore(); + this.model = this.modelService.createModel('', this.language, this.modelUri); + const { content, consume } = await this.outputContentProvider.getContent(); + this.doAppendContent(this.model, content); + consume(); + this.modelDisposable.value.add(this.outputContentProvider.onDidReset(() => this.onDidContentChange(true, true))); + this.modelDisposable.value.add(this.outputContentProvider.onDidAppend(() => this.onDidContentChange(false, false))); + this.modelDisposable.value.add(this.outputContentProvider.watch()); + this.modelDisposable.value.add(this.model.onWillDispose(() => { + this.outputContentProvider.reset(); + this.modelDisposable.value = undefined; + this.cancelModelUpdate(); + this.model = null; + })); + c(this.model); + } catch (error) { + e(error); + } + }); + return this.loadModelPromise; } private onDidContentChange(reset: boolean, appendImmediately: boolean): void { @@ -339,13 +397,13 @@ export class FileOutputChannelModel extends AbstractFileOutputChannelModel imple constructor( modelUri: URI, language: ILanguageSelection, - file: URI, + fileInfo: IOutputChannelFileInfo, @IFileService fileService: IFileService, @IModelService modelService: IModelService, @ILogService logService: ILogService, @IEditorWorkerService editorWorkerService: IEditorWorkerService, ) { - const fileOutput = new FileContentProvider(file, fileService, logService); + const fileOutput = new FileContentProvider(fileInfo, fileService, logService); super(modelUri, language, fileOutput, modelService, editorWorkerService); this.fileOutput = this._register(fileOutput); } @@ -355,16 +413,48 @@ export class FileOutputChannelModel extends AbstractFileOutputChannelModel imple } override update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void { - if (mode === OutputChannelUpdateMode.Clear || mode === OutputChannelUpdateMode.Replace) { - if (isNumber(till)) { - this.fileOutput.reset(till); - } else { - this.fileOutput.resetToEnd(); + const loadModelPromise: Promise = this.loadModelPromise ? this.loadModelPromise : Promise.resolve(); + loadModelPromise.then(() => { + if (mode === OutputChannelUpdateMode.Clear || mode === OutputChannelUpdateMode.Replace) { + if (isNumber(till)) { + this.fileOutput.reset(till); + } else { + this.fileOutput.resetToEnd(); + } } - } - this.doUpdate(mode, immediate); + this.doUpdate(mode, immediate); + }); + } + +} + +export class MultiFileOutputChannelModel extends AbstractFileOutputChannelModel implements IOutputChannelModel { + + private readonly multifileOutput: MultiFileContentProvider; + + constructor( + modelUri: URI, + language: ILanguageSelection, + filesInfos: IOutputChannelFileInfo[], + @IFileService fileService: IFileService, + @IModelService modelService: IModelService, + @ILogService logService: ILogService, + @IEditorWorkerService editorWorkerService: IEditorWorkerService, + ) { + const multifileOutput = new MultiFileContentProvider(filesInfos, fileService, logService); + super(modelUri, language, multifileOutput, modelService, editorWorkerService); + this.multifileOutput = this._register(multifileOutput); + } + + override clear(): void { + const loadModelPromise: Promise = this.loadModelPromise ? this.loadModelPromise : Promise.resolve(); + loadModelPromise.then(() => { + this.multifileOutput.resetToEnd(); + this.doUpdate(OutputChannelUpdateMode.Clear, true); + }); } + override update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void { throw new Error('Not supported'); } } class OutputChannelBackedByFile extends FileOutputChannelModel implements IOutputChannelModel { @@ -383,7 +473,7 @@ class OutputChannelBackedByFile extends FileOutputChannelModel implements IOutpu @ILogService logService: ILogService, @IEditorWorkerService editorWorkerService: IEditorWorkerService ) { - super(modelUri, language, file, fileService, modelService, logService, editorWorkerService); + super(modelUri, language, { file, name: '' }, fileService, modelService, logService, editorWorkerService); // Donot rotate to check for the file reset this.logger = loggerService.createLogger(file, { logLevel: 'always', donotRotate: true, donotUseFormatters: true, hidden: true }); @@ -459,3 +549,74 @@ export class DelegatedOutputChannelModel extends Disposable implements IOutputCh this.outputChannelModel.then(outputChannelModel => outputChannelModel.replace(value)); } } + +function combineLogEntries(outputs: { content: string; name: string }[]): string { + const timestampEntries: Date[] = []; + const combinedEntries: string[] = []; + + let startTimestampOfLastOutput: Date | undefined; + let endTimestampOfLastOutput: Date | undefined; + + for (const output of outputs) { + let startTimestamp: Date | undefined; + let timestamp: Date | undefined; + const logEntries = output.content.split('\n'); + for (let index = 0; index < logEntries.length; index++) { + const entry = logEntries[index]; + if (!entry.trim()) { + continue; + } + timestamp = new Date(entry.match(LOG_ENTRY_REGEX)?.[1]!); + if (!startTimestamp) { + startTimestamp = timestamp; + } + const entriesToAdd = [entry.replace(LOG_ENTRY_REGEX, `$1 [${output.name}] $2`)]; + const timestampsToAdd = [timestamp]; + + if (startTimestampOfLastOutput && timestamp < startTimestampOfLastOutput) { + for (index = index + 1; index < logEntries.length; index++) { + const entry = logEntries[index]; + if (!entry.trim()) { + continue; + } + timestamp = new Date(entry.match(LOG_ENTRY_REGEX)?.[1]!); + if (timestamp > startTimestampOfLastOutput) { + index--; + break; + } + entriesToAdd.push(entry.replace(LOG_ENTRY_REGEX, `$1 [${output.name}] $2`)); + timestampsToAdd.push(timestamp); + } + combinedEntries.unshift(...entriesToAdd); + timestampEntries.unshift(...timestampsToAdd); + continue; + } + + if (endTimestampOfLastOutput && timestamp > endTimestampOfLastOutput) { + for (index = index + 1; index < logEntries.length; index++) { + const entry = logEntries[index]; + if (!entry.trim()) { + continue; + } + timestamp = new Date(entry.match(LOG_ENTRY_REGEX)?.[1]!); + entriesToAdd.push(entry.replace(LOG_ENTRY_REGEX, `$1 [${output.name}] $2`)); + timestampsToAdd.push(timestamp); + } + combinedEntries.push(...entriesToAdd); + timestampEntries.push(...timestampsToAdd); + break; + } + + const idx = binarySearch(timestampEntries, timestamp, (a, b) => a.getTime() - b.getTime()); + const insertionIndex = idx < 0 ? ~idx : idx; + combinedEntries.splice(insertionIndex, 0, ...entriesToAdd); + timestampEntries.splice(insertionIndex, 0, ...timestampsToAdd); + } + + startTimestampOfLastOutput = startTimestamp; + endTimestampOfLastOutput = timestamp; + } + // Add new empty line at the end + combinedEntries.push(''); + return combinedEntries.join('\n'); +} diff --git a/src/vs/workbench/contrib/output/common/outputChannelModelService.ts b/src/vs/workbench/contrib/output/common/outputChannelModelService.ts index f075abc08e6c..f527202b532f 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModelService.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModelService.ts @@ -9,7 +9,7 @@ import { createDecorator, IInstantiationService } from '../../../../platform/ins import { IFileService } from '../../../../platform/files/common/files.js'; import { toLocalISOString } from '../../../../base/common/date.js'; import { joinPath } from '../../../../base/common/resources.js'; -import { DelegatedOutputChannelModel, FileOutputChannelModel, IOutputChannelModel } from './outputChannelModel.js'; +import { DelegatedOutputChannelModel, FileOutputChannelModel, IOutputChannelFileInfo, IOutputChannelModel, MultiFileOutputChannelModel } from './outputChannelModel.js'; import { URI } from '../../../../base/common/uri.js'; import { ILanguageSelection } from '../../../../editor/common/languages/language.js'; @@ -18,7 +18,7 @@ export const IOutputChannelModelService = createDecorator | null = null; diff --git a/src/vs/workbench/contrib/output/electron-sandbox/output.contribution.ts b/src/vs/workbench/contrib/output/electron-sandbox/output.contribution.ts index 7305828c10f4..9790ff104b90 100644 --- a/src/vs/workbench/contrib/output/electron-sandbox/output.contribution.ts +++ b/src/vs/workbench/contrib/output/electron-sandbox/output.contribution.ts @@ -39,8 +39,8 @@ registerAction2(class OpenInConsoleAction extends Action2 { return; } const descriptor = outputService.getChannelDescriptors().find(c => c.id === channel.id); - if (descriptor?.file && descriptor.file.scheme === Schemas.file) { - hostService.openExternal(descriptor.file.toString(true), 'open'); + if (descriptor?.files?.length === 1 && descriptor.files[0].scheme === Schemas.file) { + hostService.openExternal(descriptor.files[0].toString(true), 'open'); } } }); diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index 4cacd7947132..eb97f4a4e198 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -36,6 +36,7 @@ export const OUTPUT_VIEW_ID = 'workbench.panel.output'; export const CONTEXT_IN_OUTPUT = new RawContextKey('inOutput', false); export const CONTEXT_ACTIVE_FILE_OUTPUT = new RawContextKey('activeLogOutput', false); +export const CONTEXT_ACTIVE_LOG_FILE_OUTPUT = new RawContextKey('activeLogOutput.isLog', false); export const CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE = new RawContextKey('activeLogOutput.levelSettable', false); export const CONTEXT_ACTIVE_OUTPUT_LEVEL = new RawContextKey('activeLogOutput.level', ''); export const CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT = new RawContextKey('activeLogOutput.levelIsDefault', false); @@ -48,6 +49,8 @@ export const SHOW_WARNING_FILTER_CONTEXT = new RawContextKey('output.fi export const SHOW_ERROR_FILTER_CONTEXT = new RawContextKey('output.filter.error', true); export const OUTPUT_FILTER_FOCUS_CONTEXT = new RawContextKey('outputFilterFocus', false); +export const LOG_ENTRY_REGEX = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) (\[(info|trace|debug|error|warning)\])/; + export interface IOutputViewFilters { readonly onDidChange: Event; text: string; @@ -102,6 +105,16 @@ export interface IOutputService { * Allows to register on active output channel change. */ onActiveOutputChannel: Event; + + /** + * Register a compound log channel with the given channels. + */ + registerCompoundLogChannel(channels: IFileOutputChannelDescriptor[]): string; + + /** + * Save the logs to a file. + */ + saveOutputAs(...channels: IFileOutputChannelDescriptor[]): Promise; } export enum OutputChannelUpdateMode { @@ -163,18 +176,19 @@ export interface IOutputChannelDescriptor { label: string; log: boolean; languageId?: string; - file?: URI; + files?: URI[]; + fileNames?: string[]; extensionId?: string; } export interface IFileOutputChannelDescriptor extends IOutputChannelDescriptor { - file: URI; + files: URI[]; } export interface IOutputChannelRegistry { readonly onDidRegisterChannel: Event; - readonly onDidRemoveChannel: Event; + readonly onDidRemoveChannel: Event; /** * Make an output channel known to the output world. @@ -201,10 +215,10 @@ class OutputChannelRegistry implements IOutputChannelRegistry { private channels = new Map(); private readonly _onDidRegisterChannel = new Emitter(); - readonly onDidRegisterChannel: Event = this._onDidRegisterChannel.event; + readonly onDidRegisterChannel = this._onDidRegisterChannel.event; - private readonly _onDidRemoveChannel = new Emitter(); - readonly onDidRemoveChannel: Event = this._onDidRemoveChannel.event; + private readonly _onDidRemoveChannel = new Emitter(); + readonly onDidRemoveChannel = this._onDidRemoveChannel.event; public registerChannel(descriptor: IOutputChannelDescriptor): void { if (!this.channels.has(descriptor.id)) { @@ -224,8 +238,11 @@ class OutputChannelRegistry implements IOutputChannelRegistry { } public removeChannel(id: string): void { - this.channels.delete(id); - this._onDidRemoveChannel.fire(id); + const channel = this.channels.get(id); + if (channel) { + this.channels.delete(id); + this._onDidRemoveChannel.fire(channel); + } } } From 2bef1302f1e311ce15898048bede572c0644ec58 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 14 Jan 2025 13:22:05 +0100 Subject: [PATCH 0531/3587] tasks - stop spam from cancellation (#237888) --- .../contrib/tasks/browser/abstractTaskService.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 88792098d83e..2b00a6a1f054 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -83,6 +83,7 @@ import { IPaneCompositePartService } from '../../../services/panecomposite/brows import { IPathService } from '../../../services/path/common/pathService.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; +import { isCancellationError } from '../../../../base/common/errors.js'; const QUICKOPEN_HISTORY_LIMIT_CONFIG = 'task.quickOpen.history'; const PROBLEM_MATCHER_NEVER_CONFIG = 'task.problemMatchers.neverPrompt'; @@ -2055,12 +2056,14 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }; const error = (error: any) => { try { - if (error && Types.isString(error.message)) { - this._log(`Error: ${error.message}\n`); - this._showOutput(); - } else { - this._log('Unknown error received while collecting tasks from providers.'); - this._showOutput(); + if (!isCancellationError(error)) { + if (error && Types.isString(error.message)) { + this._log(`Error: ${error.message}\n`); + this._showOutput(); + } else { + this._log('Unknown error received while collecting tasks from providers.'); + this._showOutput(); + } } } finally { if (--counter === 0) { From 06b5834f4e38a52c4026bc302cdb858a2e47fe49 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 14 Jan 2025 13:32:26 +0100 Subject: [PATCH 0532/3587] chat - align sorting order of context actions (#237892) --- .../contrib/chat/browser/actions/chatContextActions.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 320eeaec1ffd..c628878c314a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -250,7 +250,7 @@ class AttachFileToChatAction extends AttachFileAction { id: MenuId.ChatCommandCenter, group: 'b_chat_context', when: ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor'), - order: 10, + order: 15, }, { id: MenuId.SearchContext, group: 'z_chat', @@ -287,7 +287,7 @@ class AttachSelectionToChatAction extends Action2 { id: MenuId.ChatCommandCenter, group: 'b_chat_context', when: ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor'), - order: 15, + order: 10, }, { id: MenuId.SearchContext, group: 'z_chat', @@ -352,7 +352,7 @@ class AttachFileToEditingSessionAction extends AttachFileAction { id: MenuId.ChatCommandCenter, group: 'c_edits_context', when: ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor'), - order: 10, + order: 15, }, { id: MenuId.SearchContext, group: 'z_chat', @@ -388,7 +388,7 @@ class AttachSelectionToEditingSessionAction extends Action2 { menu: { id: MenuId.ChatCommandCenter, group: 'c_edits_context', - order: 15, + order: 10, } }); } From dfda5c798ea846377d0c8889b5b5f96ed6aa147c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 14 Jan 2025 14:57:18 +0100 Subject: [PATCH 0533/3587] chat - allow context actions in diff editors (#237894) --- .../browser/actions/chatContextActions.ts | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index c628878c314a..b1e1de068893 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -14,7 +14,6 @@ import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { IRange, Range } from '../../../../../editor/common/core/range.js'; -import { EditorType } from '../../../../../editor/common/editorCommon.js'; import { Command } from '../../../../../editor/common/languages.js'; import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from '../../../../../editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.js'; import { localize, localize2 } from '../../../../../nls.js'; @@ -27,7 +26,8 @@ import { KeybindingWeight } from '../../../../../platform/keybinding/common/keyb import { ILabelService } from '../../../../../platform/label/common/label.js'; import { AnythingQuickAccessProviderRunOptions } from '../../../../../platform/quickinput/common/quickAccess.js'; import { IQuickInputService, IQuickPickItem, IQuickPickItemWithResource, IQuickPickSeparator, QuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; -import { ActiveEditorContext } from '../../../../common/contextkeys.js'; +import { ActiveEditorContext, TextCompareEditorActiveContext } from '../../../../common/contextkeys.js'; +import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js'; import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IExtensionService, isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js'; @@ -36,6 +36,7 @@ import { VIEW_ID as SEARCH_VIEW_ID } from '../../../../services/search/common/se import { UntitledTextEditorInput } from '../../../../services/untitled/common/untitledTextEditorInput.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { FileEditorInput } from '../../../files/browser/editors/fileEditorInput.js'; +import { TEXT_FILE_EDITOR_ID } from '../../../files/common/files.js'; import { NotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js'; import { AnythingQuickAccessProvider } from '../../../search/browser/anythingQuickAccess.js'; import { isSearchTreeFileMatch, isSearchTreeMatch } from '../../../search/browser/searchTreeModel/searchTreeCommon.js'; @@ -210,7 +211,7 @@ interface IPromptInstructionsQuickPickItem extends IQuickPickItem { abstract class AttachFileAction extends Action2 { getFiles(accessor: ServicesAccessor, ...args: any[]): URI[] { - const textEditorService = accessor.get(IEditorService); + const editorService = accessor.get(IEditorService); const contexts = Array.isArray(args[1]) ? args[1] : [args[0]]; const files = []; @@ -222,8 +223,8 @@ abstract class AttachFileAction extends Action2 { uri = context.resource; } else if (isSearchTreeMatch(context)) { uri = context.parent().resource; - } else if (!context && textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor) { - uri = textEditorService.activeEditor?.resource; + } else if (!context && editorService.activeTextEditorControl) { + uri = EditorResourceAccessor.getCanonicalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); } if (uri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(uri.scheme)) { @@ -245,11 +246,10 @@ class AttachFileToChatAction extends AttachFileAction { title: localize2('workbench.action.chat.attachFile.label', "Add File to Chat"), category: CHAT_CATEGORY, f1: false, - precondition: ChatContextKeys.enabled, + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or(ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID), TextCompareEditorActiveContext)), menu: [{ id: MenuId.ChatCommandCenter, group: 'b_chat_context', - when: ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor'), order: 15, }, { id: MenuId.SearchContext, @@ -282,11 +282,10 @@ class AttachSelectionToChatAction extends Action2 { title: localize2('workbench.action.chat.attachSelection.label', "Add Selection to Chat"), category: CHAT_CATEGORY, f1: false, - precondition: ChatContextKeys.enabled, + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or(ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID), TextCompareEditorActiveContext)), menu: [{ id: MenuId.ChatCommandCenter, group: 'b_chat_context', - when: ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor'), order: 10, }, { id: MenuId.SearchContext, @@ -298,7 +297,7 @@ class AttachSelectionToChatAction extends Action2 { override async run(accessor: ServicesAccessor, ...args: any[]): Promise { const variablesService = accessor.get(IChatVariablesService); - const textEditorService = accessor.get(IEditorService); + const editorService = accessor.get(IEditorService); const [_, matches] = args; // If we have search matches, it means this is coming from the search widget if (matches && matches.length > 0) { @@ -324,9 +323,9 @@ class AttachSelectionToChatAction extends Action2 { } } } else { - const activeEditor = textEditorService.activeTextEditorControl; - const activeUri = textEditorService.activeEditor?.resource; - if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { + const activeEditor = editorService.activeTextEditorControl; + const activeUri = EditorResourceAccessor.getCanonicalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); + if (editorService.activeTextEditorControl && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { const selection = activeEditor?.getSelection(); if (selection) { (await showChatView(accessor.get(IViewsService)))?.focusInput(); @@ -347,11 +346,10 @@ class AttachFileToEditingSessionAction extends AttachFileAction { title: localize2('workbench.action.edits.attachFile.label', "Add File to {0}", 'Copilot Edits'), category: CHAT_CATEGORY, f1: false, - precondition: ChatContextKeys.enabled, + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or(ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID), TextCompareEditorActiveContext)), menu: [{ id: MenuId.ChatCommandCenter, group: 'c_edits_context', - when: ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor'), order: 15, }, { id: MenuId.SearchContext, @@ -384,7 +382,7 @@ class AttachSelectionToEditingSessionAction extends Action2 { title: localize2('workbench.action.edits.attachSelection.label', "Add Selection to {0}", 'Copilot Edits'), category: CHAT_CATEGORY, f1: false, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor')), + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or(ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID), TextCompareEditorActiveContext)), menu: { id: MenuId.ChatCommandCenter, group: 'c_edits_context', @@ -395,11 +393,11 @@ class AttachSelectionToEditingSessionAction extends Action2 { override async run(accessor: ServicesAccessor, ...args: any[]): Promise { const variablesService = accessor.get(IChatVariablesService); - const textEditorService = accessor.get(IEditorService); + const editorService = accessor.get(IEditorService); - const activeEditor = textEditorService.activeTextEditorControl; - const activeUri = textEditorService.activeEditor?.resource; - if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { + const activeEditor = editorService.activeTextEditorControl; + const activeUri = EditorResourceAccessor.getCanonicalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); + if (editorService.activeTextEditorControl && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { const selection = activeEditor?.getSelection(); if (selection) { (await showEditsView(accessor.get(IViewsService)))?.focusInput(); From 924d7a84bf70beb5067124b1fc866b740f188f89 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:10:20 +0100 Subject: [PATCH 0534/3587] SCM - Add "Open on GitHub" action to history item hover (#237893) --- extensions/github/package.json | 13 +++++-- extensions/github/src/commands.ts | 2 +- src/vs/platform/actions/common/actions.ts | 3 +- .../contrib/scm/browser/scmHistoryViewPane.ts | 39 ++++++++++++------- .../actions/common/menusExtensionPoint.ts | 8 +++- 5 files changed, 46 insertions(+), 19 deletions(-) diff --git a/extensions/github/package.json b/extensions/github/package.json index 5ad7b7493486..d9b491a8f2ea 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -58,7 +58,7 @@ "icon": "$(globe)" }, { - "command": "github.openOnGitHub2", + "command": "github.graph.openOnGitHub", "title": "Open on GitHub", "icon": "$(github)" } @@ -79,7 +79,7 @@ "when": "git-base.gitEnabled && workspaceFolderCount != 0 && remoteName != 'codespaces'" }, { - "command": "github.openOnGitHub2", + "command": "github.graph.openOnGitHub", "when": "false" }, { @@ -141,10 +141,17 @@ ], "scm/historyItem/context": [ { - "command": "github.openOnGitHub2", + "command": "github.graph.openOnGitHub", "when": "github.hasGitHubRepo", "group": "0_view@2" } + ], + "scm/historyItem/hover": [ + { + "command": "github.graph.openOnGitHub", + "when": "github.hasGitHubRepo", + "group": "1_open@1" + } ] }, "configuration": [ diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 8630d2ef0fc4..463b91c19727 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -62,7 +62,7 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { vscode.env.openExternal(vscode.Uri.parse(link)); })); - disposables.add(vscode.commands.registerCommand('github.openOnGitHub2', async (repository: vscode.SourceControl, historyItem: vscode.SourceControlHistoryItem) => { + disposables.add(vscode.commands.registerCommand('github.graph.openOnGitHub', async (repository: vscode.SourceControl, historyItem: vscode.SourceControlHistoryItem) => { if (!repository || !historyItem) { return; } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 9f1452f4b65c..f87a62ed7ebd 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -114,7 +114,6 @@ export class MenuId { static readonly OpenEditorsContextShare = new MenuId('OpenEditorsContextShare'); static readonly ProblemsPanelContext = new MenuId('ProblemsPanelContext'); static readonly SCMInputBox = new MenuId('SCMInputBox'); - static readonly SCMChangesContext = new MenuId('SCMChangesContext'); static readonly SCMChangeContext = new MenuId('SCMChangeContext'); static readonly SCMResourceContext = new MenuId('SCMResourceContext'); static readonly SCMResourceContextShare = new MenuId('SCMResourceContextShare'); @@ -124,6 +123,8 @@ export class MenuId { static readonly SCMSourceControlInline = new MenuId('SCMSourceControlInline'); static readonly SCMSourceControlTitle = new MenuId('SCMSourceControlTitle'); static readonly SCMHistoryTitle = new MenuId('SCMHistoryTitle'); + static readonly SCMHistoryItemContext = new MenuId('SCMHistoryItemContext'); + static readonly SCMHistoryItemHover = new MenuId('SCMHistoryItemHover'); static readonly SCMHistoryItemRefContext = new MenuId('SCMHistoryItemRefContext'); static readonly SCMTitle = new MenuId('SCMTitle'); static readonly SearchContext = new MenuId('SearchContext'); diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 039b2d3b68a7..5544f8649389 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -6,7 +6,7 @@ import './media/scm.css'; import * as platform from '../../../../base/common/platform.js'; import { $, append, h, reset } from '../../../../base/browser/dom.js'; -import { IHoverOptions, IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; +import { IHoverAction, IHoverOptions, IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.js'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; @@ -242,7 +242,7 @@ registerAction2(class extends Action2 { f1: false, menu: [ { - id: MenuId.SCMChangesContext, + id: MenuId.SCMHistoryItemContext, when: ContextKeyExpr.equals('config.multiDiffEditor.experimental.enabled', true), group: '0_view', order: 1 @@ -325,7 +325,9 @@ class HistoryItemRenderer implements ITreeRenderer, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { + const provider = node.element.repository.provider; const historyItemViewModel = node.element.historyItemViewModel; const historyItem = historyItemViewModel.historyItem; const historyItemHover = this._hoverService.setupManagedHover(this.hoverDelegate, templateData.element, this._getHoverContent(node.element), { - actions: this._getHoverActions(historyItem), + actions: this._getHoverActions(provider, historyItem), }); templateData.elementDisposables.add(historyItemHover); @@ -356,7 +359,6 @@ class HistoryItemRenderer implements ITreeRenderer item[1]); + return [ { commandId: 'workbench.scm.action.graph.copyHistoryItemId', @@ -446,12 +453,18 @@ class HistoryItemRenderer implements ITreeRenderer this._clipboardService.writeText(historyItem.id) }, - { - commandId: 'workbench.scm.action.graph.copyHistoryItemMessage', - iconClass: 'codicon.codicon-copy', - label: localize('historyItemMessage', "Message"), - run: () => this._clipboardService.writeText(historyItem.message) - } + ...actions.map(action => { + const iconClass = ThemeIcon.isThemeIcon(action.item.icon) + ? ThemeIcon.asClassNameArray(action.item.icon).join('.') + : undefined; + + return { + commandId: action.id, + label: action.label, + iconClass, + run: () => action.run(historyItem) + }; + }) satisfies IHoverAction[] ]; } @@ -1614,7 +1627,7 @@ export class SCMHistoryViewPane extends ViewPane { } // Register the submenu for the original action - this._contextMenuDisposables.value.add(MenuRegistry.appendMenuItem(MenuId.SCMChangesContext, { + this._contextMenuDisposables.value.add(MenuRegistry.appendMenuItem(MenuId.SCMHistoryItemContext, { title: historyItemRefMenuItem.command.title, submenu: MenuId.for(actionId), group: historyItemRefMenuItem?.group, @@ -1643,7 +1656,7 @@ export class SCMHistoryViewPane extends ViewPane { } } - const historyItemMenuActions = this._menuService.getMenuActions(MenuId.SCMChangesContext, this.scopedContextKeyService, { + const historyItemMenuActions = this._menuService.getMenuActions(MenuId.SCMHistoryItemContext, this.scopedContextKeyService, { arg: element.repository.provider, shouldForwardArgs: true }); diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index e4fd872ab4fe..ded6c47ab61d 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -183,10 +183,16 @@ const apiMenus: IAPIMenu[] = [ }, { key: 'scm/historyItem/context', - id: MenuId.SCMChangesContext, + id: MenuId.SCMHistoryItemContext, description: localize('menus.historyItemContext', "The Source Control history item context menu"), proposed: 'contribSourceControlHistoryItemMenu' }, + { + key: 'scm/historyItem/hover', + id: MenuId.SCMHistoryItemHover, + description: localize('menus.historyItemHover', "The Source Control history item hover menu"), + proposed: 'contribSourceControlHistoryItemMenu' + }, { key: 'scm/historyItemRef/context', id: MenuId.SCMHistoryItemRefContext, From 68c2087f8f44ff8081122f649b3165f06d565687 Mon Sep 17 00:00:00 2001 From: Roland Grunberg Date: Wed, 20 Nov 2024 12:00:55 -0500 Subject: [PATCH 0535/3587] Expose adjustWhitespace to TextEditor API. - Disable adjustWhitespace in text edits with snippets so that code actions do not adjust snippet indentation by default - Adjust Emmet extension so that adjustWhitespace is enabled - Add testcase Signed-off-by: Roland Grunberg --- extensions/emmet/src/abbreviationActions.ts | 4 +-- .../src/singlefolder-tests/editor.test.ts | 31 +++++++++++++++++++ .../contrib/snippet/browser/snippetSession.ts | 2 +- .../workbench/api/browser/mainThreadEditor.ts | 5 +-- .../api/browser/mainThreadEditors.ts | 4 +-- .../workbench/api/common/extHost.protocol.ts | 3 ++ .../contrib/bulkEdit/browser/bulkTextEdits.ts | 2 +- src/vscode-dts/vscode.d.ts | 4 +++ 8 files changed, 47 insertions(+), 8 deletions(-) diff --git a/extensions/emmet/src/abbreviationActions.ts b/extensions/emmet/src/abbreviationActions.ts index 3326722905c6..30ebfe56f93b 100644 --- a/extensions/emmet/src/abbreviationActions.ts +++ b/extensions/emmet/src/abbreviationActions.ts @@ -636,7 +636,7 @@ async function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrLi for (const expandAbbrInput of expandAbbrList) { const expandedText = expandAbbr(expandAbbrInput); if (expandedText) { - await editor.insertSnippet(new vscode.SnippetString(expandedText), expandAbbrInput.rangeToReplace, { undoStopBefore: false, undoStopAfter: false }); + await editor.insertSnippet(new vscode.SnippetString(expandedText), expandAbbrInput.rangeToReplace, { undoStopBefore: false, undoStopAfter: false, adjustWhitespace: true }); insertedSnippetsCount++; } } @@ -650,7 +650,7 @@ async function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrLi const expandedText = expandAbbr(anyExpandAbbrInput); const allRanges = expandAbbrList.map(value => value.rangeToReplace); if (expandedText) { - return editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges); + return editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges, { undoStopBefore: true, undoStopAfter: true, adjustWhitespace: true }); } return false; } diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts index 070a16996844..8f3f0fb01ea5 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts @@ -94,6 +94,37 @@ suite('vscode API - editors', () => { }); }); + /** + * Given : + * This is line 1 + * | + * + * Expect : + * This is line 1 + * This is line 2 + * This is line 3 + * + * The 3rd line should not be auto-indented, as the edit already + * contains the necessary adjustment. + */ + test('insert snippet with replacement, avoid adjusting indentation', () => { + const snippetString = new SnippetString() + .appendText('This is line 2\n This is line 3'); + + return withRandomFileEditor('This is line 1\n ', (editor, doc) => { + editor.selection = new Selection( + new Position(1, 3), + new Position(1, 3) + ); + + return editor.insertSnippet(snippetString, undefined, { undoStopAfter: false, undoStopBefore: false, adjustWhitespace: false }).then(inserted => { + assert.ok(inserted); + assert.strictEqual(doc.getText(), 'This is line 1\n This is line 2\n This is line 3'); + assert.ok(doc.isDirty); + }); + }); + }); + test('insert snippet with replacement, selection as argument', () => { const snippetString = new SnippetString() .appendText('has been'); diff --git a/src/vs/editor/contrib/snippet/browser/snippetSession.ts b/src/vs/editor/contrib/snippet/browser/snippetSession.ts index 79cf57b3abf3..e0bacc7662e5 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetSession.ts @@ -582,7 +582,7 @@ export class SnippetSession { } const newNodes = parser.parseFragment(template, snippet); - SnippetSession.adjustWhitespace(model, range.getStartPosition(), true, snippet, new Set(newNodes)); + SnippetSession.adjustWhitespace(model, range.getStartPosition(), adjustWhitespace, snippet, new Set(newNodes)); snippet.resolveVariables(resolver); const snippetText = snippet.toString(); diff --git a/src/vs/workbench/api/browser/mainThreadEditor.ts b/src/vs/workbench/api/browser/mainThreadEditor.ts index 7dd21444995d..fa923804be1b 100644 --- a/src/vs/workbench/api/browser/mainThreadEditor.ts +++ b/src/vs/workbench/api/browser/mainThreadEditor.ts @@ -14,7 +14,7 @@ import { ITextModel, ITextModelUpdateOptions } from '../../../editor/common/mode import { ISingleEditOperation } from '../../../editor/common/core/editOperation.js'; import { IModelService } from '../../../editor/common/services/model.js'; import { SnippetController2 } from '../../../editor/contrib/snippet/browser/snippetController2.js'; -import { IApplyEditsOptions, IEditorPropertiesChangeData, IResolvedTextEditorConfiguration, ITextEditorConfigurationUpdate, IUndoStopOptions, TextEditorRevealType } from '../common/extHost.protocol.js'; +import { IApplyEditsOptions, IEditorPropertiesChangeData, IResolvedTextEditorConfiguration, ISnippetOptions, ITextEditorConfigurationUpdate, TextEditorRevealType } from '../common/extHost.protocol.js'; import { IEditorPane } from '../../common/editor.js'; import { equals } from '../../../base/common/arrays.js'; import { CodeEditorStateFlag, EditorState } from '../../../editor/contrib/editorState/browser/editorState.js'; @@ -509,7 +509,7 @@ export class MainThreadTextEditor { return true; } - async insertSnippet(modelVersionId: number, template: string, ranges: readonly IRange[], opts: IUndoStopOptions) { + async insertSnippet(modelVersionId: number, template: string, ranges: readonly IRange[], opts: ISnippetOptions) { if (!this._codeEditor || !this._codeEditor.hasModel()) { return false; @@ -542,6 +542,7 @@ export class MainThreadTextEditor { snippetController.apply(edits, { overwriteBefore: 0, overwriteAfter: 0, undoStopBefore: opts.undoStopBefore, undoStopAfter: opts.undoStopAfter, + adjustWhitespace: opts.adjustWhitespace, clipboardText }); diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index a61ba3c78010..41af1eeab605 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -16,7 +16,7 @@ import { CommandsRegistry } from '../../../platform/commands/common/commands.js' import { ITextEditorOptions, IResourceEditorInput, EditorActivation, EditorResolution, ITextEditorDiffInformation, isTextEditorDiffInformationEqual, ITextEditorChange } from '../../../platform/editor/common/editor.js'; import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js'; import { MainThreadTextEditor } from './mainThreadEditor.js'; -import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, TextEditorRevealType } from '../common/extHost.protocol.js'; +import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, ISnippetOptions, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, MainThreadTextEditorsShape, TextEditorRevealType } from '../common/extHost.protocol.js'; import { editorGroupToColumn, columnToEditorGroup, EditorGroupColumn } from '../../services/editor/common/editorGroupColumn.js'; import { IEditorService } from '../../services/editor/common/editorService.js'; import { IEditorGroupsService } from '../../services/editor/common/editorGroupsService.js'; @@ -347,7 +347,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return Promise.resolve(editor.applyEdits(modelVersionId, edits, opts)); } - $tryInsertSnippet(id: string, modelVersionId: number, template: string, ranges: readonly IRange[], opts: IUndoStopOptions): Promise { + $tryInsertSnippet(id: string, modelVersionId: number, template: string, ranges: readonly IRange[], opts: ISnippetOptions): Promise { const editor = this._editorLocator.getEditor(id); if (!editor) { return Promise.reject(illegalArgument(`TextEditor(${id})`)); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 9155c298fc3c..e0dfbc376fb8 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -261,6 +261,9 @@ export interface IApplyEditsOptions extends IUndoStopOptions { setEndOfLine?: EndOfLineSequence; } +export interface ISnippetOptions extends IUndoStopOptions { + adjustWhitespace?: boolean; +} export interface ITextDocumentShowOptions { position?: EditorGroupColumn; preserveFocus?: boolean; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts index 269d2bccfe1f..54bea70bd8cf 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts @@ -156,7 +156,7 @@ class EditorEditTask extends ModelEditTask { }); } } - snippetCtrl.apply(snippetEdits, { undoStopBefore: false, undoStopAfter: false }); + snippetCtrl.apply(snippetEdits, { undoStopBefore: false, undoStopAfter: false, adjustWhitespace: false }); } else { // normal edit diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index f306830ba121..83aca191ec30 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -1312,6 +1312,10 @@ declare module 'vscode' { * Add undo stop after making the edits. */ readonly undoStopAfter: boolean; + /** + * Adjust the indentation of the snippet. + */ + readonly adjustWhitespace?: boolean; }): Thenable; /** From 4f39e1da783d187b0a83e7d427748037210ea6c5 Mon Sep 17 00:00:00 2001 From: Roland Grunberg Date: Thu, 12 Dec 2024 16:12:47 -0500 Subject: [PATCH 0536/3587] Respond to review comments. - Rename adjustWhitespace to keepWhitespace - Retain default behaviour of adjusting whitespace when keepWhitespace not defined Signed-off-by: Roland Grunberg --- extensions/emmet/src/abbreviationActions.ts | 4 ++-- .../vscode-api-tests/src/singlefolder-tests/editor.test.ts | 2 +- src/vs/workbench/api/browser/mainThreadEditor.ts | 2 +- src/vs/workbench/api/browser/mainThreadEditors.ts | 4 ++-- src/vs/workbench/api/common/extHost.protocol.ts | 2 +- src/vs/workbench/api/common/extHostTextEditor.ts | 5 ++++- src/vscode-dts/vscode.d.ts | 4 ++-- 7 files changed, 13 insertions(+), 10 deletions(-) diff --git a/extensions/emmet/src/abbreviationActions.ts b/extensions/emmet/src/abbreviationActions.ts index 30ebfe56f93b..3326722905c6 100644 --- a/extensions/emmet/src/abbreviationActions.ts +++ b/extensions/emmet/src/abbreviationActions.ts @@ -636,7 +636,7 @@ async function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrLi for (const expandAbbrInput of expandAbbrList) { const expandedText = expandAbbr(expandAbbrInput); if (expandedText) { - await editor.insertSnippet(new vscode.SnippetString(expandedText), expandAbbrInput.rangeToReplace, { undoStopBefore: false, undoStopAfter: false, adjustWhitespace: true }); + await editor.insertSnippet(new vscode.SnippetString(expandedText), expandAbbrInput.rangeToReplace, { undoStopBefore: false, undoStopAfter: false }); insertedSnippetsCount++; } } @@ -650,7 +650,7 @@ async function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrLi const expandedText = expandAbbr(anyExpandAbbrInput); const allRanges = expandAbbrList.map(value => value.rangeToReplace); if (expandedText) { - return editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges, { undoStopBefore: true, undoStopAfter: true, adjustWhitespace: true }); + return editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges); } return false; } diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts index 8f3f0fb01ea5..106ef702d693 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts @@ -117,7 +117,7 @@ suite('vscode API - editors', () => { new Position(1, 3) ); - return editor.insertSnippet(snippetString, undefined, { undoStopAfter: false, undoStopBefore: false, adjustWhitespace: false }).then(inserted => { + return editor.insertSnippet(snippetString, undefined, { undoStopAfter: false, undoStopBefore: false, keepWhitespace: true }).then(inserted => { assert.ok(inserted); assert.strictEqual(doc.getText(), 'This is line 1\n This is line 2\n This is line 3'); assert.ok(doc.isDirty); diff --git a/src/vs/workbench/api/browser/mainThreadEditor.ts b/src/vs/workbench/api/browser/mainThreadEditor.ts index fa923804be1b..257e99854526 100644 --- a/src/vs/workbench/api/browser/mainThreadEditor.ts +++ b/src/vs/workbench/api/browser/mainThreadEditor.ts @@ -542,7 +542,7 @@ export class MainThreadTextEditor { snippetController.apply(edits, { overwriteBefore: 0, overwriteAfter: 0, undoStopBefore: opts.undoStopBefore, undoStopAfter: opts.undoStopAfter, - adjustWhitespace: opts.adjustWhitespace, + adjustWhitespace: !opts.keepWhitespace, clipboardText }); diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 41af1eeab605..a61ba3c78010 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -16,7 +16,7 @@ import { CommandsRegistry } from '../../../platform/commands/common/commands.js' import { ITextEditorOptions, IResourceEditorInput, EditorActivation, EditorResolution, ITextEditorDiffInformation, isTextEditorDiffInformationEqual, ITextEditorChange } from '../../../platform/editor/common/editor.js'; import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js'; import { MainThreadTextEditor } from './mainThreadEditor.js'; -import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, ISnippetOptions, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, MainThreadTextEditorsShape, TextEditorRevealType } from '../common/extHost.protocol.js'; +import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, TextEditorRevealType } from '../common/extHost.protocol.js'; import { editorGroupToColumn, columnToEditorGroup, EditorGroupColumn } from '../../services/editor/common/editorGroupColumn.js'; import { IEditorService } from '../../services/editor/common/editorService.js'; import { IEditorGroupsService } from '../../services/editor/common/editorGroupsService.js'; @@ -347,7 +347,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return Promise.resolve(editor.applyEdits(modelVersionId, edits, opts)); } - $tryInsertSnippet(id: string, modelVersionId: number, template: string, ranges: readonly IRange[], opts: ISnippetOptions): Promise { + $tryInsertSnippet(id: string, modelVersionId: number, template: string, ranges: readonly IRange[], opts: IUndoStopOptions): Promise { const editor = this._editorLocator.getEditor(id); if (!editor) { return Promise.reject(illegalArgument(`TextEditor(${id})`)); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index e0dfbc376fb8..68ba8bd88d13 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -262,7 +262,7 @@ export interface IApplyEditsOptions extends IUndoStopOptions { } export interface ISnippetOptions extends IUndoStopOptions { - adjustWhitespace?: boolean; + keepWhitespace?: boolean; } export interface ITextDocumentShowOptions { position?: EditorGroupColumn; diff --git a/src/vs/workbench/api/common/extHostTextEditor.ts b/src/vs/workbench/api/common/extHostTextEditor.ts index 0803130ffbd9..075e511da10a 100644 --- a/src/vs/workbench/api/common/extHostTextEditor.ts +++ b/src/vs/workbench/api/common/extHostTextEditor.ts @@ -495,7 +495,7 @@ export class ExtHostTextEditor { return that._applyEdit(edit); }, // --- snippet edit - insertSnippet(snippet: SnippetString, where?: Position | readonly Position[] | Range | readonly Range[], options: { undoStopBefore: boolean; undoStopAfter: boolean } = { undoStopBefore: true, undoStopAfter: true }): Promise { + insertSnippet(snippet: SnippetString, where?: Position | readonly Position[] | Range | readonly Range[], options: { undoStopBefore: boolean; undoStopAfter: boolean; keepWhitespace?: boolean } = { undoStopBefore: true, undoStopAfter: true, keepWhitespace: true }): Promise { if (that._disposed) { return Promise.reject(new Error('TextEditor#insertSnippet not possible on closed editors')); } @@ -521,6 +521,9 @@ export class ExtHostTextEditor { } } } + if (options.keepWhitespace === undefined) { + options.keepWhitespace = false; + } return _proxy.$tryInsertSnippet(id, document.value.version, snippet.value, ranges, options); }, setDecorations(decorationType: vscode.TextEditorDecorationType, ranges: Range[] | vscode.DecorationOptions[]): void { diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 83aca191ec30..99fdc6be76d4 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -1313,9 +1313,9 @@ declare module 'vscode' { */ readonly undoStopAfter: boolean; /** - * Adjust the indentation of the snippet. + * Keep whitespace of the {@link SnippetString.value} as is. */ - readonly adjustWhitespace?: boolean; + readonly keepWhitespace?: boolean; }): Thenable; /** From ed13a9538d3c5f872d43bf96a5429a7439eea835 Mon Sep 17 00:00:00 2001 From: Roland Grunberg Date: Fri, 13 Dec 2024 09:54:00 -0500 Subject: [PATCH 0537/3587] Remove incorrect method parameter default. Signed-off-by: Roland Grunberg --- src/vs/workbench/api/common/extHostTextEditor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHostTextEditor.ts b/src/vs/workbench/api/common/extHostTextEditor.ts index 075e511da10a..86a322348a8d 100644 --- a/src/vs/workbench/api/common/extHostTextEditor.ts +++ b/src/vs/workbench/api/common/extHostTextEditor.ts @@ -495,7 +495,7 @@ export class ExtHostTextEditor { return that._applyEdit(edit); }, // --- snippet edit - insertSnippet(snippet: SnippetString, where?: Position | readonly Position[] | Range | readonly Range[], options: { undoStopBefore: boolean; undoStopAfter: boolean; keepWhitespace?: boolean } = { undoStopBefore: true, undoStopAfter: true, keepWhitespace: true }): Promise { + insertSnippet(snippet: SnippetString, where?: Position | readonly Position[] | Range | readonly Range[], options: { undoStopBefore: boolean; undoStopAfter: boolean; keepWhitespace?: boolean } = { undoStopBefore: true, undoStopAfter: true }): Promise { if (that._disposed) { return Promise.reject(new Error('TextEditor#insertSnippet not possible on closed editors')); } From 4cdae9cc06d2edc924815d8ef2bd0aab6501449b Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:33:56 +0100 Subject: [PATCH 0538/3587] =?UTF-8?q?GitHub=20-=20=F0=9F=92=84=20extract?= =?UTF-8?q?=20command=20strings=20(#237899)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/github/package.json | 12 ++++++------ extensions/github/package.nls.json | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/extensions/github/package.json b/extensions/github/package.json index d9b491a8f2ea..a1d9b4eb9615 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -38,28 +38,28 @@ "commands": [ { "command": "github.publish", - "title": "Publish to GitHub" + "title": "%command.publish%" }, { "command": "github.copyVscodeDevLink", - "title": "Copy vscode.dev Link" + "title": "%command.copyVscodeDevLink%" }, { "command": "github.copyVscodeDevLinkFile", - "title": "Copy vscode.dev Link" + "title": "%command.copyVscodeDevLink%" }, { "command": "github.copyVscodeDevLinkWithoutRange", - "title": "Copy vscode.dev Link" + "title": "%command.copyVscodeDevLink%" }, { "command": "github.openOnVscodeDev", - "title": "Open in vscode.dev", + "title": "%command.openOnVscodeDev%", "icon": "$(globe)" }, { "command": "github.graph.openOnGitHub", - "title": "Open on GitHub", + "title": "%command.openOnGitHub%", "icon": "$(github)" } ], diff --git a/extensions/github/package.nls.json b/extensions/github/package.nls.json index 5ead7903af20..bceb83403eff 100644 --- a/extensions/github/package.nls.json +++ b/extensions/github/package.nls.json @@ -1,6 +1,10 @@ { "displayName": "GitHub", "description": "GitHub features for VS Code", + "command.copyVscodeDevLink": "Copy vscode.dev Link", + "command.publish": "Publish to GitHub", + "command.openOnGitHub": "Open on GitHub", + "command.openOnVscodeDev": "Open in vscode.dev", "config.branchProtection": "Controls whether to query repository rules for GitHub repositories", "config.gitAuthentication": "Controls whether to enable automatic GitHub authentication for git commands within VS Code.", "config.gitProtocol": "Controls which protocol is used to clone a GitHub repository", From 9b74f608163274d2b9e0d9b6e3821f230fd942b4 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 14 Jan 2025 15:35:11 +0100 Subject: [PATCH 0539/3587] fix #237874 (#237902) --- .../workbench/contrib/preferences/browser/keybindingsEditor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index 3c6e11132b31..f5f2925dd5c3 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -365,7 +365,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP ariaLabelledBy: 'keybindings-editor-aria-label-element', recordEnter: true, quoteRecordedKeys: true, - history: this.getMemento(StorageScope.PROFILE, StorageTarget.USER)['searchHistory'] || [], + history: new Set(this.getMemento(StorageScope.PROFILE, StorageTarget.USER)['searchHistory'] ?? []), inputBoxStyles: getInputBoxStyle({ inputBorder: settingsTextInputBorder }) From 6306b4eb7fd291b6b59c2bae8a4e786f8b4d308c Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 14 Jan 2025 16:09:27 +0100 Subject: [PATCH 0540/3587] forward user local and remote config values to ext hosts (#237898) --- .../standalone/browser/standaloneServices.ts | 3 +- .../configuration/common/configuration.ts | 5 +- .../common/configurationModels.ts | 48 ++-- .../test/common/configurationModels.test.ts | 230 +++++++++++++++++- .../api/common/extHostConfiguration.ts | 10 +- .../test/browser/extHostConfiguration.test.ts | 112 +++++++-- 6 files changed, 366 insertions(+), 42 deletions(-) diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 0e6637019c19..a19d7603763d 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -712,7 +712,8 @@ export class StandaloneConfigurationService implements IConfigurationService { defaults: emptyModel, policy: emptyModel, application: emptyModel, - user: emptyModel, + userLocal: emptyModel, + userRemote: emptyModel, workspace: emptyModel, folders: [] }; diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index e9a9143fa22a..177c4e43e9fe 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IStringDictionary } from '../../../base/common/collections.js'; import { Event } from '../../../base/common/event.js'; import * as types from '../../../base/common/types.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; @@ -182,6 +183,7 @@ export interface IConfigurationModel { contents: any; keys: string[]; overrides: IOverrides[]; + raw?: IStringDictionary; } export interface IOverrides { @@ -194,7 +196,8 @@ export interface IConfigurationData { defaults: IConfigurationModel; policy: IConfigurationModel; application: IConfigurationModel; - user: IConfigurationModel; + userLocal: IConfigurationModel; + userRemote: IConfigurationModel; workspace: IConfigurationModel; folders: [UriComponents, IConfigurationModel][]; } diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index 468614153ed5..4e220cc465d3 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -38,7 +38,7 @@ export class ConfigurationModel implements IConfigurationModel { private readonly _contents: any, private readonly _keys: string[], private readonly _overrides: IOverrides[], - readonly raw: ReadonlyArray | ConfigurationModel> | undefined, + readonly raw: IStringDictionary | ReadonlyArray | ConfigurationModel> | undefined, private readonly logService: ILogService ) { } @@ -46,8 +46,8 @@ export class ConfigurationModel implements IConfigurationModel { private _rawConfiguration: ConfigurationModel | undefined; get rawConfiguration(): ConfigurationModel { if (!this._rawConfiguration) { - if (this.raw?.length) { - const rawConfigurationModels = this.raw.map(raw => { + if (this.raw) { + const rawConfigurationModels = (Array.isArray(this.raw) ? this.raw : [this.raw]).map(raw => { if (raw instanceof ConfigurationModel) { return raw; } @@ -147,10 +147,10 @@ export class ConfigurationModel implements IConfigurationModel { const contents = objects.deepClone(this.contents); const overrides = objects.deepClone(this.overrides); const keys = [...this.keys]; - const raws = this.raw?.length ? [...this.raw] : [this]; + const raws = this.raw ? Array.isArray(this.raw) ? [...this.raw] : [this.raw] : [this]; for (const other of others) { - raws.push(...(other.raw?.length ? other.raw : [other])); + raws.push(...(other.raw ? Array.isArray(other.raw) ? other.raw : [other.raw] : [other])); if (other.isEmpty()) { continue; } @@ -172,7 +172,7 @@ export class ConfigurationModel implements IConfigurationModel { } } } - return new ConfigurationModel(contents, keys, overrides, raws.every(raw => raw instanceof ConfigurationModel) ? undefined : raws, this.logService); + return new ConfigurationModel(contents, keys, overrides, !raws.length || raws.every(raw => raw instanceof ConfigurationModel) ? undefined : raws, this.logService); } private createOverrideConfigurationModel(identifier: string): ConfigurationModel { @@ -944,7 +944,12 @@ export class Configuration { private _userConfiguration: ConfigurationModel | null = null; get userConfiguration(): ConfigurationModel { if (!this._userConfiguration) { - this._userConfiguration = this._remoteUserConfiguration.isEmpty() ? this._localUserConfiguration : this._localUserConfiguration.merge(this._remoteUserConfiguration); + if (this._remoteUserConfiguration.isEmpty()) { + this._userConfiguration = this._localUserConfiguration; + } else { + const merged = this._localUserConfiguration.merge(this._remoteUserConfiguration); + this._userConfiguration = new ConfigurationModel(merged.contents, merged.keys, merged.overrides, undefined, this.logService); + } } return this._userConfiguration; } @@ -1034,7 +1039,7 @@ export class Configuration { defaults: { contents: this._defaultConfiguration.contents, overrides: this._defaultConfiguration.overrides, - keys: this._defaultConfiguration.keys + keys: this._defaultConfiguration.keys, }, policy: { contents: this._policyConfiguration.contents, @@ -1044,12 +1049,20 @@ export class Configuration { application: { contents: this.applicationConfiguration.contents, overrides: this.applicationConfiguration.overrides, - keys: this.applicationConfiguration.keys + keys: this.applicationConfiguration.keys, + raw: Array.isArray(this.applicationConfiguration.raw) ? undefined : this.applicationConfiguration.raw }, - user: { - contents: this.userConfiguration.contents, - overrides: this.userConfiguration.overrides, - keys: this.userConfiguration.keys + userLocal: { + contents: this.localUserConfiguration.contents, + overrides: this.localUserConfiguration.overrides, + keys: this.localUserConfiguration.keys, + raw: Array.isArray(this.localUserConfiguration.raw) ? undefined : this.localUserConfiguration.raw + }, + userRemote: { + contents: this.remoteUserConfiguration.contents, + overrides: this.remoteUserConfiguration.overrides, + keys: this.remoteUserConfiguration.keys, + raw: Array.isArray(this.remoteUserConfiguration.raw) ? undefined : this.remoteUserConfiguration.raw }, workspace: { contents: this._workspaceConfiguration.contents, @@ -1095,7 +1108,8 @@ export class Configuration { const defaultConfiguration = this.parseConfigurationModel(data.defaults, logService); const policyConfiguration = this.parseConfigurationModel(data.policy, logService); const applicationConfiguration = this.parseConfigurationModel(data.application, logService); - const userConfiguration = this.parseConfigurationModel(data.user, logService); + const userLocalConfiguration = this.parseConfigurationModel(data.userLocal, logService); + const userRemoteConfiguration = this.parseConfigurationModel(data.userRemote, logService); const workspaceConfiguration = this.parseConfigurationModel(data.workspace, logService); const folders: ResourceMap = data.folders.reduce((result, value) => { result.set(URI.revive(value[0]), this.parseConfigurationModel(value[1], logService)); @@ -1105,8 +1119,8 @@ export class Configuration { defaultConfiguration, policyConfiguration, applicationConfiguration, - userConfiguration, - ConfigurationModel.createEmptyModel(logService), + userLocalConfiguration, + userRemoteConfiguration, workspaceConfiguration, folders, ConfigurationModel.createEmptyModel(logService), @@ -1116,7 +1130,7 @@ export class Configuration { } private static parseConfigurationModel(model: IConfigurationModel, logService: ILogService): ConfigurationModel { - return new ConfigurationModel(model.contents, model.keys, model.overrides, undefined, logService); + return new ConfigurationModel(model.contents, model.keys, model.overrides, model.raw, logService); } } diff --git a/src/vs/platform/configuration/test/common/configurationModels.test.ts b/src/vs/platform/configuration/test/common/configurationModels.test.ts index 9d5ff123bd6f..27e025bde098 100644 --- a/src/vs/platform/configuration/test/common/configurationModels.test.ts +++ b/src/vs/platform/configuration/test/common/configurationModels.test.ts @@ -366,7 +366,7 @@ suite('ConfigurationModel', () => { }); test('inspect when raw is not same', () => { - const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], [{ + const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], { 'a': 1, 'b': 2, 'c': 1, @@ -375,7 +375,7 @@ suite('ConfigurationModel', () => { 'a': 2, 'b': 1 } - }], new NullLogService()); + }, new NullLogService()); assert.deepStrictEqual(testObject.inspect('a'), { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); assert.deepStrictEqual(testObject.inspect('a', 'x'), { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); @@ -397,7 +397,7 @@ suite('ConfigurationModel', () => { }); test('inspect in merged configuration when raw is not same for one model', () => { - const target1 = new ConfigurationModel({ 'a': 1 }, ['a'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], [{ + const target1 = new ConfigurationModel({ 'a': 1 }, ['a'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], { 'a': 1, 'b': 2, 'c': 3, @@ -405,7 +405,7 @@ suite('ConfigurationModel', () => { 'a': 2, 'b': 4, } - }], new NullLogService()); + }, new NullLogService()); const target2 = new ConfigurationModel({ 'b': 3 }, ['b'], [], undefined, new NullLogService()); const testObject = target1.merge(target2); @@ -554,13 +554,14 @@ export class TestConfiguration extends Configuration { policyConfiguration: ConfigurationModel, applicationConfiguration: ConfigurationModel, localUserConfiguration: ConfigurationModel, + remoteUserConfiguration?: ConfigurationModel, ) { super( defaultConfiguration, policyConfiguration, applicationConfiguration, localUserConfiguration, - ConfigurationModel.createEmptyModel(new NullLogService()), + remoteUserConfiguration ?? ConfigurationModel.createEmptyModel(new NullLogService()), ConfigurationModel.createEmptyModel(new NullLogService()), new ResourceMap(), ConfigurationModel.createEmptyModel(new NullLogService()), @@ -1116,6 +1117,225 @@ suite('ConfigurationChangeEvent', () => { }); +suite('Configuration.Parse', () => { + + const logService = new NullLogService(); + ensureNoDisposablesAreLeakedInTestSuite(); + + test('parsing configuration only with local user configuration and raw is same', () => { + const configuration = new TestConfiguration( + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, 'b': 1 }, keys: ['a'] }], undefined, logService) + ); + + const actual = Configuration.parse(configuration.toData(), logService); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userLocal, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userLocal, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'x' }, undefined).userLocal, { value: undefined, override: 1, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 1 }] }); + assert.deepStrictEqual(actual.inspect('d', {}, undefined).userLocal, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'x' }, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('d', {}, undefined).userRemote, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).user, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).user, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'x' }, undefined).user, { value: undefined, override: 1, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 1 }] }); + assert.deepStrictEqual(actual.inspect('d', {}, undefined).user, undefined); + }); + + test('parsing configuration only with local user configuration and raw is not same', () => { + const configuration = new TestConfiguration( + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], { + 'a': 1, + 'b': 2, + 'c': 1, + 'd': 3, + '[x][y]': { + 'a': 2, + 'b': 1 + } + }, logService) + ); + + const actual = Configuration.parse(configuration.toData(), logService); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userLocal, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userLocal, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'x' }, undefined).userLocal, { value: 2, override: 1, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 1 }] }); + assert.deepStrictEqual(actual.inspect('d', {}, undefined).userLocal, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('e', {}, undefined).userLocal, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'x' }, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('d', {}, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('e', {}, undefined).userRemote, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).user, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).user, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'x' }, undefined).user, { value: 2, override: 1, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 1 }] }); + assert.deepStrictEqual(actual.inspect('d', {}, undefined).user, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('e', {}, undefined).user, undefined); + }); + + test('parsing configuration with local and remote user configuration and raw is same for both', () => { + const configuration = new TestConfiguration( + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + new ConfigurationModel({ 'a': 1 }, ['a'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], undefined, logService), + new ConfigurationModel({ 'b': 3 }, ['b'], [], undefined, logService) + ); + + const actual = Configuration.parse(configuration.toData(), logService); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userLocal, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userLocal, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).userLocal, undefined); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).userLocal, undefined); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).userLocal, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).userRemote, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).userRemote, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).userRemote, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).user, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).user, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).user, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).user, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).user, undefined); + }); + + test('parsing configuration with local and remote user configuration and raw is not same for local user', () => { + const configuration = new TestConfiguration( + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + new ConfigurationModel({ 'a': 1 }, ['a'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], { + 'a': 1, + 'b': 2, + 'c': 3, + '[x][y]': { + 'a': 2, + 'b': 4, + } + }, logService), + new ConfigurationModel({ 'b': 3 }, ['b'], [], undefined, logService) + ); + + const actual = Configuration.parse(configuration.toData(), logService); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userLocal, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userLocal, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).userLocal, { value: 2, override: undefined, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 4 }] }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).userLocal, { value: 2, override: 4, merged: 4, overrides: [{ identifiers: ['x', 'y'], value: 4 }] }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).userLocal, { value: 3, override: undefined, merged: 3, overrides: undefined }); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userRemote, undefined); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).userRemote, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).userRemote, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).userRemote, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).user, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).user, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).user, { value: 3, merged: 3, override: undefined, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).user, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).user, undefined); + }); + + test('parsing configuration with local and remote user configuration and raw is not same for remote user', () => { + const configuration = new TestConfiguration( + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + new ConfigurationModel({ 'b': 3 }, ['b'], [], undefined, logService), + new ConfigurationModel({ 'a': 1 }, ['a'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], { + 'a': 1, + 'b': 2, + 'c': 3, + '[x][y]': { + 'a': 2, + 'b': 4, + } + }, logService), + ); + + const actual = Configuration.parse(configuration.toData(), logService); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userLocal, undefined); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userLocal, undefined); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).userLocal, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).userLocal, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).userLocal, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userRemote, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userRemote, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).userRemote, { value: 2, override: undefined, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 4 }] }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).userRemote, { value: 2, override: 4, merged: 4, overrides: [{ identifiers: ['x', 'y'], value: 4 }] }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).userRemote, { value: 3, override: undefined, merged: 3, overrides: undefined }); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).user, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).user, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).user, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).user, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).user, undefined); + }); + + test('parsing configuration with local and remote user configuration and raw is not same for both', () => { + const configuration = new TestConfiguration( + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + ConfigurationModel.createEmptyModel(logService), + new ConfigurationModel({ 'b': 3 }, ['b'], [], { + 'a': 4, + 'b': 3 + }, logService), + new ConfigurationModel({ 'a': 1 }, ['a'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], { + 'a': 1, + 'b': 2, + 'c': 3, + '[x][y]': { + 'a': 2, + 'b': 4, + } + }, logService), + ); + + const actual = Configuration.parse(configuration.toData(), logService); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userLocal, { value: 4, override: undefined, merged: 4, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userLocal, { value: 4, override: undefined, merged: 4, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).userLocal, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).userLocal, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).userLocal, undefined); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).userRemote, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).userRemote, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).userRemote, { value: 2, override: undefined, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 4 }] }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).userRemote, { value: 2, override: 4, merged: 4, overrides: [{ identifiers: ['x', 'y'], value: 4 }] }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).userRemote, { value: 3, override: undefined, merged: 3, overrides: undefined }); + + assert.deepStrictEqual(actual.inspect('a', {}, undefined).user, { value: 1, override: undefined, merged: 1, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('a', { overrideIdentifier: 'x' }, undefined).user, { value: 1, override: 2, merged: 2, overrides: [{ identifiers: ['x', 'y'], value: 2 }] }); + assert.deepStrictEqual(actual.inspect('b', {}, undefined).user, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('b', { overrideIdentifier: 'y' }, undefined).user, { value: 3, override: undefined, merged: 3, overrides: undefined }); + assert.deepStrictEqual(actual.inspect('c', {}, undefined).user, undefined); + }); + + +}); + function toConfigurationModel(obj: any): ConfigurationModel { const parser = new ConfigurationModelParser('test', new NullLogService()); parser.parse(JSON.stringify(obj)); diff --git a/src/vs/workbench/api/common/extHostConfiguration.ts b/src/vs/workbench/api/common/extHostConfiguration.ts index 5a474e8aa922..0b03122f8eea 100644 --- a/src/vs/workbench/api/common/extHostConfiguration.ts +++ b/src/vs/workbench/api/common/extHostConfiguration.ts @@ -32,15 +32,19 @@ function lookUp(tree: any, key: string) { } } -type ConfigurationInspect = { +export type ConfigurationInspect = { key: string; defaultValue?: T; + globalLocalValue?: T; + globalRemoteValue?: T; globalValue?: T; workspaceValue?: T; workspaceFolderValue?: T; defaultLanguageValue?: T; + globalLocalLanguageValue?: T; + globalRemoteLanguageValue?: T; globalLanguageValue?: T; workspaceLanguageValue?: T; workspaceFolderLanguageValue?: T; @@ -262,11 +266,15 @@ export class ExtHostConfigProvider { key, defaultValue: deepClone(config.policy?.value ?? config.default?.value), + globalLocalValue: deepClone(config.userLocal?.value), + globalRemoteValue: deepClone(config.userRemote?.value), globalValue: deepClone(config.user?.value ?? config.application?.value), workspaceValue: deepClone(config.workspace?.value), workspaceFolderValue: deepClone(config.workspaceFolder?.value), defaultLanguageValue: deepClone(config.default?.override), + globalLocalLanguageValue: deepClone(config.userLocal?.override), + globalRemoteLanguageValue: deepClone(config.userRemote?.override), globalLanguageValue: deepClone(config.user?.override ?? config.application?.override), workspaceLanguageValue: deepClone(config.workspace?.override), workspaceFolderLanguageValue: deepClone(config.workspaceFolder?.override), diff --git a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts index 00c9af619388..4bc30da2455c 100644 --- a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts +++ b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { ExtHostWorkspace } from '../../common/extHostWorkspace.js'; -import { ExtHostConfigProvider } from '../../common/extHostConfiguration.js'; +import { ConfigurationInspect, ExtHostConfigProvider } from '../../common/extHostConfiguration.js'; import { MainThreadConfigurationShape, IConfigurationInitData } from '../../common/extHost.protocol.js'; import { ConfigurationModel, ConfigurationModelParser } from '../../../../platform/configuration/common/configurationModels.js'; import { TestRPCProtocol } from '../common/testRPCProtocol.js'; @@ -47,7 +47,8 @@ suite('ExtHostConfiguration', function () { defaults: new ConfigurationModel(contents, [], [], undefined, new NullLogService()), policy: ConfigurationModel.createEmptyModel(new NullLogService()), application: ConfigurationModel.createEmptyModel(new NullLogService()), - user: new ConfigurationModel(contents, [], [], undefined, new NullLogService()), + userLocal: new ConfigurationModel(contents, [], [], undefined, new NullLogService()), + userRemote: ConfigurationModel.createEmptyModel(new NullLogService()), workspace: ConfigurationModel.createEmptyModel(new NullLogService()), folders: [], configurationScopes: [] @@ -282,16 +283,29 @@ suite('ExtHostConfiguration', function () { { defaults: new ConfigurationModel({ 'editor': { - 'wordWrap': 'off' + 'wordWrap': 'off', + 'lineNumbers': 'on', + 'fontSize': '12px' } }, ['editor.wordWrap'], [], undefined, new NullLogService()), policy: ConfigurationModel.createEmptyModel(new NullLogService()), application: ConfigurationModel.createEmptyModel(new NullLogService()), - user: new ConfigurationModel({ + userLocal: new ConfigurationModel({ 'editor': { - 'wordWrap': 'on' + 'wordWrap': 'on', + 'lineNumbers': 'off' } - }, ['editor.wordWrap'], [], undefined, new NullLogService()), + }, ['editor.wordWrap', 'editor.lineNumbers'], [], undefined, new NullLogService()), + userRemote: new ConfigurationModel({ + 'editor': { + 'lineNumbers': 'relative' + } + }, ['editor.lineNumbers'], [], { + 'editor': { + 'lineNumbers': 'relative', + 'fontSize': '14px' + } + }, new NullLogService()), workspace: new ConfigurationModel({}, [], [], undefined, new NullLogService()), folders: [], configurationScopes: [] @@ -299,17 +313,39 @@ suite('ExtHostConfiguration', function () { new NullLogService() ); - let actual = testObject.getConfiguration().inspect('editor.wordWrap')!; + let actual: ConfigurationInspect = testObject.getConfiguration().inspect('editor.wordWrap')!; assert.strictEqual(actual.defaultValue, 'off'); + assert.strictEqual(actual.globalLocalValue, 'on'); + assert.strictEqual(actual.globalRemoteValue, undefined); assert.strictEqual(actual.globalValue, 'on'); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, undefined); actual = testObject.getConfiguration('editor').inspect('wordWrap')!; assert.strictEqual(actual.defaultValue, 'off'); + assert.strictEqual(actual.globalLocalValue, 'on'); + assert.strictEqual(actual.globalRemoteValue, undefined); assert.strictEqual(actual.globalValue, 'on'); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, undefined); + + actual = testObject.getConfiguration('editor').inspect('lineNumbers')!; + assert.strictEqual(actual.defaultValue, 'on'); + assert.strictEqual(actual.globalLocalValue, 'off'); + assert.strictEqual(actual.globalRemoteValue, 'relative'); + assert.strictEqual(actual.globalValue, 'relative'); + assert.strictEqual(actual.workspaceValue, undefined); + assert.strictEqual(actual.workspaceFolderValue, undefined); + + assert.strictEqual(testObject.getConfiguration('editor').get('fontSize'), '12px'); + + actual = testObject.getConfiguration('editor').inspect('fontSize')!; + assert.strictEqual(actual.defaultValue, '12px'); + assert.strictEqual(actual.globalLocalValue, undefined); + assert.strictEqual(actual.globalRemoteValue, '14px'); + assert.strictEqual(actual.globalValue, undefined); + assert.strictEqual(actual.workspaceValue, undefined); + assert.strictEqual(actual.workspaceFolderValue, undefined); }); test('inspect in single root context', function () { @@ -338,11 +374,12 @@ suite('ExtHostConfiguration', function () { }, ['editor.wordWrap'], [], undefined, new NullLogService()), policy: ConfigurationModel.createEmptyModel(new NullLogService()), application: ConfigurationModel.createEmptyModel(new NullLogService()), - user: new ConfigurationModel({ + userLocal: new ConfigurationModel({ 'editor': { 'wordWrap': 'on' } }, ['editor.wordWrap'], [], undefined, new NullLogService()), + userRemote: ConfigurationModel.createEmptyModel(new NullLogService()), workspace, folders, configurationScopes: [] @@ -350,26 +387,34 @@ suite('ExtHostConfiguration', function () { new NullLogService() ); - let actual1 = testObject.getConfiguration().inspect('editor.wordWrap')!; + let actual1: ConfigurationInspect = testObject.getConfiguration().inspect('editor.wordWrap')!; assert.strictEqual(actual1.defaultValue, 'off'); + assert.strictEqual(actual1.globalLocalValue, 'on'); + assert.strictEqual(actual1.globalRemoteValue, undefined); assert.strictEqual(actual1.globalValue, 'on'); assert.strictEqual(actual1.workspaceValue, 'bounded'); assert.strictEqual(actual1.workspaceFolderValue, undefined); actual1 = testObject.getConfiguration('editor').inspect('wordWrap')!; assert.strictEqual(actual1.defaultValue, 'off'); + assert.strictEqual(actual1.globalLocalValue, 'on'); + assert.strictEqual(actual1.globalRemoteValue, undefined); assert.strictEqual(actual1.globalValue, 'on'); assert.strictEqual(actual1.workspaceValue, 'bounded'); assert.strictEqual(actual1.workspaceFolderValue, undefined); - let actual2 = testObject.getConfiguration(undefined, workspaceUri).inspect('editor.wordWrap')!; + let actual2: ConfigurationInspect = testObject.getConfiguration(undefined, workspaceUri).inspect('editor.wordWrap')!; assert.strictEqual(actual2.defaultValue, 'off'); + assert.strictEqual(actual2.globalLocalValue, 'on'); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.globalValue, 'on'); assert.strictEqual(actual2.workspaceValue, 'bounded'); assert.strictEqual(actual2.workspaceFolderValue, 'bounded'); actual2 = testObject.getConfiguration('editor', workspaceUri).inspect('wordWrap')!; assert.strictEqual(actual2.defaultValue, 'off'); + assert.strictEqual(actual2.globalLocalValue, 'on'); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.globalValue, 'on'); assert.strictEqual(actual2.workspaceValue, 'bounded'); assert.strictEqual(actual2.workspaceFolderValue, 'bounded'); @@ -417,11 +462,12 @@ suite('ExtHostConfiguration', function () { }, ['editor.wordWrap'], [], undefined, new NullLogService()), policy: ConfigurationModel.createEmptyModel(new NullLogService()), application: ConfigurationModel.createEmptyModel(new NullLogService()), - user: new ConfigurationModel({ + userLocal: new ConfigurationModel({ 'editor': { 'wordWrap': 'on' } }, ['editor.wordWrap'], [], undefined, new NullLogService()), + userRemote: ConfigurationModel.createEmptyModel(new NullLogService()), workspace, folders, configurationScopes: [] @@ -429,57 +475,75 @@ suite('ExtHostConfiguration', function () { new NullLogService() ); - let actual1 = testObject.getConfiguration().inspect('editor.wordWrap')!; + let actual1: ConfigurationInspect = testObject.getConfiguration().inspect('editor.wordWrap')!; assert.strictEqual(actual1.defaultValue, 'off'); assert.strictEqual(actual1.globalValue, 'on'); + assert.strictEqual(actual1.globalLocalValue, 'on'); + assert.strictEqual(actual1.globalRemoteValue, undefined); assert.strictEqual(actual1.workspaceValue, 'bounded'); assert.strictEqual(actual1.workspaceFolderValue, undefined); actual1 = testObject.getConfiguration('editor').inspect('wordWrap')!; assert.strictEqual(actual1.defaultValue, 'off'); assert.strictEqual(actual1.globalValue, 'on'); + assert.strictEqual(actual1.globalLocalValue, 'on'); + assert.strictEqual(actual1.globalRemoteValue, undefined); assert.strictEqual(actual1.workspaceValue, 'bounded'); assert.strictEqual(actual1.workspaceFolderValue, undefined); actual1 = testObject.getConfiguration('editor').inspect('lineNumbers')!; assert.strictEqual(actual1.defaultValue, 'on'); assert.strictEqual(actual1.globalValue, undefined); + assert.strictEqual(actual1.globalLocalValue, undefined); + assert.strictEqual(actual1.globalRemoteValue, undefined); assert.strictEqual(actual1.workspaceValue, undefined); assert.strictEqual(actual1.workspaceFolderValue, undefined); - let actual2 = testObject.getConfiguration(undefined, firstRoot).inspect('editor.wordWrap')!; + let actual2: ConfigurationInspect = testObject.getConfiguration(undefined, firstRoot).inspect('editor.wordWrap')!; assert.strictEqual(actual2.defaultValue, 'off'); assert.strictEqual(actual2.globalValue, 'on'); + assert.strictEqual(actual2.globalLocalValue, 'on'); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.workspaceValue, 'bounded'); assert.strictEqual(actual2.workspaceFolderValue, 'off'); actual2 = testObject.getConfiguration('editor', firstRoot).inspect('wordWrap')!; assert.strictEqual(actual2.defaultValue, 'off'); assert.strictEqual(actual2.globalValue, 'on'); + assert.strictEqual(actual2.globalLocalValue, 'on'); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.workspaceValue, 'bounded'); assert.strictEqual(actual2.workspaceFolderValue, 'off'); actual2 = testObject.getConfiguration('editor', firstRoot).inspect('lineNumbers')!; assert.strictEqual(actual2.defaultValue, 'on'); assert.strictEqual(actual2.globalValue, undefined); + assert.strictEqual(actual2.globalLocalValue, undefined); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.workspaceValue, undefined); assert.strictEqual(actual2.workspaceFolderValue, 'relative'); actual2 = testObject.getConfiguration(undefined, secondRoot).inspect('editor.wordWrap')!; assert.strictEqual(actual2.defaultValue, 'off'); assert.strictEqual(actual2.globalValue, 'on'); + assert.strictEqual(actual2.globalLocalValue, 'on'); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.workspaceValue, 'bounded'); assert.strictEqual(actual2.workspaceFolderValue, 'on'); actual2 = testObject.getConfiguration('editor', secondRoot).inspect('wordWrap')!; assert.strictEqual(actual2.defaultValue, 'off'); assert.strictEqual(actual2.globalValue, 'on'); + assert.strictEqual(actual2.globalLocalValue, 'on'); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.workspaceValue, 'bounded'); assert.strictEqual(actual2.workspaceFolderValue, 'on'); actual2 = testObject.getConfiguration(undefined, thirdRoot).inspect('editor.wordWrap')!; assert.strictEqual(actual2.defaultValue, 'off'); assert.strictEqual(actual2.globalValue, 'on'); + assert.strictEqual(actual2.globalLocalValue, 'on'); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.workspaceValue, 'bounded'); assert.ok(Object.keys(actual2).indexOf('workspaceFolderValue') !== -1); assert.strictEqual(actual2.workspaceFolderValue, undefined); @@ -487,6 +551,8 @@ suite('ExtHostConfiguration', function () { actual2 = testObject.getConfiguration('editor', thirdRoot).inspect('wordWrap')!; assert.strictEqual(actual2.defaultValue, 'off'); assert.strictEqual(actual2.globalValue, 'on'); + assert.strictEqual(actual2.globalLocalValue, 'on'); + assert.strictEqual(actual2.globalRemoteValue, undefined); assert.strictEqual(actual2.workspaceValue, 'bounded'); assert.ok(Object.keys(actual2).indexOf('workspaceFolderValue') !== -1); assert.strictEqual(actual2.workspaceFolderValue, undefined); @@ -522,12 +588,13 @@ suite('ExtHostConfiguration', function () { }), policy: ConfigurationModel.createEmptyModel(new NullLogService()), application: ConfigurationModel.createEmptyModel(new NullLogService()), - user: toConfigurationModel({ + userLocal: toConfigurationModel({ 'editor.wordWrap': 'bounded', '[typescript]': { 'editor.lineNumbers': 'off', } }), + userRemote: ConfigurationModel.createEmptyModel(new NullLogService()), workspace: toConfigurationModel({ '[typescript]': { 'editor.wordWrap': 'unbounded', @@ -540,9 +607,11 @@ suite('ExtHostConfiguration', function () { new NullLogService() ); - let actual = testObject.getConfiguration(undefined, { uri: firstRoot, languageId: 'typescript' }).inspect('editor.wordWrap')!; + let actual: ConfigurationInspect = testObject.getConfiguration(undefined, { uri: firstRoot, languageId: 'typescript' }).inspect('editor.wordWrap')!; assert.strictEqual(actual.defaultValue, 'off'); assert.strictEqual(actual.globalValue, 'bounded'); + assert.strictEqual(actual.globalLocalValue, 'bounded'); + assert.strictEqual(actual.globalRemoteValue, undefined); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, 'bounded'); assert.strictEqual(actual.defaultLanguageValue, undefined); @@ -554,6 +623,8 @@ suite('ExtHostConfiguration', function () { actual = testObject.getConfiguration(undefined, { uri: secondRoot, languageId: 'typescript' }).inspect('editor.wordWrap')!; assert.strictEqual(actual.defaultValue, 'off'); assert.strictEqual(actual.globalValue, 'bounded'); + assert.strictEqual(actual.globalLocalValue, 'bounded'); + assert.strictEqual(actual.globalRemoteValue, undefined); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, undefined); assert.strictEqual(actual.defaultLanguageValue, undefined); @@ -582,12 +653,13 @@ suite('ExtHostConfiguration', function () { 'wordWrap': 'on' } }, ['editor.wordWrap'], [], undefined, new NullLogService()), - user: new ConfigurationModel({ + userLocal: new ConfigurationModel({ 'editor': { 'wordWrap': 'auto', 'lineNumbers': 'off' } }, ['editor.wordWrap'], [], undefined, new NullLogService()), + userRemote: ConfigurationModel.createEmptyModel(new NullLogService()), workspace: new ConfigurationModel({}, [], [], undefined, new NullLogService()), folders: [], configurationScopes: [] @@ -595,9 +667,11 @@ suite('ExtHostConfiguration', function () { new NullLogService() ); - let actual = testObject.getConfiguration().inspect('editor.wordWrap')!; + let actual: ConfigurationInspect = testObject.getConfiguration().inspect('editor.wordWrap')!; assert.strictEqual(actual.defaultValue, 'off'); assert.strictEqual(actual.globalValue, 'auto'); + assert.strictEqual(actual.globalLocalValue, 'auto'); + assert.strictEqual(actual.globalRemoteValue, undefined); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, undefined); assert.strictEqual(testObject.getConfiguration().get('editor.wordWrap'), 'auto'); @@ -605,12 +679,16 @@ suite('ExtHostConfiguration', function () { actual = testObject.getConfiguration().inspect('editor.lineNumbers')!; assert.strictEqual(actual.defaultValue, 'on'); assert.strictEqual(actual.globalValue, 'off'); + assert.strictEqual(actual.globalLocalValue, 'off'); + assert.strictEqual(actual.globalRemoteValue, undefined); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, undefined); assert.strictEqual(testObject.getConfiguration().get('editor.lineNumbers'), 'off'); actual = testObject.getConfiguration().inspect('editor.fontSize')!; assert.strictEqual(actual.defaultValue, '12px'); + assert.strictEqual(actual.globalLocalValue, undefined); + assert.strictEqual(actual.globalRemoteValue, undefined); assert.strictEqual(actual.globalValue, undefined); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, undefined); From 9559aadb775cbbd4a2805ec9da3067fc0da5158a Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Tue, 14 Jan 2025 13:12:13 +0100 Subject: [PATCH 0541/3587] Improve test (#237890) --- .../src/singlefolder-tests/proxy.test.ts | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts index ccda85b442a1..37f886b145b8 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts @@ -105,18 +105,27 @@ import { middleware, Straightforward } from 'straightforward'; await proxyListen; const proxyPort = (sf.server.address() as AddressInfo).port; - await vscode.workspace.getConfiguration().update('integration-test.http.proxy', `PROXY 127.0.0.1:${proxyPort}`, vscode.ConfigurationTarget.Global); - await delay(1000); // Wait for the configuration change to propagate. - await new Promise((resolve, reject) => { - https.get(url, res => { - if (res.statusCode === 418) { - resolve(); - } else { - reject(new Error(`Unexpected status code (expected 418): ${res.statusCode}`)); + for (let i = 0; i < 3; i++) { + await vscode.workspace.getConfiguration().update('integration-test.http.proxy', `PROXY 127.0.0.1:${proxyPort}`, vscode.ConfigurationTarget.Global); + await delay(1000); // Wait for the configuration change to propagate. + try { + await new Promise((resolve, reject) => { + https.get(url, res => { + if (res.statusCode === 418) { + resolve(); + } else { + reject(new Error(`Unexpected status code (expected 418): ${res.statusCode}`)); + } + }) + .on('error', reject); + }); + break; // Exit the loop if the request is successful + } catch (err) { + if (i === 2) { + throw err; // Rethrow the error if it's the last attempt } - }) - .on('error', reject); - }); + } + } authEnabled = true; await new Promise((resolve, reject) => { @@ -130,18 +139,27 @@ import { middleware, Straightforward } from 'straightforward'; .on('error', reject); }); - await vscode.workspace.getConfiguration().update('integration-test.http.proxyAuth', `${user}:${pass}`, vscode.ConfigurationTarget.Global); - await delay(1000); // Wait for the configuration change to propagate. - await new Promise((resolve, reject) => { - https.get(url, res => { - if (res.statusCode === 204) { - resolve(); - } else { - reject(new Error(`Unexpected status code (expected 204): ${res.statusCode}`)); + for (let i = 0; i < 3; i++) { + await vscode.workspace.getConfiguration().update('integration-test.http.proxyAuth', `${user}:${pass}`, vscode.ConfigurationTarget.Global); + await delay(1000); // Wait for the configuration change to propagate. + try { + await new Promise((resolve, reject) => { + https.get(url, res => { + if (res.statusCode === 204) { + resolve(); + } else { + reject(new Error(`Unexpected status code (expected 204): ${res.statusCode}`)); + } + }) + .on('error', reject); + }); + break; // Exit the loop if the request is successful + } catch (err) { + if (i === 2) { + throw err; // Rethrow the error if it's the last attempt } - }) - .on('error', reject); - }); + } + } } finally { sf.close(); await vscode.workspace.getConfiguration().update('integration-test.http.proxy', undefined, vscode.ConfigurationTarget.Global); From 92c30f1bf7be4214743cb7bd4d131e8a3efc30c0 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 14 Jan 2025 16:56:51 +0100 Subject: [PATCH 0542/3587] Add export actions to view overflow menu (#237907) --- .../output/browser/output.contribution.ts | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index ee82400c9c49..18d6bdb400e1 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -113,7 +113,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { this.registerOpenLogFileAction(); this.registerConfigureActiveOutputLogLevelAction(); this.registerFilterActions(); - this.registerSaveLogsAsAction(); + this.registerExportLogsAction(); } private registerSwitchOutputAction(): void { @@ -182,7 +182,11 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { id: 'workbench.action.output.addCompoundLog', title: nls.localize2('addCompoundLog', "Add Compound Log..."), category: Categories.Developer, - f1: true + f1: true, + menu: [{ + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), + }], }); } async run(accessor: ServicesAccessor): Promise { @@ -402,6 +406,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { id: MenuId.ViewTitle, when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), group: 'export', + order: 1 }], }); } @@ -695,14 +700,20 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { })); } - private registerSaveLogsAsAction(): void { + private registerExportLogsAction(): void { this._register(registerAction2(class extends Action2 { constructor() { super({ - id: `workbench.action.saveLogsAs`, - title: nls.localize2('saveLogsAs', "Save Logs As..."), + id: `workbench.action.exportLogs`, + title: nls.localize2('exportLogs', "Export Logs..."), f1: true, category: Categories.Developer, + menu: [{ + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), + group: 'export', + order: 2, + }], }); } async run(accessor: ServicesAccessor): Promise { From 3172a640240731f457e213bfe1bf40e235a8b70f Mon Sep 17 00:00:00 2001 From: simon-id Date: Tue, 14 Jan 2025 16:59:02 +0100 Subject: [PATCH 0543/3587] search: fix findMatch colors --- .../contrib/search/browser/searchTreeModel/fileMatch.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/search/browser/searchTreeModel/fileMatch.ts b/src/vs/workbench/contrib/search/browser/searchTreeModel/fileMatch.ts index dfaf589263b1..7986616810df 100644 --- a/src/vs/workbench/contrib/search/browser/searchTreeModel/fileMatch.ts +++ b/src/vs/workbench/contrib/search/browser/searchTreeModel/fileMatch.ts @@ -30,6 +30,7 @@ export class FileMatchImpl extends Disposable implements ISearchTreeFileMatch { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, zIndex: 13, className: 'currentFindMatch', + inlineClassName: 'currentFindMatchInline', overviewRuler: { color: themeColorFromId(overviewRulerFindMatchForeground), position: OverviewRulerLane.Center @@ -44,6 +45,7 @@ export class FileMatchImpl extends Disposable implements ISearchTreeFileMatch { description: 'search-find-match', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'findMatch', + inlineClassName: 'findMatchInline', overviewRuler: { color: themeColorFromId(overviewRulerFindMatchForeground), position: OverviewRulerLane.Center From 29fa0859d0e9510195753027394d9c21f82fc00e Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 14 Jan 2025 08:17:35 -0800 Subject: [PATCH 0544/3587] feat: add video support to walkthrough steps and improve media handling (#237851) --- .../platform/extensions/common/extensions.ts | 7 +-- .../browser/gettingStarted.ts | 50 +++++++++++++++++ .../browser/gettingStartedDetailsRenderer.ts | 50 +++++++++++++++++ .../browser/gettingStartedService.ts | 53 ++++++++++++++++--- .../browser/media/gettingStarted.css | 5 ++ .../common/gettingStartedContent.ts | 4 +- 6 files changed, 157 insertions(+), 12 deletions(-) diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index d6758a9b54df..b2d6194419a1 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -111,9 +111,10 @@ export interface IWalkthroughStep { readonly title: string; readonly description: string | undefined; readonly media: - | { image: string | { dark: string; light: string; hc: string }; altText: string; markdown?: never; svg?: never } - | { markdown: string; image?: never; svg?: never } - | { svg: string; altText: string; markdown?: never; image?: never }; + | { image: string | { dark: string; light: string; hc: string }; altText: string; markdown?: never; svg?: never; video?: never } + | { markdown: string; image?: never; svg?: never; video?: never } + | { svg: string; altText: string; markdown?: never; image?: never; video?: never } + | { video: string | { dark: string; light: string; hc: string }; poster: string | { dark: string; light: string; hc: string }; altText: string; markdown?: never; image?: never; svg?: never }; readonly completionEvents?: string[]; /** @deprecated use `completionEvents: 'onCommand:...'` */ readonly doneOn?: { command: string }; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index b0f5b5f009b8..9bf4d3ff8d9a 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -552,6 +552,9 @@ export class GettingStartedPage extends EditorPane { } else if (stepToExpand.media.type === 'markdown') { this.webview = this.mediaDisposables.add(this.webviewService.createWebviewElement({ options: {}, contentOptions: { localResourceRoots: [stepToExpand.media.root], allowScripts: true }, title: '', extension: undefined })); this.webview.mountTo(this.stepMediaComponent, this.window); + } else if (stepToExpand.media.type === 'video') { + this.webview = this.mediaDisposables.add(this.webviewService.createWebviewElement({ options: {}, contentOptions: { localResourceRoots: [stepToExpand.media.root], allowScripts: true }, title: '', extension: undefined })); + this.webview.mountTo(this.stepMediaComponent, this.window); } } @@ -559,6 +562,7 @@ export class GettingStartedPage extends EditorPane { this.stepsContent.classList.add('image'); this.stepsContent.classList.remove('markdown'); + this.stepsContent.classList.remove('video'); const media = stepToExpand.media; const mediaElement = $('img'); @@ -584,6 +588,7 @@ export class GettingStartedPage extends EditorPane { else if (stepToExpand.media.type === 'svg') { this.stepsContent.classList.add('image'); this.stepsContent.classList.remove('markdown'); + this.stepsContent.classList.remove('video'); const media = stepToExpand.media; this.webview.setHtml(await this.detailsRenderer.renderSVG(media.path)); @@ -621,6 +626,7 @@ export class GettingStartedPage extends EditorPane { this.stepsContent.classList.remove('image'); this.stepsContent.classList.add('markdown'); + this.stepsContent.classList.remove('video'); const media = stepToExpand.media; @@ -702,6 +708,46 @@ export class GettingStartedPage extends EditorPane { } })); } + else if (stepToExpand.media.type === 'video') { + this.stepsContent.classList.add('video'); + this.stepsContent.classList.remove('markdown'); + this.stepsContent.classList.remove('image'); + + const media = stepToExpand.media; + + const themeType = this.themeService.getColorTheme().type; + const videoPath = media.path[themeType]; + const videoPoster = media.poster ? media.poster[themeType] : undefined; + + const rawHTML = await this.detailsRenderer.renderVideo(videoPath, videoPoster); + this.webview.setHtml(rawHTML); + + let isDisposed = false; + this.stepDisposables.add(toDisposable(() => { isDisposed = true; })); + + this.stepDisposables.add(this.themeService.onDidColorThemeChange(async () => { + // Render again since color vars change + const themeType = this.themeService.getColorTheme().type; + const videoPath = media.path[themeType]; + const videoPoster = media.poster ? media.poster[themeType] : undefined; + const body = await this.detailsRenderer.renderVideo(videoPath, videoPoster); + + if (!isDisposed) { // Make sure we weren't disposed of in the meantime + this.webview.setHtml(body); + } + })); + + this.stepDisposables.add(this.webview.onMessage(async e => { + const message: string = e.message as string; + if (message === 'playVideo') { + this.telemetryService.publicLog2('gettingStarted.ActionExecuted', { + command: 'playVideo', + walkthroughId: this.currentWalkthrough?.id, + argument: stepId, + }); + } + })); + } } async selectStepLoose(id: string) { @@ -1442,6 +1488,10 @@ export class GettingStartedPage extends EditorPane { stepDescription.appendChild( $('.image-description', { 'aria-label': localize('imageShowing', "Image showing {0}", step.media.altText) }), ); + } else if (step.media.type === 'video') { + stepDescription.appendChild( + $('.video-description', { 'aria-label': localize('videoShowing', "Video showing {0}", step.media.altText) }), + ); } return $('button.getting-started-step', diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts index 53e8a273940d..01922e18f22c 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts @@ -201,6 +201,56 @@ export class GettingStartedDetailsRenderer { `; } + async renderVideo(path: URI, poster?: URI): Promise { + const nonce = generateUuid(); + + return ` + + + + + + + + + + + + + + `; + } + private async readAndCacheSVGFile(path: URI): Promise { if (!this.svgCache.has(path)) { const contents = await this.readContentsOfPath(path, false); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index 01a8fe3692b3..4b2800fe031b 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -35,6 +35,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { DefaultIconPath } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; +import { asWebviewUri } from '../../webview/common/webview.js'; export const HasMultipleNewFileEntries = new RawContextKey('hasMultipleNewFileEntries', false); @@ -83,7 +84,8 @@ export interface IWalkthroughStep { media: | { type: 'image'; path: { hcDark: URI; hcLight: URI; light: URI; dark: URI }; altText: string } | { type: 'svg'; path: URI; altText: string } - | { type: 'markdown'; path: URI; base: URI; root: URI }; + | { type: 'markdown'; path: URI; base: URI; root: URI } + | { type: 'video'; path: { hcDark: URI; hcLight: URI; light: URI; dark: URI }; poster?: { hcDark: URI; hcLight: URI; light: URI; dark: URI }; root: URI; altText: string }; } type StepProgress = { done: boolean }; @@ -205,12 +207,20 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ altText: step.media.altText, path: convertInternalMediaPathToFileURI(step.media.path).with({ query: JSON.stringify({ moduleId: 'vs/workbench/contrib/welcomeGettingStarted/common/media/' + step.media.path }) }) } - : { - type: 'markdown', - path: convertInternalMediaPathToFileURI(step.media.path).with({ query: JSON.stringify({ moduleId: 'vs/workbench/contrib/welcomeGettingStarted/common/media/' + step.media.path }) }), - base: FileAccess.asFileUri('vs/workbench/contrib/welcomeGettingStarted/common/media/'), - root: FileAccess.asFileUri('vs/workbench/contrib/welcomeGettingStarted/common/media/'), - }, + : step.media.type === 'markdown' + ? { + type: 'markdown', + path: convertInternalMediaPathToFileURI(step.media.path).with({ query: JSON.stringify({ moduleId: 'vs/workbench/contrib/welcomeGettingStarted/common/media/' + step.media.path }) }), + base: FileAccess.asFileUri('vs/workbench/contrib/welcomeGettingStarted/common/media/'), + root: FileAccess.asFileUri('vs/workbench/contrib/welcomeGettingStarted/common/media/'), + } + : { + type: 'video', + path: convertRelativeMediaPathsToWebviewURIs(FileAccess.asFileUri('vs/workbench/contrib/welcomeGettingStarted/common/media/'), step.media.path), + altText: step.media.altText, + root: FileAccess.asFileUri('vs/workbench/contrib/welcomeGettingStarted/common/media/'), + poster: step.media.poster ? convertRelativeMediaPathsToWebviewURIs(FileAccess.asFileUri('vs/workbench/contrib/welcomeGettingStarted/common/media/'), step.media.poster) : undefined + }, }); }) }); @@ -368,6 +378,16 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ altText: step.media.svg, }; } + else if (step.media.video) { + const baseURI = FileAccess.uriToFileUri(extension.extensionLocation); + media = { + type: 'video', + path: convertRelativeMediaPathsToWebviewURIs(baseURI, step.media.video), + root: FileAccess.uriToFileUri(extension.extensionLocation), + altText: step.media.altText, + poster: step.media.poster ? convertRelativeMediaPathsToWebviewURIs(baseURI, step.media.poster) : undefined + }; + } // Throw error for unknown walkthrough format else { @@ -681,6 +701,25 @@ const convertInternalMediaPathsToBrowserURIs = (path: string | { hc: string; hcL } }; +const convertRelativeMediaPathsToWebviewURIs = (basePath: URI, path: string | { hc: string; hcLight?: string; dark: string; light: string }): { hcDark: URI; hcLight: URI; dark: URI; light: URI } => { + const convertPath = (path: string) => path.startsWith('https://') + ? URI.parse(path, true) + : asWebviewUri(joinPath(basePath, path)); + + if (typeof path === 'string') { + const converted = convertPath(path); + return { hcDark: converted, hcLight: converted, dark: converted, light: converted }; + } else { + return { + hcDark: convertPath(path.hc), + hcLight: convertPath(path.hcLight ?? path.light), + light: convertPath(path.light), + dark: convertPath(path.dark) + }; + } +}; + + registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css index 0b9c875e842e..a2d3c5674ca2 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css @@ -598,6 +598,11 @@ justify-content: center; } +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent > .getting-started-media > video { + max-width: 100%; + max-height: 100%; +} + .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent > .getting-started-footer { grid-area: footer; align-self: flex-end; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index 53c8ae805113..5b01dd409146 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -67,7 +67,8 @@ export type BuiltinGettingStartedStep = { media: | { type: 'image'; path: string | { hc: string; hcLight?: string; light: string; dark: string }; altText: string } | { type: 'svg'; path: string; altText: string } - | { type: 'markdown'; path: string }; + | { type: 'markdown'; path: string } + | { type: 'video'; path: string | { hc: string; hcLight?: string; light: string; dark: string }; poster?: string | { hc: string; hcLight?: string; light: string; dark: string }; altText: string }; }; export type BuiltinGettingStartedCategory = { @@ -232,7 +233,6 @@ function createCopilotSetupStep(id: string, button: string, when: string, includ }; } - export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'Setup', From 37066ba9a9ead9175d059b35124942cde3a4ca82 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Tue, 14 Jan 2025 08:23:08 -0800 Subject: [PATCH 0545/3587] Add QuickInput section to Customize Layout (#237847) Makes it more discoverable. ref https://github.com/microsoft/vscode/issues/17268 --- .../quickInput/standaloneQuickInputService.ts | 4 + .../platform/quickinput/browser/quickInput.ts | 3 + .../browser/quickInputController.ts | 211 ++++++++++++------ .../quickinput/browser/quickInputService.ts | 4 + .../platform/quickinput/common/quickInput.ts | 6 + .../browser/actions/layoutActions.ts | 52 ++++- .../test/browser/workbenchTestServices.ts | 1 + 7 files changed, 210 insertions(+), 71 deletions(-) diff --git a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts index d46bbff59c3d..91f910e37766 100644 --- a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts +++ b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts @@ -156,6 +156,10 @@ export class StandaloneQuickInputService implements IQuickInputService { cancel(): Promise { return this.activeService.cancel(); } + + setAlignment(alignment: 'top' | 'center' | { top: number; left: number }): void { + return this.activeService.setAlignment(alignment); + } } export class QuickInputEditorContribution implements IEditorContribution { diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 432416a5a11a..1ea9c37ab5fc 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -38,6 +38,9 @@ export const inQuickInputContextKeyValue = 'inQuickInput'; export const InQuickInputContextKey = new RawContextKey(inQuickInputContextKeyValue, false, localize('inQuickInput', "Whether keyboard focus is inside the quick input control")); export const inQuickInputContext = ContextKeyExpr.has(inQuickInputContextKeyValue); +export const quickInputAlignmentContextKeyValue = 'quickInputAlignment'; +export const QuickInputAlignmentContextKey = new RawContextKey<'top' | 'center' | undefined>(quickInputAlignmentContextKeyValue, 'top', localize('quickInputAlignment', "The alignment of the quick input")); + export const quickInputTypeContextKeyValue = 'quickInputType'; export const QuickInputTypeContextKey = new RawContextKey(quickInputTypeContextKeyValue, undefined, localize('quickInputType', "The type of the currently visible quick input")); diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 783dde2dd5bd..f3ef3033729a 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -19,7 +19,7 @@ import { isString } from '../../../base/common/types.js'; import { localize } from '../../../nls.js'; import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInput, IQuickInputButton, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickWidget, QuickInputHideReason, QuickPickInput, QuickPickFocus } from '../common/quickInput.js'; import { QuickInputBox } from './quickInputBox.js'; -import { QuickInputUI, Writeable, IQuickInputStyles, IQuickInputOptions, QuickPick, backButton, InputBox, Visibilities, QuickWidget, InQuickInputContextKey, QuickInputTypeContextKey, EndOfQuickInputBoxContextKey } from './quickInput.js'; +import { QuickInputUI, Writeable, IQuickInputStyles, IQuickInputOptions, QuickPick, backButton, InputBox, Visibilities, QuickWidget, InQuickInputContextKey, QuickInputTypeContextKey, EndOfQuickInputBoxContextKey, QuickInputAlignmentContextKey } from './quickInput.js'; import { ILayoutService } from '../../layout/browser/layoutService.js'; import { mainWindow } from '../../../base/browser/window.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; @@ -331,7 +331,20 @@ export class QuickInputController extends Disposable { // Drag and Drop support this.dndController = this._register(this.instantiationService.createInstance( - QuickInputDragAndDropController, this._container, container, [titleBar, title, headerContainer])); + QuickInputDragAndDropController, + this._container, + container, + [ + { + node: titleBar, + includeChildren: true + }, + { + node: headerContainer, + includeChildren: false + } + ] + )); // DnD update layout this._register(autorun(reader => { @@ -607,6 +620,10 @@ export class QuickInputController extends Disposable { return new InputBox(ui); } + setAlignment(alignment: 'top' | 'center' | { top: number; left: number }): void { + this.dndController?.setAlignment(alignment); + } + createQuickWidget(): IQuickWidget { const ui = this.getUI(true); return new QuickWidget(ui); @@ -879,16 +896,19 @@ class QuickInputDragAndDropController extends Disposable { readonly dndViewState = observableValue<{ top?: number; left?: number; done: boolean } | undefined>(this, undefined); private readonly _snapThreshold = 20; - private readonly _snapLineHorizontalRatio = 0.15; + private readonly _snapLineHorizontalRatio = 0.25; private readonly _snapLineHorizontal: HTMLElement; private readonly _snapLineVertical1: HTMLElement; private readonly _snapLineVertical2: HTMLElement; + private _quickInputAlignmentContext = QuickInputAlignmentContextKey.bindTo(this._contextKeyService); + constructor( private _container: HTMLElement, private readonly _quickInputContainer: HTMLElement, - private _quickInputDragAreas: HTMLElement[], - @ILayoutService private readonly _layoutService: ILayoutService + private _quickInputDragAreas: { node: HTMLElement; includeChildren: boolean }[], + @ILayoutService private readonly _layoutService: ILayoutService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService ) { super(); @@ -909,87 +929,138 @@ class QuickInputDragAndDropController extends Disposable { dom.append(this._container, this._snapLineVertical2); } + setAlignment(alignment: 'top' | 'center' | { top: number; left: number }, done = true): void { + if (alignment === 'top') { + this.dndViewState.set({ + top: this._getTopSnapValue() / this._container.clientHeight, + left: (this._getCenterXSnapValue() + (this._quickInputContainer.clientWidth / 2)) / this._container.clientWidth, + done + }, undefined); + this._quickInputAlignmentContext.set('top'); + } else if (alignment === 'center') { + this.dndViewState.set({ + top: this._getCenterYSnapValue() / this._container.clientHeight, + left: (this._getCenterXSnapValue() + (this._quickInputContainer.clientWidth / 2)) / this._container.clientWidth, + done + }, undefined); + this._quickInputAlignmentContext.set('center'); + } else { + this.dndViewState.set({ top: alignment.top, left: alignment.left, done }, undefined); + this._quickInputAlignmentContext.set(undefined); + } + } + private registerMouseListeners(): void { - for (const dragArea of this._quickInputDragAreas) { - let top: number | undefined; - let left: number | undefined; + let top: number | undefined; + let left: number | undefined; + const dragArea = this._quickInputContainer; + + // Double click + this._register(dom.addDisposableGenericMouseUpListener(dragArea, (event: MouseEvent) => { + const originEvent = new StandardMouseEvent(dom.getWindow(dragArea), event); + if (originEvent.detail !== 2) { + return; + } - // Double click - this._register(dom.addDisposableGenericMouseUpListener(dragArea, (event: MouseEvent) => { - const originEvent = new StandardMouseEvent(dom.getWindow(dragArea), event); + // Ignore event if the target is not the drag area + if (!this._quickInputDragAreas.some(({ node, includeChildren }) => includeChildren ? dom.isAncestor(originEvent.target as HTMLElement, node) : originEvent.target === node)) { + return; + } - // Ignore event if the target is not the drag area - if (originEvent.target !== dragArea) { - return; - } + top = undefined; + left = undefined; - if (originEvent.detail === 2) { - top = undefined; - left = undefined; + this.dndViewState.set({ top, left, done: true }, undefined); + })); - this.dndViewState.set({ top, left, done: true }, undefined); - } - })); + // Mouse down + this._register(dom.addDisposableGenericMouseDownListener(dragArea, (e: MouseEvent) => { + const activeWindow = dom.getWindow(this._layoutService.activeContainer); + const originEvent = new StandardMouseEvent(activeWindow, e); - // Mouse down - this._register(dom.addDisposableGenericMouseDownListener(dragArea, (e: MouseEvent) => { - const activeWindow = dom.getWindow(this._layoutService.activeContainer); - const originEvent = new StandardMouseEvent(activeWindow, e); + // Ignore event if the target is not the drag area + if (!this._quickInputDragAreas.some(({ node, includeChildren }) => includeChildren ? dom.isAncestor(originEvent.target as HTMLElement, node) : originEvent.target === node)) { + return; + } - // Ignore event if the target is not the drag area - if (originEvent.target !== dragArea) { - return; + // Mouse position offset relative to dragArea + const dragAreaRect = this._quickInputContainer.getBoundingClientRect(); + const dragOffsetX = originEvent.browserEvent.clientX - dragAreaRect.left; + const dragOffsetY = originEvent.browserEvent.clientY - dragAreaRect.top; + + // Snap lines + let isMovingQuickInput = false; + const snapCoordinateYTop = this._getTopSnapValue(); + const snapCoordinateY = this._getCenterYSnapValue(); + const snapCoordinateX = this._getCenterXSnapValue(); + + // Mouse move + const mouseMoveListener = dom.addDisposableGenericMouseMoveListener(activeWindow, (e: MouseEvent) => { + const mouseMoveEvent = new StandardMouseEvent(activeWindow, e); + mouseMoveEvent.preventDefault(); + + if (!isMovingQuickInput) { + this._showSnapLines(snapCoordinateY, snapCoordinateX); + isMovingQuickInput = true; } - // Mouse position offset relative to dragArea - const dragAreaRect = this._quickInputContainer.getBoundingClientRect(); - const dragOffsetX = originEvent.browserEvent.clientX - dragAreaRect.left; - const dragOffsetY = originEvent.browserEvent.clientY - dragAreaRect.top; - - // Snap lines - let snapLinesVisible = false; - const snapCoordinateYTop = this._layoutService.activeContainerOffset.quickPickTop; - const snapCoordinateY = Math.round(this._container.clientHeight * this._snapLineHorizontalRatio); - const snapCoordinateX = Math.round(this._container.clientWidth / 2) - Math.round(this._quickInputContainer.clientWidth / 2); - - // Mouse move - const mouseMoveListener = dom.addDisposableGenericMouseMoveListener(activeWindow, (e: MouseEvent) => { - const mouseMoveEvent = new StandardMouseEvent(activeWindow, e); - mouseMoveEvent.preventDefault(); - - if (!snapLinesVisible) { - this._showSnapLines(snapCoordinateY, snapCoordinateX); - snapLinesVisible = true; + let topCoordinate = e.clientY - dragOffsetY; + // Make sure the quick input is not moved outside the container + topCoordinate = Math.max(0, Math.min(topCoordinate, this._container.clientHeight - this._quickInputContainer.clientHeight)); + const snappingToTop = Math.abs(topCoordinate - snapCoordinateYTop) < this._snapThreshold; + topCoordinate = snappingToTop ? snapCoordinateYTop : topCoordinate; + const snappingToCenter = Math.abs(topCoordinate - snapCoordinateY) < this._snapThreshold; + topCoordinate = snappingToCenter ? snapCoordinateY : topCoordinate; + top = topCoordinate / this._container.clientHeight; + + let leftCoordinate = e.clientX - dragOffsetX; + // Make sure the quick input is not moved outside the container + leftCoordinate = Math.max(0, Math.min(leftCoordinate, this._container.clientWidth - this._quickInputContainer.clientWidth)); + const snappingToCenterX = Math.abs(leftCoordinate - snapCoordinateX) < this._snapThreshold; + leftCoordinate = snappingToCenterX ? snapCoordinateX : leftCoordinate; + left = (leftCoordinate + (this._quickInputContainer.clientWidth / 2)) / this._container.clientWidth; + + this.dndViewState.set({ top, left, done: false }, undefined); + if (snappingToCenterX) { + if (snappingToTop) { + this._quickInputAlignmentContext.set('top'); + return; + } else if (snappingToCenter) { + this._quickInputAlignmentContext.set('center'); + return; } + } + this._quickInputAlignmentContext.set(undefined); + }); - let topCoordinate = e.clientY - dragOffsetY; - topCoordinate = Math.max(0, Math.min(topCoordinate, this._container.clientHeight - this._quickInputContainer.clientHeight)); - topCoordinate = Math.abs(topCoordinate - snapCoordinateYTop) < this._snapThreshold ? snapCoordinateYTop : topCoordinate; - topCoordinate = Math.abs(topCoordinate - snapCoordinateY) < this._snapThreshold ? snapCoordinateY : topCoordinate; - top = topCoordinate / this._container.clientHeight; - - let leftCoordinate = e.clientX - dragOffsetX; - leftCoordinate = Math.max(0, Math.min(leftCoordinate, this._container.clientWidth - this._quickInputContainer.clientWidth)); - leftCoordinate = Math.abs(leftCoordinate - snapCoordinateX) < this._snapThreshold ? snapCoordinateX : leftCoordinate; - left = (leftCoordinate + (this._quickInputContainer.clientWidth / 2)) / this._container.clientWidth; - - this.dndViewState.set({ top, left, done: false }, undefined); - }); - - // Mouse up - const mouseUpListener = dom.addDisposableGenericMouseUpListener(activeWindow, (e: MouseEvent) => { + // Mouse up + const mouseUpListener = dom.addDisposableGenericMouseUpListener(activeWindow, (e: MouseEvent) => { + if (isMovingQuickInput) { // Hide snaplines this._hideSnapLines(); // Save position - this.dndViewState.set({ top, left, done: true }, undefined); + const state = this.dndViewState.get(); + this.dndViewState.set({ top: state?.top, left: state?.left, done: true }, undefined); + } - // Dispose listeners - mouseMoveListener.dispose(); - mouseUpListener.dispose(); - }); - })); - } + // Dispose listeners + mouseMoveListener.dispose(); + mouseUpListener.dispose(); + }); + })); + } + + private _getTopSnapValue() { + return this._layoutService.activeContainerOffset.quickPickTop; + } + + private _getCenterYSnapValue() { + return Math.round(this._container.clientHeight * this._snapLineHorizontalRatio); + } + + private _getCenterXSnapValue() { + return Math.round(this._container.clientWidth / 2) - Math.round(this._quickInputContainer.clientWidth / 2); } private _showSnapLines(horizontal: number, vertical: number) { diff --git a/src/vs/platform/quickinput/browser/quickInputService.ts b/src/vs/platform/quickinput/browser/quickInputService.ts index ccdb69655636..8adc5abd7071 100644 --- a/src/vs/platform/quickinput/browser/quickInputService.ts +++ b/src/vs/platform/quickinput/browser/quickInputService.ts @@ -195,6 +195,10 @@ export class QuickInputService extends Themable implements IQuickInputService { return this.controller.cancel(); } + setAlignment(alignment: 'top' | 'center' | { top: number; left: number }): void { + this.controller.setAlignment(alignment); + } + override updateStyles() { if (this.hasController) { this.controller.applyStyles(this.computeStyles()); diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 39490182051d..a8aa5bca251c 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -931,4 +931,10 @@ export interface IQuickInputService { * The current quick pick that is visible. Undefined if none is open. */ currentQuickInput: IQuickInput | undefined; + + /** + * Set the alignment of the quick input. + * @param alignment either a preset or a custom alignment + */ + setAlignment(alignment: 'top' | 'center' | { top: number; left: number }): void; } diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index aed2fe31f746..b38c5c9a6316 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -32,6 +32,7 @@ import { mainWindow } from '../../../base/browser/window.js'; import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js'; import { TitlebarStyle } from '../../../platform/window/common/window.js'; import { IPreferencesService } from '../../services/preferences/common/preferences.js'; +import { QuickInputAlignmentContextKey } from '../../../platform/quickinput/browser/quickInput.js'; // Register Icons const menubarIcon = registerIcon('menuBar', Codicon.layoutMenubar, localize('menuBarIcon', "Represents the menu bar")); @@ -49,6 +50,9 @@ const panelAlignmentRightIcon = registerIcon('panel-align-right', Codicon.layout const panelAlignmentCenterIcon = registerIcon('panel-align-center', Codicon.layoutPanelCenter, localize('panelBottomCenter', "Represents the bottom panel alignment set to the center")); const panelAlignmentJustifyIcon = registerIcon('panel-align-justify', Codicon.layoutPanelJustify, localize('panelBottomJustify', "Represents the bottom panel alignment set to justified")); +const quickInputAlignmentTopIcon = registerIcon('quickInputAlignmentTop', Codicon.arrowUp, localize('quickInputAlignmentTop', "Represents quick input alignment set to the top")); +const quickInputAlignmentCenterIcon = registerIcon('quickInputAlignmentCenter', Codicon.circle, localize('quickInputAlignmentCenter', "Represents quick input alignment set to the center")); + const fullscreenIcon = registerIcon('fullscreen', Codicon.screenFull, localize('fullScreenIcon', "Represents full screen")); const centerLayoutIcon = registerIcon('centerLayoutIcon', Codicon.layoutCentered, localize('centerLayoutIcon', "Represents centered layout mode")); const zenModeIcon = registerIcon('zenMode', Codicon.target, localize('zenModeIcon', "Represents zen mode")); @@ -1284,6 +1288,42 @@ registerAction2(DecreaseViewSizeAction); registerAction2(DecreaseViewWidthAction); registerAction2(DecreaseViewHeightAction); +//#region Quick Input Alignment Actions + +registerAction2(class AlignQuickInputTopAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.alignQuickInputTop', + title: localize2('alignQuickInputTop', 'Align Quick Input Top'), + f1: false + }); + } + + run(accessor: ServicesAccessor): void { + const quickInputService = accessor.get(IQuickInputService); + quickInputService.setAlignment('top'); + } +}); + +registerAction2(class AlignQuickInputCenterAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.alignQuickInputCenter', + title: localize2('alignQuickInputCenter', 'Align Quick Input Center'), + f1: false + }); + } + + run(accessor: ServicesAccessor): void { + const quickInputService = accessor.get(IQuickInputService); + quickInputService.setAlignment('center'); + } +}); + +//#endregion + type ContextualLayoutVisualIcon = { iconA: ThemeIcon; iconB: ThemeIcon; whenA: ContextKeyExpression }; type LayoutVisualIcon = ThemeIcon | ContextualLayoutVisualIcon; @@ -1355,6 +1395,11 @@ const AlignPanelActions: CustomizeLayoutItem[] = [ CreateOptionLayoutItem('workbench.action.alignPanelJustify', PanelAlignmentContext.isEqualTo('justify'), localize('justifyPanel', "Justify"), panelAlignmentJustifyIcon), ]; +const QuickInputActions: CustomizeLayoutItem[] = [ + CreateOptionLayoutItem('workbench.action.alignQuickInputTop', QuickInputAlignmentContextKey.isEqualTo('top'), localize('top', "Top"), quickInputAlignmentTopIcon), + CreateOptionLayoutItem('workbench.action.alignQuickInputCenter', QuickInputAlignmentContextKey.isEqualTo('center'), localize('center', "Center"), quickInputAlignmentCenterIcon), +]; + const MiscLayoutOptions: CustomizeLayoutItem[] = [ CreateOptionLayoutItem('workbench.action.toggleFullScreen', IsMainWindowFullscreenContext, localize('fullscreen', "Full Screen"), fullscreenIcon), CreateOptionLayoutItem('workbench.action.toggleZenMode', InEditorZenModeContext, localize('zenMode', "Zen Mode"), zenModeIcon), @@ -1362,7 +1407,7 @@ const MiscLayoutOptions: CustomizeLayoutItem[] = [ ]; const LayoutContextKeySet = new Set(); -for (const { active } of [...ToggleVisibilityActions, ...MoveSideBarActions, ...AlignPanelActions, ...MiscLayoutOptions]) { +for (const { active } of [...ToggleVisibilityActions, ...MoveSideBarActions, ...AlignPanelActions, ...QuickInputActions, ...MiscLayoutOptions]) { for (const key of active.keys()) { LayoutContextKeySet.add(key); } @@ -1444,6 +1489,11 @@ registerAction2(class CustomizeLayoutAction extends Action2 { label: localize('panelAlignment', "Panel Alignment") }, ...AlignPanelActions.map(toQuickPickItem), + { + type: 'separator', + label: localize('quickOpen', "Quick Input Position Presets") + }, + ...QuickInputActions.map(toQuickPickItem), { type: 'separator', label: localize('layoutModes', "Modes"), diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 259de7b24b0b..90709b3205db 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -2134,6 +2134,7 @@ export class TestQuickInputService implements IQuickInputService { accept(): Promise { throw new Error('not implemented.'); } back(): Promise { throw new Error('not implemented.'); } cancel(): Promise { throw new Error('not implemented.'); } + setAlignment(alignment: 'top' | 'center' | { top: number; left: number }): void { throw new Error('not implemented.'); } } class TestLanguageDetectionService implements ILanguageDetectionService { From fc10c21738f66065ba51ae06f00fb3d1ba483b37 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 14 Jan 2025 09:42:59 -0800 Subject: [PATCH 0546/3587] testing: add a command to get tests in a uri (#237862) --- .../contrib/testing/browser/testing.contribution.ts | 9 +++++++++ src/vs/workbench/contrib/testing/common/testId.ts | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index 57543747b139..986b63856298 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -48,6 +48,7 @@ import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js' import { IViewsService } from '../../../services/views/common/viewsService.js'; import { allTestActions, discoverAndRunTests } from './testExplorerActions.js'; import './testingConfigurationUi.js'; +import { URI } from '../../../../base/common/uri.js'; registerSingleton(ITestService, TestService, InstantiationType.Delayed); registerSingleton(ITestResultStorage, TestResultStorage, InstantiationType.Delayed); @@ -279,5 +280,13 @@ CommandsRegistry.registerCommand({ } }); +CommandsRegistry.registerCommand({ + id: 'vscode.testing.getTestsInFile', + handler: async (accessor: ServicesAccessor, uri: URI) => { + const testService = accessor.get(ITestService); + return [...testService.collection.getNodeByUrl(uri)].map(t => TestId.split(t.item.extId)); + } +}); + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration(testingConfiguration); diff --git a/src/vs/workbench/contrib/testing/common/testId.ts b/src/vs/workbench/contrib/testing/common/testId.ts index 0b66669342a5..73bac84ca433 100644 --- a/src/vs/workbench/contrib/testing/common/testId.ts +++ b/src/vs/workbench/contrib/testing/common/testId.ts @@ -77,6 +77,13 @@ export class TestId { return new TestId([...base.path, b]); } + /** + * Splits a test ID into its parts. + */ + public static split(idString: string) { + return idString.split(TestIdPathParts.Delimiter); + } + /** * Gets the string ID resulting from adding b to the base ID. */ From 4ccc274b699c0571d051e1000724f8cc7a8548ef Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 14 Jan 2025 09:47:25 -0800 Subject: [PATCH 0547/3587] Update video styling in getting started details renderer (#237912) update video styling in getting started details renderer --- .../browser/gettingStartedDetailsRenderer.ts | 8 ++++++++ .../browser/media/gettingStarted.css | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts index 01922e18f22c..ca5d099418cc 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts @@ -210,6 +210,14 @@ export class GettingStartedDetailsRenderer { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css index a2d3c5674ca2..3657036b268c 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css @@ -579,6 +579,10 @@ align-self: center; } +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent.video > .getting-started-media { + height: inherit; +} + .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent.markdown > .getting-started-media { height: inherit; } From 2174db3d186ec17588853889ffa91f306a84c9b2 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Tue, 14 Jan 2025 10:06:25 -0800 Subject: [PATCH 0548/3587] fix: add key match score, revert contiguous search (#237855) --- .../preferences/browser/preferencesSearch.ts | 39 ++++++++++--------- .../preferences/browser/settingsEditor2.ts | 2 +- .../preferences/browser/settingsTreeModels.ts | 4 ++ .../preferences/common/preferences.ts | 6 ++- .../preferences/common/preferencesModels.ts | 2 + 5 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index e81c8009f79a..463b9facdaf0 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -99,8 +99,8 @@ export class LocalSearchProvider implements ISearchProvider { let orderedScore = LocalSearchProvider.START_SCORE; // Sort is not stable const settingMatcher = (setting: ISetting) => { - const { matches, matchType } = new SettingMatches(this._filter, setting, true, true, (filter, setting) => preferencesModel.findValueMatches(filter, setting), this.configurationService); - const score = this._filter === setting.key ? + const { matches, matchType, keyMatchSize } = new SettingMatches(this._filter, setting, true, true, (filter, setting) => preferencesModel.findValueMatches(filter, setting), this.configurationService); + const score = strings.equalsIgnoreCase(this._filter, setting.key) ? LocalSearchProvider.EXACT_MATCH_SCORE : orderedScore--; @@ -108,6 +108,7 @@ export class LocalSearchProvider implements ISearchProvider { { matches, matchType, + keyMatchSize, score } : null; @@ -138,6 +139,8 @@ export class LocalSearchProvider implements ISearchProvider { export class SettingMatches { readonly matches: IRange[]; matchType: SettingMatchType = SettingMatchType.None; + /** A more precise match score for key matches. */ + keyMatchSize: number = 0; constructor( searchString: string, @@ -170,29 +173,27 @@ export class SettingMatches { const keyMatchingWords: Map = new Map(); const valueMatchingWords: Map = new Map(); - const words = new Set(searchString.split(' ')); + const queryWords = new Set(searchString.split(' ')); // Key search const settingKeyAsWords: string = this._keyToLabel(setting.key); - for (const word of words) { + for (const word of queryWords) { // Check if the key contains the word. - const keyMatches = matchesWords(word, settingKeyAsWords, true); + const keyMatches = matchesWords(word, settingKeyAsWords, false); if (keyMatches?.length) { keyMatchingWords.set(word, keyMatches.map(match => this.toKeyRange(setting, match))); } } - // For now, only allow a match if all words match in the key. - if (keyMatchingWords.size === words.size) { + if (keyMatchingWords.size) { this.matchType |= SettingMatchType.KeyMatch; - } else { - keyMatchingWords.clear(); + this.keyMatchSize = keyMatchingWords.size; } // Also check if the user tried searching by id. const keyIdMatches = matchesContiguousSubString(searchString, setting.key); if (keyIdMatches?.length) { keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match))); - this.matchType |= SettingMatchType.KeyMatch; + this.matchType |= SettingMatchType.KeyIdMatch; } // Check if the match was for a language tag group setting such as [markdown]. @@ -204,9 +205,9 @@ export class SettingMatches { return [...keyRanges]; } - // Description search - if (this.searchDescription) { - for (const word of words) { + // Search the description if there were no key matches. + if (this.searchDescription && this.matchType !== SettingMatchType.None) { + for (const word of queryWords) { // Search the description lines. for (let lineIndex = 0; lineIndex < setting.description.length; lineIndex++) { const descriptionMatches = matchesContiguousSubString(word, setting.description[lineIndex]); @@ -215,7 +216,7 @@ export class SettingMatches { } } } - if (descriptionMatchingWords.size === words.size) { + if (descriptionMatchingWords.size === queryWords.size) { this.matchType |= SettingMatchType.DescriptionOrValueMatch; } else { // Clear out the match for now. We want to require all words to match in the description. @@ -232,13 +233,13 @@ export class SettingMatches { continue; } valueMatchingWords.clear(); - for (const word of words) { + for (const word of queryWords) { const valueMatches = matchesContiguousSubString(word, option); if (valueMatches?.length) { valueMatchingWords.set(word, valueMatches.map(match => this.toValueRange(setting, match))); } } - if (valueMatchingWords.size === words.size) { + if (valueMatchingWords.size === queryWords.size) { this.matchType |= SettingMatchType.DescriptionOrValueMatch; break; } else { @@ -250,13 +251,13 @@ export class SettingMatches { // Search single string value. const settingValue = this.configurationService.getValue(setting.key); if (typeof settingValue === 'string') { - for (const word of words) { + for (const word of queryWords) { const valueMatches = matchesContiguousSubString(word, settingValue); if (valueMatches?.length) { valueMatchingWords.set(word, valueMatches.map(match => this.toValueRange(setting, match))); } } - if (valueMatchingWords.size === words.size) { + if (valueMatchingWords.size === queryWords.size) { this.matchType |= SettingMatchType.DescriptionOrValueMatch; } else { // Clear out the match for now. We want to require all words to match in the value. @@ -406,6 +407,7 @@ class AiRelatedInformationSearchProvider implements IRemoteSearchProvider { setting: settingsRecord[pick], matches: [settingsRecord[pick].range], matchType: SettingMatchType.RemoteMatch, + keyMatchSize: 0, score: info.weight }); } @@ -501,6 +503,7 @@ class TfIdfSearchProvider implements IRemoteSearchProvider { setting: this._settingsRecord[pick], matches: [this._settingsRecord[pick].range], matchType: SettingMatchType.RemoteMatch, + keyMatchSize: 0, score: info.score }); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 43214c917773..eb6d7d3bb43b 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -1679,7 +1679,7 @@ export class SettingsEditor2 extends EditorPane { for (const g of this.defaultSettingsEditorModel.settingsGroups.slice(1)) { for (const sect of g.sections) { for (const setting of sect.settings) { - fullResult.filterMatches.push({ setting, matches: [], matchType: SettingMatchType.None, score: 0 }); + fullResult.filterMatches.push({ setting, matches: [], matchType: SettingMatchType.None, keyMatchSize: 0, score: 0 }); } } } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index fd773b645d5b..87ec480c25a2 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -959,6 +959,10 @@ export class SearchResultModel extends SettingsTreeModel { // Sort by match type if the match types are not the same. // The priority of the match type is given by the SettingMatchType enum. return b.matchType - a.matchType; + } else if (a.matchType === SettingMatchType.KeyMatch) { + // The match types are the same and are KeyMatch. + // Sort by the number of words matched in the key. + return b.keyMatchSize - a.keyMatchSize; } else if (a.matchType === SettingMatchType.RemoteMatch) { // The match types are the same and are RemoteMatch. // Sort by score. diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index ea3855432525..835993ab9029 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -138,13 +138,15 @@ export enum SettingMatchType { LanguageTagSettingMatch = 1 << 0, RemoteMatch = 1 << 1, DescriptionOrValueMatch = 1 << 2, - KeyMatch = 1 << 3 + KeyMatch = 1 << 3, + KeyIdMatch = 1 << 4, } export interface ISettingMatch { setting: ISetting; matches: IRange[] | null; matchType: SettingMatchType; + keyMatchSize: number; score: number; } @@ -184,7 +186,7 @@ export interface IPreferencesEditorModel { } export type IGroupFilter = (group: ISettingsGroup) => boolean | null; -export type ISettingMatcher = (setting: ISetting, group: ISettingsGroup) => { matches: IRange[]; matchType: SettingMatchType; score: number } | null; +export type ISettingMatcher = (setting: ISetting, group: ISettingsGroup) => { matches: IRange[]; matchType: SettingMatchType; keyMatchSize: number; score: number } | null; export interface ISettingsEditorModel extends IPreferencesEditorModel { readonly onDidChangeGroups: Event; diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index 6209d182d4ea..f6c0bc95c132 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -71,6 +71,7 @@ abstract class AbstractSettingsModel extends EditorModel { setting, matches: settingMatchResult && settingMatchResult.matches, matchType: settingMatchResult?.matchType ?? SettingMatchType.None, + keyMatchSize: settingMatchResult?.keyMatchSize ?? 0, score: settingMatchResult?.score ?? 0 }); } @@ -899,6 +900,7 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements setting: filteredMatch.setting, score: filteredMatch.score, matchType: filteredMatch.matchType, + keyMatchSize: filteredMatch.keyMatchSize, matches: filteredMatch.matches && filteredMatch.matches.map(match => { return new Range( match.startLineNumber - filteredMatch.setting.range.startLineNumber, From 35b2db3ed14f4949a53de4ab972b8714d7321f7f Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:15:46 -0500 Subject: [PATCH 0549/3587] Bring back expose shell's environment - bash (#237844) * bring back expose shell's environment bash * I want to see why the test is failing the build * Pass test, disable it until we enable for stable --------- Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> --- .../terminal.shellIntegration.test.ts | 17 +++++++ .../common/capabilities/capabilities.ts | 3 ++ .../shellEnvDetectionCapability.ts | 29 +++++++++++- .../common/xterm/shellIntegrationAddon.ts | 46 +++++++++++++++++++ .../common/scripts/shellIntegration-bash.sh | 19 ++++++++ 5 files changed, 113 insertions(+), 1 deletion(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts index db48c2593e07..8f094091f962 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts @@ -87,6 +87,23 @@ import { assertNoRpc } from '../utils'; await closeTerminalAsync(terminal); }); + if (platform() === 'darwin' || platform() === 'linux') { + // TODO: Enable when this is enabled in stable, otherwise it will break the stable product builds only + test.skip('Test if env is set', async () => { + const { shellIntegration } = await createTerminalAndWaitForShellIntegration(); + await new Promise(r => { + disposables.push(window.onDidChangeTerminalShellIntegration(e => { + if (e.shellIntegration.env) { + r(); + } + })); + }); + ok(shellIntegration.env); + ok(shellIntegration.env.PATH); + ok(shellIntegration.env.PATH.length > 0, 'env.PATH should have a length greater than 0'); + }); + } + test('execution events should fire in order when a command runs', async () => { const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration(); const events: string[] = []; diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index 50f39332a431..ba7597b9d926 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -155,6 +155,9 @@ export interface IShellEnvDetectionCapability { readonly onDidChangeEnv: Event>; get env(): Map; setEnvironment(envs: { [key: string]: string | undefined } | undefined, isTrusted: boolean): void; + startEnvironmentSingleVar(isTrusted: boolean): void; + setEnvironmentSingleVar(key: string, value: string | undefined, isTrusted: boolean): void; + endEnvironmentSingleVar(isTrusted: boolean): void; } export const enum CommandInvalidationReason { diff --git a/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts index 95e1d827dc43..be9577acfe93 100644 --- a/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/shellEnvDetectionCapability.ts @@ -11,7 +11,8 @@ import { equals } from '../../../../base/common/objects.js'; export class ShellEnvDetectionCapability extends Disposable implements IShellEnvDetectionCapability { readonly type = TerminalCapability.ShellEnvDetection; - private readonly _env: Map = new Map(); + private _pendingEnv: Map | undefined; + private _env: Map = new Map(); get env(): Map { return this._env; } private readonly _onDidChangeEnv = this._register(new Emitter>()); @@ -36,4 +37,30 @@ export class ShellEnvDetectionCapability extends Disposable implements IShellEnv // Convert to event and fire event this._onDidChangeEnv.fire(this._env); } + + startEnvironmentSingleVar(isTrusted: boolean): void { + if (!isTrusted) { + return; + } + this._pendingEnv = new Map(); + } + setEnvironmentSingleVar(key: string, value: string | undefined, isTrusted: boolean): void { + if (!isTrusted) { + return; + } + if (key !== undefined && value !== undefined) { + this._pendingEnv?.set(key, value); + } + } + endEnvironmentSingleVar(isTrusted: boolean): void { + if (!isTrusted) { + return; + } + if (!this._pendingEnv) { + return; + } + this._env = this._pendingEnv; + this._pendingEnv = undefined; + this._onDidChangeEnv.fire(this._env); + } } diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index ee7dbff267e5..92b1851c275c 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -239,6 +239,34 @@ const enum VSCodeOscPt { */ EnvJson = 'EnvJson', + /** + * The start of the collecting user's environment variables individually. + * Clears any environment residuals in previous sessions. + * + * Format: `OSC 633 ; EnvSingleStart ; ` + * + * WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script. + */ + EnvSingleStart = 'EnvSingleStart', + + /** + * Sets an entry of single environment variable to transactional pending map of environment variables. + * + * Format: `OSC 633 ; EnvSingleEntry ; ; ; ` + * + * WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script. + */ + EnvSingleEntry = 'EnvSingleEntry', + + /** + * The end of the collecting user's environment variables individually. + * Clears any pending environment variables and fires an event that contains user's environment. + * + * Format: `OSC 633 ; EnvSingleEnd ; ` + * + * WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script. + */ + EnvSingleEnd = 'EnvSingleEnd' } /** @@ -446,6 +474,24 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati } return true; } + case VSCodeOscPt.EnvSingleStart: { + this._createOrGetShellEnvDetection().startEnvironmentSingleVar(args[0] === this._nonce); + return true; + } + case VSCodeOscPt.EnvSingleEntry: { + const arg0 = args[0]; + const arg1 = args[1]; + const arg2 = args[2]; + if (arg0 !== undefined && arg1 !== undefined) { + const env = deserializeMessage(arg1); + this._createOrGetShellEnvDetection().setEnvironmentSingleVar(arg0, env, arg2 === this._nonce); + } + return true; + } + case VSCodeOscPt.EnvSingleEnd: { + this._createOrGetShellEnvDetection().endEnvironmentSingleVar(args[0] === this._nonce); + return true; + } case VSCodeOscPt.RightPromptStart: { this._createOrGetCommandDetection(this._terminal).handleRightPromptStart(); return true; diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index fbeaa22f90a7..3040617a5a6d 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -214,6 +214,17 @@ __vsc_update_cwd() { builtin printf '\e]633;P;Cwd=%s\a' "$(__vsc_escape_value "$__vsc_cwd")" } +__vsc_update_env() { + builtin printf '\e]633;EnvSingleStart;%s;\a' $__vsc_nonce + for var in $(compgen -v); do + if printenv "$var" >/dev/null 2>&1; then + value=$(builtin printf '%s' "${!var}") + builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$var" "$(__vsc_escape_value "$value")" $__vsc_nonce + fi + done + builtin printf '\e]633;EnvSingleEnd;%s;\a' $__vsc_nonce +} + __vsc_command_output_start() { if [[ -z "${__vsc_first_prompt-}" ]]; then builtin return @@ -240,6 +251,10 @@ __vsc_command_complete() { builtin printf '\e]633;D;%s\a' "$__vsc_status" fi __vsc_update_cwd + + if [ "$__vsc_stable" = "0" ]; then + __vsc_update_env + fi } __vsc_update_prompt() { # in command execution @@ -269,6 +284,10 @@ __vsc_precmd() { fi __vsc_first_prompt=1 __vsc_update_prompt + + if [ "$__vsc_stable" = "0" ]; then + __vsc_update_env + fi } __vsc_preexec() { From ac721a2d453dad122a74c5c51f93ce7d2dfb5859 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 14 Jan 2025 11:39:54 -0800 Subject: [PATCH 0550/3587] Prevent extensions from registering tools with copilot_ or vscode_ in the name or tag (#237919) * Allow extensions to access vscode_editing tagged tools. Can make a decision later on whether to keep that. But filter out editFile tool to avoid confusion * Prevent extensions from registering tools with copilot_ or vscode_ in the name or tag --- .../api/common/extHostLanguageModelTools.ts | 5 ++-- .../tools/languageModelToolsContribution.ts | 30 +++++++++++++------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index b2f25fb152f4..c1bf5e1b1be2 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -57,8 +57,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape throw new Error(`Invalid tool invocation token`); } - const tool = this._allTools.get(toolId); - if (tool?.tags?.includes('vscode_editing') && !isProposedApiEnabled(extension, 'chatParticipantPrivate')) { + if (toolId === 'vscode_editFile' && !isProposedApiEnabled(extension, 'chatParticipantPrivate')) { throw new Error(`Invalid tool: ${toolId}`); } @@ -87,7 +86,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape return Array.from(this._allTools.values()) .map(tool => typeConvert.LanguageModelToolDescription.to(tool)) .filter(tool => { - if (tool.tags.includes('vscode_editing')) { + if (tool.name === 'vscode_editFile') { return isProposedApiEnabled(extension, 'chatParticipantPrivate'); } diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts index e78ef045c614..e75fb61664af 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts @@ -3,22 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { IJSONSchema } from '../../../../../base/common/jsonSchema.js'; -import { DisposableMap, Disposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; import { joinPath } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { localize } from '../../../../../nls.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { ExtensionIdentifier, IExtensionManifest } from '../../../../../platform/extensions/common/extensions.js'; +import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; +import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; -import { ILanguageModelToolsService, IToolData } from '../languageModelToolsService.js'; +import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../../services/extensionManagement/common/extensionFeatures.js'; +import { isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js'; import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js'; +import { ILanguageModelToolsService, IToolData } from '../languageModelToolsService.js'; import { toolsParametersSchemaSchemaId } from './languageModelToolsParametersSchema.js'; -import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; -import { Registry } from '../../../../../platform/registry/common/platform.js'; -import { IExtensionFeatureTableRenderer, IRenderedData, ITableData, IRowData, IExtensionFeaturesRegistry, Extensions } from '../../../../services/extensionManagement/common/extensionFeatures.js'; export interface IRawToolContribution { name: string; @@ -66,8 +67,8 @@ const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.r name: { description: localize('toolName', "A unique name for this tool. This name must be a globally unique identifier, and is also used as a name when presenting this tool to a language model."), type: 'string', - // Borrow OpenAI's requirement for tool names - pattern: '^[\\w-]+$' + // [\\w-]+ is OpenAI's requirement for tool names + pattern: '^(?!copilot_|vscode_)[\\w-]+$' }, toolReferenceName: { markdownDescription: localize('toolName2', "If {0} is enabled for this tool, the user may use '#' with this name to invoke the tool in a query. Otherwise, the name is not required. Name must not contain whitespace.", '`canBeReferencedInPrompt`'), @@ -121,7 +122,8 @@ const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.r description: localize('toolTags', "A set of tags that roughly describe the tool's capabilities. A tool user may use these to filter the set of tools to just ones that are relevant for the task at hand, or they may want to pick a tag that can be used to identify just the tools contributed by this extension."), type: 'array', items: { - type: 'string' + type: 'string', + pattern: '^(?!copilot_|vscode_)' } } } @@ -160,6 +162,16 @@ export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContri continue; } + if ((rawTool.name.startsWith('copilot_') || rawTool.name.startsWith('vscode_')) && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) { + logService.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with name starting with "vscode_" or "copilot_"`); + continue; + } + + if (rawTool.tags?.some(tag => tag.startsWith('copilot_') || tag.startsWith('vscode_')) && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) { + logService.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with tags starting with "vscode_" or "copilot_"`); + continue; + } + const rawIcon = rawTool.icon; let icon: IToolData['icon'] | undefined; if (typeof rawIcon === 'string') { From a4262a0612c12d2a4e2578b5580e10c9975a9cb0 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 14 Jan 2025 21:10:15 +0100 Subject: [PATCH 0551/3587] Improve compund logs #237829 (#237922) --- .../output/browser/output.contribution.ts | 59 +++++++++- .../contrib/output/browser/outputView.ts | 57 +--------- .../output/common/outputChannelModel.ts | 106 +++++------------- .../services/output/common/output.ts | 81 +++++++++++++ 4 files changed, 174 insertions(+), 129 deletions(-) diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 18d6bdb400e1..722776ae86f6 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -41,6 +41,10 @@ import { localize, localize2 } from '../../../../nls.js'; import { viewFilterSubmenu } from '../../../browser/parts/views/viewFilter.js'; import { ViewAction } from '../../../browser/parts/views/viewPane.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { basename } from '../../../../base/common/resources.js'; + +const IMPORTED_LOG_ID_PREFIX = 'importedLog.'; // Register Service registerSingleton(IOutputService, OutputService, InstantiationType.Delayed); @@ -114,6 +118,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { this.registerConfigureActiveOutputLogLevelAction(); this.registerFilterActions(); this.registerExportLogsAction(); + this.registerImportLogAction(); } private registerSwitchOutputAction(): void { @@ -144,7 +149,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { const registerOutputChannels = (channels: IOutputChannelDescriptor[]) => { for (const channel of channels) { const title = channel.label; - const group = channel.files && channel.files.length > 1 ? '2_compound_logs' : channel.extensionId ? '0_ext_outputchannels' : '1_core_outputchannels'; + const group = (channel.files?.length && channel.files.length > 1) || channel.id.startsWith(IMPORTED_LOG_ID_PREFIX) ? '2_custom_logs' : channel.extensionId ? '0_ext_outputchannels' : '1_core_outputchannels'; registeredChannels.set(channel.id, registerAction2(class extends Action2 { constructor() { super({ @@ -186,6 +191,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { menu: [{ id: MenuId.ViewTitle, when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), + group: '2_add', }], }); } @@ -405,7 +411,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { menu: [{ id: MenuId.ViewTitle, when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), - group: 'export', + group: '1_export', order: 1 }], }); @@ -711,7 +717,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { menu: [{ id: MenuId.ViewTitle, when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), - group: 'export', + group: '1_export', order: 2, }], }); @@ -747,6 +753,53 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { } })); } + + private registerImportLogAction(): void { + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.action.importLog`, + title: nls.localize2('importLog', "Import Log..."), + f1: true, + category: Categories.Developer, + menu: [{ + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), + group: '2_add', + order: 2, + }], + }); + } + async run(accessor: ServicesAccessor): Promise { + const outputService = accessor.get(IOutputService); + const fileDialogService = accessor.get(IFileDialogService); + const result = await fileDialogService.showOpenDialog({ + title: nls.localize('importLogFile', "Import Log File"), + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: true, + filters: [{ + name: nls.localize('logFiles', "Log Files"), + extensions: ['log'] + }] + }); + + if (result?.length) { + const channelName = basename(result[0]); + const channelId = `${IMPORTED_LOG_ID_PREFIX}${Date.now()}`; + // Register and show the channel + Registry.as(Extensions.OutputChannels).registerChannel({ + id: channelId, + label: channelName, + log: true, + files: result, + fileNames: result.map(r => basename(r).split('.')[0]) + }); + outputService.showChannel(channelId); + } + } + })); + } } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(OutputContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index c6d759974add..b7a82fdb378b 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -13,7 +13,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { IContextKeyService, IContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IEditorOpenContext } from '../../../common/editor.js'; import { AbstractTextResourceEditor } from '../../../browser/parts/editor/textResourceEditor.js'; -import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT, LOG_ENTRY_REGEX } from '../../../services/output/common/output.js'; +import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT, LOG_ENTRY_REGEX, parseLogEntries, ILogEntry } from '../../../services/output/common/output.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; @@ -340,12 +340,6 @@ export class OutputEditor extends AbstractTextResourceEditor { } - -interface ILogEntry { - readonly logLevel: LogLevel; - readonly lineRange: [number, number]; -} - export class FilterController extends Disposable implements IEditorContribution { public static readonly ID = 'output.editor.contrib.filterController'; @@ -413,30 +407,8 @@ export class FilterController extends Disposable implements IEditorContribution } private computeLogEntriesIncremental(model: ITextModel, fromLine: number): void { - if (!this.logEntries) { - return; - } - - const lineCount = model.getLineCount(); - for (let lineNumber = fromLine; lineNumber <= lineCount; lineNumber++) { - const lineContent = model.getLineContent(lineNumber); - const match = LOG_ENTRY_REGEX.exec(lineContent); - if (match) { - const logLevel = this.parseLogLevel(match[3]); - const startLine = lineNumber; - let endLine = lineNumber; - - while (endLine < lineCount) { - const nextLineContent = model.getLineContent(endLine + 1); - if (model.getLineFirstNonWhitespaceColumn(endLine + 1) === 0 || LOG_ENTRY_REGEX.test(nextLineContent)) { - break; - } - endLine++; - } - - this.logEntries.push({ logLevel, lineRange: [startLine, endLine] }); - lineNumber = endLine; - } + if (this.logEntries) { + this.logEntries = this.logEntries.concat(parseLogEntries(model, fromLine)); } } @@ -456,17 +428,17 @@ export class FilterController extends Disposable implements IEditorContribution for (let i = from; i < this.logEntries.length; i++) { const entry = this.logEntries[i]; if (hasLogLevelFilter && !this.shouldShowEntry(entry, filters)) { - this.hiddenAreas.push(new Range(entry.lineRange[0], 1, entry.lineRange[1], model.getLineMaxColumn(entry.lineRange[1]))); + this.hiddenAreas.push(entry.range); continue; } if (filters.text) { - const matches = model.findMatches(filters.text, new Range(entry.lineRange[0], 1, entry.lineRange[1], model.getLineLastNonWhitespaceColumn(entry.lineRange[1])), false, false, null, false); + const matches = model.findMatches(filters.text, entry.range, false, false, null, false); if (matches.length) { for (const match of matches) { findMatchesDecorations.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION }); } } else { - this.hiddenAreas.push(new Range(entry.lineRange[0], 1, entry.lineRange[1], model.getLineMaxColumn(entry.lineRange[1]))); + this.hiddenAreas.push(entry.range); } } } @@ -509,21 +481,4 @@ export class FilterController extends Disposable implements IEditorContribution } return true; } - - private parseLogLevel(level: string): LogLevel { - switch (level.toLowerCase()) { - case 'trace': - return LogLevel.Trace; - case 'debug': - return LogLevel.Debug; - case 'info': - return LogLevel.Info; - case 'warning': - return LogLevel.Warning; - case 'error': - return LogLevel.Error; - default: - throw new Error(`Unknown log level: ${level}`); - } - } } diff --git a/src/vs/workbench/contrib/output/common/outputChannelModel.ts b/src/vs/workbench/contrib/output/common/outputChannelModel.ts index a942ccaf3c16..125f6494c095 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModel.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModel.ts @@ -21,9 +21,9 @@ import { Range } from '../../../../editor/common/core/range.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { ILogger, ILoggerService, ILogService } from '../../../../platform/log/common/log.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { LOG_ENTRY_REGEX, OutputChannelUpdateMode } from '../../../services/output/common/output.js'; +import { LOG_ENTRY_REGEX, LOG_MIME, OutputChannelUpdateMode, parseLogEntryAt } from '../../../services/output/common/output.js'; import { isCancellationError } from '../../../../base/common/errors.js'; -import { binarySearch } from '../../../../base/common/arrays.js'; +import { TextModel } from '../../../../editor/common/model/textModel.js'; export interface IOutputChannelModel extends IDisposable { readonly onDispose: Event; @@ -168,6 +168,7 @@ class MultiFileContentProvider extends Disposable implements IContentProvider { constructor( filesInfos: IOutputChannelFileInfo[], + @IInstantiationService private readonly instantiationService: IInstantiationService, @IFileService fileService: IFileService, @ILogService logService: ILogService, ) { @@ -194,12 +195,37 @@ class MultiFileContentProvider extends Disposable implements IContentProvider { async getContent(): Promise<{ readonly content: string; readonly consume: () => void }> { const outputs = await Promise.all(this.fileOutputs.map(output => output.getContent())); - const content = combineLogEntries(outputs); + const content = this.combineLogEntries(outputs); return { content, consume: () => outputs.forEach(({ consume }) => consume()) }; } + + private combineLogEntries(outputs: { content: string; name: string }[]): string { + + const logEntries: [number, string][] = []; + + for (const { content, name } of outputs) { + const model = this.instantiationService.createInstance(TextModel, content, LOG_MIME, TextModel.DEFAULT_CREATION_OPTIONS, null); + for (let lineNumber = 1; lineNumber <= model.getLineCount(); lineNumber++) { + const logEntry = parseLogEntryAt(model, lineNumber); + if (!logEntry) { + continue; + } + lineNumber = logEntry.range.endLineNumber; + const content = model.getValueInRange(logEntry.range).replace(LOG_ENTRY_REGEX, `$1 [${name}] $2`); + logEntries.push([logEntry.timestamp, content]); + } + } + + let result = ''; + for (const [, content] of logEntries.sort((a, b) => a[0] - b[0])) { + result += content + '\n'; + } + return result; + } + } export abstract class AbstractFileOutputChannelModel extends Disposable implements IOutputChannelModel { @@ -440,8 +466,9 @@ export class MultiFileOutputChannelModel extends AbstractFileOutputChannelModel @IModelService modelService: IModelService, @ILogService logService: ILogService, @IEditorWorkerService editorWorkerService: IEditorWorkerService, + @IInstantiationService instantiationService: IInstantiationService, ) { - const multifileOutput = new MultiFileContentProvider(filesInfos, fileService, logService); + const multifileOutput = new MultiFileContentProvider(filesInfos, instantiationService, fileService, logService); super(modelUri, language, multifileOutput, modelService, editorWorkerService); this.multifileOutput = this._register(multifileOutput); } @@ -549,74 +576,3 @@ export class DelegatedOutputChannelModel extends Disposable implements IOutputCh this.outputChannelModel.then(outputChannelModel => outputChannelModel.replace(value)); } } - -function combineLogEntries(outputs: { content: string; name: string }[]): string { - const timestampEntries: Date[] = []; - const combinedEntries: string[] = []; - - let startTimestampOfLastOutput: Date | undefined; - let endTimestampOfLastOutput: Date | undefined; - - for (const output of outputs) { - let startTimestamp: Date | undefined; - let timestamp: Date | undefined; - const logEntries = output.content.split('\n'); - for (let index = 0; index < logEntries.length; index++) { - const entry = logEntries[index]; - if (!entry.trim()) { - continue; - } - timestamp = new Date(entry.match(LOG_ENTRY_REGEX)?.[1]!); - if (!startTimestamp) { - startTimestamp = timestamp; - } - const entriesToAdd = [entry.replace(LOG_ENTRY_REGEX, `$1 [${output.name}] $2`)]; - const timestampsToAdd = [timestamp]; - - if (startTimestampOfLastOutput && timestamp < startTimestampOfLastOutput) { - for (index = index + 1; index < logEntries.length; index++) { - const entry = logEntries[index]; - if (!entry.trim()) { - continue; - } - timestamp = new Date(entry.match(LOG_ENTRY_REGEX)?.[1]!); - if (timestamp > startTimestampOfLastOutput) { - index--; - break; - } - entriesToAdd.push(entry.replace(LOG_ENTRY_REGEX, `$1 [${output.name}] $2`)); - timestampsToAdd.push(timestamp); - } - combinedEntries.unshift(...entriesToAdd); - timestampEntries.unshift(...timestampsToAdd); - continue; - } - - if (endTimestampOfLastOutput && timestamp > endTimestampOfLastOutput) { - for (index = index + 1; index < logEntries.length; index++) { - const entry = logEntries[index]; - if (!entry.trim()) { - continue; - } - timestamp = new Date(entry.match(LOG_ENTRY_REGEX)?.[1]!); - entriesToAdd.push(entry.replace(LOG_ENTRY_REGEX, `$1 [${output.name}] $2`)); - timestampsToAdd.push(timestamp); - } - combinedEntries.push(...entriesToAdd); - timestampEntries.push(...timestampsToAdd); - break; - } - - const idx = binarySearch(timestampEntries, timestamp, (a, b) => a.getTime() - b.getTime()); - const insertionIndex = idx < 0 ? ~idx : idx; - combinedEntries.splice(insertionIndex, 0, ...entriesToAdd); - timestampEntries.splice(insertionIndex, 0, ...timestampsToAdd); - } - - startTimestampOfLastOutput = startTimestamp; - endTimestampOfLastOutput = timestamp; - } - // Add new empty line at the end - combinedEntries.push(''); - return combinedEntries.join('\n'); -} diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index eb97f4a4e198..addfef6e93d7 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -8,6 +8,9 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { URI } from '../../../../base/common/uri.js'; import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { ITextModel } from '../../../../editor/common/model.js'; +import { LogLevel } from '../../../../platform/log/common/log.js'; +import { Range } from '../../../../editor/common/core/range.js'; /** * Mime type used by the output editor. @@ -247,3 +250,81 @@ class OutputChannelRegistry implements IOutputChannelRegistry { } Registry.add(Extensions.OutputChannels, new OutputChannelRegistry()); + +export interface ILogEntry { + readonly timestamp: number; + readonly logLevel: LogLevel; + readonly timestampRange: Range; + readonly range: Range; +} + +/** + * Parses log entries from a given text model starting from a specified line. + * + * @param model - The text model containing the log entries. + * @param fromLine - The line number to start parsing from (default is 1). + * @returns An array of log entries, each containing the log level and the range of lines it spans. + */ +export function parseLogEntries(model: ITextModel, fromLine: number = 1): ILogEntry[] { + const logEntries: ILogEntry[] = []; + for (let lineNumber = fromLine; lineNumber <= model.getLineCount(); lineNumber++) { + const logEntry = parseLogEntryAt(model, lineNumber); + if (logEntry) { + logEntries.push(logEntry); + lineNumber = logEntry.range.endLineNumber; + } + } + return logEntries; +} + + +/** + * Parses a log entry at the specified line number in the given text model. + * + * @param model - The text model containing the log entries. + * @param lineNumber - The line number at which to start parsing the log entry. + * @returns An object representing the parsed log entry, or `null` if no log entry is found at the specified line. + * + * The returned log entry object contains: + * - `timestamp`: The timestamp of the log entry as a number. + * - `logLevel`: The log level of the log entry. + * - `range`: The range of lines that the log entry spans. + */ +export function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntry | null { + const lineContent = model.getLineContent(lineNumber); + const match = LOG_ENTRY_REGEX.exec(lineContent); + if (match) { + const timestamp = new Date(match[1]).getTime(); + const timestampRange = new Range(lineNumber, 1, lineNumber, match[1].length + 1); + const logLevel = parseLogLevel(match[3]); + const startLine = lineNumber; + let endLine = lineNumber; + + while (endLine < model.getLineCount()) { + const nextLineContent = model.getLineContent(endLine + 1); + if (model.getLineFirstNonWhitespaceColumn(endLine + 1) === 0 || LOG_ENTRY_REGEX.test(nextLineContent)) { + break; + } + endLine++; + } + return { timestamp, logLevel, range: new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)), timestampRange }; + } + return null; +} + +function parseLogLevel(level: string): LogLevel { + switch (level.toLowerCase()) { + case 'trace': + return LogLevel.Trace; + case 'debug': + return LogLevel.Debug; + case 'info': + return LogLevel.Info; + case 'warning': + return LogLevel.Warning; + case 'error': + return LogLevel.Error; + default: + throw new Error(`Unknown log level: ${level}`); + } +} From 8eddff66139290e6a9b7b9f8f7a8057a96088c50 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Tue, 14 Jan 2025 12:23:14 -0800 Subject: [PATCH 0552/3587] Default to MSAL authentication :rocket: (#237920) Here we go. Ref https://github.com/microsoft/vscode/issues/178740 --- extensions/microsoft-authentication/package.json | 7 +++---- extensions/microsoft-authentication/package.nls.json | 10 ++++++++-- extensions/microsoft-authentication/src/extension.ts | 4 ++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index ba7d83aa359e..928ffa688899 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -102,7 +102,7 @@ "properties": { "microsoft-authentication.implementation": { "type": "string", - "default": "classic", + "default": "msal", "enum": [ "msal", "classic" @@ -111,10 +111,9 @@ "%microsoft-authentication.implementation.enumDescriptions.msal%", "%microsoft-authentication.implementation.enumDescriptions.classic%" ], - "description": "%microsoft-authentication.implementation.description%", + "markdownDescription": "%microsoft-authentication.implementation.description%", "tags": [ - "onExP", - "preview" + "onExP" ] } } diff --git a/extensions/microsoft-authentication/package.nls.json b/extensions/microsoft-authentication/package.nls.json index dd33d036abc7..c8e0189c08f9 100644 --- a/extensions/microsoft-authentication/package.nls.json +++ b/extensions/microsoft-authentication/package.nls.json @@ -3,9 +3,15 @@ "description": "Microsoft authentication provider", "signIn": "Sign In", "signOut": "Sign Out", - "microsoft-authentication.implementation.description": "The authentication implementation to use for signing in with a Microsoft account.", + "microsoft-authentication.implementation.description": { + "message": "The authentication implementation to use for signing in with a Microsoft account.\n\n*NOTE: The `classic` implementation is deprecated and will be removed, along with this setting, in a future release. If only the `classic` implementation works for you, please [open an issue](command:workbench.action.openIssueReporter) and explain what you are trying to log in to.*", + "comment": [ + "{Locked='[(command:workbench.action.openIssueReporter)]'}", + "The `command:` syntax will turn into a link. Do not translate it." + ] + }, "microsoft-authentication.implementation.enumDescriptions.msal": "Use the Microsoft Authentication Library (MSAL) to sign in with a Microsoft account.", - "microsoft-authentication.implementation.enumDescriptions.classic": "Use the classic authentication flow to sign in with a Microsoft account.", + "microsoft-authentication.implementation.enumDescriptions.classic": "(deprecated) Use the classic authentication flow to sign in with a Microsoft account.", "microsoft-sovereign-cloud.environment.description": { "message": "The Sovereign Cloud to use for authentication. If you select `custom`, you must also set the `#microsoft-sovereign-cloud.customEnvironment#` setting.", "comment": [ diff --git a/extensions/microsoft-authentication/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts index 9d04d16408fa..c11f10876416 100644 --- a/extensions/microsoft-authentication/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -35,8 +35,8 @@ function shouldUseMsal(expService: IExperimentationService): boolean { } Logger.info('Acquired MSAL enablement value from default. Value: false'); - // If no setting or experiment value is found, default to false - return false; + // If no setting or experiment value is found, default to true + return true; } let useMsal: boolean | undefined; From 8f4b28aedd64cf149961f620405765ae3356b42a Mon Sep 17 00:00:00 2001 From: Roland Grunberg Date: Sat, 11 Jan 2025 21:52:35 -0500 Subject: [PATCH 0553/3587] Introduce 'keepWhitespace' property for snippet text edit abstractions. - Restore original behaviour of bulk text edits adjusting whitespace - Add testcase Signed-off-by: Roland Grunberg --- .../src/singlefolder-tests/workspace.test.ts | 21 +++++++++++++++++++ .../browser/services/bulkEditService.ts | 2 +- src/vs/editor/common/languages.ts | 2 +- .../contrib/snippet/browser/snippetSession.ts | 5 +++-- src/vs/monaco.d.ts | 1 + .../api/common/extHostTypeConverters.ts | 3 ++- src/vs/workbench/api/common/extHostTypes.ts | 5 ++++- .../contrib/bulkEdit/browser/bulkTextEdits.ts | 9 ++++---- src/vscode-dts/vscode.d.ts | 5 +++++ 9 files changed, 43 insertions(+), 10 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index b871093df39b..2b5f586ebe1a 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -1226,6 +1226,27 @@ suite('vscode API - workspace', () => { assert.deepStrictEqual(edt.selections, [new vscode.Selection(0, 0, 0, 3)]); }); + test('SnippetString in WorkspaceEdit with keepWhitespace', async function (): Promise { + const file = await createRandomFile('This is line 1\n '); + + const document = await vscode.workspace.openTextDocument(file); + const edt = await vscode.window.showTextDocument(document); + + assert.ok(edt === vscode.window.activeTextEditor); + + const snippetText = new vscode.SnippetTextEdit(new vscode.Range(1, 3, 1, 3), new vscode.SnippetString('This is line 2\n This is line 3')); + snippetText.keepWhitespace = true; + const we = new vscode.WorkspaceEdit(); + we.set(document.uri, [snippetText]); + const success = await vscode.workspace.applyEdit(we); + if (edt !== vscode.window.activeTextEditor) { + return this.skip(); + } + + assert.ok(success); + assert.strictEqual(document.getText(), 'This is line 1\n This is line 2\n This is line 3'); + }); + test('Support creating binary files in a WorkspaceEdit', async function (): Promise { const fileUri = vscode.Uri.parse(`${testFs.scheme}:/${rndName()}`); diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index cbc88fafc2f0..fa12a2ab99e5 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -55,7 +55,7 @@ export class ResourceTextEdit extends ResourceEdit implements IWorkspaceTextEdit constructor( readonly resource: URI, - readonly textEdit: TextEdit & { insertAsSnippet?: boolean }, + readonly textEdit: TextEdit & { insertAsSnippet?: boolean; keepWhitespace?: boolean }, readonly versionId: number | undefined = undefined, metadata?: WorkspaceEditMetadata, ) { diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index bef68de579de..7daaaabf59ef 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1795,7 +1795,7 @@ export interface IWorkspaceFileEdit { export interface IWorkspaceTextEdit { resource: URI; - textEdit: TextEdit & { insertAsSnippet?: boolean }; + textEdit: TextEdit & { insertAsSnippet?: boolean; keepWhitespace?: boolean }; versionId: number | undefined; metadata?: WorkspaceEditMetadata; } diff --git a/src/vs/editor/contrib/snippet/browser/snippetSession.ts b/src/vs/editor/contrib/snippet/browser/snippetSession.ts index e0bacc7662e5..30e42dd539a6 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetSession.ts @@ -375,6 +375,7 @@ const _defaultOptions: ISnippetSessionInsertOptions = { export interface ISnippetEdit { range: Range; template: string; + keepWhitespace?: boolean; } export class SnippetSession { @@ -569,7 +570,7 @@ export class SnippetSession { let offset = 0; for (let i = 0; i < snippetEdits.length; i++) { - const { range, template } = snippetEdits[i]; + const { range, template, keepWhitespace } = snippetEdits[i]; // gaps between snippet edits are appended as text nodes. this // ensures placeholder-offsets are later correct @@ -582,7 +583,7 @@ export class SnippetSession { } const newNodes = parser.parseFragment(template, snippet); - SnippetSession.adjustWhitespace(model, range.getStartPosition(), adjustWhitespace, snippet, new Set(newNodes)); + SnippetSession.adjustWhitespace(model, range.getStartPosition(), keepWhitespace !== undefined ? !keepWhitespace : adjustWhitespace, snippet, new Set(newNodes)); snippet.resolveVariables(resolver); const snippetText = snippet.toString(); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 5db429fff414..f7b065aaaa8d 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7973,6 +7973,7 @@ declare namespace monaco.languages { resource: Uri; textEdit: TextEdit & { insertAsSnippet?: boolean; + keepWhitespace?: boolean; }; versionId: number | undefined; metadata?: WorkspaceEditMetadata; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index b7f3d7e8b5ec..7381e90d0df3 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -657,7 +657,8 @@ export namespace WorkspaceEdit { textEdit: { range: Range.from(entry.range), text: entry.edit.value, - insertAsSnippet: true + insertAsSnippet: true, + keepWhitespace: entry.keepWhitespace }, versionId: !toCreate.has(entry.uri) ? versionInfo?.getTextDocumentVersion(entry.uri) : undefined, metadata: entry.metadata diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index abb6a3e737b2..1eb1860d421c 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -766,6 +766,8 @@ export class SnippetTextEdit implements vscode.SnippetTextEdit { snippet: SnippetString; + keepWhitespace?: boolean; + constructor(range: Range, snippet: SnippetString) { this.range = range; this.snippet = snippet; @@ -809,6 +811,7 @@ export interface IFileSnippetTextEdit { readonly range: vscode.Range; readonly edit: vscode.SnippetString; readonly metadata?: vscode.WorkspaceEditEntryMetadata; + readonly keepWhitespace?: boolean; } export interface IFileCellEdit { @@ -938,7 +941,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { this.replaceNotebookCells(uri, edit.range, edit.newCells, metadata); } } else if (SnippetTextEdit.isSnippetTextEdit(edit)) { - this._edits.push({ _type: FileEditType.Snippet, uri, range: edit.range, edit: edit.snippet, metadata }); + this._edits.push({ _type: FileEditType.Snippet, uri, range: edit.range, edit: edit.snippet, metadata, keepWhitespace: edit.keepWhitespace }); } else { this._edits.push({ _type: FileEditType.Text, uri, edit, metadata }); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts index 54bea70bd8cf..4e4ec817f8bd 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts @@ -25,7 +25,7 @@ import { ISnippetEdit } from '../../../../editor/contrib/snippet/browser/snippet type ValidationResult = { canApply: true } | { canApply: false; reason: URI }; -type ISingleSnippetEditOperation = ISingleEditOperation & { insertAsSnippet?: boolean }; +type ISingleSnippetEditOperation = ISingleEditOperation & { insertAsSnippet?: boolean; keepWhitespace?: boolean }; class ModelEditTask implements IDisposable { @@ -80,7 +80,7 @@ class ModelEditTask implements IDisposable { } else { range = Range.lift(textEdit.range); } - this._edits.push({ ...EditOperation.replaceMove(range, textEdit.text), insertAsSnippet: textEdit.insertAsSnippet }); + this._edits.push({ ...EditOperation.replaceMove(range, textEdit.text), insertAsSnippet: textEdit.insertAsSnippet, keepWhitespace: textEdit.keepWhitespace }); } validate(): ValidationResult { @@ -152,11 +152,12 @@ class EditorEditTask extends ModelEditTask { if (edit.range && edit.text !== null) { snippetEdits.push({ range: Range.lift(edit.range), - template: edit.insertAsSnippet ? edit.text : SnippetParser.escape(edit.text) + template: edit.insertAsSnippet ? edit.text : SnippetParser.escape(edit.text), + keepWhitespace: edit.keepWhitespace }); } } - snippetCtrl.apply(snippetEdits, { undoStopBefore: false, undoStopAfter: false, adjustWhitespace: false }); + snippetCtrl.apply(snippetEdits, { undoStopBefore: false, undoStopAfter: false }); } else { // normal edit diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 99fdc6be76d4..ed378dce6bad 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -3780,6 +3780,11 @@ declare module 'vscode' { */ snippet: SnippetString; + /** + * Whether the snippet edit should be applied with existing whitespace preserved. + */ + keepWhitespace?: boolean; + /** * Create a new snippet edit. * From 115baa15e7624f0709c1516441006c0f85017de9 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:09:22 -0800 Subject: [PATCH 0554/3587] bump distro (#237925) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e6b539b597af..96bc64d0f521 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.97.0", - "distro": "d631a7e71cfab757e241fd75ecb1594c6b427920", + "distro": "a408bc8c56673f8240be72eeeb7d29908e0e3511", "author": { "name": "Microsoft Corporation" }, From f6b5206b33e8cfe1ea7dba2ede2953154c7b38ea Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 14 Jan 2025 15:31:01 -0800 Subject: [PATCH 0555/3587] Pick up latest TS for building VS COde --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5e7d7f307c10..caa4903a0c97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,7 +154,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^5.8.0-dev.20250110", + "typescript": "^5.8.0-dev.20250114", "typescript-eslint": "^8.8.0", "util": "^0.12.4", "webpack": "^5.94.0", @@ -17610,9 +17610,9 @@ "dev": true }, "node_modules/typescript": { - "version": "5.8.0-dev.20250110", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.0-dev.20250110.tgz", - "integrity": "sha512-+qwHVEvUm4CeQGtZIvlwE8HmRFcBMV4F/8OPKv+mIyGRGx4Chrj2v0VCsReVJwRdjjs6Dat/lPzkJW1E18+eOg==", + "version": "5.8.0-dev.20250114", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.0-dev.20250114.tgz", + "integrity": "sha512-DGtuEPL692JPjTQHFmP810EklYi8ndHgCWt61kRjQfaO25LFpxuB9ZNVs79u15t9JpkeIB6WLKpjY/JiRCzYMw==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index e6b539b597af..e791fbec28f4 100644 --- a/package.json +++ b/package.json @@ -212,7 +212,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^5.8.0-dev.20250110", + "typescript": "^5.8.0-dev.20250114", "typescript-eslint": "^8.8.0", "util": "^0.12.4", "webpack": "^5.94.0", From fcac4d7634f9dbb77f7c5f51453272a8e47fd723 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 14 Jan 2025 15:33:50 -0800 Subject: [PATCH 0556/3587] Pick up latest TS 5.7 recovery release --- extensions/package-lock.json | 8 ++++---- extensions/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/package-lock.json b/extensions/package-lock.json index 04f0bfcde5c1..c27ce93440e7 100644 --- a/extensions/package-lock.json +++ b/extensions/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "typescript": "^5.7.2" + "typescript": "^5.7.3" }, "devDependencies": { "@parcel/watcher": "2.5.0", @@ -897,9 +897,9 @@ } }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/extensions/package.json b/extensions/package.json index e70678990aff..128832816339 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "^5.7.2" + "typescript": "^5.7.3" }, "scripts": { "postinstall": "node ./postinstall.mjs" From 1db1071148a1efa3b7ad7592d64507ef52536a3e Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 14 Jan 2025 19:02:08 -0800 Subject: [PATCH 0557/3587] Make editFile tool safer (#237937) --- .../contrib/chat/browser/tools/tools.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/tools.ts b/src/vs/workbench/contrib/chat/browser/tools/tools.ts index cbd33bfce930..448b262887a4 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/tools.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/tools.ts @@ -17,6 +17,8 @@ import { IChatEditingService } from '../../common/chatEditingService.js'; import { ChatModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js'; export class BuiltinToolsContribution extends Disposable implements IWorkbenchContribution { @@ -67,7 +69,9 @@ class EditTool implements IToolData, IToolImpl { constructor( @IChatService private readonly chatService: IChatService, @IChatEditingService private readonly chatEditingService: IChatEditingService, - @ICodeMapperService private readonly codeMapperService: ICodeMapperService + @ICodeMapperService private readonly codeMapperService: ICodeMapperService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @ILanguageModelIgnoredFilesService private readonly ignoredFilesService: ILanguageModelIgnoredFilesService ) { this.inputSchema = { type: 'object', @@ -94,16 +98,23 @@ class EditTool implements IToolData, IToolImpl { throw new Error('toolInvocationToken is required for this tool'); } - const parameters = invocation.parameters as EditToolParams; if (!parameters.filePath || !parameters.explanation || !parameters.code) { throw new Error(`Invalid tool input: ${JSON.stringify(parameters)}`); } + const uri = URI.file(parameters.filePath); + if (!this.workspaceContextService.isInsideWorkspace(uri)) { + return { content: [{ kind: 'text', value: `Error: file ${parameters.filePath} can't be edited because it's not inside the current workspace` }] }; + } + + if (await this.ignoredFilesService.fileIsIgnored(uri, token)) { + return { content: [{ kind: 'text', value: `Error: file ${parameters.filePath} can't be edited because it is configured to be ignored by Copilot` }] }; + } + const model = this.chatService.getSession(invocation.context?.sessionId) as ChatModel; const request = model.getRequests().at(-1)!; - const uri = URI.file(parameters.filePath); model.acceptResponseProgress(request, { kind: 'markdownContent', content: new MarkdownString('\n````\n') From fce85e9d2722559857a28a7332bea88d24a3f0cb Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 15 Jan 2025 09:38:56 +0100 Subject: [PATCH 0558/3587] uses /edit editor UX for "Apply In Editor" (#237944) * WIP * uses /edit editor UX for "Apply In Editor" fixes https://github.com/microsoft/vscode-copilot/issues/8577 --- src/vs/base/common/iterator.ts | 12 ++- .../browser/actions/chatCodeblockActions.ts | 2 +- .../browser/actions/codeBlockOperations.ts | 2 +- .../chatEditingModifiedFileEntry.ts | 13 ++++ .../browser/chatEditing/chatEditingService.ts | 75 +++++++++++++++---- .../browser/chatEditing/chatEditingSession.ts | 33 ++++++-- .../contrib/chat/browser/chatEditorActions.ts | 15 ++-- .../chat/browser/chatEditorController.ts | 55 +++++++++----- .../contrib/chat/common/chatEditingService.ts | 17 +++-- .../browser/inlineChatController.ts | 59 +++++++++------ .../contrib/scm/browser/quickDiffModel.ts | 21 +++++- 11 files changed, 218 insertions(+), 86 deletions(-) diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index ca55068751d0..fedcfe7edefa 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isIterable } from './types.js'; + export namespace Iterable { export function is(thing: any): thing is Iterable { @@ -90,9 +92,13 @@ export namespace Iterable { } } - export function* concat(...iterables: Iterable[]): Iterable { - for (const iterable of iterables) { - yield* iterable; + export function* concat(...iterables: (Iterable | T)[]): Iterable { + for (const item of iterables) { + if (isIterable(item)) { + yield* item; + } else { + yield item; + } } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 248bbd7da531..33b9e4c323cc 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -558,7 +558,7 @@ export function registerChatCodeCompareBlockActions() { const inlineChatController = InlineChatController.get(editorToApply); if (inlineChatController) { editorToApply.revealLineInCenterIfOutsideViewport(firstEdit.range.startLineNumber); - inlineChatController.reviewEdits(firstEdit.range, textEdits, CancellationToken.None); + inlineChatController.reviewEdits(textEdits, CancellationToken.None); response.setEditApplied(item, 1); return true; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index 5fafadbd6c02..f206b6e1065c 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -266,7 +266,7 @@ export class ApplyCodeBlockOperation { const inlineChatController = InlineChatController.get(codeEditor); if (inlineChatController) { let isOpen = true; - const promise = inlineChatController.reviewEdits(codeEditor.getSelection(), edits, tokenSource.token); + const promise = inlineChatController.reviewEdits(edits, tokenSource.token); promise.finally(() => { isOpen = false; tokenSource.dispose(); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index e022f402c79c..604bf523b600 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -134,6 +134,8 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie private readonly _diffTrimWhitespace: IObservable; + private _refCounter: number = 1; + constructor( resourceRef: IReference, private readonly _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void }, @@ -199,6 +201,17 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie })); } + override dispose(): void { + if (--this._refCounter === 0) { + super.dispose(); + } + } + + acquire() { + this._refCounter++; + return this; + } + private _clearCurrentEditLineDecoration() { this._editDecorations = this.doc.deltaDecorations(this._editDecorations, []); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts index 9a2b6ae27eac..cadf30ab1602 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts @@ -9,9 +9,11 @@ import { CancellationToken, CancellationTokenSource } from '../../../../../base/ import { Codicon } from '../../../../../base/common/codicons.js'; import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Iterable } from '../../../../../base/common/iterator.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { LinkedList } from '../../../../../base/common/linkedList.js'; import { ResourceMap } from '../../../../../base/common/map.js'; -import { derived, IObservable, observableValue, runOnChange, ValueWithChangeEventFromObservable } from '../../../../../base/common/observable.js'; +import { derived, IObservable, observableValue, observableValueOpts, runOnChange, ValueWithChangeEventFromObservable } from '../../../../../base/common/observable.js'; import { compare } from '../../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { isString } from '../../../../../base/common/types.js'; @@ -37,6 +39,7 @@ import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { applyingChatEditsContextKey, applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingAgentSupportsReadonlyReferencesContextKey, chatEditingMaxFileAssignmentName, chatEditingResourceContextKey, ChatEditingSessionState, decidedChatEditingResourceContextKey, defaultChatEditingMaxFileLimit, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, IChatEditingSessionStream, IChatRelatedFile, IChatRelatedFilesProvider, IModifiedFileEntry, inChatEditingSessionContextKey, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel, IChatTextEditGroup } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; +import { ChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js'; import { ChatEditingSession } from './chatEditingSession.js'; import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; @@ -50,6 +53,17 @@ export class ChatEditingService extends Disposable implements IChatEditingServic private readonly _currentSessionObs = observableValue(this, null); private readonly _currentSessionDisposables = this._register(new DisposableStore()); + private readonly _adhocSessionsObs = observableValueOpts>({ equalsFn: (a, b) => false }, new LinkedList()); + + readonly editingSessionsObs: IObservable = derived(r => { + const result = Array.from(this._adhocSessionsObs.read(r)); + const globalSession = this._currentSessionObs.read(r); + if (globalSession) { + result.push(globalSession); + } + return result; + }); + private readonly _currentAutoApplyOperationObs = observableValue(this, null); get currentAutoApplyOperation(): CancellationTokenSource | null { return this._currentAutoApplyOperationObs.get(); @@ -63,9 +77,6 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return this._currentSessionObs; } - private readonly _onDidChangeEditingSession = this._register(new Emitter()); - public readonly onDidChangeEditingSession = this._onDidChangeEditingSession.event; - private _editingSessionFileLimitPromise: Promise; private _editingSessionFileLimit: number | undefined; get editingSessionFileLimit() { @@ -111,13 +122,14 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return decidedEntries.map(entry => entry.entryId); })); this._register(bindContextKey(hasUndecidedChatEditingResourceContextKey, contextKeyService, (reader) => { - const currentSession = this._currentSessionObs.read(reader); - if (!currentSession) { - return; + + for (const session of this.editingSessionsObs.read(reader)) { + const entries = session.entries.read(reader); + const decidedEntries = entries.filter(entry => entry.state.read(reader) === WorkingSetEntryState.Modified); + return decidedEntries.length > 0; } - const entries = currentSession.entries.read(reader); - const decidedEntries = entries.filter(entry => entry.state.read(reader) === WorkingSetEntryState.Modified); - return decidedEntries.length > 0; + + return false; })); this._register(bindContextKey(hasAppliedChatEditsContextKey, contextKeyService, (reader) => { const currentSession = this._currentSessionObs.read(reader); @@ -211,6 +223,18 @@ export class ChatEditingService extends Disposable implements IChatEditingServic } + private _lookupEntry(uri: URI): ChatEditingModifiedFileEntry | undefined { + + for (const item of Iterable.concat(this.editingSessionsObs.get())) { + const candidate = item.getEntry(uri); + if (candidate instanceof ChatEditingModifiedFileEntry) { + // make sure to ref-count this object + return candidate.acquire(); + } + } + return undefined; + } + private async _createEditingSession(chatSessionId: string): Promise { if (this._currentSessionObs.get()) { throw new BugIndicatingError('Cannot have more than one active editing session'); @@ -218,7 +242,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic this._currentSessionDisposables.clear(); - const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, this._editingSessionFileLimitPromise); + const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, this._editingSessionFileLimitPromise, this._lookupEntry.bind(this)); await session.init(); // listen for completed responses, run the code mapper and apply the edits to this edit session @@ -227,14 +251,33 @@ export class ChatEditingService extends Disposable implements IChatEditingServic this._currentSessionDisposables.add(session.onDidDispose(() => { this._currentSessionDisposables.clear(); this._currentSessionObs.set(null, undefined); - this._onDidChangeEditingSession.fire(); - })); - this._currentSessionDisposables.add(session.onDidChange(() => { - this._onDidChangeEditingSession.fire(); })); this._currentSessionObs.set(session, undefined); - this._onDidChangeEditingSession.fire(); + return session; + } + + async createAdhocEditingSession(chatSessionId: string): Promise { + const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, this._editingSessionFileLimitPromise, this._lookupEntry.bind(this)); + await session.init(); + + const list = this._adhocSessionsObs.get(); + const removeSession = list.unshift(session); + + const store = new DisposableStore(); + this._store.add(store); + + store.add(this.installAutoApplyObserver(session)); + + store.add(session.onDidDispose(e => { + removeSession(); + this._adhocSessionsObs.set(list, undefined); + this._store.deleteAndLeak(store); + store.dispose(); + })); + + this._adhocSessionsObs.set(list, undefined); + return session; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 4a1a7d6bf893..32afe69b4f0d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -163,6 +163,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio constructor( public readonly chatSessionId: string, private editingSessionFileLimitPromise: Promise, + private _lookupExternalEntry: (uri: URI) => ChatEditingModifiedFileEntry | undefined, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IModelService private readonly _modelService: IModelService, @ILanguageService private readonly _languageService: ILanguageService, @@ -670,23 +671,39 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } return existingEntry; } - const initialContent = this._initialFileContents.get(resource); - // This gets manually disposed in .dispose() or in .restoreSnapshot() - const entry = await this._createModifiedFileEntry(resource, responseModel, false, initialContent); - if (!initialContent) { - this._initialFileContents.set(resource, entry.initialContent); + + let entry: ChatEditingModifiedFileEntry; + const existingExternalEntry = this._lookupExternalEntry(resource); + if (existingExternalEntry) { + entry = existingExternalEntry; + } else { + const initialContent = this._initialFileContents.get(resource); + // This gets manually disposed in .dispose() or in .restoreSnapshot() + entry = await this._createModifiedFileEntry(resource, responseModel, false, initialContent); + if (!initialContent) { + this._initialFileContents.set(resource, entry.initialContent); + } } + // If an entry is deleted e.g. reverting a created file, // remove it from the entries and don't show it in the working set anymore // so that it can be recreated e.g. through retry - this._register(entry.onDidDelete(() => { + const listener = entry.onDidDelete(() => { const newEntries = this._entriesObs.get().filter(e => !isEqual(e.modifiedURI, entry.modifiedURI)); this._entriesObs.set(newEntries, undefined); this._workingSet.delete(entry.modifiedURI); this._editorService.closeEditors(this._editorService.findEditors(entry.modifiedURI)); - entry.dispose(); + + if (!existingExternalEntry) { + // don't dispose entries that are not yours! + entry.dispose(); + } + + this._store.delete(listener); this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); - })); + }); + this._store.add(listener); + const entriesArr = [...this._entriesObs.get(), entry]; this._entriesObs.set(entriesArr, undefined); this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts index 75822df0d721..ebd21d41a0bb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts @@ -61,14 +61,15 @@ abstract class NavigateAction extends Action2 { if (!isCodeEditor(editor) || !editor.hasModel()) { return; } - - const session = chatEditingService.currentEditingSession; - if (!session) { + const ctrl = ChatEditorController.get(editor); + if (!ctrl) { return; } - const ctrl = ChatEditorController.get(editor); - if (!ctrl) { + const session = chatEditingService.editingSessionsObs.get() + .find(candidate => candidate.getEntry(editor.getModel().uri)); + + if (!session) { return; } @@ -167,7 +168,9 @@ abstract class AcceptDiscardAction extends Action2 { return; } - const session = chatEditingService.currentEditingSession; + const session = chatEditingService.editingSessionsObs.get() + .find(candidate => candidate.getEntry(uri)); + if (!session) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index 03ea41e0ba40..f6f4e917c80f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -73,8 +73,8 @@ export class ChatEditorController extends Disposable implements IEditorContribut constructor( private readonly _editor: ICodeEditor, - @IInstantiationService private readonly _instantiationService: IInstantiationService, @IChatEditingService private readonly _chatEditingService: IChatEditingService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IEditorService private readonly _editorService: IEditorService, @IContextKeyService contextKeyService: IContextKeyService, @@ -92,27 +92,31 @@ export class ChatEditorController extends Disposable implements IEditorContribut this._store.add(autorun(r => { - const session = this._chatEditingService.currentEditingSessionObs.read(r); - this._ctxRequestInProgress.set(session?.state.read(r) === ChatEditingSessionState.StreamingEdits); + let isStreamingEdits = false; + for (const session of _chatEditingService.editingSessionsObs.read(r)) { + isStreamingEdits ||= session.state.read(r) === ChatEditingSessionState.StreamingEdits; + } + this._ctxRequestInProgress.set(isStreamingEdits); })); - const entryForEditor = derived(r => { const model = modelObs.read(r); - const session = this._chatEditingService.currentEditingSessionObs.read(r); - if (!session) { - return undefined; + if (!model) { + return; } - const entries = session.entries.read(r); - const idx = model?.uri - ? entries.findIndex(e => isEqual(e.modifiedURI, model.uri)) - : -1; + for (const session of _chatEditingService.editingSessionsObs.read(r)) { + const entries = session.entries.read(r); + const idx = model?.uri + ? entries.findIndex(e => isEqual(e.modifiedURI, model.uri)) + : -1; - if (idx < 0) { - return undefined; + if (idx >= 0) { + return { session, entry: entries[idx], entries, idx }; + } } - return { session, entry: entries[idx], entries, idx }; + + return undefined; }); @@ -185,13 +189,17 @@ export class ChatEditorController extends Disposable implements IEditorContribut // ---- readonly while streaming const shouldBeReadOnly = derived(this, r => { - const value = this._chatEditingService.currentEditingSessionObs.read(r); - if (!value || value.state.read(r) !== ChatEditingSessionState.StreamingEdits) { - return false; - } const model = modelObs.read(r); - return model ? value.readEntry(model.uri, r) : undefined; + if (!model) { + return undefined; + } + for (const session of _chatEditingService.editingSessionsObs.read(r)) { + if (session.readEntry(model.uri, r) && session.state.read(r) === ChatEditingSessionState.StreamingEdits) { + return true; + } + } + return false; }); @@ -570,7 +578,14 @@ export class ChatEditorController extends Disposable implements IEditorContribut return; } - const entry = this._chatEditingService.currentEditingSessionObs.get()?.getEntry(this._editor.getModel().uri); + let entry: IModifiedFileEntry | undefined; + for (const session of this._chatEditingService.editingSessionsObs.get()) { + entry = session.getEntry(this._editor.getModel().uri); + if (entry) { + break; + } + } + if (!entry) { return; } diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index ec99832ce0c1..d4db6b9695e8 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -24,11 +24,6 @@ export interface IChatEditingService { _serviceBrand: undefined; - /** - * emitted when a session is created, changed or disposed - */ - readonly onDidChangeEditingSession: Event; - readonly currentEditingSessionObs: IObservable; readonly currentEditingSession: IChatEditingSession | null; @@ -38,9 +33,21 @@ export interface IChatEditingService { startOrContinueEditingSession(chatSessionId: string): Promise; getOrRestoreEditingSession(): Promise; + + hasRelatedFilesProviders(): boolean; registerRelatedFilesProvider(handle: number, provider: IChatRelatedFilesProvider): IDisposable; getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>; + + /** + * All editing sessions, sorted by recency, e.g the last created session comes first. + */ + readonly editingSessionsObs: IObservable; + + /** + * Creates a new short lived editing session + */ + createAdhocEditingSession(chatSessionId: string): Promise; } export interface IChatRequestDraft { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index d32358f537db..a68ca5c47cd6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -12,6 +12,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { MovingAverage } from '../../../../base/common/numbers.js'; +import { autorun } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { assertType } from '../../../../base/common/types.js'; @@ -40,6 +41,7 @@ import { showChatView } from '../../chat/browser/chat.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; import { ChatAgentLocation } from '../../chat/common/chatAgents.js'; import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; +import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from '../../chat/common/chatModel.js'; import { IChatService } from '../../chat/common/chatService.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; @@ -144,6 +146,7 @@ export class InlineChatController implements IEditorContribution { @IDialogService private readonly _dialogService: IDialogService, @IContextKeyService contextKeyService: IContextKeyService, @IChatService private readonly _chatService: IChatService, + @IChatEditingService private readonly _chatEditingService: IChatEditingService, @IEditorService private readonly _editorService: IEditorService, @INotebookEditorService notebookEditorService: INotebookEditorService, ) { @@ -1134,44 +1137,56 @@ export class InlineChatController implements IEditorContribution { return this._currentRun; } - async reviewEdits(anchor: IRange, stream: AsyncIterable, token: CancellationToken) { + async reviewEdits(stream: AsyncIterable, token: CancellationToken) { if (!this._editor.hasModel()) { return false; } - const session = await this._inlineChatSessionService.createSession(this._editor, { wholeRange: anchor, headless: true }, token); - if (!session) { + const uri = this._editor.getModel().uri; + const chatModel = this._chatService.startSession(ChatAgentLocation.Editor, token); + + if (!chatModel) { return false; } - const request = session.chatModel.addRequest({ text: 'DUMMY', parts: [] }, { variables: [] }, 0); - const run = this.run({ - existingSession: session, - headless: true - }); + const editSession = await this._chatEditingService.createAdhocEditingSession(chatModel.sessionId); - await Event.toPromise(Event.filter(this._onDidEnterState.event, candidate => candidate === State.SHOW_REQUEST)); + // + const store = new DisposableStore(); + store.add(chatModel); + store.add(editSession); + // STREAM + const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); + assertType(chatRequest.response); + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); for await (const chunk of stream) { - session.chatModel.acceptResponseProgress(request, { kind: 'textEdit', uri: this._editor.getModel()!.uri, edits: chunk }); + + if (token.isCancellationRequested) { + chatRequest.response.cancel(); + break; + } + + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: chunk, done: false }); } + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); - if (token.isCancellationRequested) { - session.chatModel.cancelRequest(request); - } else { - session.chatModel.completeResponse(request); + if (!token.isCancellationRequested) { + chatRequest.response.complete(); } - await Event.toPromise(Event.filter(this._onDidEnterState.event, candidate => candidate === State.WAIT_FOR_INPUT)); + const whenDecided = new Promise(resolve => { + store.add(autorun(r => { + if (!editSession.entries.read(r).some(e => e.state.read(r) === WorkingSetEntryState.Modified)) { + resolve(undefined); + } + })); + }); - if (session.hunkData.pending === 0) { - // no real changes, just cancel - this.cancelSession(); - } + await raceCancellation(whenDecided, token); + + store.dispose(); - const dispo = token.onCancellationRequested(() => this.cancelSession()); - await raceCancellation(run, token); - dispo.dispose(); return true; } } diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts index c8a3d8b07dd9..ce0f83a212f5 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts @@ -28,6 +28,7 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; import { Emitter, Event } from '../../../../base/common/event.js'; +import { autorun, autorunWithStore } from '../../../../base/common/observable.js'; export const IQuickDiffModelService = createDecorator('IQuickDiffModelService'); @@ -155,7 +156,18 @@ export class QuickDiffModel extends Disposable { })); this._register(this.quickDiffService.onDidChangeQuickDiffProviders(() => this.triggerDiff())); - this._register(this._chatEditingService.onDidChangeEditingSession(() => this.triggerDiff())); + + this._register(autorunWithStore((r, store) => { + for (const session of this._chatEditingService.editingSessionsObs.read(r)) { + store.add(autorun(r => { + for (const entry of session.entries.read(r)) { + entry.state.read(r); // signal + } + this.triggerDiff(); + })); + } + })); + this.triggerDiff(); } @@ -344,9 +356,10 @@ export class QuickDiffModel extends Disposable { } const uri = this._model.resource; - const session = this._chatEditingService.currentEditingSession; - if (session && session.getEntry(uri)?.state.get() === WorkingSetEntryState.Modified) { - // disable dirty diff when doing chat edits + // disable dirty diff when doing chat edits + const isBeingModifiedByChatEdits = this._chatEditingService.editingSessionsObs.get() + .some(session => session.getEntry(uri)?.state.get() === WorkingSetEntryState.Modified); + if (isBeingModifiedByChatEdits) { return Promise.resolve([]); } From d720132ea3be1059aacc9c66360a92fdaf665e40 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:00:01 +0100 Subject: [PATCH 0559/3587] Disable pointer events for NES control to allow word selection (#237946) fixes https://github.com/microsoft/vscode-copilot/issues/11649 --- .../browser/view/inlineEdits/wordReplacementView.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index 9797502d7b03..2fdb445f9838 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -120,6 +120,7 @@ export class WordReplacementView extends Disposable { borderRadius: '4px', boxSizing: 'border-box', background: 'var(--vscode-inlineEdit-originalChangedTextBackground)', + pointerEvents: 'none', } }, []), n.div({ @@ -131,6 +132,7 @@ export class WordReplacementView extends Disposable { border: '1px solid var(--vscode-editorHoverWidget-border)', //background: 'rgba(122, 122, 122, 0.12)', looks better background: 'var(--vscode-inlineEdit-wordReplacementView-background)', + pointerEvents: 'none', } }, []), From 71ac7457c767c43e7e0ee86a8f5e319a3e32bb8f Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:05:56 +0100 Subject: [PATCH 0560/3587] Fix halo effect removal in inline completions (#237881) * fix halo effect removal * :lipstick: --- .../browser/model/inlineCompletionsModel.ts | 9 ++++----- .../browser/view/inlineEdits/sideBySideDiff.ts | 6 +++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index d1e6cbd30045..2d39e288ecc7 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -56,7 +56,8 @@ export class InlineCompletionsModel extends Disposable { private readonly _editorObs = observableCodeEditor(this._editor); private readonly _acceptCompletionDecorationTimer = this._register(new MutableDisposable()); - private readonly _acceptCompletionDecoration: IModelDecorationOptions = { + private readonly _acceptCompletionDecorationCollection = this._editor.createDecorationsCollection(); + private readonly _acceptCompletionDecorationOptions: IModelDecorationOptions = { description: 'inline-completion-accepted', className: 'inline-completion-accepted', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges @@ -581,8 +582,6 @@ export class InlineCompletionsModel extends Disposable { completion.source.addRef(); } - this._acceptCompletionDecorationTimer.clear(); - editor.pushUndoStop(); if (completion.snippetInfo) { editor.executeEdits( @@ -605,8 +604,8 @@ export class InlineCompletionsModel extends Disposable { editor.setSelections(selections, 'inlineCompletionAccept'); if (state.kind === 'inlineEdit') { - const acceptEditsDecorations = editor.createDecorationsCollection(modifiedRanges.map(r => ({ range: r, options: this._acceptCompletionDecoration }))); - this._acceptCompletionDecorationTimer.value = disposableTimeout(() => acceptEditsDecorations.clear(), 2500); + this._acceptCompletionDecorationCollection.set(modifiedRanges.map(r => ({ range: r, options: this._acceptCompletionDecorationOptions }))); + this._acceptCompletionDecorationTimer.value = disposableTimeout(() => this._acceptCompletionDecorationCollection.clear(), 2500); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index 67c8dfb38fa5..1894f06327bd 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -99,12 +99,12 @@ export const modifiedBorder = registerColor( export const acceptedDecorationBackgroundColor = registerColor( 'inlineEdit.acceptedBackground', { - light: transparent(modifiedChangedTextOverlayColor, 0.5), - dark: transparent(modifiedChangedTextOverlayColor, 0.5), + light: transparent(modifiedChangedTextOverlayColor, 0.75), + dark: transparent(modifiedChangedTextOverlayColor, 0.75), hcDark: modifiedChangedTextOverlayColor, hcLight: modifiedChangedTextOverlayColor }, - localize('inlineEdit.acceptedBackground', 'Background color for the accepted text after appying an inline edit.'), + localize('inlineEdit.acceptedBackground', 'Background color for the accepted text after applying an inline edit.'), true ); From 5ba4503b8b5e9a4e92b4df90a55a6a7b91c31c15 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 15 Jan 2025 12:09:46 +0100 Subject: [PATCH 0561/3587] mark MappedEditsProvider as deprecated (#237950) --- .../vscode.proposed.mappedEditsProvider.d.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts index fb10b6a3385c..b677ab05a49f 100644 --- a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts @@ -5,18 +5,27 @@ declare module 'vscode' { + /** + * @deprecated Part of MappedEditsProvider, use `MappedEditsProvider2` instead. + */ export interface DocumentContextItem { readonly uri: Uri; readonly version: number; readonly ranges: Range[]; } + /** + * @deprecated Part of MappedEditsProvider, use `MappedEditsProvider2` instead. + */ export interface ConversationRequest { // eslint-disable-next-line local/vscode-dts-string-type-literals readonly type: 'request'; readonly message: string; } + /** + * @deprecated Part of MappedEditsProvider, use `MappedEditsProvider2` instead. + */ export interface ConversationResponse { // eslint-disable-next-line local/vscode-dts-string-type-literals readonly type: 'response'; @@ -25,6 +34,9 @@ declare module 'vscode' { readonly references?: DocumentContextItem[]; } + /** + * @deprecated Part of MappedEditsProvider, use `MappedEditsProvider2` instead. + */ export interface MappedEditsContext { readonly documents: DocumentContextItem[][]; /** @@ -36,6 +48,7 @@ declare module 'vscode' { /** * Interface for providing mapped edits for a given document. + * @deprecated Use `MappedEditsProvider2` instead. */ export interface MappedEditsProvider { /** @@ -55,6 +68,9 @@ declare module 'vscode' { ): ProviderResult; } + /** + * Interface for providing mapped edits for a given document. + */ export interface MappedEditsRequest { readonly codeBlocks: { code: string; resource: Uri; markdownBeforeBlock?: string }[]; } @@ -79,6 +95,9 @@ declare module 'vscode' { } namespace chat { + /** + * @deprecated Use `MappedEditsProvider2` instead. + */ export function registerMappedEditsProvider(documentSelector: DocumentSelector, provider: MappedEditsProvider): Disposable; export function registerMappedEditsProvider2(provider: MappedEditsProvider2): Disposable; From 588ec342a0ce15f8f049d66b41d6970a0ad46432 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 15 Jan 2025 13:42:31 +0100 Subject: [PATCH 0562/3587] fix filtering compund logs (#237955) --- src/vs/workbench/contrib/output/browser/outputView.ts | 7 +++---- .../workbench/contrib/output/common/outputChannelModel.ts | 5 +++-- src/vs/workbench/services/output/common/output.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index b7a82fdb378b..00b34b22af79 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -13,7 +13,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { IContextKeyService, IContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IEditorOpenContext } from '../../../common/editor.js'; import { AbstractTextResourceEditor } from '../../../browser/parts/editor/textResourceEditor.js'; -import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT, LOG_ENTRY_REGEX, parseLogEntries, ILogEntry } from '../../../services/output/common/output.js'; +import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT, parseLogEntries, ILogEntry, parseLogEntryAt } from '../../../services/output/common/output.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; @@ -397,8 +397,7 @@ export class FilterController extends Disposable implements IEditorContribution private computeLogEntries(model: ITextModel): void { this.logEntries = undefined; - const firstLine = model.getLineContent(1); - if (!LOG_ENTRY_REGEX.test(firstLine)) { + if (!parseLogEntryAt(model, 1)) { return; } @@ -423,7 +422,7 @@ export class FilterController extends Disposable implements IEditorContribution const findMatchesDecorations: IModelDeltaDecoration[] = []; if (this.logEntries) { - const hasLogLevelFilter = !filters.trace || !filters.debug || !filters.info || !filters.warning || filters.error; + const hasLogLevelFilter = !filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error; if (hasLogLevelFilter || filters.text) { for (let i = from; i < this.logEntries.length; i++) { const entry = this.logEntries[i]; diff --git a/src/vs/workbench/contrib/output/common/outputChannelModel.ts b/src/vs/workbench/contrib/output/common/outputChannelModel.ts index 125f6494c095..156e39095444 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModel.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModel.ts @@ -21,7 +21,7 @@ import { Range } from '../../../../editor/common/core/range.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { ILogger, ILoggerService, ILogService } from '../../../../platform/log/common/log.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { LOG_ENTRY_REGEX, LOG_MIME, OutputChannelUpdateMode, parseLogEntryAt } from '../../../services/output/common/output.js'; +import { LOG_MIME, OutputChannelUpdateMode, parseLogEntryAt } from '../../../services/output/common/output.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { TextModel } from '../../../../editor/common/model/textModel.js'; @@ -214,7 +214,8 @@ class MultiFileContentProvider extends Disposable implements IContentProvider { continue; } lineNumber = logEntry.range.endLineNumber; - const content = model.getValueInRange(logEntry.range).replace(LOG_ENTRY_REGEX, `$1 [${name}] $2`); + const lineContent = model.getLineContent(lineNumber); + const content = `${lineContent.substring(0, logEntry.timestampRange.endColumn - 1)} [${name}]${lineContent.substring(logEntry.timestampRange.endColumn - 1)}`; logEntries.push([logEntry.timestamp, content]); } } diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index addfef6e93d7..719ad37e79cd 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -52,8 +52,6 @@ export const SHOW_WARNING_FILTER_CONTEXT = new RawContextKey('output.fi export const SHOW_ERROR_FILTER_CONTEXT = new RawContextKey('output.filter.error', true); export const OUTPUT_FILTER_FOCUS_CONTEXT = new RawContextKey('outputFilterFocus', false); -export const LOG_ENTRY_REGEX = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) (\[(info|trace|debug|error|warning)\])/; - export interface IOutputViewFilters { readonly onDidChange: Event; text: string; @@ -251,6 +249,8 @@ class OutputChannelRegistry implements IOutputChannelRegistry { Registry.add(Extensions.OutputChannels, new OutputChannelRegistry()); +const LOG_ENTRY_REGEX = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s(?:\[(?!info|trace|debug|error|warning).*?\]\s)?(\[(info|trace|debug|error|warning)\])/; + export interface ILogEntry { readonly timestamp: number; readonly logLevel: LogLevel; From 96e03e0d94694e41823cb4e4ddbe31397d621374 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 15 Jan 2025 14:43:17 +0100 Subject: [PATCH 0563/3587] Revert "Engineering - disable binskim (#237562)" (#237959) This reverts commit b7b6f26c6214e93095ab090b27cb069a85adc276. --- build/azure-pipelines/product-build.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 94da67c3ff82..39075d822831 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -178,9 +178,6 @@ extends: template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines parameters: sdl: - binskim: - enabled: false - justificationForDisabling: "BinSkim rebaselining is failing" tsa: enabled: true configFile: $(Build.SourcesDirectory)/build/azure-pipelines/config/tsaoptions.json From 57e8c28877f99a6cd8c02dd66484e04701ddc678 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:30:43 +0100 Subject: [PATCH 0564/3587] GitHub - link provider for various hovers (#237961) * Initial implementation * Refactor code, add link to blame decoration * Add links to timeline hover * Saving my work * Update remote order for "Open on GitHub" action * Bug fixes * Add link provider for graph hover * Rename method --- extensions/git-base/src/api/api1.ts | 8 +++-- extensions/git-base/src/api/git-base.d.ts | 4 ++- extensions/git-base/src/remoteSource.ts | 22 +++++++++--- extensions/git/src/blame.ts | 16 +++++---- extensions/git/src/historyProvider.ts | 24 +++++++++---- extensions/git/src/remoteSource.ts | 35 +++++++++++++++++-- extensions/git/src/timelineProvider.ts | 22 ++++++------ extensions/git/src/typings/git-base.d.ts | 3 +- extensions/github/src/commands.ts | 8 +++-- extensions/github/src/remoteSourceProvider.ts | 30 ++++++++++++++-- extensions/github/src/typings/git-base.d.ts | 4 ++- extensions/github/src/util.ts | 2 ++ src/vs/workbench/api/browser/mainThreadSCM.ts | 6 +--- .../workbench/api/common/extHost.protocol.ts | 1 + .../vscode.proposed.scmHistoryProvider.d.ts | 1 + 15 files changed, 141 insertions(+), 45 deletions(-) diff --git a/extensions/git-base/src/api/api1.ts b/extensions/git-base/src/api/api1.ts index 74edc7f4452c..2c6689452111 100644 --- a/extensions/git-base/src/api/api1.ts +++ b/extensions/git-base/src/api/api1.ts @@ -5,7 +5,7 @@ import { Command, Disposable, commands } from 'vscode'; import { Model } from '../model'; -import { getRemoteSourceActions, getRemoteSourceControlHistoryItemCommands, pickRemoteSource } from '../remoteSource'; +import { getRemoteSourceActions, getRemoteSourceControlHistoryItemCommands, pickRemoteSource, provideRemoteSourceLinks } from '../remoteSource'; import { GitBaseExtensionImpl } from './extension'; import { API, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction, RemoteSourceProvider } from './git-base'; @@ -21,10 +21,14 @@ export class ApiImpl implements API { return getRemoteSourceActions(this._model, url); } - getRemoteSourceControlHistoryItemCommands(url: string): Promise { + getRemoteSourceControlHistoryItemCommands(url: string): Promise { return getRemoteSourceControlHistoryItemCommands(this._model, url); } + provideRemoteSourceLinks(url: string, content: string): Promise { + return provideRemoteSourceLinks(this._model, url, content); + } + registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable { return this._model.registerRemoteSourceProvider(provider); } diff --git a/extensions/git-base/src/api/git-base.d.ts b/extensions/git-base/src/api/git-base.d.ts index 37dd2c4229c4..540aa5831460 100644 --- a/extensions/git-base/src/api/git-base.d.ts +++ b/extensions/git-base/src/api/git-base.d.ts @@ -9,7 +9,8 @@ export { ProviderResult } from 'vscode'; export interface API { registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; getRemoteSourceActions(url: string): Promise; - getRemoteSourceControlHistoryItemCommands(url: string): Promise; + getRemoteSourceControlHistoryItemCommands(url: string): Promise; + provideRemoteSourceLinks(url: string, content: string): Promise; pickRemoteSource(options: PickRemoteSourceOptions): Promise; } @@ -85,4 +86,5 @@ export interface RemoteSourceProvider { getRemoteSourceControlHistoryItemCommands?(url: string): ProviderResult; getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; + provideRemoteSourceLinks?(url: string, content: string): Promise; } diff --git a/extensions/git-base/src/remoteSource.ts b/extensions/git-base/src/remoteSource.ts index 8d8d4ab102f5..cf570e3aa006 100644 --- a/extensions/git-base/src/remoteSource.ts +++ b/extensions/git-base/src/remoteSource.ts @@ -123,18 +123,30 @@ export async function getRemoteSourceActions(model: Model, url: string): Promise return remoteSourceActions; } -export async function getRemoteSourceControlHistoryItemCommands(model: Model, url: string): Promise { +export async function getRemoteSourceControlHistoryItemCommands(model: Model, url: string): Promise { const providers = model.getRemoteProviders(); const remoteSourceCommands = []; for (const provider of providers) { - const providerCommands = await provider.getRemoteSourceControlHistoryItemCommands?.(url); - if (providerCommands?.length) { - remoteSourceCommands.push(...providerCommands); + remoteSourceCommands.push(...(await provider.getRemoteSourceControlHistoryItemCommands?.(url) ?? [])); + } + + return remoteSourceCommands.length > 0 ? remoteSourceCommands : undefined; +} + +export async function provideRemoteSourceLinks(model: Model, url: string, content: string): Promise { + const providers = model.getRemoteProviders(); + + for (const provider of providers) { + const parsedContent = await provider.provideRemoteSourceLinks?.(url, content); + if (!parsedContent) { + continue; } + + content = parsedContent; } - return remoteSourceCommands; + return content; } export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise; diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 9ab8e3e58bd8..6f9d0cc88e4f 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -12,7 +12,7 @@ import { BlameInformation, Commit } from './git'; import { fromGitUri, isGitUri } from './uri'; import { emojify, ensureEmojis } from './emoji'; import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation } from './staging'; -import { getRemoteSourceControlHistoryItemCommands } from './remoteSource'; +import { getRemoteSourceControlHistoryItemCommands, provideRemoteSourceLinks } from './remoteSource'; function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean { return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive); @@ -205,6 +205,7 @@ export class GitBlameController { async getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation, includeCommitDetails = false): Promise { let commitInformation: Commit | undefined; + let commitMessageWithLinks: string | undefined; const remoteSourceCommands: Command[] = []; const repository = this._model.getRepository(documentUri); @@ -217,12 +218,15 @@ export class GitBlameController { } // Remote commands - const defaultRemote = repository.getDefaultRemote(); const unpublishedCommits = await repository.getUnpublishedCommits(); - - if (defaultRemote?.fetchUrl && !unpublishedCommits.has(blameInformation.hash)) { - remoteSourceCommands.push(...await getRemoteSourceControlHistoryItemCommands(defaultRemote.fetchUrl)); + if (!unpublishedCommits.has(blameInformation.hash)) { + remoteSourceCommands.push(...await getRemoteSourceControlHistoryItemCommands(repository)); } + + // Link provider + commitMessageWithLinks = await provideRemoteSourceLinks( + repository, + commitInformation?.message ?? blameInformation.subject ?? ''); } const markdownString = new MarkdownString(); @@ -254,7 +258,7 @@ export class GitBlameController { } // Subject | Message - markdownString.appendMarkdown(`${emojify(commitInformation?.message ?? blameInformation.subject ?? '')}\n\n`); + markdownString.appendMarkdown(`${emojify(commitMessageWithLinks ?? commitInformation?.message ?? blameInformation.subject ?? '')}\n\n`); markdownString.appendMarkdown(`---\n\n`); // Short stats diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 6d65c5226b67..bda106f46d58 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -12,6 +12,7 @@ import { Branch, LogOptions, Ref, RefType } from './api/git'; import { emojify, ensureEmojis } from './emoji'; import { Commit } from './git'; import { OperationKind, OperationResult } from './operation'; +import { provideRemoteSourceLinks } from './remoteSource'; function toSourceControlHistoryItemRef(repository: Repository, ref: Ref): SourceControlHistoryItemRef { const rootUri = Uri.file(repository.root); @@ -264,22 +265,33 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec await ensureEmojis(); - return commits.map(commit => { + const historyItems: SourceControlHistoryItem[] = []; + for (const commit of commits) { + const message = emojify(commit.message); + const messageWithLinks = await provideRemoteSourceLinks(this.repository, message) ?? message; + + const newLineIndex = message.indexOf('\n'); + const subject = newLineIndex !== -1 + ? `${message.substring(0, newLineIndex)}\u2026` + : message; + const references = this._resolveHistoryItemRefs(commit); - return { + historyItems.push({ id: commit.hash, parentIds: commit.parents, - message: emojify(commit.message), + subject, + message: messageWithLinks, author: commit.authorName, authorEmail: commit.authorEmail, - icon: new ThemeIcon('git-commit'), displayId: getCommitShortHash(Uri.file(this.repository.root), commit.hash), timestamp: commit.authorDate?.getTime(), statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, references: references.length !== 0 ? references : undefined - }; - }); + } satisfies SourceControlHistoryItem); + } + + return historyItems; } catch (err) { this.logger.error(`[GitHistoryProvider][provideHistoryItems] Failed to get history items with options '${JSON.stringify(options)}': ${err}`); return []; diff --git a/extensions/git/src/remoteSource.ts b/extensions/git/src/remoteSource.ts index dfdb36fc11f5..86daccedf952 100644 --- a/extensions/git/src/remoteSource.ts +++ b/extensions/git/src/remoteSource.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Command } from 'vscode'; import { PickRemoteSourceOptions, PickRemoteSourceResult } from './typings/git-base'; import { GitBaseApi } from './git-base'; +import { Repository } from './repository'; export async function pickRemoteSource(options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise; export async function pickRemoteSource(options: PickRemoteSourceOptions & { branch: true }): Promise; @@ -16,6 +18,35 @@ export async function getRemoteSourceActions(url: string) { return GitBaseApi.getAPI().getRemoteSourceActions(url); } -export async function getRemoteSourceControlHistoryItemCommands(url: string) { - return GitBaseApi.getAPI().getRemoteSourceControlHistoryItemCommands(url); +export async function getRemoteSourceControlHistoryItemCommands(repository: Repository): Promise { + if (repository.remotes.length === 0) { + return []; + } + + const getCommands = async (repository: Repository, remoteName: string): Promise => { + const remote = repository.remotes.find(r => r.name === remoteName && r.fetchUrl); + return remote ? GitBaseApi.getAPI().getRemoteSourceControlHistoryItemCommands(remote.fetchUrl!) : undefined; + }; + + // upstream -> origin -> first + return await getCommands(repository, 'upstream') + ?? await getCommands(repository, 'origin') + ?? await getCommands(repository, repository.remotes[0].name) + ?? []; +} + +export async function provideRemoteSourceLinks(repository: Repository, content: string): Promise { + if (repository.remotes.length === 0) { + return undefined; + } + + const getDocumentLinks = async (repository: Repository, remoteName: string): Promise => { + const remote = repository.remotes.find(r => r.name === remoteName && r.fetchUrl); + return remote ? GitBaseApi.getAPI().provideRemoteSourceLinks(remote.fetchUrl!, content) : undefined; + }; + + // upstream -> origin -> first + return await getDocumentLinks(repository, 'upstream') + ?? await getDocumentLinks(repository, 'origin') + ?? await getDocumentLinks(repository, repository.remotes[0].name); } diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index b243d72ed4be..90d202cc4152 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -12,7 +12,7 @@ import { CommandCenter } from './commands'; import { OperationKind, OperationResult } from './operation'; import { getCommitShortHash } from './util'; import { CommitShortStat } from './git'; -import { getRemoteSourceControlHistoryItemCommands } from './remoteSource'; +import { getRemoteSourceControlHistoryItemCommands, provideRemoteSourceLinks } from './remoteSource'; export class GitTimelineItem extends TimelineItem { static is(item: TimelineItem): item is GitTimelineItem { @@ -215,25 +215,27 @@ export class GitTimelineProvider implements TimelineProvider { const openComparison = l10n.t('Open Comparison'); - const defaultRemote = repo.getDefaultRemote(); const unpublishedCommits = await repo.getUnpublishedCommits(); - const remoteSourceCommands: Command[] = defaultRemote?.fetchUrl - ? await getRemoteSourceControlHistoryItemCommands(defaultRemote.fetchUrl) - : []; + const remoteSourceCommands = await getRemoteSourceControlHistoryItemCommands(repo); + + const items: GitTimelineItem[] = []; + for (let index = 0; index < commits.length; index++) { + const c = commits[index]; - const items = commits.map((c, i) => { const date = dateType === 'authored' ? c.authorDate : c.commitDate; const message = emojify(c.message); - const item = new GitTimelineItem(c.hash, commits[i + 1]?.hash ?? `${c.hash}^`, message, date?.getTime() ?? 0, c.hash, 'git:file:commit'); + const item = new GitTimelineItem(c.hash, commits[index + 1]?.hash ?? `${c.hash}^`, message, date?.getTime() ?? 0, c.hash, 'git:file:commit'); item.iconPath = new ThemeIcon('git-commit'); if (showAuthor) { item.description = c.authorName; } const commitRemoteSourceCommands = !unpublishedCommits.has(c.hash) ? remoteSourceCommands : []; - item.setItemDetails(uri, c.hash, c.authorName!, c.authorEmail, dateFormatter.format(date), message, c.shortStat, commitRemoteSourceCommands); + const messageWithLinks = await provideRemoteSourceLinks(repo, message) ?? message; + + item.setItemDetails(uri, c.hash, c.authorName!, c.authorEmail, dateFormatter.format(date), messageWithLinks, c.shortStat, commitRemoteSourceCommands); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { @@ -244,8 +246,8 @@ export class GitTimelineProvider implements TimelineProvider { }; } - return item; - }); + items.push(item); + } if (options.cursor === undefined) { const you = l10n.t('You'); diff --git a/extensions/git/src/typings/git-base.d.ts b/extensions/git/src/typings/git-base.d.ts index 37dd2c4229c4..3b61341d8063 100644 --- a/extensions/git/src/typings/git-base.d.ts +++ b/extensions/git/src/typings/git-base.d.ts @@ -9,8 +9,9 @@ export { ProviderResult } from 'vscode'; export interface API { registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; getRemoteSourceActions(url: string): Promise; - getRemoteSourceControlHistoryItemCommands(url: string): Promise; + getRemoteSourceControlHistoryItemCommands(url: string): Promise; pickRemoteSource(options: PickRemoteSourceOptions): Promise; + provideRemoteSourceLinks(url: string, content: string): Promise; } export interface GitBaseExtension { diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 463b91c19727..47b6aea454b3 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -85,10 +85,12 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { return; } - // Default remote (origin, or the first remote) - const defaultRemote = remotes.find(r => r.name === 'origin') ?? remotes[0]; + // Default remote (upstream -> origin -> first) + const remote = remotes.find(r => r.name === 'upstream') + ?? remotes.find(r => r.name === 'origin') + ?? remotes[0]; - const link = getCommitLink(defaultRemote.fetchUrl!, historyItem.id); + const link = getCommitLink(remote.fetchUrl!, historyItem.id); vscode.env.openExternal(vscode.Uri.parse(link)); })); diff --git a/extensions/github/src/remoteSourceProvider.ts b/extensions/github/src/remoteSourceProvider.ts index 0c2ef1668327..e79931c74155 100644 --- a/extensions/github/src/remoteSourceProvider.ts +++ b/extensions/github/src/remoteSourceProvider.ts @@ -7,7 +7,7 @@ import { Command, Uri, env, l10n, workspace } from 'vscode'; import { RemoteSourceProvider, RemoteSource, RemoteSourceAction } from './typings/git-base'; import { getOctokit } from './auth'; import { Octokit } from '@octokit/rest'; -import { getRepositoryFromQuery, getRepositoryFromUrl } from './util'; +import { getRepositoryFromQuery, getRepositoryFromUrl, ISSUE_EXPRESSION } from './util'; import { getBranchLink, getVscodeDevHost } from './links'; function asRemoteSource(raw: any): RemoteSource { @@ -137,10 +137,10 @@ export class GithubRemoteSourceProvider implements RemoteSourceProvider { }]; } - async getRemoteSourceControlHistoryItemCommands(url: string): Promise { + async getRemoteSourceControlHistoryItemCommands(url: string): Promise { const repository = getRepositoryFromUrl(url); if (!repository) { - return []; + return undefined; } return [{ @@ -150,4 +150,28 @@ export class GithubRemoteSourceProvider implements RemoteSourceProvider { arguments: [url] }]; } + + provideRemoteSourceLinks(url: string, content: string): string | undefined { + const repository = getRepositoryFromUrl(url); + if (!repository) { + return undefined; + } + + return content.replace( + ISSUE_EXPRESSION, + (match, _group1, owner: string | undefined, repo: string | undefined, _group2, number: string | undefined) => { + if (!number || Number.isNaN(parseInt(number))) { + return match; + } + + const label = owner && repo + ? `${owner}/${repo}#${number}` + : `#${number}`; + + owner = owner ?? repository.owner; + repo = repo ?? repository.repo; + + return `[${label}](https://github.com/${owner}/${repo}/issues/${number})`; + }); + } } diff --git a/extensions/github/src/typings/git-base.d.ts b/extensions/github/src/typings/git-base.d.ts index 37dd2c4229c4..548369b1f0f2 100644 --- a/extensions/github/src/typings/git-base.d.ts +++ b/extensions/github/src/typings/git-base.d.ts @@ -9,8 +9,9 @@ export { ProviderResult } from 'vscode'; export interface API { registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; getRemoteSourceActions(url: string): Promise; - getRemoteSourceControlHistoryItemCommands(url: string): Promise; + getRemoteSourceControlHistoryItemCommands(url: string): Promise; pickRemoteSource(options: PickRemoteSourceOptions): Promise; + provideRemoteSourceLinks(url: string, content: string): ProviderResult; } export interface GitBaseExtension { @@ -85,4 +86,5 @@ export interface RemoteSourceProvider { getRemoteSourceControlHistoryItemCommands?(url: string): ProviderResult; getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; + provideRemoteSourceLinks?(url: string, content: string): ProviderResult; } diff --git a/extensions/github/src/util.ts b/extensions/github/src/util.ts index 3d8bf4a40bef..5289bb931814 100644 --- a/extensions/github/src/util.ts +++ b/extensions/github/src/util.ts @@ -37,3 +37,5 @@ export function getRepositoryFromQuery(query: string): { owner: string; repo: st export function repositoryHasGitHubRemote(repository: Repository) { return !!repository.state.remotes.find(remote => remote.fetchUrl ? getRepositoryFromUrl(remote.fetchUrl) : undefined); } + +export const ISSUE_EXPRESSION = /(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/g; diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 5b22d1f069fd..2157d1ef5775 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -49,11 +49,7 @@ function toISCMHistoryItem(historyItemDto: SCMHistoryItemDto): ISCMHistoryItem { ...r, icon: getIconFromIconDto(r.icon) })); - const newLineIndex = historyItemDto.message.indexOf('\n'); - const subject = newLineIndex === -1 ? - historyItemDto.message : `${historyItemDto.message.substring(0, newLineIndex)}\u2026`; - - return { ...historyItemDto, subject, references }; + return { ...historyItemDto, references }; } function toISCMHistoryItemRef(historyItemRefDto?: SCMHistoryItemRefDto, color?: ColorIdentifier): ISCMHistoryItemRef | undefined { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index e8de9400531e..478f2d6669cb 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1603,6 +1603,7 @@ export interface SCMHistoryItemRefsChangeEventDto { export interface SCMHistoryItemDto { readonly id: string; readonly parentIds: string[]; + readonly subject: string; readonly message: string; readonly displayId?: string; readonly author?: string; diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index 8ab6b7cbd665..0c888bdfa9f6 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -48,6 +48,7 @@ declare module 'vscode' { export interface SourceControlHistoryItem { readonly id: string; readonly parentIds: string[]; + readonly subject: string; readonly message: string; readonly displayId?: string; readonly author?: string; From aee2a11cd9221853526a0037b0cd442c81825886 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 15 Jan 2025 07:32:45 -0800 Subject: [PATCH 0565/3587] refactor prompt syntax related code (#237938) * [refactor]: add II of `PromptContentsProviderBase` and the `cancelPreviousCalls` decorator * [refactor]: add II of `FilePromptContentsProvider` and `TextModelPromptContentsProvider`, improve docs * [refactor]: add unit tests for the `Decoder.transform()` utility * [refactor]: working on unit tests for the `FilePromptContentsProvider` * [refactor]: add `TextModelPromptParser`, fix unit tests for `PromptFileReference` * [refactor]: cleanup * [refactor]: simplify `prompt parsers` by removeing the `PromptLine` abstraction * [refactoring]: improve `prompt syntax` related files structure --- src/vs/base/common/codecs/baseDecoder.ts | 14 +- src/vs/base/common/decorators.ts | 2 + .../common/decorators/cancelPreviousCalls.ts | 168 +++++++ src/vs/base/common/trackedDisposable.ts | 37 ++ .../test/common/cancelPreviousCalls.test.ts | 303 ++++++++++++ src/vs/base/test/common/testUtils.ts | 33 ++ .../markdownCodec/tokens/markdownLink.ts | 13 + .../test/common/codecs/linesDecoder.test.ts | 4 +- .../editor/test/common/utils/testDecoder.ts | 14 +- .../chatInstructionsAttachment.ts | 48 +- .../contrib/chat/browser/chatInputPart.ts | 2 +- .../browser/contrib/chatDynamicVariables.ts | 8 +- .../chatDynamicVariables/chatFileReference.ts | 14 +- .../chat/common/promptFileReference.ts | 438 ------------------ .../chat/common/promptFileReferenceErrors.ts | 48 +- .../codecs}/chatPromptCodec.ts | 0 .../codecs}/chatPromptDecoder.ts | 0 .../codecs}/tokens/fileReference.ts | 0 .../filePromptContentsProvider.ts | 97 ++++ .../promptContentsProviderBase.ts | 154 ++++++ .../textModelContentsProvider.ts | 86 ++++ .../promptSyntax/contentProviders/types.d.ts | 35 ++ .../promptSyntax/parsers/basePromptParser.ts | 413 +++++++++++++++++ .../promptSyntax/parsers/filePromptParser.ts | 35 ++ .../parsers/textModelPromptParser.ts | 35 ++ .../common/promptSyntax/parsers/types.d.ts | 68 +++ .../codecs/chatPromptCodec.test.ts | 16 +- .../codecs/chatPromptDecoder.test.ts | 16 +- .../filePromptContentsProvider.test.ts | 102 ++++ .../promptFileReference.test.ts | 303 ++++++------ 30 files changed, 1866 insertions(+), 640 deletions(-) create mode 100644 src/vs/base/common/decorators/cancelPreviousCalls.ts create mode 100644 src/vs/base/common/trackedDisposable.ts create mode 100644 src/vs/base/test/common/cancelPreviousCalls.test.ts delete mode 100644 src/vs/workbench/contrib/chat/common/promptFileReference.ts rename src/vs/workbench/contrib/chat/common/{codecs/chatPromptCodec => promptSyntax/codecs}/chatPromptCodec.ts (100%) rename src/vs/workbench/contrib/chat/common/{codecs/chatPromptCodec => promptSyntax/codecs}/chatPromptDecoder.ts (100%) rename src/vs/workbench/contrib/chat/common/{codecs/chatPromptCodec => promptSyntax/codecs}/tokens/fileReference.ts (100%) create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.d.ts create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts rename src/vs/workbench/contrib/chat/test/common/{ => promptSyntax}/codecs/chatPromptCodec.test.ts (75%) rename src/vs/workbench/contrib/chat/test/common/{ => promptSyntax}/codecs/chatPromptDecoder.test.ts (76%) create mode 100644 src/vs/workbench/contrib/chat/test/common/promptSyntax/contentProviders/filePromptContentsProvider.test.ts rename src/vs/workbench/contrib/chat/test/common/{ => promptSyntax}/promptFileReference.test.ts (60%) diff --git a/src/vs/base/common/codecs/baseDecoder.ts b/src/vs/base/common/codecs/baseDecoder.ts index 5404e3df0bf4..f28ab6c7d2c1 100644 --- a/src/vs/base/common/codecs/baseDecoder.ts +++ b/src/vs/base/common/codecs/baseDecoder.ts @@ -5,9 +5,10 @@ import { assert } from '../assert.js'; import { Emitter } from '../event.js'; +import { IDisposable } from '../lifecycle.js'; import { ReadableStream } from '../stream.js'; import { AsyncDecoder } from './asyncDecoder.js'; -import { Disposable, IDisposable } from '../lifecycle.js'; +import { TrackedDisposable } from '../trackedDisposable.js'; /** * Event names of {@link ReadableStream} stream. @@ -23,7 +24,7 @@ export type TStreamListenerNames = 'data' | 'error' | 'end'; export abstract class BaseDecoder< T extends NonNullable, K extends NonNullable = NonNullable, -> extends Disposable implements ReadableStream { +> extends TrackedDisposable implements ReadableStream { /** * Flag that indicates if the decoder stream has ended. */ @@ -67,6 +68,11 @@ export abstract class BaseDecoder< 'Cannot start stream that has already ended.', ); + assert( + !this.disposed, + 'Cannot start stream that has already disposed.', + ); + this.stream.on('data', this.tryOnStreamData); this.stream.on('error', this.onStreamError); this.stream.on('end', this.onStreamEnd); @@ -313,6 +319,10 @@ export abstract class BaseDecoder< } public override dispose(): void { + if (this.disposed) { + return; + } + this.onStreamEnd(); this.stream.destroy(); diff --git a/src/vs/base/common/decorators.ts b/src/vs/base/common/decorators.ts index 9a40158d9e6a..0f02b3ede2ca 100644 --- a/src/vs/base/common/decorators.ts +++ b/src/vs/base/common/decorators.ts @@ -127,3 +127,5 @@ export function throttle(delay: number, reducer?: IDebounceReducer, initia }; }); } + +export { cancelPreviousCalls } from './decorators/cancelPreviousCalls.js'; diff --git a/src/vs/base/common/decorators/cancelPreviousCalls.ts b/src/vs/base/common/decorators/cancelPreviousCalls.ts new file mode 100644 index 000000000000..b0a3089bc31a --- /dev/null +++ b/src/vs/base/common/decorators/cancelPreviousCalls.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assertDefined } from '../types.js'; +import { Disposable, DisposableMap } from '../lifecycle.js'; +import { CancellationTokenSource, CancellationToken } from '../cancellation.js'; + +/** + * Helper type that represents a function that has an optional {@linkcode CancellationToken} + * argument argument at the end of the arguments list. + * + * @typeparam `TFunction` - Type of the function arguments list of which will be extended + * with an optional {@linkcode CancellationToken} argument. + */ +type TWithOptionalCancellationToken = TFunction extends (...args: infer TArgs) => infer TReturn + ? (...args: [...TArgs, cancellatioNToken?: CancellationToken]) => TReturn + : never; + +/** + * Decorator that provides a mechanism to cancel previous calls of the decorated method + * by providing a `cancellation token` as the last argument of the method, which gets + * cancelled immediately on subsequent call of the decorated method. + * + * Therefore to use this decorator, the two conditions must be met: + * + * - the decorated method must have an *optional* {@linkcode CancellationToken} argument at + * the end of the arguments list + * - the object that the decorated method belongs to must implement the {@linkcode Disposable}; + * this requirement comes from the internal implementation of the decorator that + * creates new resources that need to be eventually disposed by someone + * + * @typeparam `TObject` - Object type that the decorated method belongs to. + * @typeparam `TArgs` - Argument list of the decorated method. + * @typeparam `TReturn` - Return value type of the decorated method. + * + * ### Examples + * + * ```typescript + * // let's say we have a class that implements the `Disposable` interface that we want + * // to use the decorator on + * class Example extends Disposable { + * async doSomethingAsync(arg1: number, arg2: string): Promise { + * // do something async.. + * await new Promise(resolve => setTimeout(resolve, 1000)); + * } + * } + * ``` + * + * ```typescript + * // to do that we need to add the `CancellationToken` argument to the end of args list + * class Example extends Disposable { + * @cancelPreviousCalls + * async doSomethingAsync(arg1: number, arg2: string, cancellationToken?: CancellationToken): Promise { + * console.log(`call with args ${arg1} and ${arg2} initiated`); + * + * // the decorator will create the cancellation token automatically + * assertDefined( + * cancellationToken, + * `The method must now have the `CancellationToken` passed to it.`, + * ); + * + * cancellationToken.onCancellationRequested(() => { + * console.log(`call with args ${arg1} and ${arg2} was cancelled`); + * }); + * + * // do something async.. + * await new Promise(resolve => setTimeout(resolve, 1000)); + * + * // check cancellation token state after the async operations + * console.log( + * `call with args ${arg1} and ${arg2} completed, canceled?: ${cancellationToken.isCancellationRequested}`, + * ); + * } + * } + * + * const example = new Example(); + * // call the decorate method first time + * example.doSomethingAsync(1, 'foo'); + * // wait for 500ms which is less than 1000ms of the async operation in the first call + * await new Promise(resolve => setTimeout(resolve, 500)); + * // calling the decorate method second time cancels the token passed to the first call + * example.doSomethingAsync(2, 'bar'); + * ``` + */ +export function cancelPreviousCalls< + TObject extends Disposable, + TArgs extends unknown[], + TReturn extends unknown, +>( + _proto: TObject, + methodName: string, + descriptor: TypedPropertyDescriptor TReturn>>, +) { + const originalMethod = descriptor.value; + + assertDefined( + originalMethod, + `Method '${methodName}' is not defined.`, + ); + + // we create the global map that contains `TObjectRecord` for each object instance that + // uses this decorator, which itself contains a `{method name} -> TMethodRecord` mapping + // for each decorated method on the object; the `TMethodRecord` record stores current + // `cancellationTokenSource`, token of which was passed to the previous call of the method + const objectRecords = new WeakMap>(); + + // decorate the original method with the following logic that upon a new invocation + // of the method cancels the cancellation token that was passed to a previous call + descriptor.value = function ( + this: TObject, + ...args: Parameters + ): TReturn { + // get or create a record for the current object instance + // the creation is done once per each object instance + let record = objectRecords.get(this); + if (!record) { + record = new DisposableMap(); + objectRecords.set(this, record); + + this._register({ + dispose: () => { + objectRecords.get(this)?.dispose(); + objectRecords.delete(this); + }, + }); + } + + // when the decorated method is called again and there is a cancellation token + // source exists from a previous call, cancel and dispose it, then remove it + record.get(methodName)?.dispose(true); + + // now we need to provide a cancellation token to the original method + // as the last argument, there are two cases to consider: + // - (common case) the arguments list does not have a cancellation token + // as the last argument, - in this case we need to add a new one + // - (possible case) - the arguments list already has a cancellation token + // as the last argument, - in this case we need to reuse the token when + // we create ours, and replace the old token with the new one + // therefore, + + // get the last argument of the arguments list and if it is present, + // reuse it as the token for the new cancellation token source + const lastArgument = (args.length > 0) + ? args[args.length - 1] + : undefined; + const token = CancellationToken.isCancellationToken(lastArgument) + ? lastArgument + : undefined; + + const cancellationSource = new CancellationTokenSource(token); + record.set(methodName, cancellationSource); + + // then update or add cancelaltion token at the end of the arguments list + if (CancellationToken.isCancellationToken(lastArgument)) { + args[args.length - 1] = cancellationSource.token; + } else { + args.push(cancellationSource.token); + } + + // finally invoke the original method passing original arguments and + // the new cancellation token at the end of the arguments list + return originalMethod.call(this, ...args); + }; + + return descriptor; +} diff --git a/src/vs/base/common/trackedDisposable.ts b/src/vs/base/common/trackedDisposable.ts new file mode 100644 index 000000000000..7c848c552118 --- /dev/null +++ b/src/vs/base/common/trackedDisposable.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../base/common/lifecycle.js'; + +/** + * Disposable class that tracks its own {@linkcode disposed} state. + */ +export class TrackedDisposable extends Disposable { + + /** + * Tracks disposed state of this object. + */ + private _disposed = false; + + /** + * Check if the current object was already disposed. + */ + public get disposed(): boolean { + return this._disposed; + } + + /** + * Dispose current object if not already disposed. + * @returns + */ + public override dispose(): void { + if (this.disposed) { + return; + } + + this._disposed = true; + super.dispose(); + } +} diff --git a/src/vs/base/test/common/cancelPreviousCalls.test.ts b/src/vs/base/test/common/cancelPreviousCalls.test.ts new file mode 100644 index 000000000000..d5432efe313b --- /dev/null +++ b/src/vs/base/test/common/cancelPreviousCalls.test.ts @@ -0,0 +1,303 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Disposable } from '../../common/lifecycle.js'; +import { CancellationToken } from '../../common/cancellation.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; +import { cancelPreviousCalls } from '../../common/decorators/cancelPreviousCalls.js'; + +suite('cancelPreviousCalls decorator', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + class MockDisposable extends Disposable { + /** + * Arguments that the {@linkcode doSomethingAsync} method was called with. + */ + private readonly callArgs1: ([number, string, CancellationToken | undefined])[] = []; + + /** + * Arguments that the {@linkcode doSomethingElseAsync} method was called with. + */ + private readonly callArgs2: ([number, string, CancellationToken | undefined])[] = []; + + /** + * Returns the arguments that the {@linkcode doSomethingAsync} method was called with. + */ + public get callArguments1() { + return this.callArgs1; + } + + /** + * Returns the arguments that the {@linkcode doSomethingElseAsync} method was called with. + */ + public get callArguments2() { + return this.callArgs2; + } + + @cancelPreviousCalls + async doSomethingAsync(arg1: number, arg2: string, cancellationToken?: CancellationToken): Promise { + this.callArgs1.push([arg1, arg2, cancellationToken]); + + await new Promise(resolve => setTimeout(resolve, 25)); + } + + @cancelPreviousCalls + async doSomethingElseAsync(arg1: number, arg2: string, cancellationToken?: CancellationToken): Promise { + this.callArgs2.push([arg1, arg2, cancellationToken]); + + await new Promise(resolve => setTimeout(resolve, 25)); + } + } + + test('should call method with CancellationToken', async () => { + const instance = disposables.add(new MockDisposable()); + + await instance.doSomethingAsync(1, 'foo'); + + const callArguments = instance.callArguments1; + assert.strictEqual( + callArguments.length, + 1, + `The 'doSomethingAsync' method must be called just once.`, + ); + + const args = callArguments[0]; + assert( + args.length === 3, + `The 'doSomethingAsync' method must be called with '3' arguments, got '${args.length}'.`, + ); + + const arg1 = args[0]; + const arg2 = args[1]; + const arg3 = args[2]; + + assert.strictEqual( + arg1, + 1, + `The 'doSomethingAsync' method call must have the correct 1st argument.`, + ); + + assert.strictEqual( + arg2, + 'foo', + `The 'doSomethingAsync' method call must have the correct 2nd argument.`, + ); + + assert( + CancellationToken.isCancellationToken(arg3), + `The last argument of the 'doSomethingAsync' method must be a 'CancellationToken', got '${arg3}'.`, + ); + + assert( + arg3.isCancellationRequested === false, + `The 'CancellationToken' argument must not yet be cancelled.`, + ); + + assert( + instance.callArguments2.length === 0, + `The 'doSomethingElseAsync' method must not be called.`, + ); + }); + + test('cancel token of the previous call when method is called again', async () => { + const instance = disposables.add(new MockDisposable()); + + instance.doSomethingAsync(1, 'foo'); + await new Promise(resolve => setTimeout(resolve, 10)); + instance.doSomethingAsync(2, 'bar'); + + const callArguments = instance.callArguments1; + assert.strictEqual( + callArguments.length, + 2, + `The 'doSomethingAsync' method must be called twice.`, + ); + + const call1Args = callArguments[0]; + assert( + call1Args.length === 3, + `The first call of the 'doSomethingAsync' method must have '3' arguments, got '${call1Args.length}'.`, + ); + + assert.strictEqual( + call1Args[0], + 1, + `The first call of the 'doSomethingAsync' method must have the correct 1st argument.`, + ); + + assert.strictEqual( + call1Args[1], + 'foo', + `The first call of the 'doSomethingAsync' method must have the correct 2nd argument.`, + ); + + assert( + CancellationToken.isCancellationToken(call1Args[2]), + `The first call of the 'doSomethingAsync' method must have the 'CancellationToken' as the 3rd argument.`, + ); + + assert( + call1Args[2].isCancellationRequested === true, + `The 'CancellationToken' of the first call must be cancelled.`, + ); + + const call2Args = callArguments[1]; + assert( + call2Args.length === 3, + `The second call of the 'doSomethingAsync' method must have '3' arguments, got '${call1Args.length}'.`, + ); + + assert.strictEqual( + call2Args[0], + 2, + `The second call of the 'doSomethingAsync' method must have the correct 1st argument.`, + ); + + assert.strictEqual( + call2Args[1], + 'bar', + `The second call of the 'doSomethingAsync' method must have the correct 2nd argument.`, + ); + + assert( + CancellationToken.isCancellationToken(call2Args[2]), + `The second call of the 'doSomethingAsync' method must have the 'CancellationToken' as the 3rd argument.`, + ); + + assert( + call2Args[2].isCancellationRequested === false, + `The 'CancellationToken' of the second call must be cancelled.`, + ); + + assert( + instance.callArguments2.length === 0, + `The 'doSomethingElseAsync' method must not be called.`, + ); + }); + + test('different method calls must not interfere with each other', async () => { + const instance = disposables.add(new MockDisposable()); + + instance.doSomethingAsync(10, 'baz'); + await new Promise(resolve => setTimeout(resolve, 10)); + instance.doSomethingElseAsync(25, 'qux'); + + assert.strictEqual( + instance.callArguments1.length, + 1, + `The 'doSomethingAsync' method must be called once.`, + ); + + const call1Args = instance.callArguments1[0]; + assert( + call1Args.length === 3, + `The first call of the 'doSomethingAsync' method must have '3' arguments, got '${call1Args.length}'.`, + ); + + assert.strictEqual( + call1Args[0], + 10, + `The first call of the 'doSomethingAsync' method must have the correct 1st argument.`, + ); + + assert.strictEqual( + call1Args[1], + 'baz', + `The first call of the 'doSomethingAsync' method must have the correct 2nd argument.`, + ); + + assert( + CancellationToken.isCancellationToken(call1Args[2]), + `The first call of the 'doSomethingAsync' method must have the 'CancellationToken' as the 3rd argument.`, + ); + + assert( + call1Args[2].isCancellationRequested === false, + `The 'CancellationToken' of the first call must not be cancelled.`, + ); + + assert.strictEqual( + instance.callArguments2.length, + 1, + `The 'doSomethingElseAsync' method must be called once.`, + ); + + const call2Args = instance.callArguments2[0]; + assert( + call2Args.length === 3, + `The first call of the 'doSomethingElseAsync' method must have '3' arguments, got '${call1Args.length}'.`, + ); + + assert.strictEqual( + call2Args[0], + 25, + `The first call of the 'doSomethingElseAsync' method must have the correct 1st argument.`, + ); + + assert.strictEqual( + call2Args[1], + 'qux', + `The first call of the 'doSomethingElseAsync' method must have the correct 2nd argument.`, + ); + + assert( + CancellationToken.isCancellationToken(call2Args[2]), + `The first call of the 'doSomethingElseAsync' method must have the 'CancellationToken' as the 3rd argument.`, + ); + + assert( + call2Args[2].isCancellationRequested === false, + `The 'CancellationToken' of the second call must be cancelled.`, + ); + + instance.doSomethingElseAsync(105, 'uxi'); + + assert.strictEqual( + instance.callArguments1.length, + 1, + `The 'doSomethingAsync' method must be called once.`, + ); + + assert.strictEqual( + instance.callArguments2.length, + 2, + `The 'doSomethingElseAsync' method must be called twice.`, + ); + + assert( + call1Args[2].isCancellationRequested === false, + `The 'CancellationToken' of the first call must not be cancelled.`, + ); + + const call3Args = instance.callArguments2[1]; + assert( + CancellationToken.isCancellationToken(call3Args[2]), + `The last argument of the second call of the 'doSomethingElseAsync' method must be a 'CancellationToken'.`, + ); + + assert( + call2Args[2].isCancellationRequested, + `The 'CancellationToken' of the first call must be cancelled.`, + ); + + assert( + call3Args[2].isCancellationRequested === false, + `The 'CancellationToken' of the second call must not be cancelled.`, + ); + + assert.strictEqual( + call3Args[0], + 105, + `The second call of the 'doSomethingElseAsync' method must have the correct 1st argument.`, + ); + + assert.strictEqual( + call3Args[1], + 'uxi', + `The second call of the 'doSomethingElseAsync' method must have the correct 2nd argument.`, + ); + }); +}); diff --git a/src/vs/base/test/common/testUtils.ts b/src/vs/base/test/common/testUtils.ts index 1e291422f413..8c059122f8c3 100644 --- a/src/vs/base/test/common/testUtils.ts +++ b/src/vs/base/test/common/testUtils.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { randomInt } from '../../common/numbers.js'; + export function flakySuite(title: string, fn: () => void) /* Suite */ { return suite(title, function () { @@ -17,3 +19,34 @@ export function flakySuite(title: string, fn: () => void) /* Suite */ { fn.call(this); }); } + +/** + * Helper function that allows to await for a specified amount of time. + * @param ms The amount of time to wait in milliseconds. + */ +export const wait = (ms: number): Promise => { + return new Promise(resolve => setTimeout(resolve, ms)); +}; + +/** + * Helper function that allows to await for a random amount of time. + * @param maxMs The `maximum` amount of time to wait, in milliseconds. + * @param minMs [`optional`] The `minimum` amount of time to wait, in milliseconds. + */ +export const waitRandom = (maxMs: number, minMs: number = 0): Promise => { + return wait(randomInt(maxMs, minMs)); +}; + +/** + * (pseudo)Random boolean generator. + * + * ## Examples + * + * ```typsecript + * randomBoolean(); // generates either `true` or `false` + * ``` + * + */ +export const randomBoolean = (): boolean => { + return Math.random() > 0.5; +}; diff --git a/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts b/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts index 174365c45599..4874ac7789f7 100644 --- a/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts +++ b/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts @@ -13,6 +13,11 @@ import { assert } from '../../../../../base/common/assert.js'; * value reflects the position of the token in the original data. */ export class MarkdownLink extends MarkdownToken { + /** + * Check if this `markdown link` points to a valid URL address. + */ + public readonly isURL: boolean; + constructor( /** * The starting line number of the link (1-based indexing). @@ -64,6 +69,14 @@ export class MarkdownLink extends MarkdownToken { columnNumber + caption.length + reference.length, ), ); + + // set up the `isURL` flag based on the current + try { + new URL(this.path); + this.isURL = true; + } catch { + this.isURL = false; + } } public override get text(): string { diff --git a/src/vs/editor/test/common/codecs/linesDecoder.test.ts b/src/vs/editor/test/common/codecs/linesDecoder.test.ts index b3e6c91be133..cc8f391176d9 100644 --- a/src/vs/editor/test/common/codecs/linesDecoder.test.ts +++ b/src/vs/editor/test/common/codecs/linesDecoder.test.ts @@ -42,9 +42,9 @@ export class TestLinesDecoder extends TestDecoder { } suite('LinesDecoder', () => { - suite('produces expected tokens', () => { - const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + suite('produces expected tokens', () => { test('input starts with line data', async () => { const test = testDisposables.add(new TestLinesDecoder()); diff --git a/src/vs/editor/test/common/utils/testDecoder.ts b/src/vs/editor/test/common/utils/testDecoder.ts index bd0a7a07e6cc..d78dc0124d9f 100644 --- a/src/vs/editor/test/common/utils/testDecoder.ts +++ b/src/vs/editor/test/common/utils/testDecoder.ts @@ -10,21 +10,9 @@ import { BaseToken } from '../../../common/codecs/baseToken.js'; import { assertDefined } from '../../../../base/common/types.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { WriteableStream } from '../../../../base/common/stream.js'; +import { randomBoolean } from '../../../../base/test/common/testUtils.js'; import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; -/** - * (pseudo)Random boolean generator. - * - * ## Examples - * - * ```typsecript - * randomBoolean(); // generates either `true` or `false` - * ``` - */ -const randomBoolean = (): boolean => { - return Math.random() > 0.5; -}; - /** * A reusable test utility that asserts that the given decoder * produces the expected `expectedTokens` sequence of tokens. diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts index 40288ee891fb..e531ad4ebeed 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts @@ -9,9 +9,9 @@ import { Emitter } from '../../../../../base/common/event.js'; import { basename } from '../../../../../base/common/resources.js'; import { assertDefined } from '../../../../../base/common/types.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { PromptFileReference, TErrorCondition } from '../../common/promptFileReference.js'; +import { FilePromptParser } from '../../common/promptSyntax/parsers/filePromptParser.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { FileOpenFailed, NonPromptSnippetFile, RecursiveReference } from '../../common/promptFileReferenceErrors.js'; +import { FailedToResolveContentsStream, FileOpenFailed, NonPromptSnippetFile, ParseError, RecursiveReference } from '../../common/promptFileReferenceErrors.js'; /** * Well-known localized error messages. @@ -49,15 +49,14 @@ export class ChatInstructionsAttachmentModel extends Disposable { * Private reference of the underlying prompt instructions * reference instance. */ - private readonly _reference: PromptFileReference; + private readonly _reference: FilePromptParser; /** * Get the prompt instructions reference instance. */ - public get reference(): PromptFileReference { + public get reference(): FilePromptParser { return this._reference; } - /** * Get `URI` for the main reference and `URI`s of all valid * child references it may contain. @@ -78,7 +77,7 @@ export class ChatInstructionsAttachmentModel extends Disposable { // otherwise return `URI` for the main reference and // all valid child `URI` references it may contain return [ - ...reference.validFileReferenceUris, + ...reference.allValidReferencesUris, reference.uri, ]; } @@ -126,11 +125,9 @@ export class ChatInstructionsAttachmentModel extends Disposable { * @returns Error message. */ private getErrorMessage( - error: TErrorCondition, + error: ParseError, isRootError: boolean, ): string { - const { uri } = error; - // if a child error - the error is somewhere in the nested references tree, // then use message prefix to highlight that this is not a root error const prefix = (!isRootError) @@ -138,8 +135,8 @@ export class ChatInstructionsAttachmentModel extends Disposable { : ''; // if failed to open a file, return approprivate message and the file path - if (error instanceof FileOpenFailed) { - return `${prefix}${errorMessages.fileOpenFailed} '${uri.path}'.`; + if (error instanceof FileOpenFailed || error instanceof FailedToResolveContentsStream) { + return `${prefix}${errorMessages.fileOpenFailed} '${error.uri.path}'.`; } // if a recursion, provide the entire recursion path so users can use @@ -160,15 +157,23 @@ export class ChatInstructionsAttachmentModel extends Disposable { } /** - * Collect all failures that may have occurred during the process - * of resolving references in the entire references tree. + * Collect all failures that may have occurred during the process of resolving + * references in the entire references tree, including the current root reference. * * @returns List of errors in the references tree. */ - private collectErrorConditions(): TErrorCondition[] { - return this.reference - // get all references (including the root) as a flat array - .flatten() + private collectErrorConditions(): ParseError[] { + const result: ParseError[] = []; + + // add error conditions of this object + if (this._reference.errorCondition) { + result.push(this._reference.errorCondition); + } + + // collect error conditions of all child references + const childErrorConditions = this.reference + // get entire reference tree + .allReferences // filter out children without error conditions or // the ones that are non-prompt snippet files .filter((childReference) => { @@ -177,7 +182,7 @@ export class ChatInstructionsAttachmentModel extends Disposable { return errorCondition && !(errorCondition instanceof NonPromptSnippetFile); }) // map to error condition objects - .map((childReference): TErrorCondition => { + .map((childReference): ParseError => { const { errorCondition } = childReference; // `must` always be `true` because of the `filter` call above @@ -188,6 +193,9 @@ export class ChatInstructionsAttachmentModel extends Disposable { return errorCondition; }); + result.push(...childErrorConditions); + + return result; } /** @@ -242,7 +250,7 @@ export class ChatInstructionsAttachmentModel extends Disposable { super(); this._onUpdate.fire = this._onUpdate.fire.bind(this._onUpdate); - this._reference = this._register(this.initService.createInstance(PromptFileReference, uri)) + this._reference = this._register(this.initService.createInstance(FilePromptParser, uri, [])) .onUpdate(this._onUpdate.fire); } @@ -251,7 +259,7 @@ export class ChatInstructionsAttachmentModel extends Disposable { * that it may contain. */ public resolve(): this { - this._reference.resolve(); + this._reference.start(); return this; } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 78a14c2e3fbe..031fa500a951 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -195,7 +195,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge continue; } - for (const childUri of variable.validFileReferenceUris) { + for (const childUri of variable.allValidReferencesUris) { contextArr.push({ id: variable.id, name: basename(childUri.path), diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 8a557e382a92..8418d0591f51 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -25,8 +25,8 @@ import { ChatWidget, IChatWidgetContrib } from '../chatWidget.js'; import { IChatRequestVariableValue, IChatVariablesService, IDynamicVariable } from '../../common/chatVariables.js'; import { ISymbolQuickPickItem } from '../../../search/browser/symbolsQuickAccess.js'; import { ChatFileReference } from './chatDynamicVariables/chatFileReference.js'; -import { PromptFileReference } from '../../common/promptFileReference.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { BasePromptParser } from '../../common/promptSyntax/parsers/basePromptParser.js'; export const dynamicVariableDecorationType = 'chat-dynamic-variable'; @@ -130,7 +130,7 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC addReference(ref: IDynamicVariable): void { // use `ChatFileReference` for file references and `IDynamicVariable` for other variables - const promptSnippetsEnabled = PromptFileReference.promptSnippetsEnabled(this.configService); + const promptSnippetsEnabled = BasePromptParser.promptSnippetsEnabled(this.configService); const variable = (ref.id === 'vscode.file' && promptSnippetsEnabled) ? this.instantiationService.createInstance(ChatFileReference, ref) : ref; @@ -141,13 +141,13 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC // if the `prompt snippets` feature is enabled, and file is a `prompt snippet`, // start resolving nested file references immediately and subscribe to updates - if (variable instanceof ChatFileReference && variable.isPromptSnippetFile) { + if (variable instanceof ChatFileReference && variable.isPromptSnippet) { // subscribe to variable changes variable.onUpdate(() => { this.updateDecorations(); }); // start resolving the file references - variable.resolve(); + variable.start(); } } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts index 15bf36b7017f..9b225f8c1897 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts @@ -7,25 +7,25 @@ import { URI } from '../../../../../../base/common/uri.js'; import { assert } from '../../../../../../base/common/assert.js'; import { IDynamicVariable } from '../../../common/chatVariables.js'; import { IRange } from '../../../../../../editor/common/core/range.js'; +import { FilePromptParser } from '../../../common/promptSyntax/parsers/filePromptParser.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; -import { PromptFileReference } from '../../../common/promptFileReference.js'; -import { IFileService } from '../../../../../../platform/files/common/files.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; /** * A wrapper class for an `IDynamicVariable` object that that adds functionality * to parse nested file references of this variable. - * See {@link PromptFileReference} for details. + * See {@link FilePromptParser} for details. */ -export class ChatFileReference extends PromptFileReference implements IDynamicVariable { +export class ChatFileReference extends FilePromptParser implements IDynamicVariable { /** * @throws if the `data` reference is no an instance of `URI`. */ constructor( public readonly reference: IDynamicVariable, - @ILogService logService: ILogService, - @IFileService fileService: IFileService, + @IInstantiationService initService: IInstantiationService, @IConfigurationService configService: IConfigurationService, + @ILogService logService: ILogService, ) { const { data } = reference; @@ -34,7 +34,7 @@ export class ChatFileReference extends PromptFileReference implements IDynamicVa `Variable data must be an URI, got '${data}'.`, ); - super(data, logService, fileService, configService); + super(data, [], initService, configService, logService); } /** diff --git a/src/vs/workbench/contrib/chat/common/promptFileReference.ts b/src/vs/workbench/contrib/chat/common/promptFileReference.ts deleted file mode 100644 index e31f7da57225..000000000000 --- a/src/vs/workbench/contrib/chat/common/promptFileReference.ts +++ /dev/null @@ -1,438 +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 { URI } from '../../../../base/common/uri.js'; -import { Emitter } from '../../../../base/common/event.js'; -import { extUri } from '../../../../base/common/resources.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { Location } from '../../../../editor/common/languages.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { ChatPromptCodec } from './codecs/chatPromptCodec/chatPromptCodec.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { FileOpenFailed, NonPromptSnippetFile, RecursiveReference } from './promptFileReferenceErrors.js'; -import { FileChangesEvent, FileChangeType, IFileService, IFileStreamContent } from '../../../../platform/files/common/files.js'; - -/** - * Error conditions that may happen during the file reference resolution. - */ -export type TErrorCondition = FileOpenFailed | RecursiveReference | NonPromptSnippetFile; - -/** - * File extension for the prompt snippet files. - */ -const PROMP_SNIPPET_FILE_EXTENSION: string = '.prompt.md'; - -/** - * Configuration key for the prompt snippets feature. - */ -const PROMPT_SNIPPETS_CONFIG_KEY: string = 'chat.experimental.prompt-snippets'; - -/** - * Represents a file reference in the chatbot prompt, e.g. `#file:./path/to/file.md`. - * Contains logic to resolve all nested file references in the target file and all - * referenced child files recursively, if any. - * - * ## Examples - * - * ```typescript - * const fileReference = new PromptFileReference( - * URI.file('/path/to/file.md'), - * fileService, - * ); - * - * // subscribe to updates to the file reference tree - * fileReference.onUpdate(() => { - * // .. do something with the file reference tree .. - * // e.g. get URIs of all resolved file references in the tree - * const resolved = fileReference - * // get all file references as a flat array - * .flatten() - * // remove self from the list if only child references are needed - * .slice(1) - * // filter out unresolved references - * .filter(reference => reference.resolveFailed === flase) - * // convert to URIs only - * .map(reference => reference.uri); - * - * console.log(resolved); - * }); - * - * // *optional* if need to re-resolve file references when target files change - * // note that this does not sets up filesystem listeners for nested file references - * fileReference.addFilesystemListeners(); - * - * // start resolving the file reference tree; this can also be `await`ed if needed - * // to wait for the resolution on the main file reference to complete (the nested - * // references can still be resolving in the background) - * fileReference.resolve(); - * - * // don't forget to dispose when no longer needed! - * fileReference.dispose(); - * ``` - */ -export class PromptFileReference extends Disposable { - /** - * Child references of the current one. - */ - protected readonly children: PromptFileReference[] = []; - - /** - * The event is fired when nested prompt snippet references are updated, if any. - */ - private readonly _onUpdate = this._register(new Emitter()); - - private _errorCondition?: TErrorCondition; - /** - * If file reference resolution fails, this attribute will be set - * to an error instance that describes the error condition. - */ - public get errorCondition(): TErrorCondition | undefined { - return this._errorCondition; - } - - /** - * Whether file reference resolution was attempted at least once. - */ - private _resolveAttempted: boolean = false; - /** - * Whether file references resolution failed. - * Set to `undefined` if the `resolve` method hasn't been ever called yet. - */ - public get resolveFailed(): boolean | undefined { - if (!this._resolveAttempted) { - return undefined; - } - - return !!this._errorCondition; - } - - constructor( - private readonly _uri: URI | Location, - @ILogService private readonly logService: ILogService, - @IFileService private readonly fileService: IFileService, - @IConfigurationService private readonly configService: IConfigurationService, - ) { - super(); - this.onFilesChanged = this.onFilesChanged.bind(this); - - // make sure the variable is updated on file changes - this.addFilesystemListeners(); - } - - /** - * Subscribe to the `onUpdate` event. - * @param callback - */ - public onUpdate(callback: () => unknown): this { - this._register(this._onUpdate.event(callback)); - - return this; - } - - /** - * Check if the prompt snippets feature is enabled. - * @see {@link PROMPT_SNIPPETS_CONFIG_KEY} - */ - public static promptSnippetsEnabled( - configService: IConfigurationService, - ): boolean { - const value = configService.getValue(PROMPT_SNIPPETS_CONFIG_KEY); - - if (!value) { - return false; - } - - if (typeof value === 'string') { - return value.trim().toLowerCase() === 'true'; - } - - return !!value; - } - - /** - * Check if the current reference points to a prompt snippet file. - */ - public get isPromptSnippetFile(): boolean { - return this.uri.path.endsWith(PROMP_SNIPPET_FILE_EXTENSION); - } - - /** - * Associated URI of the reference. - */ - public get uri(): URI { - return this._uri instanceof URI - ? this._uri - : this._uri.uri; - } - - /** - * Get the directory name of the file reference. - */ - public get dirname() { - return URI.joinPath(this.uri, '..'); - } - - /** - * Check if the current reference points to a given resource. - */ - public sameUri(other: URI | Location): boolean { - const otherUri = other instanceof URI ? other : other.uri; - - return this.uri.toString() === otherUri.toString(); - } - - /** - * Add file system event listeners for the current file reference. - */ - private addFilesystemListeners(): this { - this._register( - this.fileService.onDidFilesChange(this.onFilesChanged), - ); - - return this; - } - - /** - * Event handler for the `onDidFilesChange` event. - */ - private onFilesChanged(event: FileChangesEvent) { - const fileChanged = event.contains(this.uri, FileChangeType.UPDATED); - const fileDeleted = event.contains(this.uri, FileChangeType.DELETED); - const fileAdded = event.contains(this.uri, FileChangeType.ADDED); - - // if the change does not relate to the current file, nothing to do - if (!fileChanged && !fileDeleted && !fileAdded) { - return; - } - - // handle file changes only for prompt snippet files but in the case a file was - // deleted, it does not matter if it was a prompt - we still need to handle it by - // calling the `resolve()` method, which will set an error condition if the file - // does not exist anymore, or of it is not a prompt snippet file - if (fileChanged && !this.isPromptSnippetFile) { - return; - } - - // if we receive an `add` event, validate that the file was previously deleted, because - // that is the only way we could have end up in this state of the file reference object - if (fileAdded && (!this._errorCondition || !(this._errorCondition instanceof FileOpenFailed))) { - this.logService.warn( - [ - `Received 'add' event for file at '${this.uri.path}', but it was not previously deleted.`, - 'This most likely indicates a bug in our logic, so please report it.', - ].join(' '), - ); - } - - // if file is changed or deleted, re-resolve the file reference - // in the case when the file is deleted, this should result in - // failure to open the file, so the `errorCondition` field will - // be updated to an appropriate error instance and the `children` - // field will be cleared up - this.resolve(); - } - - /** - * Get file stream, if the file exsists. - */ - private async getFileStream(): Promise { - try { - // read the file first - const result = await this.fileService.readFileStream(this.uri); - - // if file exists but not a prompt snippet file, set appropriate error - // condition and return null so we don't resolve nested references in it - if (this.uri.path.endsWith(PROMP_SNIPPET_FILE_EXTENSION) === false) { - this._errorCondition = new NonPromptSnippetFile(this.uri); - - return null; - } - - return result; - } catch (error) { - this._errorCondition = new FileOpenFailed(this.uri, error); - - return null; - } - } - - /** - * Resolve the current file reference on the disk and - * all nested file references that may exist in the file. - * - * @param waitForChildren Whether need to block until all child references are resolved. - */ - public async resolve( - waitForChildren: boolean = false, - ): Promise { - return await this.resolveReference(waitForChildren); - } - - /** - * Private implementation of the {@link resolve} method, that allows - * to pass `seenReferences` list to the recursive calls to prevent - * infinite file reference recursion. - */ - private async resolveReference( - waitForChildren: boolean = false, - seenReferences: string[] = [], - ): Promise { - // remove current error condition from the previous resolve attempt, if any - delete this._errorCondition; - - // dispose current child references, if any exist from a previous resolve - this.disposeChildren(); - - // to prevent infinite file recursion, we keep track of all references in - // the current branch of the file reference tree and check if the current - // file reference has been already seen before - if (seenReferences.includes(this.uri.path)) { - seenReferences.push(this.uri.path); - - this._errorCondition = new RecursiveReference(this.uri, seenReferences); - this._resolveAttempted = true; - this._onUpdate.fire(); - - return this; - } - - // we don't care if reading the file fails below, hence can add the path - // of the current reference to the `seenReferences` set immediately, - - // even if the file doesn't exist, we would never end up in the recursion - seenReferences.push(this.uri.path); - - // try to get stream for the contents of the file, it may - // fail to multiple reasons, e.g. file doesn't exist, etc. - const fileStream = await this.getFileStream(); - this._resolveAttempted = true; - - // failed to open the file, nothing to resolve - if (fileStream === null) { - this._onUpdate.fire(); - - return this; - } - - // get all file references in the file contents - const references = await ChatPromptCodec.decode(fileStream.value).consumeAll(); - - // recursively resolve all references and add to the `children` array - // - // Note! we don't register the children references as disposables here, because we dispose them - // explicitly in the `dispose` override method of this class. This is done to prevent - // the disposables store to be littered with already-disposed child instances due to - // the fact that the `resolve` method can be called multiple times on target file changes - const childPromises = []; - for (const reference of references) { - const childUri = extUri.resolvePath(this.dirname, reference.path); - - const child = new PromptFileReference( - childUri, - this.logService, - this.fileService, - this.configService, - ); - - // subscribe to child updates - child.onUpdate( - this._onUpdate.fire.bind(this._onUpdate), - ); - this.children.push(child); - - // start resolving the child in the background, including its children - // Note! we have to clone the `seenReferences` list here to ensure that - // different tree branches don't interfere with each other as we - // care about the parent references when checking for recursion - childPromises.push( - child.resolveReference(waitForChildren, [...seenReferences]), - ); - } - - // if should wait for all children to resolve, block here - if (waitForChildren) { - await Promise.all(childPromises); - } - - this._onUpdate.fire(); - - return this; - } - - /** - * Dispose current child file references. - */ - private disposeChildren(): this { - for (const child of this.children) { - child.dispose(); - } - - this.children.length = 0; - this._onUpdate.fire(); - - return this; - } - - /** - * Flatten the current file reference tree into a single array. - */ - public flatten(): readonly PromptFileReference[] { - const result = []; - - // then add self to the result - result.push(this); - - // get flattened children references first - for (const child of this.children) { - result.push(...child.flatten()); - } - - return result; - } - - /** - * Get list of all valid child references. - */ - public get validChildReferences(): readonly PromptFileReference[] { - return this.flatten() - // skip the root reference itself (this variable) - .slice(1) - // filter out unresolved references - .filter((reference) => { - return (reference.resolveFailed === false) || - (reference.errorCondition instanceof NonPromptSnippetFile); - }); - } - - /** - * Get list of all valid child references as URIs. - */ - public get validFileReferenceUris(): readonly URI[] { - return this.validChildReferences - .map(child => child.uri); - } - - /** - * Check if the current reference is equal to a given one. - */ - public equals(other: PromptFileReference): boolean { - if (!this.sameUri(other.uri)) { - return false; - } - - return true; - } - - /** - * Returns a string representation of this reference. - */ - public override toString() { - return `#file:${this.uri.path}`; - } - - public override dispose() { - this.disposeChildren(); - super.dispose(); - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts b/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts index 60da24e52a7f..0b56fcef0e87 100644 --- a/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts +++ b/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts @@ -6,11 +6,15 @@ import { URI } from '../../../../base/common/uri.js'; /** - * Base resolve error class used when file reference resolution fails. + * Base prompt parsing error class. */ -abstract class ResolveError extends Error { +export abstract class ParseError extends Error { + /** + * Error type name. + */ + public readonly abstract errorType: string; + constructor( - public readonly uri: URI, message?: string, options?: ErrorOptions, ) { @@ -36,10 +40,44 @@ abstract class ResolveError extends Error { } } +/** + * A generic error for failing to resolve prompt contents stream. + */ +export class FailedToResolveContentsStream extends ParseError { + public override errorType = 'FailedToResolveContentsStream'; + + constructor( + public readonly uri: URI, + public readonly originalError: unknown, + ) { + super( + `Failed to resolve prompt contents stream for '${uri.toString()}': ${originalError}.`, + ); + } +} + + +/** + * Base resolve error class used when file reference resolution fails. + */ +export abstract class ResolveError extends ParseError { + public abstract override errorType: string; + + constructor( + public readonly uri: URI, + message?: string, + options?: ErrorOptions, + ) { + super(message, options); + } +} + /** * Error that reflects the case when attempt to open target file fails. */ export class FileOpenFailed extends ResolveError { + public override errorType = 'FileOpenError'; + constructor( uri: URI, public readonly originalError: unknown, @@ -66,6 +104,8 @@ export class FileOpenFailed extends ResolveError { * ``` */ export class RecursiveReference extends ResolveError { + public override errorType = 'RecursiveReferenceError'; + constructor( uri: URI, public readonly recursivePath: string[], @@ -114,6 +154,8 @@ export class RecursiveReference extends ResolveError { * a prompt snippet file, hence was not attempted to be resolved. */ export class NonPromptSnippetFile extends ResolveError { + public override errorType = 'NonPromptSnippetFileError'; + constructor( uri: URI, message: string = '', diff --git a/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/chatPromptCodec.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts similarity index 100% rename from src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/chatPromptCodec.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts diff --git a/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/chatPromptDecoder.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptDecoder.ts similarity index 100% rename from src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/chatPromptDecoder.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptDecoder.ts diff --git a/src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/tokens/fileReference.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/fileReference.ts similarity index 100% rename from src/vs/workbench/contrib/chat/common/codecs/chatPromptCodec/tokens/fileReference.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/fileReference.ts diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts new file mode 100644 index 000000000000..48e5252c05ee --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPromptContentsProvider } from './types.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { assertDefined } from '../../../../../../base/common/types.js'; +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 { FileChangesEvent, FileChangeType, IFileService } from '../../../../../../platform/files/common/files.js'; + +/** + * Prompt contents provider for a file on the disk referenced by the provided {@linkcode URI}. + */ +export class FilePromptContentProvider extends PromptContentsProviderBase implements IPromptContentsProvider { + constructor( + public readonly uri: URI, + @IFileService private readonly fileService: IFileService, + ) { + super(); + + // make sure the object is updated on file changes + this._register( + this.fileService.onDidFilesChange((event) => { + // if file was added or updated, forward the event to + // the `getContentsStream()` produce a new stream for file contents + if (event.contains(this.uri, FileChangeType.ADDED, FileChangeType.UPDATED)) { + // we support only full file parsing right now because + // the event doesn't contain a list of changed lines + return this.onChangeEmitter.fire('full'); + } + + // if file was deleted, forward the event to + // the `getContentsStream()` produce an error + if (event.contains(this.uri, FileChangeType.DELETED)) { + return this.onChangeEmitter.fire(event); + } + }), + ); + } + + /** + * Creates a stream of lines from the file based on the changes listed in + * the provided event. + * + * @param event - event that describes the changes in the file; `'full'` is + * the special value that means that all contents have changed + * @param cancellationToken - token that cancels this operation + */ + protected async getContentsStream( + _event: FileChangesEvent | 'full', + cancellationToken?: CancellationToken, + ): Promise { + if (cancellationToken?.isCancellationRequested) { + throw new CancellationError(); + } + + // get the binary stream of the file contents + let fileStream; + try { + fileStream = await this.fileService.readFileStream(this.uri); + } catch (error) { + throw new FileOpenFailed(this.uri, error); + } + + assertDefined( + fileStream, + new FileOpenFailed(this.uri, 'Failed to open file stream.'), + ); + + // after the promise above complete, this object can be already disposed or + // the cancellation could be requested, in that case destroy the stream and + // throw cancellation error + if (this.disposed || cancellationToken?.isCancellationRequested) { + fileStream.value.destroy(); + throw new CancellationError(); + } + + // if URI doesn't point to a prompt snippet file, don't try to resolve it + if (!this.isPromptSnippet()) { + throw new NonPromptSnippetFile(this.uri); + } + + return fileStream.value; + } + + /** + * String representation of this object. + */ + public override toString() { + return `file-prompt-contents-provider:${this.uri.path}`; + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts new file mode 100644 index 000000000000..bc1c47b8bd7b --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPromptContentsProvider } from './types.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 { TrackedDisposable } from '../../../../../../base/common/trackedDisposable.js'; +import { FailedToResolveContentsStream, ParseError } from '../../promptFileReferenceErrors.js'; +import { cancelPreviousCalls } from '../../../../../../base/common/decorators/cancelPreviousCalls.js'; + +/** + * File extension for the prompt snippets. + */ +export const PROMP_SNIPPET_FILE_EXTENSION: string = '.prompt.md'; + +/** + * Base class for prompt contents providers. Classes that extend this one are responsible to: + * + * - implement the {@linkcode getContentsStream} method to provide the contents stream + * of a prompt; this method should throw a `ParseError` or its derivative if the contents + * cannot be parsed for any reason + * - fire a {@linkcode TChangeEvent} event on the {@linkcode onChangeEmitter} event when + * prompt contents change + * - misc: + * - provide the {@linkcode uri} property that represents the URI of a prompt that + * the contents are for + * - implement the {@linkcode toString} method to return a string representation of this + * provider type to aid with debugging/tracing + */ +export abstract class PromptContentsProviderBase< + TChangeEvent extends NonNullable, +> extends TrackedDisposable implements IPromptContentsProvider { + /** + * Internal event emitter for the prompt contents change event. Classes that extend + * this abstract class are responsible to use this emitter to fire the contents change + * event when the prompt contents get modified. + */ + protected readonly onChangeEmitter = this._register(new Emitter()); + + constructor() { + super(); + // ensure that the `onChangeEmitter` always fires with the correct context + this.onChangeEmitter.fire = this.onChangeEmitter.fire.bind(this.onChangeEmitter); + // subscribe to the change event emitted by an extending class + this._register(this.onChangeEmitter.event(this.onContentsChanged, this)); + } + + /** + * Function to get contents stream for the provider. This function should + * throw a `ParseError` or its derivative if the contents cannot be parsed. + * + * @param changesEvent The event that triggered the change. The special + * `'full'` value means that everything has changed hence entire prompt + * contents need to be re-parsed from scratch. + */ + protected abstract getContentsStream( + changesEvent: TChangeEvent | 'full', + cancellationToken?: CancellationToken, + ): Promise; + + /** + * URI reference associated with the prompt contents. + */ + public abstract readonly uri: URI; + + /** + * Return a string representation of this object + * for debugging/tracing purposes. + */ + public abstract override toString(): string; + + /** + * Event emitter for the prompt contents change event. + * See {@linkcode onContentChanged} for more details. + */ + private readonly onContentChangedEmitter = this._register(new Emitter()); + + /** + * Event that fires when the prompt contents change. The event is either + * a `VSBufferReadableStream` stream with changed contents or an instance of + * the `ParseError` class representing a parsing failure case. + * + * `Note!` this field is meant to be used by the external consumers of the prompt + * contents provider that the classes that extend this abstract class. + * Please use the {@linkcode onChangeEmitter} event to provide a change + * event in your prompt contents implementation instead. + */ + public readonly onContentChanged = this.onContentChangedEmitter.event; + + /** + * Internal common implementation of the event that should be fired when + * prompt contents change. + */ + @cancelPreviousCalls + private onContentsChanged( + event: TChangeEvent | 'full', + cancellationToken?: CancellationToken, + ): this { + const promise = (cancellationToken?.isCancellationRequested) + ? Promise.reject(new CancellationError()) + : this.getContentsStream(event, cancellationToken); + + promise + .then((stream) => { + if (cancellationToken?.isCancellationRequested || this.disposed) { + stream.destroy(); + throw new CancellationError(); + } + + this.onContentChangedEmitter.fire(stream); + }) + .catch((error) => { + if (error instanceof ParseError) { + this.onContentChangedEmitter.fire(error); + + return; + } + + this.onContentChangedEmitter.fire( + new FailedToResolveContentsStream(this.uri, error), + ); + }); + + return this; + } + + /** + * Start producing the prompt contents data. + */ + public start(): this { + assert( + !this.disposed, + 'Cannot start contents provider that was already disposed.', + ); + + // `'full'` means "everything has changed" + this.onContentsChanged('full'); + + return this; + } + + /** + * Check if the current URI points to a prompt snippet. + */ + public isPromptSnippet(): boolean { + return this.uri.path.endsWith(PROMP_SNIPPET_FILE_EXTENSION); + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts new file mode 100644 index 000000000000..b76f374819d3 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from '../../../../../../base/common/buffer.js'; +import { ITextModel } from '../../../../../../editor/common/model.js'; +import { CancellationError } from '../../../../../../base/common/errors.js'; +import { PromptContentsProviderBase } from './promptContentsProviderBase.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { newWriteableStream, ReadableStream } from '../../../../../../base/common/stream.js'; +import { IModelContentChangedEvent } from '../../../../../../editor/common/textModelEvents.js'; + +/** + * Prompt contents provider for a {@linkcode ITextModel} instance. + */ +export class TextModelContentsProvider extends PromptContentsProviderBase { + /** + * URI component of the prompt associated with this contents provider. + */ + public readonly uri = this.model.uri; + + constructor( + private readonly model: ITextModel, + ) { + super(); + + this._register(this.model.onWillDispose(this.dispose.bind(this))); + this._register(this.model.onDidChangeContent(this.onChangeEmitter.fire)); + } + + /** + * Creates a stream of binary data from the text model based on the changes + * listed in the provided event. + * + * Note! this method implements a basic logic which does not take into account + * the `_event` argument for incremental updates. This needs to be improved. + * + * @param _event - event that describes the changes in the text model; `'full'` is + * the special value that means that all contents have changed + * @param cancellationToken - token that cancels this operation + */ + protected override async getContentsStream( + _event: IModelContentChangedEvent | 'full', + cancellationToken?: CancellationToken, + ): Promise> { + const stream = newWriteableStream(null); + const linesCount = this.model.getLineCount(); + + // provide the changed lines to the stream incrementaly and asynchronously + // to avoid blocking the main thread and save system resources used + let i = 0; + const interval = setInterval(() => { + if (this.model.isDisposed() || cancellationToken?.isCancellationRequested) { + clearInterval(interval); + stream.error(new CancellationError()); + stream.end(); + return; + } + + // write the current line to the stream + stream.write( + VSBuffer.fromString(this.model.getLineContent(i)), + ); + + // use the next line in the next iteration + i++; + + // if we have written all lines, end the stream and stop + // the interval timer + if (i >= linesCount) { + clearInterval(interval); + stream.end(); + } + }, 1); + + return stream; + } + + /** + * String representation of this object. + */ + public override toString() { + return `text-model-prompt-contents-provider:${this.uri.path}`; + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.d.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.d.ts new file mode 100644 index 000000000000..5d03ef2848ce --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.d.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../../../base/common/uri.js'; +import { ParseError } from '../../promptFileReferenceErrors.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; + +/** + * Interface for a prompt contents provider. Prompt contents providers are + * responsible for providing contents of a prompt as a byte streams and + * allow to subscribe to the change events of the prompt contents. + */ +export interface IPromptContentsProvider extends IDisposable { + /** + * URI component of the prompt associated with this contents provider. + */ + readonly uri: URI; + + /** + * Start the contents provider to produce the underlying contents. + */ + start(): this; + + /** + * Event that fires when the prompt contents change. The event is either a + * {@linkcode VSBufferReadableStream} stream with changed contents or + * an instance of the {@linkcode ParseError} error. + */ + onContentChanged( + callback: (streamOrError: VSBufferReadableStream | ParseError) => void, + ): IDisposable; +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts new file mode 100644 index 000000000000..fb3badc49641 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -0,0 +1,413 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPromptFileReference } from './types.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ChatPromptCodec } from '../codecs/chatPromptCodec.js'; +import { Emitter } from '../../../../../../base/common/event.js'; +import { FileReference } from '../codecs/tokens/fileReference.js'; +import { IPromptContentsProvider } from '../contentProviders/types.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 { TrackedDisposable } from '../../../../../../base/common/trackedDisposable.js'; +import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; +import { FileOpenFailed, NonPromptSnippetFile, RecursiveReference, ParseError } from '../../promptFileReferenceErrors.js'; + +/** + * Error conditions that may happen during the file reference resolution. + */ +export type TErrorCondition = FileOpenFailed | RecursiveReference | NonPromptSnippetFile; + +/** + * File extension for the prompt snippets. + */ +export const PROMP_SNIPPET_FILE_EXTENSION: string = '.prompt.md'; + +/** + * Configuration key for the prompt snippets feature. + */ +const PROMPT_SNIPPETS_CONFIG_KEY: string = 'chat.experimental.prompt-snippets'; + +/** + * Base prompt parser class that provides a common interface for all + * prompt parsers that are responsible for parsing chat prompt syntax. + */ +export abstract class BasePromptParser extends TrackedDisposable { + + /** + * List of file references in the current branch of the file reference tree. + */ + private readonly _references: PromptFileReference[] = []; + + /** + * The event is fired when lines or their content change. + */ + private readonly _onUpdate = this._register(new Emitter()); + + /** + * Subscribe to the `onUpdate` event that is fired when prompt tokens are updated. + * @param callback The callback function to be called on updates. + */ + public onUpdate(callback: () => void): this { + this._register(this._onUpdate.event(callback)); + + return this; + } + + private _errorCondition?: ParseError; + + /** + * If file reference resolution fails, this attribute will be set + * to an error instance that describes the error condition. + */ + public get errorCondition(): ParseError | undefined { + return this._errorCondition; + } + + /** + * Whether file reference resolution was attempted at least once. + */ + private _resolveAttempted: boolean = false; + + /** + * Whether file references resolution failed. + * Set to `undefined` if the `resolve` method hasn't been ever called yet. + */ + public get resolveFailed(): boolean | undefined { + if (!this._resolveAttempted) { + return undefined; + } + + return !!this._errorCondition; + } + + constructor( + private readonly promptContentsProvider: T, + seenReferences: string[] = [], + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IConfigurationService protected readonly configService: IConfigurationService, + @ILogService protected readonly logService: ILogService, + ) { + super(); + + this._onUpdate.fire = this._onUpdate.fire.bind(this._onUpdate); + this._register(promptContentsProvider); + + // to prevent infinite file recursion, we keep track of all references in + // the current branch of the file reference tree and check if the current + // file reference has been already seen before + if (seenReferences.includes(this.uri.path)) { + seenReferences.push(this.uri.path); + + this._errorCondition = new RecursiveReference(this.uri, seenReferences); + this._resolveAttempted = true; + this._onUpdate.fire(); + + return this; + } + + // we don't care if reading the file fails below, hence can add the path + // of the current reference to the `seenReferences` set immediately, - + // even if the file doesn't exist, we would never end up in the recursion + seenReferences.push(this.uri.path); + + let currentStream: VSBufferReadableStream | undefined; + this._register( + this.promptContentsProvider.onContentChanged((streamOrError) => { + // destroy previously received stream + currentStream?.destroy(); + + if (!(streamOrError instanceof ParseError)) { + // save the current stream object so it can be destroyed when/if + // a new stream is received + currentStream = streamOrError; + } + + // process the the received message + this.onContentsChanged(streamOrError, seenReferences); + }), + ); + } + + /** + * Handler the event event that is triggered when prompt contents change. + * + * @param streamOrError Either a binary stream of file contents, or an error object + * that was generated during the reference resolve attempt. + * @param seenReferences List of parent references that we've have already seen + * during the process of traversing the references tree. It's + * used to prevent the tree navigation to fall into an infinite + * references recursion. + */ + private onContentsChanged( + streamOrError: VSBufferReadableStream | ParseError, + seenReferences: string[], + ): void { + // set the flag indicating that reference resolution was attempted + this._resolveAttempted = true; + + // prefix for all log messages produced by this callback + const logPrefix = `[prompt parser][${basename(this.uri)}]`; + + // dispose all currently existing references + this.disposeReferences(); + + // if an error received, set up the error condition and stop + if (streamOrError instanceof ParseError) { + this._errorCondition = streamOrError; + this._onUpdate.fire(); + + return; + } + + // cleanup existing error condition (if any) + delete this._errorCondition; + + // decode the byte stream to a stream of prompt tokens + const stream = ChatPromptCodec.decode(streamOrError); + + // on error or stream end, dispose the stream + stream.on('error', (error) => { + stream.dispose(); + + this.logService.warn( + `${logPrefix} received an error on the chat prompt decoder stream: ${error}`, + ); + }); + stream.on('end', stream.dispose.bind(stream)); + + // when some tokens received, process and store the references + stream.on('data', (token) => { + if (token instanceof FileReference) { + this.onReference(token, [...seenReferences]); + } + + // note! the `isURL` is a simple check and needs to be improved to truly + // handle only file references, ignoring broken URLs or references + if (token instanceof MarkdownLink && !token.isURL) { + this.onReference(token, [...seenReferences]); + } + }); + + // calling `start` on a disposed stream throws, so we warn and return instead + if (stream.disposed) { + this.logService.warn( + `${logPrefix} cannot start stream that has been already disposed, aborting`, + ); + + return; + } + + // start receiving data on the stream + stream.start(); + } + + /** + * Handle a new reference token inside prompt contents. + */ + private onReference( + token: FileReference | MarkdownLink, + seenReferences: string[], + ): this { + const fileReference = this.instantiationService + .createInstance(PromptFileReference, token, this.dirname, seenReferences); + + this._references.push(fileReference); + + fileReference.onUpdate(this._onUpdate.fire); + fileReference.start(); + + this._onUpdate.fire(); + + return this; + } + + /** + * Dispose all currently held references. + */ + private disposeReferences() { + for (const reference of [...this._references]) { + reference.dispose(); + } + + this._references.length = 0; + } + + /** + * Start the prompt parser. + */ + public start(): this { + // if already in error state, nothing to do + if (this.errorCondition) { + return this; + } + + this.promptContentsProvider.start(); + + return this; + } + + /** + * Associated URI of the prompt. + */ + public get uri(): URI { + return this.promptContentsProvider.uri; + } + + /** + * Get the parent folder of the file reference. + */ + public get dirname() { + return URI.joinPath(this.uri, '..'); + } + + /** + * Check if the prompt snippets feature is enabled. + * @see {@link PROMPT_SNIPPETS_CONFIG_KEY} + */ + public static promptSnippetsEnabled( + configService: IConfigurationService, + ): boolean { + const value = configService.getValue(PROMPT_SNIPPETS_CONFIG_KEY); + + if (!value) { + return false; + } + + if (typeof value === 'string') { + return value.trim().toLowerCase() === 'true'; + } + + return !!value; + } + + /** + * Get a list of immediate child references of the prompt. + */ + public get references(): readonly IPromptFileReference[] { + return [...this._references]; + } + + /** + * Get a list of all references of the prompt, including + * all possible nested references its children may contain. + */ + public get allReferences(): readonly IPromptFileReference[] { + const result: IPromptFileReference[] = []; + + for (const reference of this.references) { + result.push(reference); + + if (reference.type === 'file') { + result.push(...reference.allReferences); + } + } + + return result; + } + + /** + * Get list of all valid references. + */ + public get allValidReferences(): readonly IPromptFileReference[] { + return this.allReferences + // filter out unresolved references + .filter((reference) => { + return !reference.resolveFailed; + }); + } + + /** + * Get list of all valid child references as URIs. + */ + public get allValidReferencesUris(): readonly URI[] { + return this.allValidReferences + .map(child => child.uri); + } + + /** + * Check if the current reference points to a given resource. + */ + public sameUri(otherUri: URI): boolean { + 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(PROMP_SNIPPET_FILE_EXTENSION); + } + + /** + * Check if the current reference points to a prompt snippet file. + */ + public get isPromptSnippet(): boolean { + return BasePromptParser.isPromptSnippet(this.uri); + } + + /** + * Returns a string representation of this object. + */ + public override toString(): string { + return `prompt:${this.uri.path}`; + } + + /** + * @inheritdoc + */ + public override dispose() { + if (this.disposed) { + return; + } + + this.disposeReferences(); + this._onUpdate.fire(); + + super.dispose(); + } +} + +/** + * Prompt file reference object represents any file reference inside prompt + * text contents. For instanve the file variable(`#file:/path/to/file.md`) + * or a markdown link(`[#file:file.md](/path/to/file.md)`). + */ +export class PromptFileReference extends BasePromptParser implements IPromptFileReference { + public readonly type = 'file'; + + public readonly range = this.token.range; + public readonly path: string = this.token.path; + public readonly text: string = this.token.text; + + constructor( + public readonly token: FileReference | MarkdownLink, + dirname: URI, + seenReferences: string[] = [], + @IInstantiationService initService: IInstantiationService, + @IConfigurationService configService: IConfigurationService, + @ILogService logService: ILogService, + ) { + const fileUri = extUri.resolvePath(dirname, token.path); + const provider = initService.createInstance(FilePromptContentProvider, fileUri); + + super(provider, seenReferences, initService, configService, logService); + } + + /** + * Returns a string representation of this object. + */ + public override toString() { + const prefix = (this.token instanceof FileReference) + ? FileReference.TOKEN_START + : 'md-link:'; + + return `${prefix}${this.uri.path}`; + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts new file mode 100644 index 000000000000..ca47af1ed50e --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../../../base/common/uri.js'; +import { BasePromptParser } from './basePromptParser.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; + +/** + * Class capable of parsing prompt syntax out of a provided file, + * including all the nested child file references it may have. + */ +export class FilePromptParser extends BasePromptParser { + constructor( + uri: URI, + seenReferences: string[] = [], + @IInstantiationService initService: IInstantiationService, + @IConfigurationService configService: IConfigurationService, + @ILogService logService: ILogService, + ) { + const contentsProvider = initService.createInstance(FilePromptContentProvider, uri); + super(contentsProvider, seenReferences, initService, configService, logService); + } + + /** + * Returns a string representation of this object. + */ + public override toString() { + return `file-prompt:${this.uri.path}`; + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts new file mode 100644 index 000000000000..c00847b447b5 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BasePromptParser } from './basePromptParser.js'; +import { ITextModel } from '../../../../../../editor/common/model.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { TextModelContentsProvider } from '../contentProviders/textModelContentsProvider.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; + +/** + * Class capable of parsing prompt syntax out of a provided text + * model, including all the nested child file references it may have. + */ +export class TextModelPromptParser extends BasePromptParser { + constructor( + model: ITextModel, + seenReferences: string[] = [], + @IInstantiationService initService: IInstantiationService, + @IConfigurationService configService: IConfigurationService, + @ILogService logService: ILogService, + ) { + const contentsProvider = initService.createInstance(TextModelContentsProvider, model); + super(contentsProvider, seenReferences, initService, configService, logService); + } + + /** + * Returns a string representation of this object. + */ + public override toString() { + return `text-model-prompt:${this.uri.path}`; + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts new file mode 100644 index 000000000000..37876a79e575 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../../../base/common/uri.js'; +import { ParseError } from '../../promptFileReferenceErrors.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; + +/** + * List of all available prompt reference types. + */ +type PromptReferenceTypes = 'file'; + +/** + * Interface for a generic prompt reference. + */ +export interface IPromptReference extends IDisposable { + /** + * Type of the prompt reference. + */ + readonly type: PromptReferenceTypes; + /** + * URI component of the associated with this reference. + */ + readonly uri: URI; + + /** + * Flag that indicates if resolving this reference failed. + * The `undefined` means that no attempt to resolve the reference + * was made so far or such an attempt is still in progress. + * + * See also {@linkcode errorCondition}. + */ + readonly resolveFailed: boolean | undefined; + + /** + * If failed to resolve the reference this property contains an error + * object that describes the failure reason. + * + * See also {@linkcode resolveFailed}. + */ + readonly errorCondition: ParseError | undefined; + + /** + * All references that the current reference may have, + * including the all possible nested child references. + */ + allReferences: readonly IPromptFileReference[]; + + /** + * All *valid* references that the current reference may have, + * including the all possible nested child references. + * + * A valid reference is the one that points to an existing resource, + * without creating a circular reference loop or having any other + * issues that would make the reference resolve logic to fail. + */ + allValidReferences: readonly IPromptFileReference[]; +} + +/** + * The special case of the {@linkcode IPromptReference} that pertains + * to a file resource on the disk. + */ +export interface IPromptFileReference extends IPromptReference { + readonly type: 'file'; +} diff --git a/src/vs/workbench/contrib/chat/test/common/codecs/chatPromptCodec.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptCodec.test.ts similarity index 75% rename from src/vs/workbench/contrib/chat/test/common/codecs/chatPromptCodec.test.ts rename to src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptCodec.test.ts index 01fd02cd1c05..dceacea29d5c 100644 --- a/src/vs/workbench/contrib/chat/test/common/codecs/chatPromptCodec.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptCodec.test.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { VSBuffer } from '../../../../../../base/common/buffer.js'; -import { Range } from '../../../../../../editor/common/core/range.js'; -import { newWriteableStream } from '../../../../../../base/common/stream.js'; -import { TestDecoder } from '../../../../../../editor/test/common/utils/testDecoder.js'; -import { FileReference } from '../../../common/codecs/chatPromptCodec/tokens/fileReference.js'; -import { ChatPromptCodec } from '../../../common/codecs/chatPromptCodec/chatPromptCodec.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { ChatPromptDecoder, TChatPromptToken } from '../../../common/codecs/chatPromptCodec/chatPromptDecoder.js'; +import { VSBuffer } from '../../../../../../../base/common/buffer.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; +import { newWriteableStream } from '../../../../../../../base/common/stream.js'; +import { TestDecoder } from '../../../../../../../editor/test/common/utils/testDecoder.js'; +import { ChatPromptCodec } from '../../../../common/promptSyntax/codecs/chatPromptCodec.js'; +import { FileReference } from '../../../../common/promptSyntax/codecs/tokens/fileReference.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { ChatPromptDecoder, TChatPromptToken } from '../../../../common/promptSyntax/codecs/chatPromptDecoder.js'; /** * A reusable test utility that asserts that a `ChatPromptDecoder` instance diff --git a/src/vs/workbench/contrib/chat/test/common/codecs/chatPromptDecoder.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptDecoder.test.ts similarity index 76% rename from src/vs/workbench/contrib/chat/test/common/codecs/chatPromptDecoder.test.ts rename to src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptDecoder.test.ts index 56f4e30059cd..1f73d7febae4 100644 --- a/src/vs/workbench/contrib/chat/test/common/codecs/chatPromptDecoder.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptDecoder.test.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { VSBuffer } from '../../../../../../base/common/buffer.js'; -import { Range } from '../../../../../../editor/common/core/range.js'; -import { newWriteableStream } from '../../../../../../base/common/stream.js'; -import { TestDecoder } from '../../../../../../editor/test/common/utils/testDecoder.js'; -import { FileReference } from '../../../common/codecs/chatPromptCodec/tokens/fileReference.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; -import { ChatPromptDecoder, TChatPromptToken } from '../../../common/codecs/chatPromptCodec/chatPromptDecoder.js'; +import { VSBuffer } from '../../../../../../../base/common/buffer.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; +import { newWriteableStream } from '../../../../../../../base/common/stream.js'; +import { TestDecoder } from '../../../../../../../editor/test/common/utils/testDecoder.js'; +import { FileReference } from '../../../../common/promptSyntax/codecs/tokens/fileReference.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { MarkdownLink } from '../../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; +import { ChatPromptDecoder, TChatPromptToken } from '../../../../common/promptSyntax/codecs/chatPromptDecoder.js'; /** * A reusable test utility that asserts that a `ChatPromptDecoder` instance diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/contentProviders/filePromptContentsProvider.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/contentProviders/filePromptContentsProvider.test.ts new file mode 100644 index 000000000000..42b2933defce --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/contentProviders/filePromptContentsProvider.test.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { VSBuffer } from '../../../../../../../base/common/buffer.js'; +import { Schemas } from '../../../../../../../base/common/network.js'; +import { randomInt } from '../../../../../../../base/common/numbers.js'; +import { assertDefined } from '../../../../../../../base/common/types.js'; +import { wait } from '../../../../../../../base/test/common/testUtils.js'; +import { ReadableStream } from '../../../../../../../base/common/stream.js'; +import { IFileService } from '../../../../../../../platform/files/common/files.js'; +import { FileService } from '../../../../../../../platform/files/common/fileService.js'; +import { NullPolicyService } from '../../../../../../../platform/policy/common/policy.js'; +import { Line } from '../../../../../../../editor/common/codecs/linesCodec/tokens/line.js'; +import { ILogService, NullLogService } from '../../../../../../../platform/log/common/log.js'; +import { LinesDecoder } from '../../../../../../../editor/common/codecs/linesCodec/linesDecoder.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; +import { ConfigurationService } from '../../../../../../../platform/configuration/common/configurationService.js'; +import { InMemoryFileSystemProvider } from '../../../../../../../platform/files/common/inMemoryFilesystemProvider.js'; +import { FilePromptContentProvider } from '../../../../common/promptSyntax/contentProviders/filePromptContentsProvider.js'; +import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; + +suite('FilePromptContentsProvider', function () { + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + setup(async () => { + const nullPolicyService = new NullPolicyService(); + const nullLogService = testDisposables.add(new NullLogService()); + const nullFileService = testDisposables.add(new FileService(nullLogService)); + const nullConfigService = testDisposables.add(new ConfigurationService( + URI.file('/config.json'), + nullFileService, + nullPolicyService, + nullLogService, + )); + instantiationService = testDisposables.add(new TestInstantiationService()); + + const fileSystemProvider = testDisposables.add(new InMemoryFileSystemProvider()); + testDisposables.add(nullFileService.registerProvider(Schemas.file, fileSystemProvider)); + + instantiationService.stub(IFileService, nullFileService); + instantiationService.stub(ILogService, nullLogService); + instantiationService.stub(IConfigurationService, nullConfigService); + }); + + test('provides contents of a file', async function () { + const fileService = instantiationService.get(IFileService); + + const fileName = `file-${randomInt(10000)}.prompt.md`; + const fileUri = URI.file(`/${fileName}`); + + if (await fileService.exists(fileUri)) { + await fileService.del(fileUri); + } + await fileService.writeFile(fileUri, VSBuffer.fromString('Hello, world!')); + await wait(5); + + const contentsProvider = testDisposables.add(instantiationService.createInstance( + FilePromptContentProvider, + fileUri, + )); + + let streamOrError: ReadableStream | Error | undefined; + testDisposables.add(contentsProvider.onContentChanged((event) => { + streamOrError = event; + })); + contentsProvider.start(); + + await wait(25); + + assertDefined( + streamOrError, + 'The `streamOrError` must be defined.', + ); + + assert( + !(streamOrError instanceof Error), + `Provider must produce a byte stream, got '${streamOrError}'.`, + ); + + const stream = new LinesDecoder(streamOrError); + + const receivedLines = await stream.consumeAll(); + assert.strictEqual( + receivedLines.length, + 1, + 'Must read the correct number of lines from the provider.', + ); + + const expectedLine = new Line(1, 'Hello, world!'); + const receivedLine = receivedLines[0]; + assert( + receivedLine.equals(expectedLine), + `Expected to receive '${expectedLine}', got '${receivedLine}'.`, + ); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts similarity index 60% rename from src/vs/workbench/contrib/chat/test/common/promptFileReference.test.ts rename to src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index e281ff697fc2..589fb8dc40e1 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -4,22 +4,29 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { URI } from '../../../../../base/common/uri.js'; -import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { Schemas } from '../../../../../base/common/network.js'; -import { isWindows } from '../../../../../base/common/platform.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { IFileService } from '../../../../../platform/files/common/files.js'; -import { FileService } from '../../../../../platform/files/common/fileService.js'; -import { NullPolicyService } from '../../../../../platform/policy/common/policy.js'; -import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; -import { PromptFileReference, TErrorCondition } from '../../common/promptFileReference.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { ConfigurationService } from '../../../../../platform/configuration/common/configurationService.js'; -import { InMemoryFileSystemProvider } from '../../../../../platform/files/common/inMemoryFilesystemProvider.js'; -import { FileOpenFailed, RecursiveReference, NonPromptSnippetFile } from '../../common/promptFileReferenceErrors.js'; -import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { VSBuffer } from '../../../../../../base/common/buffer.js'; +import { Schemas } from '../../../../../../base/common/network.js'; +import { extUri } from '../../../../../../base/common/resources.js'; +import { isWindows } from '../../../../../../base/common/platform.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { IPromptFileReference } from '../../../common/promptSyntax/parsers/types.js'; +import { FileService } from '../../../../../../platform/files/common/fileService.js'; +import { NullPolicyService } from '../../../../../../platform/policy/common/policy.js'; +import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; +import { TErrorCondition } from '../../../common/promptSyntax/parsers/basePromptParser.js'; +import { FileReference } from '../../../common/promptSyntax/codecs/tokens/fileReference.js'; +import { FilePromptParser } from '../../../common/promptSyntax/parsers/filePromptParser.js'; +import { wait, waitRandom, randomBoolean } from '../../../../../../base/test/common/testUtils.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +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'; /** * Represents a file system node. @@ -46,32 +53,25 @@ interface IFolder extends IFilesystemNode { * Represents a file reference with an expected * error condition value for testing purposes. */ -class ExpectedReference extends PromptFileReference { +class ExpectedReference { + /** + * URI component of the expected reference. + */ + public readonly uri: URI; + constructor( - uri: URI, - public readonly error: TErrorCondition | undefined, + dirname: URI, + public readonly lineToken: FileReference, + public readonly errorCondition?: TErrorCondition, ) { - const nullLogService = new NullLogService(); - const nullPolicyService = new NullPolicyService(); - const nullFileService = new FileService(nullLogService); - const nullConfigService = new ConfigurationService( - URI.file('/config.json'), - nullFileService, - nullPolicyService, - nullLogService, - ); - super(uri, nullLogService, nullFileService, nullConfigService); - - this._register(nullFileService); - this._register(nullConfigService); + this.uri = extUri.resolvePath(dirname, lineToken.path); } /** - * Override the error condition getter to - * return the provided expected error value. + * String representation of the expected reference. */ - public override get errorCondition() { - return this.error; + public toString(): string { + return `file-prompt:${this.uri.path}`; } } @@ -83,17 +83,11 @@ class TestPromptFileReference extends Disposable { private readonly fileStructure: IFolder, private readonly rootFileUri: URI, private readonly expectedReferences: ExpectedReference[], - @ILogService private readonly logService: ILogService, @IFileService private readonly fileService: IFileService, - @IConfigurationService private readonly configService: IConfigurationService, + @IInstantiationService private readonly initService: IInstantiationService, ) { super(); - // ensure all the expected references are disposed - for (const expectedReference of this.expectedReferences) { - this._register(expectedReference); - } - // create in-memory file system const fileSystemProvider = this._register(new InMemoryFileSystemProvider()); this._register(this.fileService.registerProvider(Schemas.file, fileSystemProvider)); @@ -109,35 +103,38 @@ class TestPromptFileReference extends Disposable { this.fileStructure, ); + // randomly test with and without delay to ensure that the file + // reference resolution is not suseptible to race conditions + if (randomBoolean()) { + await waitRandom(5); + } + // start resolving references for the specified root file - const rootReference = this._register(new PromptFileReference( - this.rootFileUri, - this.logService, - this.fileService, - this.configService, - )); + const rootReference = this._register( + this.initService.createInstance( + FilePromptParser, + this.rootFileUri, + [], + ), + ).start(); + + // nested child references are resolved asynchronously in + // the background and the process can take some time to complete + await wait(50); // resolve the root file reference including all nested references - const resolvedReferences = (await rootReference.resolve(true)).flatten(); - - assert.strictEqual( - resolvedReferences.length, - this.expectedReferences.length, - [ - `\nExpected(${this.expectedReferences.length}): [\n ${this.expectedReferences.join('\n ')}\n]`, - `Received(${resolvedReferences.length}): [\n ${resolvedReferences.join('\n ')}\n]`, - ].join('\n') - ); + const resolvedReferences: readonly (IPromptFileReference | undefined)[] = rootReference.allReferences; for (let i = 0; i < this.expectedReferences.length; i++) { const expectedReference = this.expectedReferences[i]; const resolvedReference = resolvedReferences[i]; assert( - resolvedReference.equals(expectedReference), + (resolvedReference) && + (resolvedReference.uri.toString() === expectedReference.uri.toString()), [ - `Expected ${i}th resolved reference to be ${expectedReference}`, - `got ${resolvedReference}.`, + `Expected ${i}th resolved reference URI to be '${expectedReference.uri}'`, + `got '${resolvedReference?.uri}'.`, ].join(', '), ); @@ -160,6 +157,15 @@ class TestPromptFileReference extends Disposable { ].join(', '), ); } + + assert.strictEqual( + resolvedReferences.length, + this.expectedReferences.length, + [ + `\nExpected(${this.expectedReferences.length}): [\n ${this.expectedReferences.join('\n ')}\n]`, + `Received(${resolvedReferences.length}): [\n ${resolvedReferences.join('\n ')}\n]`, + ].join('\n') + ); } /** @@ -193,6 +199,28 @@ class TestPromptFileReference extends Disposable { } } +/** + * Create expected file reference for testing purposes. + * + * @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. + */ +const createTestFileReference = ( + filePath: string, + lineNumber: number, + startColumnNumber: number, +): FileReference => { + const range = new Range( + lineNumber, + startColumnNumber, + lineNumber, + startColumnNumber + `#file:${filePath}`.length, + ); + + return new FileReference(range, filePath); +}; + suite('PromptFileReference (Unix)', function () { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -250,7 +278,7 @@ suite('PromptFileReference (Unix)', function () { children: [ { name: 'file4.prompt.md', - contents: 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference\n\nand some non-prompt #file:./some-non-prompt-file.js', + 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', }, { name: 'file.txt', @@ -261,7 +289,7 @@ suite('PromptFileReference (Unix)', function () { children: [ { name: 'another-file.prompt.md', - contents: 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)', + contents: 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)', }, { name: 'one_more_file_just_in_case.prompt.md', @@ -269,10 +297,6 @@ suite('PromptFileReference (Unix)', function () { }, ], }, - { - name: 'some-non-prompt-file.js', - contents: 'some-non-prompt-file.js contents', - }, ], }, ], @@ -287,43 +311,46 @@ suite('PromptFileReference (Unix)', function () { * The expected references to be resolved. */ [ - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './file2.prompt.md'), - undefined, - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/file3.prompt.md'), - undefined, - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md'), - undefined, - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder/file.txt'), + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 2, 14), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1'), + createTestFileReference( + `/${rootFolderName}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md`, + 3, + 26, + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭'), + createTestFileReference('../file.txt', 1, 35), new NonPromptSnippetFile( URI.joinPath(rootUri, './folder1/some-other-folder/file.txt'), - 'Ughh oh!', + 'Ughh oh, that is not a prompt file!', ), - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder/file4.prompt.md'), - undefined, - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder/some-non-existing/file.prompt.md'), + ), + new ExpectedReference( + rootUri, + createTestFileReference('./folder1/some-other-folder/file4.prompt.md', 3, 14), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + createTestFileReference('./some-non-existing/file.prompt.md', 1, 30), new FileOpenFailed( URI.joinPath(rootUri, './folder1/some-other-folder/some-non-existing/file.prompt.md'), - 'Some error message.', + 'Failed to open non-existring prompt snippets file', ), - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder/some-non-prompt-file.js'), - new NonPromptSnippetFile( - URI.joinPath(rootUri, './folder1/some-other-folder/some-non-prompt-file.js'), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + createTestFileReference('./some-non-prompt-file.md', 5, 13), + new FileOpenFailed( + URI.joinPath(rootUri, './folder1/some-other-folder/some-non-prompt-file.md'), 'Oh no!', ), - )), + ), ] )); @@ -400,25 +427,25 @@ suite('PromptFileReference (Unix)', function () { * The expected references to be resolved. */ [ - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './file2.prompt.md'), - undefined, - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/file3.prompt.md'), - undefined, - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md'), - undefined, - )), + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 2, 9), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1'), + createTestFileReference( + `${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md`, + 3, + 23, + ), + ), /** - * This reference should be resolved as - * a recursive reference error condition. - * (the absolute reference case) + * This reference should be resolved with a recursive + * reference error condition. (the absolute reference case) */ - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './file2.prompt.md'), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭'), + createTestFileReference(`${rootFolder}/file2.prompt.md`, 2, 6), new RecursiveReference( URI.joinPath(rootUri, './file2.prompt.md'), [ @@ -428,29 +455,36 @@ suite('PromptFileReference (Unix)', function () { '/infinite-recursion/file2.prompt.md', ], ), - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder/file4.prompt.md'), + ), + new ExpectedReference( + rootUri, + createTestFileReference('./folder1/some-other-folder/file4.prompt.md', 3, 14), undefined, - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-non-existing/file.prompt.md'), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + createTestFileReference('../some-non-existing/file.prompt.md', 1, 30), new FileOpenFailed( URI.joinPath(rootUri, './folder1/some-non-existing/file.prompt.md'), - 'Some error message.', + 'Uggh ohh!', + ), + ), + new ExpectedReference( + rootUri, + createTestFileReference( + `${rootFolder}/folder1/some-other-folder/file5.prompt.md`, + 5, + 1, ), - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder/file5.prompt.md'), undefined, - )), + ), /** - * This reference should be resolved as - * a recursive reference error condition. - * (the relative reference case) + * This reference should be resolved with a recursive + * reference error condition. (the relative reference case) */ - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './file2.prompt.md'), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + createTestFileReference('../../file2.prompt.md', 1, 36), new RecursiveReference( URI.joinPath(rootUri, './file2.prompt.md'), [ @@ -459,14 +493,15 @@ suite('PromptFileReference (Unix)', function () { '/infinite-recursion/file2.prompt.md', ], ), - )), - testDisposables.add(new ExpectedReference( - URI.joinPath(rootUri, './file1.md'), + ), + new ExpectedReference( + rootUri, + createTestFileReference('./file1.md', 6, 2), new NonPromptSnippetFile( URI.joinPath(rootUri, './file1.md'), 'Uggh oh!', ), - )), + ), ] )); From 36a79b5ce6bc386e36be5ec32c2f5a0741341e00 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:24:18 +0100 Subject: [PATCH 0566/3587] Git - move stage/unstage commands to the `...` menu (#237966) --- extensions/git/package.json | 54 ++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index db47053cd32c..a05c557a2b13 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -2075,16 +2075,6 @@ "group": "navigation", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInNotebookTextDiffEditor && resourceScheme =~ /^git$|^file$/" }, - { - "command": "git.stage", - "group": "navigation@1", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && git.activeResourceHasUnstagedChanges" - }, - { - "command": "git.unstage", - "group": "navigation@1", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && git.activeResourceHasStagedChanges" - }, { "command": "git.openChange", "group": "navigation@2", @@ -2101,30 +2091,50 @@ "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && editorLangId == git-commit" }, { - "command": "git.stageSelectedRanges", + "command": "git.stashApplyEditor", + "alt": "git.stashPopEditor", + "group": "navigation@1", + "when": "config.git.enabled && !git.missing && resourceScheme == git-stash" + }, + { + "command": "git.stashDropEditor", + "group": "navigation@2", + "when": "config.git.enabled && !git.missing && resourceScheme == git-stash" + }, + { + "command": "git.stage", + "group": "2_git@1", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && git.activeResourceHasUnstagedChanges" + }, + { + "command": "git.unstage", + "group": "2_git@2", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && git.activeResourceHasStagedChanges" + }, + { + "command": "git.stage", "group": "2_git@1", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file" }, { - "command": "git.unstageSelectedRanges", + "command": "git.stageSelectedRanges", "group": "2_git@2", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == git" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file" }, { - "command": "git.revertSelectedRanges", + "command": "git.unstage", "group": "2_git@3", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == git" }, { - "command": "git.stashApplyEditor", - "alt": "git.stashPopEditor", - "group": "navigation@1", - "when": "config.git.enabled && !git.missing && resourceScheme == git-stash" + "command": "git.unstageSelectedRanges", + "group": "2_git@4", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == git" }, { - "command": "git.stashDropEditor", - "group": "navigation@2", - "when": "config.git.enabled && !git.missing && resourceScheme == git-stash" + "command": "git.revertSelectedRanges", + "group": "2_git@5", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && isInDiffRightEditor && !isEmbeddedDiffEditor && resourceScheme == file" } ], "editor/context": [ From d8f1111c6c69e8345061e783f93dd24df9a57a08 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 15 Jan 2025 12:23:11 -0500 Subject: [PATCH 0567/3587] expose env for zsh + test --- .../terminal.shellIntegration.test.ts | 19 +++++++++++++++++-- .../common/scripts/shellIntegration-rc.zsh | 15 +++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts index 8f094091f962..e644b0cf8824 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts @@ -30,7 +30,7 @@ import { assertNoRpc } from '../utils'; disposables.length = 0; }); - function createTerminalAndWaitForShellIntegration(): Promise<{ terminal: Terminal; shellIntegration: TerminalShellIntegration }> { + function createTerminalAndWaitForShellIntegration(shellPath?: string): Promise<{ terminal: Terminal; shellIntegration: TerminalShellIntegration }> { return new Promise<{ terminal: Terminal; shellIntegration: TerminalShellIntegration }>(resolve => { disposables.push(window.onDidChangeTerminalShellIntegration(e => { if (e.terminal === terminal) { @@ -42,7 +42,7 @@ import { assertNoRpc } from '../utils'; })); const terminal = platform() === 'win32' ? window.createTerminal() - : window.createTerminal({ shellPath: '/bin/bash' }); + : window.createTerminal({ shellPath: shellPath ?? '/bin/bash' }); terminal.show(); }); } @@ -102,8 +102,23 @@ import { assertNoRpc } from '../utils'; ok(shellIntegration.env.PATH); ok(shellIntegration.env.PATH.length > 0, 'env.PATH should have a length greater than 0'); }); + + test.skip('Test if zsh env is set', async () => { + const { shellIntegration } = await createTerminalAndWaitForShellIntegration('/bin/zsh'); + await new Promise(r => { + disposables.push(window.onDidChangeTerminalShellIntegration(e => { + if (e.shellIntegration.env) { + r(); + } + })); + }); + ok(shellIntegration.env); + ok(shellIntegration.env.PATH); + ok(shellIntegration.env.PATH.length > 0, 'env.PATH should have a length greater than 0'); + }); } + test('execution events should fire in order when a command runs', async () => { const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration(); const events: string[] = []; diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index 54a13d575f16..9e85f892b28c 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -111,6 +111,17 @@ __vsc_update_cwd() { builtin printf '\e]633;P;Cwd=%s\a' "$(__vsc_escape_value "${PWD}")" } +__vsc_update_env() { + builtin printf '\e]633;EnvStart;%s;\a' $__vsc_nonce + for var in ${(k)parameters}; do + if printenv "$var" >/dev/null 2>&1; then + value=$(builtin printf '%s' "${(P)var}") + builtin printf '\e]633;EnvEntry;%s;%s;%s\a' "$var" "$(__vsc_escape_value "$value")" $__vsc_nonce + fi + done + builtin printf '\e]633;EnvEnd;%s;\a' $__vsc_nonce +} + __vsc_command_output_start() { builtin printf '\e]633;E;%s;%s\a' "$(__vsc_escape_value "${__vsc_current_command}")" $__vsc_nonce builtin printf '\e]633;C\a' @@ -139,6 +150,8 @@ __vsc_command_complete() { builtin printf '\e]633;D;%s\a' "$__vsc_status" fi __vsc_update_cwd + # Is there stable/insider flag in zsh? + __vsc_update_env } if [[ -o NOUNSET ]]; then @@ -173,6 +186,8 @@ __vsc_precmd() { # non null __vsc_update_prompt fi + # TODO: Is there stable/insider flag in zsh? + __vsc_update_env } __vsc_preexec() { From e8d359e0aa78fb11f12a0293a89f16a8c9c2466f Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Wed, 15 Jan 2025 18:34:24 +0100 Subject: [PATCH 0568/3587] Improve the updating of the current inline completion when the user modifies the file and hide it when necessary (#237964) * Improve the updating of the current inline completion when the user modifies the file and hide it when necessary * Use an OffsetEdit only for inline edits * Skip failing integration test --- .../src/singlefolder-tests/proxy.test.ts | 2 +- .../browser/model/computeGhostText.ts | 2 +- .../browser/model/inlineCompletionsModel.ts | 13 ++ .../browser/model/inlineCompletionsSource.ts | 128 +++++++++++++++++- .../browser/model/provideInlineCompletions.ts | 16 +++ 5 files changed, 152 insertions(+), 9 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts index 37f886b145b8..9e29c8e6f282 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts @@ -77,7 +77,7 @@ import { middleware, Straightforward } from 'straightforward'; } }); - test('basic auth', async () => { + test.skip('basic auth', async () => { const url = 'https://example.com'; // Need to use non-local URL because local URLs are excepted from proxying. const user = 'testuser'; const pass = 'testpassword'; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/computeGhostText.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/computeGhostText.ts index f5bf0689028c..2e9b53e49a1c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/computeGhostText.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/computeGhostText.ts @@ -159,7 +159,7 @@ function deletedCharacters(changes: readonly IDiffChange[]): number { * * The parenthesis are preprocessed to ensure that they match correctly. */ -function smartDiff(originalValue: string, newValue: string, smartBracketMatching: boolean): (readonly IDiffChange[]) | undefined { +export function smartDiff(originalValue: string, newValue: string, smartBracketMatching: boolean): (readonly IDiffChange[]) | undefined { if (originalValue.length > 5000 || newValue.length > 5000) { // We don't want to work on strings that are too big return undefined; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index 2d39e288ecc7..dd2d478b27dd 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -98,6 +98,19 @@ export class InlineCompletionsModel extends Disposable { } } })); + this._register(autorun(reader => { + /** @description handle text edits collapsing */ + const inlineCompletions = this._source.inlineCompletions.read(reader); + if (!inlineCompletions) { + return; + } + for (const inlineCompletion of inlineCompletions.inlineCompletions) { + const singleEdit = inlineCompletion.toSingleTextEdit(reader); + if (singleEdit.isEmpty) { + this.stop(); + } + } + })); this._register(autorun(reader => { this._editorObs.versionId.read(reader); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index 79b3b8faacb3..5de9c2f31a90 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -7,13 +7,15 @@ import { CancellationToken, CancellationTokenSource } from '../../../../../base/ import { equalsIfDefined, itemEquals } from '../../../../../base/common/equals.js'; import { matchesSubString } from '../../../../../base/common/filters.js'; import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, IReader, ITransaction, derivedOpts, disposableObservableValue, observableFromEvent, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { IObservable, IReader, ISettableObservable, ITransaction, derivedOpts, disposableObservableValue, observableFromEvent, observableValue, transaction } from '../../../../../base/common/observable.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; +import { OffsetEdit, SingleOffsetEdit } from '../../../../common/core/offsetEdit.js'; +import { OffsetRange } from '../../../../common/core/offsetRange.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; import { SingleTextEdit } from '../../../../common/core/textEdit.js'; @@ -23,6 +25,8 @@ import { ILanguageConfigurationService } from '../../../../common/languages/lang import { EndOfLinePreference, ITextModel } from '../../../../common/model.js'; import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js'; import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js'; +import { IModelContentChange, IModelContentChangedEvent } from '../../../../common/textModelEvents.js'; +import { smartDiff } from './computeGhostText.js'; import { InlineCompletionItem, InlineCompletionProviderResult, provideInlineCompletions } from './provideInlineCompletions.js'; import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; @@ -53,8 +57,15 @@ export class InlineCompletionsSource extends Disposable { ) { super(); - this._register(this._textModel.onDidChangeContent(() => { + this._register(this._textModel.onDidChangeContent((e) => { this._updateOperation.clear(); + + const inlineCompletions = this.inlineCompletions.get(); + if (inlineCompletions) { + transaction(tx => { + inlineCompletions.acceptTextModelChangeEvent(e, tx); + }); + } })); } @@ -260,6 +271,12 @@ export class UpToDateInlineCompletions implements IDisposable { ); } + public acceptTextModelChangeEvent(e: IModelContentChangedEvent, tx: ITransaction) { + for (const inlineCompletion of this._inlineCompletions) { + inlineCompletion.acceptTextModelChangeEvent(e, tx); + } + } + public clone(): this { this._refCount++; return this; @@ -310,10 +327,20 @@ export class InlineCompletionWithUpdatedRange { } private readonly _updatedRange = derivedOpts({ owner: this, equalsFn: Range.equalsRange }, reader => { - this._modelVersion.read(reader); - return this._textModel.getDecorationRange(this.decorationId); + if (this._inlineEdit.read(reader)) { + const edit = this.toSingleTextEdit(reader); + return (edit.isEmpty ? null : edit.range); + } else { + this._modelVersion.read(reader); + return this._textModel.getDecorationRange(this.decorationId); + } }); + /** + * This will be null for ghost text completions + */ + private _inlineEdit: ISettableObservable; + constructor( public readonly inlineCompletion: InlineCompletionItem, public readonly decorationId: string, @@ -321,14 +348,100 @@ export class InlineCompletionWithUpdatedRange { private readonly _modelVersion: IObservable, public readonly request: UpdateRequest, ) { + const inlineCompletions = this.inlineCompletion.source.inlineCompletions.items; + if (inlineCompletions.length > 0 && inlineCompletions[inlineCompletions.length - 1].isInlineEdit) { + this._inlineEdit = observableValue(this, this._toIndividualEdits(this.inlineCompletion.range, this.inlineCompletion.insertText)); + } else { + this._inlineEdit = observableValue(this, null); + } + } + + private _toIndividualEdits(range: Range, _replaceText: string): OffsetEdit { + const originalText = this._textModel.getValueInRange(range); + const replaceText = _replaceText.replace(/\r\n|\r|\n/g, this._textModel.getEOL()); + const diffs = smartDiff(originalText, replaceText, false); + const startOffset = this._textModel.getOffsetAt(range.getStartPosition()); + if (!diffs || diffs.length === 0) { + return new OffsetEdit( + [new SingleOffsetEdit(OffsetRange.ofStartAndLength(startOffset, originalText.length), replaceText)] + ); + } + return new OffsetEdit( + diffs.map(diff => { + const originalRange = OffsetRange.ofStartAndLength(startOffset + diff.originalStart, diff.originalLength); + const modifiedText = replaceText.substring(diff.modifiedStart, diff.modifiedStart + diff.modifiedLength); + return new SingleOffsetEdit(originalRange, modifiedText); + }) + ); + } + + public acceptTextModelChangeEvent(e: IModelContentChangedEvent, tx: ITransaction): void { + const offsetEdit = this._inlineEdit.get(); + if (!offsetEdit) { + return; + } + const newEdits = offsetEdit.edits.map(edit => acceptTextModelChange(edit, e.changes)); + const emptyEdit = newEdits.find(edit => edit.isEmpty); + if (emptyEdit) { + // A change collided with one of our edits, so we will have to drop the completion + this._inlineEdit.set(new OffsetEdit([emptyEdit]), tx); + return; + } + this._inlineEdit.set(new OffsetEdit(newEdits), tx); + + function acceptTextModelChange(edit: SingleOffsetEdit, changes: readonly IModelContentChange[]): SingleOffsetEdit { + let start = edit.replaceRange.start; + let end = edit.replaceRange.endExclusive; + let newText = edit.newText; + for (let i = changes.length - 1; i >= 0; i--) { + const change = changes[i]; + if (change.rangeOffset >= end) { + // the change happens after the completion range + continue; + } + if (change.rangeOffset + change.rangeLength <= start) { + // the change happens before the completion range + start += change.text.length - change.rangeLength; + end += change.text.length - change.rangeLength; + continue; + } + + // The change intersects the completion, so we will have to drop the completion + start = change.rangeOffset; + end = change.rangeOffset; + newText = ''; + } + return new SingleOffsetEdit(new OffsetRange(start, end), newText); + } } public toInlineCompletion(reader: IReader | undefined): InlineCompletionItem { - return this.inlineCompletion.withRange(this._updatedRange.read(reader) ?? emptyRange); + const singleTextEdit = this.toSingleTextEdit(reader); + return this.inlineCompletion.withRangeInsertTextAndFilterText(singleTextEdit.range, singleTextEdit.text, singleTextEdit.text); } public toSingleTextEdit(reader: IReader | undefined): SingleTextEdit { - return new SingleTextEdit(this._updatedRange.read(reader) ?? emptyRange, this.inlineCompletion.insertText); + this._modelVersion.read(reader); + const offsetEdit = this._inlineEdit.read(reader); + if (!offsetEdit) { + return new SingleTextEdit(this._updatedRange.read(reader) ?? emptyRange, this.inlineCompletion.insertText); + } + + const startOffset = offsetEdit.edits[0].replaceRange.start; + const endOffset = offsetEdit.edits[offsetEdit.edits.length - 1].replaceRange.endExclusive; + const overallOffsetRange = new OffsetRange(startOffset, endOffset); + const overallLnColRange = Range.fromPositions( + this._textModel.getPositionAt(overallOffsetRange.start), + this._textModel.getPositionAt(overallOffsetRange.endExclusive) + ); + let text = this._textModel.getValueInRange(overallLnColRange); + for (let i = offsetEdit.edits.length - 1; i >= 0; i--) { + const edit = offsetEdit.edits[i]; + const relativeStartOffset = edit.replaceRange.start - startOffset; + const relativeEndOffset = edit.replaceRange.endExclusive - startOffset; + text = text.substring(0, relativeStartOffset) + edit.newText + text.substring(relativeEndOffset); + } + return new SingleTextEdit(overallLnColRange, text); } public isVisible(model: ITextModel, cursorPosition: Position, reader: IReader | undefined): boolean { @@ -382,7 +495,8 @@ export class InlineCompletionWithUpdatedRange { } private _toFilterTextReplacement(reader: IReader | undefined): SingleTextEdit { - return new SingleTextEdit(this._updatedRange.read(reader) ?? emptyRange, this.inlineCompletion.filterText); + const inlineCompletion = this.toInlineCompletion(reader); + return new SingleTextEdit(inlineCompletion.range, inlineCompletion.filterText); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index d6b357f0b142..d9d1de0342d8 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -363,6 +363,7 @@ export class InlineCompletionItem { */ readonly source: InlineCompletionList, ) { + // TODO: these statements are no-ops filterText = filterText.replace(/\r\n|\r/g, '\n'); insertText = filterText.replace(/\r\n|\r/g, '\n'); } @@ -389,6 +390,21 @@ export class InlineCompletionItem { ); } + public withRangeInsertTextAndFilterText(updatedRange: Range, updatedInsertText: string, updatedFilterText: string): InlineCompletionItem { + return new InlineCompletionItem( + updatedFilterText, + this.command, + this.shownCommand, + updatedRange, + updatedInsertText, + this.snippetInfo, + this.cursorShowRange, + this.additionalTextEdits, + this.sourceInlineCompletion, + this.source, + ); + } + public hash(): string { return JSON.stringify({ insertText: this.insertText, range: this.range.toString() }); } From 7fadaa1ddc1c03dcd9a58fab063dac74284eb63e Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 15 Jan 2025 10:17:33 -0800 Subject: [PATCH 0569/3587] [instructions]: make the instruction attachment icon to have the default color (#237980) --- src/vs/workbench/contrib/chat/browser/media/chat.css | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 5b11f23b492c..12a8ce771838 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -941,9 +941,6 @@ have to be updated for changes to the rules above, or to support more deeply nes color: inherit; text-decoration: none; } -.chat-attached-context .chat-prompt-instructions-attachment .monaco-icon-label::before { - color: var(--vscode-notificationsWarningIcon-foreground); -} .chat-attached-context .chat-prompt-instructions-attachment .chat-implicit-hint { opacity: 0.7; font-size: .9em; From 3699bd55bf55ecf24a90df7334930449221f3b7e Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Wed, 15 Jan 2025 10:53:51 -0800 Subject: [PATCH 0570/3587] Show tooltip by adding title attribute for HTTP/HTTPS schemes (#237983) --- .../contrib/welcomeGettingStarted/browser/gettingStarted.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 9bf4d3ff8d9a..7e0597591871 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -1386,7 +1386,8 @@ export class GettingStartedPage extends EditorPane { } } } else { - const link = this.instantiationService.createInstance(Link, p, node, { opener: (href) => this.runStepCommand(href) }); + const nodeWithTitle: ILink = matchesScheme(node.href, Schemas.http) || matchesScheme(node.href, Schemas.https) ? { ...node, title: node.href } : node; + const link = this.instantiationService.createInstance(Link, p, nodeWithTitle, { opener: (href) => this.runStepCommand(href) }); this.detailsPageDisposables.add(link); } } From 1b5b9ae4b222665e360a95e9d6f9a4f255f2be94 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 15 Jan 2025 11:09:38 -0800 Subject: [PATCH 0571/3587] Fix codicon sizing in rendered chat markdown The codicon is currently rendered at a fixed size instead of adapting the the actual text size --- src/vs/workbench/contrib/chat/browser/media/chat.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 12a8ce771838..1be9de5c8681 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -200,6 +200,10 @@ margin-bottom: 8px; } +.interactive-item-container .value .rendered-markdown .codicon { + font-size: inherit; +} + .interactive-item-container .value .rendered-markdown blockquote { margin: 0px; padding: 0px 16px 0 10px; From bfe7f28f8eb63d8bba51e69e772e8795396033f2 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 15 Jan 2025 20:43:55 +0100 Subject: [PATCH 0572/3587] Improve the logic combining compound log entries by finding index while insertion (#237986) --- .../output/common/outputChannelModel.ts | 87 +++++++++++++++---- .../services/output/common/output.ts | 17 ++++ 2 files changed, 88 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/output/common/outputChannelModel.ts b/src/vs/workbench/contrib/output/common/outputChannelModel.ts index 156e39095444..96a3e02d518d 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModel.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModel.ts @@ -21,9 +21,10 @@ import { Range } from '../../../../editor/common/core/range.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { ILogger, ILoggerService, ILogService } from '../../../../platform/log/common/log.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { LOG_MIME, OutputChannelUpdateMode, parseLogEntryAt } from '../../../services/output/common/output.js'; +import { ILogEntry, LOG_MIME, logEntryIterator, OutputChannelUpdateMode } from '../../../services/output/common/output.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { TextModel } from '../../../../editor/common/model/textModel.js'; +import { binarySearch } from '../../../../base/common/arrays.js'; export interface IOutputChannelModel extends IDisposable { readonly onDispose: Event; @@ -204,27 +205,81 @@ class MultiFileContentProvider extends Disposable implements IContentProvider { private combineLogEntries(outputs: { content: string; name: string }[]): string { - const logEntries: [number, string][] = []; + outputs = outputs.filter(output => !!output.content); - for (const { content, name } of outputs) { + if (outputs.length === 0) { + return ''; + } + + const timestamps: number[] = []; + const contents: string[] = []; + const process = (model: ITextModel, logEntry: ILogEntry, name: string): [number, string] => { + const lineContent = model.getLineContent(logEntry.range.endLineNumber); + const content = `${lineContent.substring(0, logEntry.timestampRange.endColumn - 1)} [${name}]${lineContent.substring(logEntry.timestampRange.endColumn - 1)}`; + return [logEntry.timestamp, content]; + }; + + const model = this.instantiationService.createInstance(TextModel, outputs[0].content, LOG_MIME, TextModel.DEFAULT_CREATION_OPTIONS, null); + try { + for (const [timestamp, content] of logEntryIterator(model, (e) => process(model, e, outputs[0].name))) { + timestamps.push(timestamp); + contents.push(content); + } + } finally { + model.dispose(); + } + + for (let index = 1; index < outputs.length; index++) { + const { content, name } = outputs[index]; const model = this.instantiationService.createInstance(TextModel, content, LOG_MIME, TextModel.DEFAULT_CREATION_OPTIONS, null); - for (let lineNumber = 1; lineNumber <= model.getLineCount(); lineNumber++) { - const logEntry = parseLogEntryAt(model, lineNumber); - if (!logEntry) { - continue; + try { + const iterator = logEntryIterator(model, (e) => process(model, e, name)); + let next = iterator.next(); + while (!next.done) { + const [timestamp, content] = next.value; + const timestampsToAdd = [timestamp]; + const contentsToAdd = [content]; + + let insertionIndex; + + // If the timestamp is greater than or equal to the last timestamp, + // we can just append all the entries at the end + if (timestamp >= timestamps[timestamps.length - 1]) { + insertionIndex = timestamps.length; + for (next = iterator.next(); !next.done; next = iterator.next()) { + timestampsToAdd.push(next.value[0]); + contentsToAdd.push(next.value[1]); + } + } + else { + if (timestamp <= timestamps[0]) { + // If the timestamp is less than or equal to the first timestamp + // then insert at the beginning + insertionIndex = 0; + } else { + // Otherwise, find the insertion index + const idx = binarySearch(timestamps, timestamp, (a, b) => a - b); + insertionIndex = idx < 0 ? ~idx : idx; + } + + // Collect all entries that have a timestamp less than or equal to the timestamp at the insertion index + for (next = iterator.next(); !next.done && next.value[0] <= timestamps[insertionIndex]; next = iterator.next()) { + timestampsToAdd.push(next.value[0]); + contentsToAdd.push(next.value[1]); + } + } + + contents.splice(insertionIndex, 0, ...contentsToAdd); + timestamps.splice(insertionIndex, 0, ...timestampsToAdd); } - lineNumber = logEntry.range.endLineNumber; - const lineContent = model.getLineContent(lineNumber); - const content = `${lineContent.substring(0, logEntry.timestampRange.endColumn - 1)} [${name}]${lineContent.substring(logEntry.timestampRange.endColumn - 1)}`; - logEntries.push([logEntry.timestamp, content]); + } finally { + model.dispose(); } } - let result = ''; - for (const [, content] of logEntries.sort((a, b) => a[0] - b[0])) { - result += content + '\n'; - } - return result; + // Add a newline at the end + contents.push(''); + return contents.join('\n'); } } diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index 719ad37e79cd..0de5b386b368 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -312,6 +312,23 @@ export function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntr return null; } +/** + * Iterator for log entries from a model with a processing function. + * + * @param model - The text model containing the log entries. + * @param process - A function to process each log entry. + * @returns An iterable iterator for processed log entries. + */ +export function* logEntryIterator(model: ITextModel, process: (logEntry: ILogEntry) => T): IterableIterator { + for (let lineNumber = 1; lineNumber <= model.getLineCount(); lineNumber++) { + const logEntry = parseLogEntryAt(model, lineNumber); + if (logEntry) { + yield process(logEntry); + lineNumber = logEntry.range.endLineNumber; + } + } +} + function parseLogLevel(level: string): LogLevel { switch (level.toLowerCase()) { case 'trace': From 06e2304623138938677badf99a6d88f47df9b25f Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:16:33 +0100 Subject: [PATCH 0573/3587] Add line replacement functionality to inline edits view (#237971) line replacement view --- .../browser/view/inlineEdits/view.ts | 71 ++++++++---- .../view/inlineEdits/wordReplacementView.ts | 106 +++++++++++++----- 2 files changed, 129 insertions(+), 48 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index b9ae59af7088..c632f5e67c06 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -11,6 +11,7 @@ import { observableCodeEditor } from '../../../../../browser/observableCodeEdito import { EditorOption } from '../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../common/core/lineRange.js'; import { Position } from '../../../../../common/core/position.js'; +import { Range } from '../../../../../common/core/range.js'; import { SingleTextEdit, StringText } from '../../../../../common/core/textEdit.js'; import { TextLength } from '../../../../../common/core/textLength.js'; import { DetailedLineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../../../../../common/diff/rangeMapping.js'; @@ -113,7 +114,7 @@ export class InlineEditsView extends Disposable { private readonly _inlineDiffViewState = derived(this, reader => { const e = this._uiState.read(reader); if (!e) { return undefined; } - if (e.state.kind === 'wordReplacements') { + if (e.state.kind === 'wordReplacements' || e.state.kind === 'lineReplacement') { return undefined; } return { @@ -130,10 +131,14 @@ export class InlineEditsView extends Disposable { if (e.range.isEmpty()) { return store.add(this._instantiationService.createInstance(WordInsertView, this._editorObs, e)); } else { - return store.add(this._instantiationService.createInstance(WordReplacementView, this._editorObs, e)); + return store.add(this._instantiationService.createInstance(WordReplacementView, this._editorObs, e, [e])); } }).recomputeInitiallyAndOnChange(this._store); + protected readonly _lineReplacementView = mapObservableArrayCached(this, this._uiState.map(s => s?.state.kind === 'lineReplacement' ? [s.state] : []), (e, store) => { // TODO: no need for map here, how can this be done with observables + return store.add(this._instantiationService.createInstance(WordReplacementView, this._editorObs, e.edit, e.replacements)); + }).recomputeInitiallyAndOnChange(this._store); + private readonly _useGutterIndicator = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useGutterIndicator); protected readonly _indicator = this._register(autorunWithStore((reader, store) => { @@ -176,23 +181,51 @@ export class InlineEditsView extends Disposable { if (diff.length === 1 && diff[0].original.length === 1 && diff[0].modified.length === 1) { const inner = diff.flatMap(d => d.innerChanges!); - const originalText = edit.originalText.getValueOfRange(inner[0].originalRange); - const modifiedText = newText.getValueOfRange(inner[0].modifiedRange); - if (inner.every( - m => (m.originalRange.isEmpty() && this._useWordInsertionView.read(reader) === 'whenPossible' - || !m.originalRange.isEmpty() && this._useWordReplacementView.read(reader) === 'whenPossible') - && TextLength.ofRange(m.originalRange).columnCount < 100 - && TextLength.ofRange(m.modifiedRange).columnCount < 100 - // If multiple word replacements, check that they are all the same - && edit.originalText.getValueOfRange(m.originalRange) === originalText - && newText.getValueOfRange(m.modifiedRange) === modifiedText - )) { - return { - kind: 'wordReplacements' as const, - replacements: inner.map(i => - new SingleTextEdit(i.originalRange, modifiedText) - ) - }; + const canUseWordReplacementView = inner.every(m => ( + m.originalRange.isEmpty() && this._useWordInsertionView.read(reader) === 'whenPossible' || + !m.originalRange.isEmpty() && this._useWordReplacementView.read(reader) === 'whenPossible' + )); + + if (canUseWordReplacementView) { + const allInnerModifiedTexts = inner.map(m => newText.getValueOfRange(m.modifiedRange)); + const allInnerOriginalTexts = inner.map(m => edit.originalText.getValueOfRange(m.originalRange)); + const allInnerEditsAreTheSame = allInnerModifiedTexts.every(text => text === allInnerModifiedTexts[0]) && allInnerOriginalTexts.every(text => text === allInnerOriginalTexts[0]); + const allInnerChangesNotToLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); + + if (allInnerEditsAreTheSame && allInnerChangesNotToLong) { + return { + kind: 'wordReplacements' as const, + replacements: inner.map(i => + new SingleTextEdit(i.originalRange, allInnerModifiedTexts[0]) + ) + }; + } else { + function getPrefixTrimLength(originalLine: string, editedLine: string) { + let startTrim = 0; + while (originalLine[startTrim] === editedLine[startTrim] && originalLine[startTrim] === ' ') { + startTrim++; + } + return startTrim; + } + + const replacements = inner.map((m, i) => new SingleTextEdit(m.originalRange, allInnerModifiedTexts[i])); + + const originalLine = edit.originalText.getLineAt(edit.originalLineRange.startLineNumber); + const editedLine = newText.getLineAt(edit.modifiedLineRange.startLineNumber); + const trimLength = Math.min(getPrefixTrimLength(originalLine, editedLine), replacements[0].range.startColumn - 1); + + const textEdit = edit.lineEdit.toSingleTextEdit(edit.originalText); + const lineEdit = new SingleTextEdit( + new Range(textEdit.range.startLineNumber, textEdit.range.startColumn + trimLength, textEdit.range.endLineNumber, textEdit.range.endColumn), + textEdit.text.slice(trimLength) + ); + + return { + kind: 'lineReplacement' as const, + edit: lineEdit, + replacements, + }; + } } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index 2fdb445f9838..dc24e0bbb972 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { constObservable, derived } from '../../../../../../base/common/observable.js'; +import { constObservable, derived, mapObservableArrayCached } from '../../../../../../base/common/observable.js'; import { editorHoverStatusBarBackground } from '../../../../../../platform/theme/common/colorRegistry.js'; import { registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; import { ObservableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; @@ -53,33 +53,65 @@ export class WordReplacementView extends Disposable { renderLines(new LineSource([tokens]), RenderOptions.fromEditor(this._editor.editor).withSetWidth(false), [], this._line, true); }); + private readonly _editLocations = mapObservableArrayCached(this, constObservable(this._innerEdits), (edit, store) => { + const start = this._editor.observePosition(constObservable(edit.range.getStartPosition()), store); + const end = this._editor.observePosition(constObservable(edit.range.getEndPosition()), store); + return { start, end, edit }; + }).recomputeInitiallyAndOnChange(this._store); + private readonly _layout = derived(this, reader => { this._text.read(reader); - const start = this._start.read(reader); - const end = this._end.read(reader); - if (!start || !end) { + const widgetStart = this._start.read(reader); + const widgetEnd = this._end.read(reader); + + if (!widgetStart || !widgetEnd || widgetStart.x > widgetEnd.x) { return undefined; } + const contentLeft = this._editor.layoutInfoContentLeft.read(reader); const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); - if (start.x > end.x) { - return undefined; - } - const original = Rect.fromLeftTopWidthHeight(start.x + contentLeft - this._editor.scrollLeft.read(reader), start.y, end.x - start.x, lineHeight); + const scrollLeft = this._editor.scrollLeft.read(reader); const w = this._editor.getOption(EditorOption.fontInfo).read(reader).typicalHalfwidthCharacterWidth; - const modified = Rect.fromLeftTopWidthHeight(original.left + 20, original.top + lineHeight + 5, this._edit.text.length * w + 5, original.height); - const background = Rect.hull([original, modified]).withMargin(4); + const modifiedLeftOffset = 20; + const modifiedTopOffset = 5; + + const originalLine = Rect.fromLeftTopWidthHeight(widgetStart.x + contentLeft - scrollLeft, widgetStart.y, widgetEnd.x - widgetStart.x, lineHeight); + const modifiedLine = Rect.fromLeftTopWidthHeight(originalLine.left + modifiedLeftOffset, originalLine.top + lineHeight + modifiedTopOffset, this._edit.text.length * w + 5, originalLine.height); + const background = Rect.hull([originalLine, modifiedLine]).withMargin(4); + + let textLengthDelta = 0; + const editLocations = this._editLocations.read(reader); + const innerEdits = []; + for (const editLocation of editLocations) { + const editStart = editLocation.start.read(reader); + const editEnd = editLocation.end.read(reader); + const edit = editLocation.edit; + + if (!editStart || !editEnd || editStart.x > editEnd.x) { + return; + } + + const original = Rect.fromLeftTopWidthHeight(editStart.x + contentLeft - scrollLeft, editStart.y, editEnd.x - editStart.x, lineHeight); + const modified = Rect.fromLeftTopWidthHeight(original.left + modifiedLeftOffset + textLengthDelta * w, original.top + lineHeight + modifiedTopOffset, edit.text.length * w + 5, original.height); + + textLengthDelta += edit.text.length - (edit.range.endColumn - edit.range.startColumn); + + innerEdits.push({ original, modified }); + } + + const lowerBackground = background.intersectVertical(new OffsetRange(originalLine.bottom, Number.MAX_SAFE_INTEGER)); + const lowerText = new Rect(lowerBackground.left + modifiedLeftOffset + 6, lowerBackground.top + modifiedTopOffset, lowerBackground.right, lowerBackground.bottom); // TODO: left seems slightly off? zooming? return { - original, - modified, + originalLine, + modifiedLine, background, - lowerBackground: background.intersectVertical(new OffsetRange(original.bottom, Number.MAX_SAFE_INTEGER)), + innerEdits, + lowerBackground, + lowerText, }; }); - - private readonly _div = n.div({ class: 'word-replacement', }, [ @@ -89,40 +121,55 @@ export class WordReplacementView extends Disposable { return []; } + const edits = layout.read(reader).innerEdits; + return [ n.div({ style: { position: 'absolute', ...rectToProps(reader => layout.read(reader).lowerBackground), borderRadius: '4px', - background: 'var(--vscode-editor-background)' - } + background: 'var(--vscode-editor-background)', + }, }, []), n.div({ style: { position: 'absolute', - ...rectToProps(reader => layout.read(reader).modified), - borderRadius: '4px', padding: '0px', - textAlign: 'center', - background: 'var(--vscode-inlineEdit-modifiedChangedTextBackground)', + boxSizing: 'border-box', + ...rectToProps(reader => layout.read(reader).lowerText), fontFamily: this._editor.getOption(EditorOption.fontFamily), fontSize: this._editor.getOption(EditorOption.fontSize), fontWeight: this._editor.getOption(EditorOption.fontWeight), + pointerEvents: 'none', } - }, [ - this._line, - ]), - n.div({ + }, [this._line]), + ...edits.map(edit => n.div({ style: { position: 'absolute', - ...rectToProps(reader => layout.read(reader).original), + top: edit.modified.top, + left: edit.modified.left, + width: edit.modified.width, + height: edit.modified.height, + borderRadius: '4px', + + background: 'var(--vscode-inlineEdit-modifiedChangedTextBackground)', + pointerEvents: 'none', + } + }), []), + ...edits.map(edit => n.div({ + style: { + position: 'absolute', + top: edit.original.top, + left: edit.original.left, + width: edit.original.width, + height: edit.original.height, borderRadius: '4px', boxSizing: 'border-box', background: 'var(--vscode-inlineEdit-originalChangedTextBackground)', pointerEvents: 'none', } - }, []), + }, [])), n.div({ style: { position: 'absolute', @@ -143,8 +190,8 @@ export class WordReplacementView extends Disposable { fill: 'none', style: { position: 'absolute', - left: derived(reader => layout.read(reader).modified.left - 15), - top: derived(reader => layout.read(reader).modified.top), + left: derived(reader => layout.read(reader).modifiedLine.left - 15), + top: derived(reader => layout.read(reader).modifiedLine.top), } }, [ n.svgElem('path', { @@ -165,6 +212,7 @@ export class WordReplacementView extends Disposable { private readonly _editor: ObservableCodeEditor, /** Must be single-line in both sides */ private readonly _edit: SingleTextEdit, + private readonly _innerEdits: SingleTextEdit[], @ILanguageService private readonly _languageService: ILanguageService, ) { super(); From 37a00332c76db607eed78a3b6f1461ae141aca15 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 15 Jan 2025 21:21:58 +0100 Subject: [PATCH 0574/3587] remove old MappedEditsProvider --- .../typescript-language-features/package.json | 1 - .../mappedCodeEditProvider.ts | 66 ----------- .../src/languageProvider.ts | 1 - .../tsconfig.json | 1 - extensions/vscode-api-tests/package.json | 1 - .../singlefolder-tests/mappedEdits.test.ts | 105 ------------------ src/vs/editor/common/languages.ts | 56 ---------- .../common/services/languageFeatures.ts | 4 +- .../services/languageFeaturesService.ts | 3 +- src/vs/monaco.d.ts | 25 ----- .../api/browser/mainThreadLanguageFeatures.ts | 21 ---- .../workbench/api/common/extHost.api.impl.ts | 5 +- .../workbench/api/common/extHost.protocol.ts | 2 - .../api/common/extHostApiCommands.ts | 19 ---- .../api/common/extHostLanguageFeatures.ts | 66 +---------- .../api/common/extHostTypeConverters.ts | 65 ----------- .../common/mappedEdits.contribution.ts | 51 --------- 17 files changed, 6 insertions(+), 486 deletions(-) delete mode 100644 extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts delete mode 100644 extensions/vscode-api-tests/src/singlefolder-tests/mappedEdits.test.ts delete mode 100644 src/vs/workbench/contrib/mappedEdits/common/mappedEdits.contribution.ts diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index ac6d0487a4c3..f46ec4e619cf 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -10,7 +10,6 @@ "enabledApiProposals": [ "workspaceTrust", "multiDocumentHighlightProvider", - "mappedEditsProvider", "codeActionAI", "codeActionRanges", "editorHoverVerbosityLevel" diff --git a/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts b/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts deleted file mode 100644 index 06ce5557b6c3..000000000000 --- a/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as vscode from 'vscode'; -import { API } from '../tsServer/api'; -import { FileSpan } from '../tsServer/protocol/protocol'; -import { ITypeScriptServiceClient } from '../typescriptService'; -import { conditionalRegistration, requireMinVersion } from './util/dependentRegistration'; -import { Range, WorkspaceEdit } from '../typeConverters'; -import { DocumentSelector } from '../configuration/documentSelector'; - -class TsMappedEditsProvider implements vscode.MappedEditsProvider { - constructor( - private readonly client: ITypeScriptServiceClient - ) { } - - async provideMappedEdits(document: vscode.TextDocument, codeBlocks: string[], context: vscode.MappedEditsContext, token: vscode.CancellationToken): Promise { - if (!this.isEnabled()) { - return; - } - - const file = this.client.toOpenTsFilePath(document); - if (!file) { - return; - } - - const response = await this.client.execute('mapCode', { - file, - mapping: { - contents: codeBlocks, - focusLocations: context.documents.map(documents => { - return documents.flatMap((contextItem): FileSpan[] => { - const file = this.client.toTsFilePath(contextItem.uri); - if (!file) { - return []; - } - return contextItem.ranges.map((range): FileSpan => ({ file, ...Range.toTextSpan(range) })); - }); - }), - } - }, token); - if (response.type !== 'response' || !response.body) { - return; - } - - return WorkspaceEdit.fromFileCodeEdits(this.client, response.body); - } - - private isEnabled(): boolean { - return vscode.workspace.getConfiguration('typescript').get('experimental.mappedCodeEdits.enabled', false); - } -} - -export function register( - selector: DocumentSelector, - client: ITypeScriptServiceClient, -) { - return conditionalRegistration([ - requireMinVersion(client, API.v540) - ], () => { - const provider = new TsMappedEditsProvider(client); - return vscode.chat.registerMappedEditsProvider(selector.semantic, provider); - }); -} diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index feb47f096830..f2d52f21fa73 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -79,7 +79,6 @@ export default class LanguageProvider extends Disposable { import('./languageFeatures/inlayHints').then(provider => this._register(provider.register(selector, this.description, this.client, this.fileConfigurationManager, this.telemetryReporter))), import('./languageFeatures/jsDocCompletions').then(provider => this._register(provider.register(selector, this.description, this.client, this.fileConfigurationManager))), import('./languageFeatures/linkedEditing').then(provider => this._register(provider.register(selector, this.client))), - import('./languageFeatures/mappedCodeEditProvider').then(provider => this._register(provider.register(selector, this.client))), import('./languageFeatures/organizeImports').then(provider => this._register(provider.register(selector, this.client, this.commandManager, this.fileConfigurationManager, this.telemetryReporter))), import('./languageFeatures/quickFix').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.client.diagnosticsManager, this.telemetryReporter))), import('./languageFeatures/refactor').then(provider => this._register(provider.register(selector, this.client, cachedNavTreeResponse, this.fileConfigurationManager, this.commandManager, this.telemetryReporter))), diff --git a/extensions/typescript-language-features/tsconfig.json b/extensions/typescript-language-features/tsconfig.json index 0b599aecc60c..8ac3a8735b1f 100644 --- a/extensions/typescript-language-features/tsconfig.json +++ b/extensions/typescript-language-features/tsconfig.json @@ -13,7 +13,6 @@ "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.codeActionAI.d.ts", "../../src/vscode-dts/vscode.proposed.codeActionRanges.d.ts", - "../../src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts", "../../src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts", "../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts", "../../src/vscode-dts/vscode.proposed.documentPaste.d.ts", diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 60b35b4dd9e1..d67a7cf4dac9 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -28,7 +28,6 @@ "fsChunks", "interactive", "languageStatusText", - "mappedEditsProvider", "nativeWindowHandle", "notebookCellExecutionState", "notebookDeprecated", diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/mappedEdits.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/mappedEdits.test.ts deleted file mode 100644 index 2fa19a2bd0a8..000000000000 --- a/extensions/vscode-api-tests/src/singlefolder-tests/mappedEdits.test.ts +++ /dev/null @@ -1,105 +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 path from 'path'; -import * as vscode from 'vscode'; -import assert from 'assert'; - -suite('mapped edits provider', () => { - - test('mapped edits does not provide edits for unregistered langs', async function () { - - const uri = vscode.Uri.file(path.join(vscode.workspace.rootPath || '', './myFile.ts')); - - const tsDocFilter = [{ language: 'json' }]; - - const r1 = vscode.chat.registerMappedEditsProvider(tsDocFilter, { - provideMappedEdits: (_doc: vscode.TextDocument, codeBlocks: string[], context: vscode.MappedEditsContext, _token: vscode.CancellationToken) => { - - assert((context as any).selections.length === 1); // context.selections is for backward compat - assert(context.documents.length === 1); - - const edit = new vscode.WorkspaceEdit(); - const text = codeBlocks.join('\n//----\n'); - edit.replace(uri, context.documents[0][0].ranges[0], text); - return edit; - } - }); - await vscode.workspace.openTextDocument(uri); - const result = await vscode.commands.executeCommand>( - 'vscode.executeMappedEditsProvider', - uri, - [ - '// hello', - `function foo() {\n\treturn 1;\n}`, - ], - { - documents: [ - [ - { - uri, - version: 1, - ranges: [ - new vscode.Range(new vscode.Position(0, 0), new vscode.Position(1, 0)) - ] - } - ] - ] - } - ); - r1.dispose(); - - assert(result === null, 'returned null'); - }); - - test('mapped edits provides a single edit replacing the selection', async function () { - - const uri = vscode.Uri.file(path.join(vscode.workspace.rootPath || '', './myFile.ts')); - - const tsDocFilter = [{ language: 'typescript' }]; - - const r1 = vscode.chat.registerMappedEditsProvider(tsDocFilter, { - provideMappedEdits: (_doc: vscode.TextDocument, codeBlocks: string[], context: vscode.MappedEditsContext, _token: vscode.CancellationToken) => { - - const edit = new vscode.WorkspaceEdit(); - const text = codeBlocks.join('\n//----\n'); - edit.replace(uri, context.documents[0][0].ranges[0], text); - return edit; - } - }); - - await vscode.workspace.openTextDocument(uri); - const result = await vscode.commands.executeCommand>( - 'vscode.executeMappedEditsProvider', - uri, - [ - '// hello', - `function foo() {\n\treturn 1;\n}`, - ], - { - documents: [ - [ - { - uri, - version: 1, - ranges: [ - new vscode.Range(new vscode.Position(0, 0), new vscode.Position(1, 0)) - ] - } - ] - ] - } - ); - r1.dispose(); - - assert(result, 'non null response'); - const edits = result.get(uri); - assert(edits.length === 1); - assert(edits[0].range.start.line === 0); - assert(edits[0].range.start.character === 0); - assert(edits[0].range.end.line === 1); - assert(edits[0].range.end.character === 0); - }); -}); diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index cc98d8d2d1e2..b622496c2573 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -2341,62 +2341,6 @@ export interface DocumentDropEditProvider { resolveDocumentDropEdit?(edit: DocumentDropEdit, token: CancellationToken): Promise; } -export interface DocumentContextItem { - readonly uri: URI; - readonly version: number; - readonly ranges: IRange[]; -} - -export interface MappedEditsContext { - /** The outer array is sorted by priority - from highest to lowest. The inner arrays contain elements of the same priority. */ - readonly documents: DocumentContextItem[][]; - /** - * @internal - */ - readonly conversation?: (ConversationRequest | ConversationResponse)[]; -} - -/** - * @internal - */ -export interface ConversationRequest { - readonly type: 'request'; - readonly message: string; -} - -/** - * @internal - */ -export interface ConversationResponse { - readonly type: 'response'; - readonly message: string; - readonly references?: DocumentContextItem[]; -} - -export interface MappedEditsProvider { - /** - * @internal - */ - readonly displayName: string; // internal - - /** - * Provider maps code blocks from the chat into a workspace edit. - * - * @param document The document to provide mapped edits for. - * @param codeBlocks Code blocks that come from an LLM's reply. - * "Apply in Editor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. - * @param context The context for providing mapped edits. - * @param token A cancellation token. - * @returns A provider result of text edits. - */ - provideMappedEdits( - document: model.ITextModel, - codeBlocks: string[], - context: MappedEditsContext, - token: CancellationToken - ): Promise; -} - export interface IInlineEdit { text: string; range: IRange; diff --git a/src/vs/editor/common/services/languageFeatures.ts b/src/vs/editor/common/services/languageFeatures.ts index 13977f1c1f67..0379781a0de2 100644 --- a/src/vs/editor/common/services/languageFeatures.ts +++ b/src/vs/editor/common/services/languageFeatures.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { LanguageFeatureRegistry, NotebookInfoResolver } from '../languageFeatureRegistry.js'; -import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentDropEditProvider, DocumentPasteEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, MappedEditsProvider, MultiDocumentHighlightProvider, NewSymbolNamesProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, InlineEditProvider } from '../languages.js'; +import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentDropEditProvider, DocumentPasteEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, MultiDocumentHighlightProvider, NewSymbolNamesProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, InlineEditProvider } from '../languages.js'; import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; export const ILanguageFeaturesService = createDecorator('ILanguageFeaturesService'); @@ -77,8 +77,6 @@ export interface ILanguageFeaturesService { readonly documentDropEditProvider: LanguageFeatureRegistry; - readonly mappedEditsProvider: LanguageFeatureRegistry; - // -- setNotebookTypeResolver(resolver: NotebookInfoResolver | undefined): void; diff --git a/src/vs/editor/common/services/languageFeaturesService.ts b/src/vs/editor/common/services/languageFeaturesService.ts index 5c8d6de69198..02ac4418841d 100644 --- a/src/vs/editor/common/services/languageFeaturesService.ts +++ b/src/vs/editor/common/services/languageFeaturesService.ts @@ -5,7 +5,7 @@ import { URI } from '../../../base/common/uri.js'; import { LanguageFeatureRegistry, NotebookInfo, NotebookInfoResolver } from '../languageFeatureRegistry.js'; -import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DocumentPasteEditProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, MultiDocumentHighlightProvider, DocumentHighlightProvider, DocumentDropEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, MappedEditsProvider, NewSymbolNamesProvider, InlineEditProvider } from '../languages.js'; +import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DocumentPasteEditProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, MultiDocumentHighlightProvider, DocumentHighlightProvider, DocumentDropEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, NewSymbolNamesProvider, InlineEditProvider } from '../languages.js'; import { ILanguageFeaturesService } from './languageFeatures.js'; import { InstantiationType, registerSingleton } from '../../../platform/instantiation/common/extensions.js'; @@ -45,7 +45,6 @@ export class LanguageFeaturesService implements ILanguageFeaturesService { readonly documentSemanticTokensProvider = new LanguageFeatureRegistry(this._score.bind(this)); readonly documentDropEditProvider = new LanguageFeatureRegistry(this._score.bind(this)); readonly documentPasteEditProvider = new LanguageFeatureRegistry(this._score.bind(this)); - readonly mappedEditsProvider: LanguageFeatureRegistry = new LanguageFeatureRegistry(this._score.bind(this)); private _notebookTypeResolver?: NotebookInfoResolver; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 461852b5196d..c4ed9acd9acf 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -8137,31 +8137,6 @@ declare namespace monaco.languages { provideDocumentRangeSemanticTokens(model: editor.ITextModel, range: Range, token: CancellationToken): ProviderResult; } - export interface DocumentContextItem { - readonly uri: Uri; - readonly version: number; - readonly ranges: IRange[]; - } - - export interface MappedEditsContext { - /** The outer array is sorted by priority - from highest to lowest. The inner arrays contain elements of the same priority. */ - readonly documents: DocumentContextItem[][]; - } - - export interface MappedEditsProvider { - /** - * Provider maps code blocks from the chat into a workspace edit. - * - * @param document The document to provide mapped edits for. - * @param codeBlocks Code blocks that come from an LLM's reply. - * "Apply in Editor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. - * @param context The context for providing mapped edits. - * @param token A cancellation token. - * @returns A provider result of text edits. - */ - provideMappedEdits(document: editor.ITextModel, codeBlocks: string[], context: MappedEditsContext, token: CancellationToken): Promise; - } - export interface IInlineEdit { text: string; range: IRange; diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 98faa3e3814a..753d4e8a51b1 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -1006,13 +1006,6 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } return provider.resolveDocumentOnDropFileData(requestId, dataId); } - - // --- mapped edits - - $registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[], displayName: string): void { - const provider = new MainThreadMappedEditsProvider(displayName, handle, this._proxy, this._uriIdentService); - this._registrations.set(handle, this._languageFeaturesService.mappedEditsProvider.register(selector, provider)); - } } class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider { @@ -1249,17 +1242,3 @@ export class MainThreadDocumentRangeSemanticTokensProvider implements languages. } } -export class MainThreadMappedEditsProvider implements languages.MappedEditsProvider { - - constructor( - public readonly displayName: string, - private readonly _handle: number, - private readonly _proxy: ExtHostLanguageFeaturesShape, - private readonly _uriService: IUriIdentityService, - ) { } - - async provideMappedEdits(document: ITextModel, codeBlocks: string[], context: languages.MappedEditsContext, token: CancellationToken): Promise { - const res = await this._proxy.$provideMappedEdits(this._handle, document.uri, codeBlocks, context, token); - return res ? reviveWorkspaceEditDto(res, this._uriService) : null; - } -} diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index c1919e713638..8e8e6d71054d 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1437,9 +1437,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatVariableResolver'); return extHostChatVariables.registerVariableResolver(extension, id, name, userDescription, modelDescription, isSlow, resolver, fullName, icon?.id); }, - registerMappedEditsProvider(selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider) { + registerMappedEditsProvider(_selector: vscode.DocumentSelector, _provider: vscode.MappedEditsProvider) { checkProposedApiEnabled(extension, 'mappedEditsProvider'); - return extHostLanguageFeatures.registerMappedEditsProvider(extension, selector, provider); + // no longer supported + return { dispose() { } }; }, registerMappedEditsProvider2(provider: vscode.MappedEditsProvider2) { checkProposedApiEnabled(extension, 'mappedEditsProvider'); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 478f2d6669cb..f2b2d33f06bb 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -470,7 +470,6 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $resolvePasteFileData(handle: number, requestId: number, dataId: string): Promise; $resolveDocumentOnDropFileData(handle: number, requestId: number, dataId: string): Promise; $setLanguageConfiguration(handle: number, languageId: string, configuration: ILanguageConfigurationDto): void; - $registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[], displayName: string): void; } export interface MainThreadLanguagesShape extends IDisposable { @@ -2334,7 +2333,6 @@ export interface ExtHostLanguageFeaturesShape { $releaseTypeHierarchy(handle: number, sessionId: string): void; $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise; $releaseDocumentOnDropEdits(handle: number, cacheId: number): void; - $provideMappedEdits(handle: number, document: UriComponents, codeBlocks: string[], context: IMappedEditsContextDto, token: CancellationToken): Promise; $provideInlineEdit(handle: number, document: UriComponents, context: languages.IInlineEditContext, token: CancellationToken): Promise; $freeInlineEdit(handle: number, pid: number): void; } diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 8167b0ca3fa9..1d08ef210822 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -534,25 +534,6 @@ const newCommands: ApiCommand[] = [ ], ApiCommandResult.Void ), - // --- mapped edits - new ApiCommand( - 'vscode.executeMappedEditsProvider', '_executeMappedEditsProvider', 'Execute Mapped Edits Provider', - [ - ApiCommandArgument.Uri, - ApiCommandArgument.StringArray, - new ApiCommandArgument( - 'MappedEditsContext', - 'Mapped Edits Context', - (v: unknown) => typeConverters.MappedEditsContext.is(v), - (v: vscode.MappedEditsContext) => typeConverters.MappedEditsContext.from(v) - ) - ], - new ApiCommandResult( - 'A promise that resolves to a workspace edit or null', - (value) => { - return value ? typeConverters.WorkspaceEdit.to(value) : null; - }) - ), // --- inline chat new ApiCommand( 'vscode.editorChat.start', 'inlineChat.start', 'Invoke a new editor chat session', diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 1594dc6f749d..70e460334a94 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -2166,57 +2166,6 @@ class DocumentDropEditAdapter { } } -class MappedEditsAdapter { - - constructor( - private readonly _documents: ExtHostDocuments, - private readonly _provider: vscode.MappedEditsProvider, - ) { } - - async provideMappedEdits( - resource: UriComponents, - codeBlocks: string[], - context: extHostProtocol.IMappedEditsContextDto, - token: CancellationToken - ): Promise { - - const uri = URI.revive(resource); - const doc = this._documents.getDocument(uri); - - const reviveContextItem = (item: extHostProtocol.IDocumentContextItemDto) => ({ - uri: URI.revive(item.uri), - version: item.version, - ranges: item.ranges.map(r => typeConvert.Range.to(r)), - } satisfies vscode.DocumentContextItem); - - - const usedContext = context.documents.map(docSubArray => docSubArray.map(reviveContextItem)); - - const ctx = { - documents: usedContext, - selections: usedContext[0]?.[0]?.ranges ?? [], // @ulugbekna: this is a hack for backward compatibility - conversation: context.conversation?.map(c => { - if (c.type === 'response') { - return { - type: 'response', - message: c.message, - references: c.references?.map(reviveContextItem) - } satisfies vscode.ConversationResponse; - } else { - return { - type: 'request', - message: c.message, - } satisfies vscode.ConversationRequest; - } - }) - }; - - const mappedEdits = await this._provider.provideMappedEdits(doc, codeBlocks, ctx, token); - - return mappedEdits ? typeConvert.WorkspaceEdit.from(mappedEdits) : null; - } -} - type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | MultiDocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentPasteEditProvider | DocumentFormattingAdapter | RangeFormattingAdapter @@ -2227,7 +2176,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter | InlineValuesAdapter | LinkedEditingRangeAdapter | InlayHintsAdapter | InlineCompletionAdapter - | DocumentDropEditAdapter | MappedEditsAdapter | NewSymbolNamesAdapter | InlineEditAdapter; + | DocumentDropEditAdapter | NewSymbolNamesAdapter | InlineEditAdapter; class AdapterData { constructor( @@ -2941,19 +2890,6 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF this._withAdapter(handle, DocumentDropEditAdapter, adapter => Promise.resolve(adapter.releaseDropEdits(cacheId)), undefined, undefined); } - // --- mapped edits - - registerMappedEditsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider): vscode.Disposable { - const handle = this._addNewAdapter(new MappedEditsAdapter(this._documents, provider), extension); - this._proxy.$registerMappedEditsProvider(handle, this._transformDocumentSelector(selector, extension), extension.displayName ?? extension.name); - return this._createDisposable(handle); - } - - $provideMappedEdits(handle: number, document: UriComponents, codeBlocks: string[], context: extHostProtocol.IMappedEditsContextDto, token: CancellationToken): Promise { - return this._withAdapter(handle, MappedEditsAdapter, adapter => - Promise.resolve(adapter.provideMappedEdits(document, codeBlocks, context, token)), null, token); - } - // --- copy/paste actions registerDocumentPasteEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentPasteEditProvider, metadata: vscode.DocumentPasteProviderMetadata): vscode.Disposable { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 203ebfae86d3..42beaa8feba2 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1619,71 +1619,6 @@ export namespace LanguageSelector { } } -export namespace MappedEditsContext { - - export function is(v: unknown): v is vscode.MappedEditsContext { - return ( - !!v && typeof v === 'object' && - 'documents' in v && - Array.isArray(v.documents) && - v.documents.every( - subArr => Array.isArray(subArr) && - subArr.every(DocumentContextItem.is)) - ); - } - - export function from(extContext: vscode.MappedEditsContext): Dto { - return { - documents: extContext.documents.map((subArray) => - subArray.map(DocumentContextItem.from) - ), - conversation: extContext.conversation?.map(item => ( - (item.type === 'request') ? - { - type: 'request', - message: item.message, - } : - { - type: 'response', - message: item.message, - result: item.result ? ChatAgentResult.from(item.result) : undefined, - references: item.references?.map(DocumentContextItem.from) - } - )) - }; - - } -} - -export namespace DocumentContextItem { - - export function is(item: unknown): item is vscode.DocumentContextItem { - return ( - typeof item === 'object' && - item !== null && - 'uri' in item && URI.isUri(item.uri) && - 'version' in item && typeof item.version === 'number' && - 'ranges' in item && Array.isArray(item.ranges) && item.ranges.every((r: unknown) => r instanceof types.Range) - ); - } - - export function from(item: vscode.DocumentContextItem): Dto { - return { - uri: item.uri, - version: item.version, - ranges: item.ranges.map(r => Range.from(r)), - }; - } - - export function to(item: Dto): vscode.DocumentContextItem { - return { - uri: URI.revive(item.uri), - version: item.version, - ranges: item.ranges.map(r => Range.to(r)), - }; - } -} - export namespace NotebookRange { export function from(range: vscode.NotebookRange): ICellRange { diff --git a/src/vs/workbench/contrib/mappedEdits/common/mappedEdits.contribution.ts b/src/vs/workbench/contrib/mappedEdits/common/mappedEdits.contribution.ts deleted file mode 100644 index 3470efba634f..000000000000 --- a/src/vs/workbench/contrib/mappedEdits/common/mappedEdits.contribution.ts +++ /dev/null @@ -1,51 +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 { CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; -import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; -import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import * as languages from '../../../../editor/common/languages.js'; - -CommandsRegistry.registerCommand( - '_executeMappedEditsProvider', - async ( - accessor: ServicesAccessor, - documentUri: URI, - codeBlocks: string[], - context: languages.MappedEditsContext - ): Promise => { - - const modelService = accessor.get(ITextModelService); - const langFeaturesService = accessor.get(ILanguageFeaturesService); - - const document = await modelService.createModelReference(documentUri); - - let result: languages.WorkspaceEdit | null = null; - - try { - const providers = langFeaturesService.mappedEditsProvider.ordered(document.object.textEditorModel); - - if (providers.length > 0) { - const mostRelevantProvider = providers[0]; - - const cancellationTokenSource = new CancellationTokenSource(); - - result = await mostRelevantProvider.provideMappedEdits( - document.object.textEditorModel, - codeBlocks, - context, - cancellationTokenSource.token - ); - } - } finally { - document.dispose(); - } - - return result; - } -); From f5e4cd6ab21d0e51a68bdf1e0cd163e1b3de5692 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:31:06 +0100 Subject: [PATCH 0575/3587] Highlight only new text ranges (#237990) highlight real ranges --- .../browser/model/inlineCompletionsModel.ts | 15 ++++++++------- .../browser/model/inlineCompletionsSource.ts | 3 ++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index dd2d478b27dd..6e9a3969605f 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -19,6 +19,7 @@ import { EditorOption } from '../../../../common/config/editorOptions.js'; import { CursorColumns } from '../../../../common/core/cursorColumns.js'; import { EditOperation } from '../../../../common/core/editOperation.js'; import { LineRange } from '../../../../common/core/lineRange.js'; +import { OffsetRange } from '../../../../common/core/offsetRange.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; import { Selection } from '../../../../common/core/selection.js'; @@ -31,7 +32,7 @@ import { EndOfLinePreference, IModelDecorationOptions, ITextModel, TrackedRangeS import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js'; import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js'; import { SnippetController2 } from '../../../snippet/browser/snippetController2.js'; -import { addPositions, getEndPositionsAfterApplying, getModifiedRangesAfterApplying, substringPos, subtractPositions } from '../utils.js'; +import { addPositions, getEndPositionsAfterApplying, substringPos, subtractPositions } from '../utils.js'; import { computeGhostText } from './computeGhostText.js'; import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostTextsOrReplacementsEqual } from './ghostText.js'; import { InlineCompletionWithUpdatedRange, InlineCompletionsSource } from './inlineCompletionsSource.js'; @@ -577,6 +578,7 @@ export class InlineCompletionsModel extends Disposable { } let completion: InlineCompletionItem; + const acceptDecorationOffsetRanges: OffsetRange[] = []; const state = this.state.get(); if (state?.kind === 'ghostText') { @@ -586,6 +588,7 @@ export class InlineCompletionsModel extends Disposable { completion = state.inlineCompletion.toInlineCompletion(undefined); } else if (state?.kind === 'inlineEdit') { completion = state.inlineCompletion.toInlineCompletion(undefined); + acceptDecorationOffsetRanges.push(...(state.inlineCompletion.inlineEdit?.getNewTextRanges() ?? [])); } else { return; } @@ -608,18 +611,16 @@ export class InlineCompletionsModel extends Disposable { SnippetController2.get(editor)?.insert(completion.snippetInfo.snippet, { undoStopBefore: false }); } else { const edits = state.edits; - const modifiedRanges = getModifiedRangesAfterApplying(edits); - const selections = modifiedRanges.map(r => Selection.fromPositions(r.getEndPosition())); + const selections = getEndPositionsAfterApplying(edits).map(p => Selection.fromPositions(p)); editor.executeEdits('inlineSuggestion.accept', [ ...edits.map(edit => EditOperation.replace(edit.range, edit.text)), ...completion.additionalTextEdits ]); editor.setSelections(selections, 'inlineCompletionAccept'); - if (state.kind === 'inlineEdit') { - this._acceptCompletionDecorationCollection.set(modifiedRanges.map(r => ({ range: r, options: this._acceptCompletionDecorationOptions }))); - this._acceptCompletionDecorationTimer.value = disposableTimeout(() => this._acceptCompletionDecorationCollection.clear(), 2500); - } + const newTextRanges = acceptDecorationOffsetRanges.map(r => Range.fromPositions(this.textModel.getPositionAt(r.start), this.textModel.getPositionAt(r.endExclusive))); + this._acceptCompletionDecorationCollection.set(newTextRanges.map(r => ({ range: r, options: this._acceptCompletionDecorationOptions }))); + this._acceptCompletionDecorationTimer.value = disposableTimeout(() => this._acceptCompletionDecorationCollection.clear(), 2500); } // Reset before invoking the command, as the command might cause a follow up trigger (which we don't want to reset). diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index 5de9c2f31a90..272646b1bc8a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -339,7 +339,8 @@ export class InlineCompletionWithUpdatedRange { /** * This will be null for ghost text completions */ - private _inlineEdit: ISettableObservable; + public _inlineEdit: ISettableObservable; + public get inlineEdit() { return this._inlineEdit.get(); } constructor( public readonly inlineCompletion: InlineCompletionItem, From a8954090a67ffa46f5f3a4977133ad071fea33b2 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 15 Jan 2025 13:14:22 -0800 Subject: [PATCH 0576/3587] Use `nodenext` for module too --- src/tsconfig.base.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index a817569101c9..fc5247fab0c7 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -1,7 +1,8 @@ { "compilerOptions": { - "module": "es2022", + "module": "nodenext", "moduleResolution": "nodenext", + "moduleDetection": "legacy", "experimentalDecorators": true, "noImplicitReturns": true, "noImplicitOverride": true, From e38038261475f66c6794078ea056f204c1809e55 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 15 Jan 2025 22:20:08 +0100 Subject: [PATCH 0577/3587] support opening all outputs in editor (#237998) --- .../output/browser/output.contribution.ts | 18 +++----- .../electron-sandbox/output.contribution.ts | 46 ------------------- src/vs/workbench/workbench.desktop.main.ts | 3 -- 3 files changed, 7 insertions(+), 60 deletions(-) delete mode 100644 src/vs/workbench/contrib/output/electron-sandbox/output.contribution.ts diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 722776ae86f6..58d802b533d2 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -10,7 +10,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuId, registerAction2, Action2, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { OutputService } from './outputServices.js'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, OUTPUT_FILTER_FOCUS_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT } from '../../../services/output/common/output.js'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, OUTPUT_FILTER_FOCUS_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT } from '../../../services/output/common/output.js'; import { OutputViewPane } from './outputView.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; @@ -99,7 +99,6 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { constructor( @IOutputService private readonly outputService: IOutputService, @IEditorService private readonly editorService: IEditorService, - @IFilesConfigurationService private readonly fileConfigurationService: IFilesConfigurationService, ) { super(); this.registerActions(); @@ -368,11 +367,10 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { isHiddenByDefault: true }], icon: Codicon.goToFile, - precondition: CONTEXT_ACTIVE_FILE_OUTPUT }); } async run(): Promise { - that.openActiveOutputFile(); + that.openActiveOutput(); } })); } @@ -392,11 +390,10 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { isHiddenByDefault: true }], icon: Codicon.emptyWindow, - precondition: CONTEXT_ACTIVE_FILE_OUTPUT }); } async run(): Promise { - that.openActiveOutputFile(AUX_WINDOW_GROUP); + that.openActiveOutput(AUX_WINDOW_GROUP); } })); } @@ -426,12 +423,11 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { })); } - private async openActiveOutputFile(group?: AUX_WINDOW_GROUP_TYPE): Promise { - const fileOutputChannelDescriptor = this.getFileOutputChannelDescriptor(); - if (fileOutputChannelDescriptor) { - await this.fileConfigurationService.updateReadonly(fileOutputChannelDescriptor.files[0], true); + private async openActiveOutput(group?: AUX_WINDOW_GROUP_TYPE): Promise { + const channel = this.outputService.getActiveChannel(); + if (channel) { await this.editorService.openEditor({ - resource: fileOutputChannelDescriptor.files[0], + resource: channel.uri, options: { pinned: true, }, diff --git a/src/vs/workbench/contrib/output/electron-sandbox/output.contribution.ts b/src/vs/workbench/contrib/output/electron-sandbox/output.contribution.ts deleted file mode 100644 index 9790ff104b90..000000000000 --- a/src/vs/workbench/contrib/output/electron-sandbox/output.contribution.ts +++ /dev/null @@ -1,46 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Codicon } from '../../../../base/common/codicons.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; -import { localize2 } from '../../../../nls.js'; -import { registerAction2, Action2, MenuId } from '../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { IsMacNativeContext } from '../../../../platform/contextkey/common/contextkeys.js'; -import { INativeHostService } from '../../../../platform/native/common/native.js'; -import { OUTPUT_VIEW_ID, CONTEXT_ACTIVE_FILE_OUTPUT, IOutputService } from '../../../services/output/common/output.js'; - - -registerAction2(class OpenInConsoleAction extends Action2 { - constructor() { - super({ - id: `workbench.action.openActiveLogOutputFileNative`, - title: localize2('openActiveOutputFileNative', "Open Output in Console"), - menu: [{ - id: MenuId.ViewTitle, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), IsMacNativeContext), - group: 'navigation', - order: 6, - isHiddenByDefault: true - }], - icon: Codicon.goToFile, - precondition: ContextKeyExpr.and(CONTEXT_ACTIVE_FILE_OUTPUT, IsMacNativeContext) - }); - } - - async run(accessor: ServicesAccessor): Promise { - const outputService = accessor.get(IOutputService); - const hostService = accessor.get(INativeHostService); - const channel = outputService.getActiveChannel(); - if (!channel) { - return; - } - const descriptor = outputService.getChannelDescriptors().find(c => c.id === channel.id); - if (descriptor?.files?.length === 1 && descriptor.files[0].scheme === Schemas.file) { - hostService.openExternal(descriptor.files[0].toString(true), 'open'); - } - } -}); diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 755d3c95bebf..6cb01c728741 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -127,9 +127,6 @@ import './contrib/remote/electron-sandbox/remote.contribution.js'; // Configuration Exporter import './contrib/configExporter/electron-sandbox/configurationExportHelper.contribution.js'; -// Output View -import './contrib/output/electron-sandbox/output.contribution.js'; - // Terminal import './contrib/terminal/electron-sandbox/terminal.contribution.js'; From a969b09781affa1b9bbd5d28cc9d3c8d2de4dd68 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 15 Jan 2025 22:33:02 +0100 Subject: [PATCH 0578/3587] clean up: remove watching existence of log file (#238000) --- .../contrib/logs/common/logs.contribution.ts | 58 ++++++------------- 1 file changed, 17 insertions(+), 41 deletions(-) diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 11c7aa061f24..90fae135d50a 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -12,15 +12,12 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as import { IFileService, whenProviderRegistered } from '../../../../platform/files/common/files.js'; import { IOutputChannelRegistry, IOutputService, Extensions } from '../../../services/output/common/output.js'; import { Disposable, DisposableMap, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; -import { CONTEXT_LOG_LEVEL, ILogService, ILoggerResource, ILoggerService, LogLevel, LogLevelToString, isLogLevel } from '../../../../platform/log/common/log.js'; +import { CONTEXT_LOG_LEVEL, ILoggerResource, ILoggerService, LogLevel, LogLevelToString, isLogLevel } from '../../../../platform/log/common/log.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { URI } from '../../../../base/common/uri.js'; import { Event } from '../../../../base/common/event.js'; import { windowLogId, showWindowLogActionId } from '../../../services/log/common/logConstants.js'; -import { createCancelablePromise, timeout } from '../../../../base/common/async.js'; -import { CancellationError, getErrorMessage, isCancellationError } from '../../../../base/common/errors.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { createCancelablePromise } from '../../../../base/common/async.js'; import { IDefaultLogLevelsService } from './defaultLogLevels.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { CounterSet } from '../../../../base/common/map.js'; @@ -61,7 +58,6 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { private readonly loggerDisposables = this._register(new DisposableMap()); constructor( - @ILogService private readonly logService: ILogService, @ILoggerService private readonly loggerService: ILoggerService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IFileService private readonly fileService: IFileService, @@ -149,25 +145,21 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { const disposables = new DisposableStore(); const promise = createCancelablePromise(async token => { await whenProviderRegistered(logger.resource, this.fileService); - try { - await this.whenFileExists(logger.resource, 1, token); - const existingChannel = this.outputChannelRegistry.getChannel(logger.id); - const remoteLogger = existingChannel?.files?.[0].scheme === Schemas.vscodeRemote ? this.loggerService.getRegisteredLogger(existingChannel.files[0]) : undefined; - if (remoteLogger) { - this.deregisterLogChannel(remoteLogger); - } - const hasToAppendRemote = existingChannel && logger.resource.scheme === Schemas.vscodeRemote; - const id = hasToAppendRemote ? `${logger.id}.remote` : logger.id; - const label = hasToAppendRemote ? nls.localize('remote name', "{0} (Remote)", logger.name ?? logger.id) : logger.name ?? logger.id; - this.outputChannelRegistry.registerChannel({ id, label, files: [logger.resource], log: true, extensionId: logger.extensionId }); - disposables.add(toDisposable(() => this.outputChannelRegistry.removeChannel(id))); - if (remoteLogger) { - this.registerLogChannel(remoteLogger); - } - } catch (error) { - if (!isCancellationError(error)) { - this.logService.error('Error while registering log channel', logger.resource.toString(), getErrorMessage(error)); - } + if (token.isCancellationRequested) { + return; + } + const existingChannel = this.outputChannelRegistry.getChannel(logger.id); + const remoteLogger = existingChannel?.files?.[0].scheme === Schemas.vscodeRemote ? this.loggerService.getRegisteredLogger(existingChannel.files[0]) : undefined; + if (remoteLogger) { + this.deregisterLogChannel(remoteLogger); + } + const hasToAppendRemote = existingChannel && logger.resource.scheme === Schemas.vscodeRemote; + const id = hasToAppendRemote ? `${logger.id}.remote` : logger.id; + const label = hasToAppendRemote ? nls.localize('remote name', "{0} (Remote)", logger.name ?? logger.id) : logger.name ?? logger.id; + this.outputChannelRegistry.registerChannel({ id, label, files: [logger.resource], log: true, extensionId: logger.extensionId }); + disposables.add(toDisposable(() => this.outputChannelRegistry.removeChannel(id))); + if (remoteLogger) { + this.registerLogChannel(remoteLogger); } }); disposables.add(toDisposable(() => promise.cancel())); @@ -178,22 +170,6 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { this.loggerDisposables.deleteAndDispose(logger.resource.toString()); } - private async whenFileExists(file: URI, trial: number, token: CancellationToken): Promise { - const exists = await this.fileService.exists(file); - if (exists) { - return; - } - if (token.isCancellationRequested) { - throw new CancellationError(); - } - if (trial > 10) { - throw new Error(`Timed out while waiting for file to be created`); - } - this.logService.debug(`[Registering Log Channel] File does not exist. Waiting for 1s to retry.`, file.toString()); - await timeout(1000, token); - await this.whenFileExists(file, trial + 1, token); - } - private registerShowWindowLogAction(): void { this._register(registerAction2(class ShowWindowLogAction extends Action2 { constructor() { From e3cf0d3034afcb04d68711904de914765beacc5c Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 15 Jan 2025 15:39:57 -0600 Subject: [PATCH 0579/3587] update sounds (#237970) --- .../browser/media/foldedAreas.mp3 | Bin 38080 -> 22528 bytes .../browser/media/quickFixes.mp3 | Bin 38080 -> 29440 bytes .../browser/media/save.mp3 | Bin 33472 -> 29440 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/vs/platform/accessibilitySignal/browser/media/foldedAreas.mp3 b/src/vs/platform/accessibilitySignal/browser/media/foldedAreas.mp3 index 802a7a6751483bf05439600afa450bf96d542185..deaae4622d0bec6dda05410f7713c9d2db9fea8c 100644 GIT binary patch literal 22528 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 delta 9586 zcmY+H1y~ec^!K-b1(xov1pz^Z4vD23mXt7-Zly&~XX#W*U@4`MQc@{(X;47ATLg;` z6jX$L@%R6~@AJI(InSAU&-tFYGtd3Zof&*Rlvo8zQ?&wg6!?Mvn(qI0WaZ`lCnBpX z`@bnUrT@Ppd*1$w$jSq{|C^HjzZ&WE)JteZ;D5vaom%>z|9^r1I*Kyd4eN4rApLOK z8Otj?49&3G{jwgSpwq)iBRAWWnWJRObO9o}1$Q+7oSNK&6=IxSqUnTX4w=MwSlIxr z9!s2ZE>pCJ>{up3Ty|4iOGzeDfL=K(KOeSUBglab^dzv0h0k_pvn}<;qA(olTzLcO zGO>v;Z3d(7ZJ@U{-G>~y#U7CApzWUi8Q+dES7qfVb$-;{jjlsd6!}T}u zYrh*mdCZ_6WgJSu&zyYjkx7^{ddu0}&1FN|9d*@vIR3J@|BlFgR$N~kKFu>;&l=57 zDKnYrij$9Vk)SnYA|sP>_+{CBLMUoTfD88++wDh)TBo6FuWTN6`pWVg^U~I#H6CHAt5~ zc{2)+4JLwJpJ~`pNSrRFD;%MNy5R4MAu7U|QDpcF_^zAKF%)$X51t6XR4F23$RP%B z;u;l^9zF&zE#P8ZD$(Vvrq%fkxrkpm=Tw#2N2T zRfOY!Ajl!DWc+iY)R3Fx5K%Z}1-Ap@`5_yANDg)kM(ir-s;P+mX^YQG>t z7~AN@SWu{HscW*-5>q zbJ7{<&q}5}cz?C&o`YfN!jb+J(oF~wS|G>7NpTy=*#sMc)1%e-nVBgVAar&#FbaQ3 zS6F1E58i7ek{uJ~4SjHl<`-lJ0+mKaZQ(_6qqKGb8}E3ugL|jeRlRKl5%YI01m`Lr z29dk4G*&zO`QqXI#rO(XGkZ$fnf%!V9xqHEo$`^U_elTR(LYy10!gRWz%|I$5{JA! zZ*3;$0w<^cOyQpz0{(fP-VxWg@dDO9zBppu4-<|F#ynLyI_512$_lwicO+B-YFl)InesC^Gi_f?kX>b4OZ4`Nxy*ZR8)}4C`(pezn}GoyAWf@ z$KuoqcS4XpWU^GkQT|7%v#x54kR}bSs!Tm|y=479yz}+ISP^7NC^f!cJX6-EmX4Dq z`Nox|vAP z7phbWY6{%q2(k;}Xq06>bjeZd1MEdH=~;q=K+32Xs1YJ6y}-^jL3iZRPVmu9>?u!Z=}2_R$GqvGfnb%KK_|RdcO0Lv*h|(sJQ#-1oQOyFuvt|!mMePy>2a@P@1fc%t zYU(H`zSv)U+<5b~YnQW=0b{K$sHtE^{_`*J=?}RG;e+qnqKEh-}qzU2BDeB?&42ZIgKVv z15b;78_-IN`X*877s0~ve5|INEwJ{+lx6iY0VX~kK`k2kCvR3sXN91aI8DH}%m0@D z9R7T13FMxvqV;|0_x(1CeL7XLB%~Bz9_a`u!)$;mJQU#WQU=U4Y1v)`9rNUnTRTNV zTuhkRF7az-VeM`UtlpWIhmTxxl}i8h>5(LGDN)$jA_T~|+h;+kub4wu!!=TM_Df4> zMQ4~?oYm7G{hOy4LX@{R`j0cVHbyi6yk$hge1nm0XB!5m5rm$0u6Yl0 zrd^F59+~l9vq3%b?9oc`eOl|g`fE9SbM{@f14~o#cx|pc`pSwJyqg2x3^#62 zQ7~KoER#cLStdH}u|tuWvTOQPy2Z;Ye+#frMe~UWG0^a%lKBtG@WQcCzJkFsmO|{a zZ3DxL6Nz^ZDT+S`XT;)b{=R%8?^8WXDmq@8`L@u+@@rHZ{gDvFgLa74Wqu#T9|rW0 zp^-vVpf625{hbPl>R?L|1#2e{Jz5nB7&||epaZpAZqHc8Rg>H?@xDpeBuv<#CZ*i@ z?&;8(qDgnA69c=-p}>@=1;yj<6~5Y7%t_~Oghbf zb5ai0Oh^ljx5|DeFw4;-ipgR07Xin}Y~V895-^7e0wH)QfS33P5GLAQj*l9QYXvFj zsG$_pcD4nzx31D2sAjhH?6G*JKw``eF-}wl^ra#FK8jSm$NX<|g1(=X9)LayMURs? z>@r*LAo%Q)=bs%jEgFHOH>ade=T9r?RFD5Ri9De3MO^SY#a1|B!<`BvsYjlM>_?8N z>Cpg)ty!dhm0l6R&&`QrJZA{R*g)W4MReeQjIe<{N@<|4679YlE!|E$^bx8Ax>L4B zAtj38q6x_9_`t?amvqySxAxJ~=H;Kg{W!Z8FxX-GM1>E6h+S|R^~JAo&U4{760OY| zO(l0G&3}R?8hfN$q%S1WPxhwLmfTvmggl|j8G_LVKZ0a;Et6AGxtBeC5*f+gYom=QX5nr-~p+^Qn*RW90jxlUww;G!0%!TDF95 zusaX;4IV-T#u9QTLC<2t+yr)M3nV>K^~J6?R_1mbrFH!seJEl}Ct0njmrRnmq$d1@ z;=M!4rwD!&lbq7VA(c~3QUA^BZ)$EY9{U`hzbfqj{m~XVRiuTqH#w_pCB-Zt6lDe6 z!F&TUF`s~I7)~^_nWzmV@^)%*cd=1JabIpuW_v9u!fT6@whsDw< zCLWKwA9a9hhZW(r@M}2Ae;d(VJRHY!&X5e&`@>T_sAohc5rCrG9Mb$rD-iiNR}?aa z(}HSI7i~5#sJTG1EVr@=Fidl$X^kxS?0Qb0d-;S?iO@wf`pr)6&kt^#r%X;bzph74 zl?_REvmMhND&!~B)V<&KD^72+4IYRtU)fpQJ^s@k@h8PIGHk@^(Oc9zXYCQX?pG$# z`$sACSllq%nr#YCJr4?s;X)ztVw%C+v-ty00m`gPuAaYcWItMOmkM~CYbp`|NRM}i&Wf8LNL z-OoSR>ZOs%6Vk9plCoch9|F!px1__n!vTxPK)?d}3Q#5H0aGqsc$u#6kRg1EhBM@m zj29Je_mW2zgQh`|h*4yQGg~G2#jh)TzN_M1Z6UIUUsqX#EariXm)2WAsZ2p2PxRjH zs(jM&sd@O$t&gP{IUvav z9EW`o9HT<>xf}vb1XV~I!2#z?pvFtHsOL)+we;icpVK-<)5^!o#{$;=j={)i91|HW zGrxYd^4s*j&TFEjSa2iC4V3`>2T>^=KE(EmU z(fuxL+d*u1d^g#99mK@i;Ah)Q_w`tN_x3yHryZX!LJ6`O^$z84#^U;AC8i^5RV_Ck zUfjXGX;75Da>0t(IrOoTE6*joCmCykhj;3XYjive_X`I8aSDpe#z{ej4G@%gUj$OE zA)D7%JA19?ftzW76O-EU%`^hf-zDc>=RWl*lmxF;&#yo!UG&?ec935bt$NvUjnnGL{F83O@-4GQ&NGLV zh1-ABE&`;trqU@Emt`)FzLI0^GLb&h<^e8X5&-ZT-W~9OAe7OQEMY(y(K70Og~PU) zwV#tj`ZRdm_ff3$`bBEVt&U^YW`XR^LGyZv??JcJW2wxuQ^?BpY?>q$6`V%c_sKS{ z20v||I9km~eI*^B-!3tpUds~5v=WJhnTeMxqk|SA1ngj{$uDV9v;Tn9lan;Oy8IT&KaVcGDOmOU4X9A z2tarE*NVSrjs7YRjCsx~CirMEw9y-k$myy$nR1KzljhNe%y=kp=jUCQ0Z9j~K=9RY zSPzTwo@$w~c7jzApe66(%-D^*P|7GkCT#*^GA)|Df&7FUX3t0)4=^s-y;Lzh=bwww znU7&rF4r}bf3{^~WAxfO&!21=4om$V{wz4qp|U};>NV~f)l1=TR_xh+b`<>J%81;6 z{9UDKOQ%~mPa}er4Zl6GxVwRowup3;!w*Qv@gyb7UW1286Sc5_E&dsBAF~Ar;_m^( zN%J_kEWvK==|yhO4NP5o@CcFK^Ygg)^`nVvxmR6BN0D`l^;#z^S*k5!eX8P98fTwV zj8lh7EV*|BN0C33UaP5{ppQYl-({gQZyx8}{~8{8`ON2U?TeoQ2i#t<>W^Kv=C@Uz za6Uggn^pC*C6PQTJNIB6Y3z0QhCHm$b50&IuiSRcLG~>tfL#J_&yHEU&CUqH5O7{k z`(XLQs{X;k_Z5M)D?G-o&k-i9@q$in*=U1H;+eW!R1ZWCFQT3X9}vLbh4I^a!fYwO zCFee*Dwy16974-UUB1p`y|GB@KY7LfW>q8P<>&?0-?rli5qm3liSG|+U!AQ5IaE}3 zN8Ee!*T=em_%{7stHbGTN$5;jK#D6lm$xy;M3@M-Ce?W9+$Mn|xv;&Ayk@4p(d7QVLz(qXUi+RwYQg;{g46WQngLDY^%46$mSZ$#aN(RTcxrxG?n zo1D&d02YzKulbEbtg`(m>7hJZvrO_^aYHu_m%>ZB594MnQ1%m{zS`3O%0VB?(h>EI z8TwSjS>DfueU9gt`&!`bY7frs(NR8CCKeb+@t~Zmyq!MU9%PM3q8ae%sr7}Kx|2bx zo$pGK)2)l(RH;Oe_;%1K783q6sl=GeczHWNknhY`YU;Um;P(wW&&BOG1M?@31rE0h zhyU!!y&6!Vp{}3_Z5@=8`pogeKEHgSC1q&3?UzBd>s8`vd!a_wpzPcGb9n_PukwrE z&**);s&M`iN&r{)3n|#noX^S4^cdWJ?4CuBZQ zSkQ~*jkR^9C6JNnl0`evGt!D{?Y|h@npMUoQ8hR@>n%iT!%bVBamV;%-m$*!dh12q zL)CRsV>N82JiCsC+u+_yVU_u|#1ZVz+H5tdCa>yM*IrPH*@%UsG3iq{YI5P%JK8}S z;R^(;LCUZ0N9?p)^cYL0K=NV?nmQ8jXRPK&_3GG1LFimDx6?lc6ABzmO1b3KGH0)} z7~L+&W%xM1+)*fWD=v_9@D)2_H?UW5+4q<3$HScMLXivyo$I21Gdlj7J$n6giQ4b* zSaN(>)&&G#bR6s{d1btO>687UZf5h_u5FmfHB&!@^kuyi`!Uozj`895NjlTqy{Hmh zgYFc@hkyN9hFyxap#JsZ;mw8Gcxt$pJO>1Fk)gOW?aRp0p_tV_hVt?7K&o!|;vN2U zN(26Iq3h<>=L_rG+qD|I*0obcs9H5i&eKI(gEqV3KSuI#2GYDv>zbC?2Gr7jbjUw) zH(vYHsTH5aA|p@ftEKjb*Cha7wE$5prtvJL;Ss!G4Yq%oaXyINax^<~ueElst1xj> zxERO0MhGh<-u%FWqDI9+t)|odVu3vWr0QP8x;_=WLRSc*_Z&yC%J)IWkfy_4mlnN8!f+dC8Ns=Z|%7-l=$9TQoFCGXZ%rDKa4N^+s$yGxaHtZ?sEeA5^f zxT@16<(i?u1bonMYL?-k5keHH0hSQUaU{l@n$g4-MBfgInhyE_V#rT*+$O;*^WkK6^qc~Y3nNW z#?(7s>?wZJKUB-DTQ_)6L5%4TnOFUl<*vbCgta=3>N!xj$}IaL-)$SP)oTLE8Wq%J zv<;g^WyENr5o;QNEhO;#l7j#5B{^G{A<%$J*?@?BZn|G6^9=eAShK^{R>fBZ)@j5A zDsy~fe;(91T6b|!f8Bs?f)SUv5?fq{M>j_O7Yhds~K@w|L|vN!62o-)MoQ zb2LY<%*)^9Tb8X{KGC%oJXSgVc57xTtsk)7OlST?{-bA;{Vnz`0RQ94jV(34J>hXv)2TXK7Q{cmQ z3G`tuH!U#ZFxn!Yr0ZJz&`FxMcLy2>U6(Cf>&arcsp=Po-*Wn26=Z099{b(B2Imt# zoj$m=y;=ZECJWvgg}+~IFy+1f#Et5v*Hx|sRUVO_qHR~Tz{^S}yFqh<17p5l8Lan^ za=pEC!`QL}cEDeXk<6jX*J0=4j(A>X-{DYZ3H>pn*;!cFz~o;J!2Q~aUNE0}vmQ5R z?v!HYs4&mi=Ul6FUPE3>t%Iqt!+YWDRUYcD*T+xfQ>C|gEFKm}e3=kZYN3-^-yDDN z`5?%AR^s=LAt;v5ko&Adrm(7SaPa+hXuMa|JNiulb5Q_o`}*E*cZf!!=)k*7E9zk_ zG#M=GgxS|C;j@m5Ko-;q!Yx!ZJvv;%CMhAe;3p_qVPR_5I8xRRXEcro4%%T}+Aegz zbL&#_-tLLw!i!wql=11^`X#^FyqUVoJUH|ZKh*T!Ws8nv<`=_CmLg61VP2mGvc#5|Jzj9V z&F$a2Bl8f{&0c(hUTQq9;POq5`kepN;?*aX_rMj=$zQTnDDN4q`I}9{019q=i(Wx`-2B9=RbxD zA&69Eb$Fu8M%q?9F+k+1C1mb^w%#X6zv8jx3=MeavLvf>~DVIPLNGX3eoor&{A*LR31+ds_hh!ktZ)$ce(NEae8OHbuy}r zNyEj!!ibsVyq9@lQqRHh$vrM1R?Ud51K94iI;vM6l=kk==KzWYF zoodGVC@Z9-nYOdq^hQQXZK>sl%p@ zbw4w07gfTq(>6(=^y>UAZppMuDGVv7s7$I^=ewxCz9IH#|}wA`OLdZF<= zhorHUdY$=7_0wmo315BJqRTCaW*68ow%FjfzG+r35z8CYOBThpmiFy?Ltn;P&_IQf zNdnckrU$~j`Bz)>WkxGH%t6Jrn*$*W+bc~G2PfMb^Y_|!0y;XEQdeIhq84+>3EFqU znl7u|8M^IMV-Rf=tJ6Sf{D9ySl(TGPxwedr>J5B$9ipKWNk0%7TNN-}V$`0GsMpn? zek__JAXQYT1oloT@L1Yx(kT`HXp#$Nt7cLp&yKezJS=#pV%+ZY)Uz1U}FJFF+-0El^4N}?KlUh#IKFJRS zD|V}=mx2~nL%a%iNQ$LjlTh>sUdjuBDOHnvVcfrbouj-Rf(!ev8+IG@J`-r|`xH*` zpuRwOD9|h@&nC;Sw!;Rj5;d?8)G5(stge<|w9Tu23WQfzS6enOOq$BPZR^kXad@n@ z&FxiI{Z*o|XZ6>|4EOw?m)S}mQkCV#owQ=JHJnOyO%-1CCy{X~tnM&VQOPYHKGigg zDqPTD4`!%rZaWU*4Eo1V8U&##bQzu^W0_o9i3^A`GF~!Sp}kq=0nvjlb}ey3Gk5`U zl`!j&C})=8k^q6|L>NK|DTLL=NAtTfl0i!}u?zzEPu2(%G?^eZ10~cThNu%&Bq&5b zfJS>aH&0TOmyUHMP;sWrk`}&syd;sns!Ux6e$kPMnPQOK4-zxp9MoqDK$SqX<`n$6 zvjK+ibv^re$lN^NoVplS0xA;@!QUlx%QM;c#&~cjL@K+R>a27h49{J2U1v8Z9kHb% zNry&WP7LKS-wi(|d09{N$%S|Gie+|Mc-a+h5b$e{mM_xRQJBzB8-rKhK}QjiG*M5d=tNzm&2NI?|}YY{){nDz^~a>oRcT>+hOg}xU7Cjn zm&sau%oY!p8NmMGoqs?OA!M?iDsV%tz%sWh{C;sJ*2S1lG@b9>rs(l#zMyH#c*l7N z__pfCXFI*s)DeAYTx7+Q(UFmRsZ=?_GM_M++zFolNiQvCdri%+ItnHq?yPqX`<#9! ztb~5gcN8-Iz$L(PS#>5vTqSA2x3sA;KU330g@q%WI4kkD!wKeS)@%5nQ;AJZh4 zZEoKu+!jTw7I_r)(=$n1iN@{Oua9O{*f?!HML27M%Zk32d~<6a@{YREUt%Aj)5p{J z>ssk2kWbp2AiAa9Q2%44SAU59WtrY?xX^dT;K}Cq2JRNr6@`jT7js4Q0~ITcI3A(p z$DiGH?f{vG3bQ#IGC698o>|sh%bu}zi+gKAC!bq8#Z+BfqtX)Mtyv9-o6z|vkf#*W zc*{R*7?zMnY<4mB=+cXQk0>dv&igT}4~K1dU^RPLpz^^vlir?!1Yp6~i`-Hp$p zR-}^EpG%~PNTn3a1$uNDtOEnDCInQZb3oH+{WUkckgUL1Gvqe1nAsjrAEd1nWV%+5 zGsET^P;@~BWN4Ha)cw^tT>76-BeVv@?KMknEVMqdeKu0qb7u14K76pc{467RqTJ#b zY)D*ed)3|XwVcX<@oG`Qcu)qy%bZaAkD(YmT%AE9T8x9}-Mzi<*`vfEj`>3ijP!^N@@e+d zw95GQMT?^HrSX55=IC(hZZnVFWcDmzLmUy(MKk~Ehz zHiIN=f7W*!qW0;kN4i=j$iMc3RK{>VZ>4z42e@34;lAn#TgIV$>4^X=IDymwAApw za~$sE*s~-38SwE?fB|1X1}UHZYDV&p&>dpE(-!`j1JMBZ+^ zx*xQ0cCmD^JU16BYwv~{n+LYs@*?sg=O(ZF&((0&dQUq|xcpQ7lUiGQ*iC!)l+&0R zKCmeKHxU^m?Z3G+A#!?DvJ?PXm6=;u-iNqpL*psGDs ze*T)O<#@cV^l%g^F|G)sX*1bD2T8(Zc5z1Y-+(}p5ig_Xlo%Os+4~U`FwIWJ71}in zMls5 z4$w>tBL$BYTroNofKe5>?+0u_G!axQG-Vhhf23nqa{~0=8JQ^^77=6h*^>39)NCRW z!m&fil(_SH-~Z~lC18rtdJq^HUT@!%2*D&ES5PSs2m_R>D;-}vt{GiC;Zu@8~4FIkRVN{uB^ls!vnED=gdi%>H|){w0X6@wC`s0eLl zR7AE^w8+vzdm=(Ozfqs(cl~;<=Z{~1J)i4)&3(DdxsQA1exKLt^kMwhh4!T0c58=yU3noeOT8>*UWmOBLLgMw(#H(w|(IoHo7$B)hQBvpOS6AAtl?i9hHIAp{*$4twf=u^V`&C{^%nPEy}3F%S^d)+x5MAf z4fOxnFg5(IPJV?0TUlG#{PT(bv^CcMuQ&bQcmIEGEHdF09swQ!9swQ!9swQ!9swQ! z9swQ!9swQ!9)bT90?5xA%CBQFrT==tj&Wa5cBBE^u#(+c)JgW-O8%8L?NX5HGX5~} zm(3`#{Y9vQU80dz{w7(38kDVXW(xntBR_w=`G4UT9DCllA>4p21E0FSJWR{%ej=R4 zPew7wt?|I6dexaRm0@4ms}rJKP51}0+TTxTN7I;N#ut7LN}Tzz>GO*hrL_FwxZ*Yj z1&^zlY8S;ckgBU^;L=Q0)OsU+u}G$EVBYmL2FKR6w`ts${_P40sVxh@})|jl~!7Nu168s-YU)oVWxQ}D$H#` zT)v9cT?=wWJfrLERgF8}8VC0t)4yDTd)|6t(xllVdWJFgZcEG2>aSpM$v#$_zuh`- zs;%+k7z9D)in8D;^2AQ#5d9p!J4CXyF`wiknB4`ZgF7fH8V#Hi?WC)d?t02mK4N6V z{By*C_Q|7a0qhbywLjc$AgOh(>5{>+n(U)1KA)FaU2_Man%}>-j5S&WUXJmq z28UHpsCp+mMNV=+57i#JeT ziHtZl9n+SB=8fyewQET#HFl(cy;2cg@i-T5@VmJ2BZwnX7DRoUJg`lt{BQu2Xiohi z?iPw&acbL7e7m21TsSoXR8w8zq^M7U7?l7@s1|g86g0%%f+nzy!0{-7%ngrPog^5S zR=+=NbiXyrslGTK)i7u#)D&WHto-NANq^C#*FR2pOy1nSBJ>th&tlYN3b_!=W?c_L z?-3NIKuY#`>@(mH`MSWG;5BSBonU@2{I>6 z=m>bpy`9F-pAN%?cXq*Y3_BRb&VkYF8!#E9zy!)Z9Iy=-8gU#y>|b6X;5b3q&xh4s z5-uf1-EQCq1S1;KO*XAND|l^SbGXFT^kkg&3bd~AP*d+yu}0~R#yeGWrU(0#TcYld zVtnNDkFZW$GU%cS?OEEmOJNe}LIx4Uc2cg4eCRt93oUR5JDM~OC6I)hLJDRyOPGe1 zbxZ;3X%d>QnGANrW|SMS5L*=%r%1wl6d_pJk_IbLmKME@(pjGe?|`#9K}&-hT&qI%~K*HI!)de3K{eE^=NW6 zWQ$>`U_m|hzE-vH@@)PI0l`|uBZUtFOL|N{NSr+jjNj%+{e!%oq}}%K2^xHiNWi#B(z`;h82v`a%gGDGoum?pE&?Pibt^3ljV+w%c@N)Zkjn!T|vB#+R*b5re z?jf1SKcD!m)p+~8NSHR)uTVK@jZ}W3&0Bolmbe;;tQ@FcFn@L`X zlm6J-MZXCxymXuop0_2I!_Aqj$IS+IE1SirB0(|cqF7ZRrpB*<$D-Ll4UH!;IDAY% z#gYW*_{`)qXBR26Bj~4b83c5ifEs}a?&oqk|q=7`~sKc%b~F|JIKY1x?5L znf`N|!*pD)2t1C-`FKFg9GqB5U6-tZMNRGPQHYl*vroUl4Q63xz?1Dlpx%qwif@@) z^d|#gTsJkw&xA!J1*s3w(l-+;AD~3Wm&K7~sL=eq2t1K94CcDv6`Su|gox~RAjjSc z_OcT(PV7ODsS7me+Nf#?HNX6Zq$WSe{u?_25aKERG1n9^b+-z)b;at=sSO_8+WH=j%iQi?X zgMJLep|9iP(3Jp80vsoiG|%m$W=F{J))y6z^ONC6I6}nw^Upqco|VO=4C|&_96zU= z`*QGU>AWKjc@;K%)M&bHK?E_Rb?uC8lAcatKhNuH-{_FE$Mx|2EsOK~_r6ab;Q+Lw zLFlG~u&K2;tP*Dqdq3`ht3WxdOmT$;fCdaG8{idm3<0p%IIy=8ibrW+$JnGhtYt?6 zp)*$GkT;|yxT7KC)4hTY$|6K7vS}NC-a2!>IPcPXo%>!Wg+&*Y%yt+bwUQAL<{ts8 zG9=x?%-;;<8wZrW_{ZSJO*8rNvO*#Gfx-DF&SRD{GO^1u7X3*B=xNw$asEj<|CDmr zIb2lLM__UNl4UAdE?7jg*QF$W01@4)Sd<1(Ly^^N`2Zi9g-xY1sVV?VjiO%{26rV7 zS>lJ`o8-1f_whCUKLP}s+MVB9PZwV5jWJz#2_tPAk%`!@3C6+3hP>4t*So^?CbNe3 zy_2PX@+j#IlrjnPn}Unr-?1W3zVkb@!+5U(x=5hOuzs9dU> zEz3#1=9wHbn*Xz(UcuLpssv0kUT!l=dvVC(u-6yirK@j-H2yH8<$sQUJF^x>I;|gR zcMAsNvm~9FdgKqTkBe7ooJsyFyefF1F}#ZVt#ABzXl+FUW8($p(m_>E09IhwH32Go zWm;4!l`cARRPi=-Gq%_XxLg6g8hq*`I(ba_%q+^Q=7DxpIEtLDB0k#DZa%f+i)PsP z^Vm}_uYVjwdS{OwgpZYp>Q&SxaK^7~MLvEh$XvM+YhXEdnARggD_$ z701t-5kgP|dnLE62|wG8kK&@Qs2HLAsbF0kL1MzwoNeKO89aRDrY<+G{b-+K8BgB)C~ zUSFe_-|~IAnmcB_^Sc_hW*zHmTU)m>Z{bT3cURSW?pgB?TTY6gnnAfk79n6QsohL9 z2N|}V98@l@4Q025>A}mH7ST>;U7sHP=pm<-yi4j$(M0a5TMv%8XCM!e`NK5s2W@&4 z%%3n0&@rKWbH0NJGP`VFCL1Csa&EQ@&%JR7`F72L2tEo)^Oq5pveu`eVfl6xU6z0* zlH17uV@p?sF%)1BwfV8h_k;9p;T%zO1=nKMY8woOqbw6(@9|Bk9bsva{-z3cWW)Php>1GNw46tHVa|0!{g%C3d4$WNt^o#PZh zloKplIc$P5&q2Y)M$Z(`fFkJ>7>Pk;R;wA8P$0Ab5FY=A#_Fju%AE&R9%;hSb*3Nb z5?Uuqmc4tY@UC&=WX0tEM$3t^==g6|r9&I8U2Dgw>;FDoMUcV#*>;=ig!Jp2_VnHe z+q9L`{T*d5O53HKb~zB7G!yCHVF^oB_$XZ!1&lDj24%He^*(H2Fq#Raq&OM+2%BE1 zJX6?nI!^J7*p%*j%n8P#KZSIRtHIr)bE!iCAMC}V(|xlstwfRWFvjU+%AEJc^`Cvl zZtaZk{L%95t7qdD(O&fd|J>@5H3jh#7Ji{EwM^VK%&hmp;#zA@Y?R$~hN6%TYH}-r zWM8V=@csJxpR{>3K1K%h%Tu)_er3`E*1KPSgKt2rYIHh8Bx)at8@AYygei`HXCn%L zrGEx7O83=j{C00jT*-vKS)QofhNOw~Hs8qEbaQ$8NVRUME*omg+M}>t$JJXcRY705 zS}c}E>$xHBOu-+x^EDnu0Mw_bOEO?Yw=+qYL?#F|lj& zR=pXItwm;Mzn&uPva_%}(uTJMqv~09u_{-=tEOhR8Z^HsuKdDcKrgC3ex5J*sDH@U z{pCJ|#XYlhK;_hcv;pv@YcrH41I&gmh7(ljbP4qK9aLdb0yKLTL&gNEj?9;h)%KA0 zP_hHf6h#u#T5MF7%q$DzpEnGrb!?beXk5K8x_S~hbN}TF=X2`UY;<+2*D)&$l0`Po z^jK%_tk{bDSnTiX*N*uh1&W$l%X?b$2+&Pb(BN-=GWYbA6^n_bivW5bjAy)76H+M$ z>vr*jX6i`@zY-KgU)$rlK-k8;kxl(D_gvHaRmVBOOt#u15o-39Sc(vpUyJ-&YJ_+o>Co1ch!hHtX%~=TENz8(Gb1F@+OR;b!1;f3vvV>(RYA^;$$MYMf>c8gBW@vOj^n`r!v*VfyiE;C# z=+at?^;PGqYH4MSMpxgtxT&r-JI49tr`vlD zAg2+BXIDm*a%VhBC8FlrTQZ7eJysLUYs`AE=5}Y6V4s}kE^+4uh3~EWVV~kJbMVfL zUrIMhpFhov1U>(G>V0A*KO;VaFQ0)yi!xAB&*I(CDRc}(OaoUy%IAb(`W4^>r=3T6 zFsNonh}$m9`QU_rdu3gEMt7k~mk(6*1CT?yr;cIt#zn^^G=izhpJDgSq`z$35JJlg?<)-&)ND`Jx9RS>WHN zw^!sZ=U>h+laOOr%`nIi34nF01fa3*F|FECzDx`@IWC(YBY+a6yL9Irqwlzn#;LfZ zyEw$`sV_VJcqDC8&j*v(7bom|gUdhc>%6varuf5Ma`YjcfNK1?X8c4ijoTNuEYyuQ zc0~jDo9&dr^fe{p8!h4Rp(CeF53|bPN{%Tk4N71!iX$w*h=*6waX1iU1Kx+H5i`Ye z+0~k&4>Vuei&ZGg^4V?Bbi15&xx2w-WExW|b%1rxZ1mf{i3JPEGYfqqwX`C{2YI)n zxA+N5$8WNdg(Wzfhw;0mbhaWf1gw0JITdd4eMC0+c z1^O%b%kbe$65jbqx+Q5jJ@dW*Qy7rn8IVo`SwfAonq_=&2vrNOfg{qfX277I>aO+-46C67myD+u{U~k8cN?z%nR4lrbK^452Yy||A>X|rk)UUpWybl zaK7uAG)Pm0mZXQrMx?v@l@pZ4tTKd2WCty>ECE+dv}7xhQ1oG@C>z5BWMJ@=#9n}2 zx?A~k{B7)_v2(K3!lgulu6zlH8LapWFt+tnljuPo{kNMf`g4VjdE{e_o+*upEVC=- z`Xe^)gV*cX`LLR#l=7LgM?7{-`Zxz~6CUz@yhzRUqW-0)2kq;e1Mv>Y*P&zdZpW;> zd+!Y~2IvV#y<2HNzH*n8@T#gw(*wQ1eVzHAkM;r#R-%Lw_zX%+F46X*l1C-8t*E(} z5e!vDfX~@!2P7)!&oqi~g{RzGH9Q2|z|vJIt?nG6uR^(r#EUPE;yG{Iww^_@X)`Tv zb*c7yeiVJ}4Qb}g*)d)%PZAqO8CyHe7k#cm7B0>?z|H6jhAD1B#%^&b@F(_4!^5Y< z;Bxj#csX4F2boY6z$FTn#=rmBc-48DEAZIylr!LzP;6< zRMAQ9!xXu!Lw3OgL6+WHi>M+q<(rUaqw_*b-?T_Pnx#i&d!s9*wXWP6F6FxydN=4f z*N=;ficCgZtVJpp9)!Y$7%v0-K(XOy{4iX=5i$u$j)f0Wc4?rg+i??sC1r@2gNmIn z?uzsb$TL~elEt4SvL^0=vUgnBUlioQ%EzTP7?w z5Yn-SW3g-D5R|Xsb>EF}H(dnc&j2xIPVhrU~=-7l~1 zZzDb1h!Jw?<6EB=WJ{0fits$RPehaJ52cQdiSlSdbJwp7fu~N3`5(v!BQmnHDPkGb za^A82g;DU-^8*X^YwYJTXmg@CWbVi6h4e3LrjKXOa-c3J!YOGjuP}%$tw!#=D!0)R%2C%(7h#-@<4u7h2~B7cqulCCW2c6$HZgCQAs{OO*mE zg~2KIW+<5*fOAMja~=hDtjn@xo$(JprD(fbtFZZaepR>ADfw=yS!Ik?)sC$bUaJwz zmum~^T~l8Lk)N@>pIQgr236L}pKk{$_ga}MV2OcN<_AH3;FWlZ)gsVgmW0eVBHUGG z;`F)?AGD&E!EKgLjgBQ|nLw00m<1jgeo^Rw>nM#dhE9bAC?Wz3Z|Wnk?nv!5su_r& z=EcPc1K{qgl;W>Y;rGn)YDm2b+EUYFe`ku=k0D)*OYqQ%i?Z`Zs}N4EJMtP?_-56B z$c!u`ZfqdNuDAW!!_Ruo%D0R-d%S|BLisQ@{o=o9J$|_=vhT6aOLgajAUWqe_F2(0 zWzye{;H5NcfDQ#iG6Xib*I*hW4k%KYP8-=&fn+*f|Dar_D`&`)MPR{ZdTN-Lo6?(zeB~?IqY0`=p zE<08ue9(pJM>-a37x{3ftbzVX_tRH$@HQLn;I(!K=$z!N2NO!u8N6o9eusJ-BB z2sf#DanYY*fI05LUi2sZgBh8Yn3eC0F!wz&IEL~sHxW{l|dP@Mb z+o~86b78XmmFP`VUEWzH1A#7QjwRDKKflH4$VIM&BiRY1d1}i^J|i70pKWb(zUlMS zd#wWGW_k~+)S_K*!xuW_yLIAvo+JzGs_d&mew~BQ zef6H5?V1gR6QCNyx8Ax&JJ2O?2glWLk&kA zbpGJyl;C^s?)9L+Vb+Pn@e>78b{E{>G%~ME-LJjiFeF!LfIadPCSW zy|FzV*XWlB{A2nK_#sjn7ACY~=(Y}G)mR1#R6+^s)tZtd_y(MZ?bx>u$)6)Qh^i7a zXRj^MDVYehio=2~e-rr#Ibe5F%X{nN>A=Js|5~F7f zDT}ABcS=#^p40v+hr7D^oIh@dD~>P>HV$4kioy87p`7oAh0nLbi~i&RjK6y$ntT3} z)@C|J%kdT6?J+CTJGePS6^<}ElL_Ew zmhD!2;Edl%bxKP)%Hf_U#-=6&!;nnEduKaz+#e@i-R-xddc9n0hH>`Wk#3z&9fFt_USRH^6|fc4#(W3zI*G7O&&{_cL^Qq{~5tZqGMD5<7w z5WGenUIh{jCksa47>XsVNJrxUg^EUb$cd&t@f`*`NUG%Mqudngw2HB4Zkb7^z?%BR zAkF-b14kMR`>n(e9k`e<&YW8kPV2V6ggijz-;E;e6AQKSE^Civ8T4zz3q*&a^=h?v zW{__&E;6a{R}@bc@(RCpE`6~rh?p!nvU_|@*^#F&a>v`|n_n~#=Bcjo(W#ws267fnALY4x{| z1`9_-9{DzwWcW_1PQ6z2we3~a6V}nJT(pi>NM|J$&j`3r{q{LhP_{pW* z^voi>t)-(k_AMQkByD)*MH~I&c5YvEtu9zo3Z^0+Z68Ad0g~e!1-5E8UHm*WgjVloS!5`-> zyMD?ewQCv-o8O;ox4ii#H3!SUr9C_MoY~BGF;t{gXB{&6Yar`Z^cKzdJch0A`s#;l zOVn3eV*u+}lpm3u0_9wsnw1b!d~J&#}t(9ujX%i{u7jhK%;$Jl0l z^Ex(@spMjrn|Ur3Hf6F;3K5U2>9s3x)735U{Ic=e_n2HCZH2I$TcQb-Cb%-9=avHx zB=4xonjg{O-b0_?x){iZkKt7oZwLf(YT60+2Wg4)UV=pC$&R&(kq+2Q`*swNf>^Ri zAe9z_!rL#L1y+sJnXiHkFv>ZC9(;yBPmeY`N-mez2~u=xu_+ zgSlayo_B?BGi;B=q%yW(TD!Nd3b;pQ+Fm|p;o8XUdZTk^JU^2dxngr^z&eU}0N#ca zV03X$B!4De8N@F7lMXQE9yjB^=R`!=N>|f%Zx$8u{<7!~aoJL6*0>&(1MY%50536t zp@KaW$QKO-La#A)0?i|Kl<_wP;x>hsODj1S3a(X^e{|J~U$N9a>eBehlG^Fo0i-|2 z4LOBO8yrQxL@w0MC)YVugbFRcfL9?^ornr9tMbf<{gtfX&e?}{_Vvbe`;8)6u$W9;F>Y;}X#TsmAq3B#dWh}bN;4Zr7@#U-D# z?RQ5%YMqf06(!yuhI7d2`DD^V!U4BW+irX2?#|6k_*#|zVe5}&jURMm-X2~p)zI6n z;Q!Ukq&BgsAO}x!U*&hl+y0hS_~97tFpcV*uiH!FYm&;h7vI2K{%Su{8A6}*y_e3^ zr=Mh^s46538yim%ghSXEExK@ZBESm`GuE9*%k&RxUZRk)FEG5`=9z#^#rz?6 z{y}pLxc5&}htg8}oM`&tqFEjzi~eXV+XBtn*FT2t)431&P?hPi)J(`s8=Tr3`bsuA zgHZ@pSW25db2aRpfzK(v2|l{%qhF3A?P?)y?xe`99ujwE5Yb1zeW_9_QpNVNuAFtn z#tHW6jnUWm^s4Ao{y^p%x$*w|@$vBO13M*1>lLMC(yS$9waCKEwNS9^$8AO=GAoIM zXUTJ)I}jwHhC~ywMno`_35@_J!DjqB+&UFAh7G0L&b&KR<-V*Um7@&qp-T=6`r5_w zlDl zB<^Unzo@EApuVWT(@;Utsr^#bW}S5e{ZFkGy%~Bd3u(ut_RijHIW9UGUn|>$!Cy+k zi{41z=)*nw`JO{Tf>Ak%I_;^=9#I#X@s!*$&Td+nkP|!t)svdRI|7T&%{68UGT0(Q zfXEM3a&)1W;yZI~l1d_%3unr+Mz#(%lV^&XPgrRXCc|%fIvyCjF^DMMK(vr)-9g$1 z+PwDE-CeSG<5%GNGacCRC<|+~`U5sApGnK{-3nejYHs=be&6I$SQc$!Fy%*qZ`|Dr zJ3tcpb5GX5&maP9!Zt-iV`NMk`!={IUL+1`(4`n6-;f6Q$Dc60>*$}M)RglrM4Cak zDCE)4z5mX6E8P#x*TPD1mpRX~D*A)0-lEH5c6SQ-w>}dnb^Kv^=tlIp{cm0)6)zN? z>mji!s}a}6NsB8XA@z~j0x`A`AUys%H^0)27!l%wWsyv%gF5cyo zr@we%F`q539!=4AlDq1jbI5D~g;u#NgSme>Z`zv~iV}_2kYp-=chzPROP$dK!Xa-p zHE)(8gxO5%;ae6o{&+w4(KAbrndd*sV^)Y*4#dR zHW)&RGKRO;JTkU?q%S8eP8xay`|#fZnVVoO=vDPHSO^>`(AC(SB8*yZ7t|4?))#oJ z_sjaS;Lw4#iv=%R95OsED6g&@M3DRssF=8(GQG@TH(zxZGc^@;?02?p3)9jmU7LVH zJD<{}>}nbV)Vh)sgEg*!+4eeaUcPzAJPlbm`*_ixGCEpPuP*+1C^c{dI~L=(nZLhe zK!j>fACWl8dCCuK-ZbK7Z2z6bU_=0jL`oRx=vMFr#Kz%U1(d`jhTlXqN~{Y?W&Jy2 z{!fZ((H}N9r-81E*E2)$Q-a2}jR49S>;R4l@S=<*90vVgeYnDISBm1UTk0(QtE2qS z4elR*6d_Xv0|1DwHpacF03^V`;voyreb{6=^lv_TOi`TQ>wvj`bBO=ZvHjzZD5#@b za{ZyIjp4b+D~B3bw1%JS5Aoma&w>Bw(fRiu+&}(cxc-p2(bA>HFu#KSHU9vxg#D|= z`^6*hKNI-J9{~Q}|KH&@<@xhJ^WyFOlK{`3KY8$Ke@B4l&)@Olwf>U;&!0be@M?cY zfalNO@#3}qlK{`3KY8$Ke@B4l&)@Olwf>U;&!0be@M?cYfalNO@#3}qlK{`3KY8$K ze@B4l&)@Olwf>U;&!0be@M?cYfalNO@#3}qlK{`3KY8$Ke@B4l&)@Olwf>U;&!0be z@M?cYfalNO@#3}qlK{`3KY8$Ke@B4l&)@Olwf>U;&!0be@M?cYfalNO@#3}qlfeH0 Dq&%c$ delta 9966 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 diff --git a/src/vs/platform/accessibilitySignal/browser/media/save.mp3 b/src/vs/platform/accessibilitySignal/browser/media/save.mp3 index f21230080be8a2d917055c6a28b34f547f968d42..68a9cc83565c89ab0142d7a54deb4c06a28e14af 100644 GIT binary patch literal 29440 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 From 722cd3698cda9fb11bed8bfc5a1d3df715f9c840 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 15 Jan 2025 15:41:48 -0600 Subject: [PATCH 0580/3587] add `.` and `../` completions (#237836) --- .../browser/terminalCompletionService.ts | 170 +++++++-- .../suggest/browser/terminalSuggestAddon.ts | 11 +- .../browser/terminalCompletionService.test.ts | 344 +++++++++++++----- .../suggest/browser/simpleCompletionModel.ts | 4 + 4 files changed, 393 insertions(+), 136 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index fac59f23a364..be3976432613 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -2,10 +2,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; import { basename } from '../../../../../base/common/path.js'; +import { isWindows } from '../../../../../base/common/platform.js'; import { URI } from '../../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; @@ -62,6 +64,7 @@ export interface TerminalResourceRequestConfig { foldersRequested?: boolean; cwd?: URI; pathSeparator: string; + shouldNormalizePrefix?: boolean; } @@ -199,6 +202,10 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo } async resolveResources(resourceRequestConfig: TerminalResourceRequestConfig, promptValue: string, cursorPosition: number, provider: string): Promise { + if (resourceRequestConfig.shouldNormalizePrefix) { + // for tests, make sure the right path separator is used + promptValue = promptValue.replaceAll(/[\\/]/g, resourceRequestConfig.pathSeparator); + } const cwd = URI.revive(resourceRequestConfig.cwd); const foldersRequested = resourceRequestConfig.foldersRequested ?? false; const filesRequested = resourceRequestConfig.filesRequested ?? false; @@ -213,46 +220,128 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo const resourceCompletions: ITerminalCompletion[] = []; const cursorPrefix = promptValue.substring(0, cursorPosition); - const endsWithSpace = cursorPrefix.endsWith(' '); - const lastWord = endsWithSpace ? '' : cursorPrefix.split(' ').at(-1) ?? ''; - for (const stat of fileStat.children) { - let kind: TerminalCompletionItemKind | undefined; - if (foldersRequested && stat.isDirectory) { - kind = TerminalCompletionItemKind.Folder; - } - if (filesRequested && !stat.isDirectory && (stat.isFile || stat.resource.scheme === Schemas.file)) { - kind = TerminalCompletionItemKind.File; - } - if (kind === undefined) { - continue; - } - const isDirectory = kind === TerminalCompletionItemKind.Folder; - const fileName = basename(stat.resource.fsPath); - - let label; - if (!lastWord.startsWith('.' + resourceRequestConfig.pathSeparator) && !lastWord.startsWith('..' + resourceRequestConfig.pathSeparator)) { - // add a dot to the beginning of the label if it doesn't already have one - label = `.${resourceRequestConfig.pathSeparator}${fileName}`; - } else { - if (lastWord.endsWith(resourceRequestConfig.pathSeparator)) { - label = `${lastWord}${fileName}`; - } else { - label = `${lastWord}${resourceRequestConfig.pathSeparator}${fileName}`; + const useForwardSlash = !resourceRequestConfig.shouldNormalizePrefix && isWindows; + + // The last word (or argument). When the cursor is following a space it will be the empty + // string + const lastWord = cursorPrefix.endsWith(' ') ? '' : cursorPrefix.split(' ').at(-1) ?? ''; + + // Get the nearest folder path from the prefix. This ignores everything after the `/` as + // they are what triggers changes in the directory. + let lastSlashIndex: number; + if (useForwardSlash) { + lastSlashIndex = Math.max(lastWord.lastIndexOf('\\'), lastWord.lastIndexOf('/')); + } else { + lastSlashIndex = lastWord.lastIndexOf(resourceRequestConfig.pathSeparator); + } + + // The _complete_ folder of the last word. For example if the last word is `./src/file`, + // this will be `./src/`. This also always ends in the path separator if it is not the empty + // string and path separators are normalized on Windows. + let lastWordFolder = lastSlashIndex === -1 ? '' : lastWord.slice(0, lastSlashIndex + 1); + if (isWindows) { + lastWordFolder = lastWordFolder.replaceAll('/', '\\'); + } + + const lastWordFolderHasDotPrefix = lastWordFolder.match(/^\.\.?[\\\/]/); + + // Add current directory. This should be shown at the top because it will be an exact match + // and therefore highlight the detail, plus it improves the experience when runOnEnter is + // used. + // + // For example: + // - `|` -> `.`, this does not have the trailing `/` intentionally as it's common to + // complete the current working directory and we do not want to complete `./` when + // `runOnEnter` is used. + // - `./src/|` -> `./src/` + if (foldersRequested) { + resourceCompletions.push({ + label: lastWordFolder.length === 0 ? '.' : lastWordFolder, + provider, + kind: TerminalCompletionItemKind.Folder, + isDirectory: true, + isFile: false, + detail: getFriendlyFolderPath(cwd, resourceRequestConfig.pathSeparator), + replacementIndex: cursorPosition - lastWord.length, + replacementLength: lastWord.length + }); + } + + // Handle absolute paths differently to avoid adding `./` prefixes + // TODO: Deal with git bash case + const isAbsolutePath = useForwardSlash + ? /^[a-zA-Z]:\\/.test(lastWord) + : lastWord.startsWith(resourceRequestConfig.pathSeparator) && lastWord.endsWith(resourceRequestConfig.pathSeparator); + + // Add all direct children files or folders + // + // For example: + // - `cd ./src/` -> `cd ./src/folder1`, ... + if (!isAbsolutePath) { + for (const stat of fileStat.children) { + let kind: TerminalCompletionItemKind | undefined; + if (foldersRequested && stat.isDirectory) { + kind = TerminalCompletionItemKind.Folder; } - if (lastWord.length && lastWord.at(-1) !== resourceRequestConfig.pathSeparator && lastWord.at(-1) !== '.') { - label = `.${resourceRequestConfig.pathSeparator}${fileName}`; + if (filesRequested && !stat.isDirectory && (stat.isFile || stat.resource.scheme === Schemas.file)) { + kind = TerminalCompletionItemKind.File; } + if (kind === undefined) { + continue; + } + const isDirectory = kind === TerminalCompletionItemKind.Folder; + const resourceName = basename(stat.resource.fsPath); + + let label = `${lastWordFolder}${resourceName}`; + + // Normalize suggestion to add a ./ prefix to the start of the path if there isn't + // one already. We may want to change this behavior in the future to go with + // whatever format the user has + if (!lastWordFolderHasDotPrefix) { + label = `.${resourceRequestConfig.pathSeparator}${label}`; + } + + // Ensure directories end with a path separator + if (isDirectory && !label.endsWith(resourceRequestConfig.pathSeparator)) { + label = `${label}${resourceRequestConfig.pathSeparator}`; + } + + // Normalize path separator to `\` on Windows. It should act the exact same as `/` but + // suggestions should all use `\` + if (useForwardSlash) { + label = label.replaceAll('/', '\\'); + } + + resourceCompletions.push({ + label, + provider, + kind, + isDirectory, + isFile: kind === TerminalCompletionItemKind.File, + replacementIndex: cursorPosition - lastWord.length, + replacementLength: lastWord.length + }); } - if (isDirectory && !label.endsWith(resourceRequestConfig.pathSeparator)) { - label = label + resourceRequestConfig.pathSeparator; - } + } + + // Add parent directory to the bottom of the list because it's not as useful as other suggestions + // + // For example: + // - `|` -> `../` + // - `./src/|` -> `./src/../` + // + // On Windows, the path seprators are normalized to `\`: + // - `./src/|` -> `.\src\..\` + if (!isAbsolutePath && foldersRequested) { + const parentDir = URI.joinPath(cwd, '..' + resourceRequestConfig.pathSeparator); resourceCompletions.push({ - label, + label: lastWordFolder + '..' + resourceRequestConfig.pathSeparator, provider, - kind, - isDirectory, - isFile: kind === TerminalCompletionItemKind.File, + kind: TerminalCompletionItemKind.Folder, + detail: getFriendlyFolderPath(parentDir, resourceRequestConfig.pathSeparator), + isDirectory: true, + isFile: false, replacementIndex: cursorPosition - lastWord.length, replacementLength: lastWord.length }); @@ -261,3 +350,16 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return resourceCompletions.length ? resourceCompletions : undefined; } } + +function getFriendlyFolderPath(uri: URI, pathSeparator: string): string { + let path = uri.fsPath; + // Ensure folders end with the path separator to differentiate presentation from files + if (!path.endsWith(pathSeparator)) { + path += pathSeparator; + } + // Ensure drive is capitalized on Windows + if (pathSeparator === '\\' && path.match(/^[a-zA-Z]:\\/)) { + path = `${path[0].toUpperCase()}:${path.slice(2)}`; + } + return path; +} diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index dc709289fba6..ce5cf56a980b 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -27,7 +27,7 @@ import { SimpleCompletionItem } from '../../../../services/suggest/browser/simpl import { LineContext, SimpleCompletionModel } from '../../../../services/suggest/browser/simpleCompletionModel.js'; import { ISimpleSelectedSuggestion, SimpleSuggestWidget } from '../../../../services/suggest/browser/simpleSuggestWidget.js'; import type { ISimpleSuggestWidgetFontInfo } from '../../../../services/suggest/browser/simpleSuggestWidgetRenderer.js'; -import { ITerminalCompletion, ITerminalCompletionService, TerminalCompletionItemKind } from './terminalCompletionService.js'; +import { ITerminalCompletionService, TerminalCompletionItemKind } from './terminalCompletionService.js'; import { TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; @@ -58,7 +58,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest private _enableWidget: boolean = true; private _pathSeparator: string = sep; private _isFilteringDirectories: boolean = false; - private _mostRecentCompletion?: ITerminalCompletion; // TODO: Remove these in favor of prompt input state private _leadingLineContent?: string; @@ -193,12 +192,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._leadingLineContent = this._promptInputModel.prefix; } - if (this._mostRecentCompletion?.isDirectory && completions.every(e => e.isDirectory)) { - completions.push(this._mostRecentCompletion); - } - this._mostRecentCompletion = undefined; - - let normalizedLeadingLineContent = this._leadingLineContent; // If there is a single directory in the completions: @@ -504,8 +497,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest SuggestAddon.lastAcceptedCompletionTimestamp = 0; } - this._mostRecentCompletion = completion; - const commonPrefixLen = commonPrefixLength(replacementText, completionText); const commonPrefix = replacementText.substring(replacementText.length - 1 - commonPrefixLen, replacementText.length - 1); const completionSuffix = completionText.substring(commonPrefixLen); diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index 34f73a15d25c..174e1ea1e5e2 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -5,7 +5,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { IFileService, IFileStatWithMetadata, IResolveMetadataFileOptions } from '../../../../../../platform/files/common/files.js'; -import { TerminalCompletionService, TerminalCompletionItemKind, TerminalResourceRequestConfig } from '../../browser/terminalCompletionService.js'; +import { TerminalCompletionService, TerminalCompletionItemKind, TerminalResourceRequestConfig, ITerminalCompletion } from '../../browser/terminalCompletionService.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import assert from 'assert'; import { isWindows } from '../../../../../../base/common/platform.js'; @@ -14,15 +14,45 @@ import { createFileStat } from '../../../../../test/common/workbenchTestServices import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +const pathSeparator = isWindows ? '\\' : '/'; + +interface IAssertionTerminalCompletion { + label: string; + detail?: string; + kind?: TerminalCompletionItemKind; +} + +interface IAssertionCommandLineConfig { + replacementIndex: number; + replacementLength: number; +} + +function assertCompletions(actual: ITerminalCompletion[] | undefined, expected: IAssertionTerminalCompletion[], expectedConfig: IAssertionCommandLineConfig) { + assert.deepStrictEqual( + actual?.map(e => ({ + label: e.label, + detail: e.detail ?? '', + kind: e.kind ?? TerminalCompletionItemKind.Folder, + replacementIndex: e.replacementIndex, + replacementLength: e.replacementLength, + })), expected.map(e => ({ + label: e.label.replaceAll('/', pathSeparator), + detail: e.detail ? e.detail.replaceAll('/', pathSeparator) : '', + kind: e.kind ?? TerminalCompletionItemKind.Folder, + replacementIndex: expectedConfig.replacementIndex, + replacementLength: expectedConfig.replacementLength, + })) + ); +} + suite('TerminalCompletionService', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); let instantiationService: TestInstantiationService; let configurationService: TestConfigurationService; let validResources: URI[]; let childResources: { resource: URI; isFile?: boolean; isDirectory?: boolean }[]; - const pathSeparator = isWindows ? '\\' : '/'; let terminalCompletionService: TerminalCompletionService; - const provider: string = 'testProvider'; + const provider = 'testProvider'; setup(() => { instantiationService = store.add(new TestInstantiationService()); @@ -37,7 +67,7 @@ suite('TerminalCompletionService', () => { }, async resolve(resource: URI, options: IResolveMetadataFileOptions): Promise { return createFileStat(resource, undefined, undefined, undefined, childResources); - } + }, }); terminalCompletionService = store.add(instantiationService.createInstance(TerminalCompletionService)); validResources = []; @@ -46,9 +76,7 @@ suite('TerminalCompletionService', () => { suite('resolveResources should return undefined', () => { test('if cwd is not provided', async () => { - const resourceRequestConfig: TerminalResourceRequestConfig = { - pathSeparator - }; + const resourceRequestConfig: TerminalResourceRequestConfig = { pathSeparator }; const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider); assert(!result); }); @@ -67,129 +95,261 @@ suite('TerminalCompletionService', () => { suite('resolveResources should return folder completions', () => { setup(() => { validResources = [URI.parse('file:///test')]; - const childFolder = { resource: URI.parse('file:///test/folder1/'), name: 'folder1', isDirectory: true, isFile: false }; - const childFile = { resource: URI.parse('file:///test/file1.txt'), name: 'file1.txt', isDirectory: false, isFile: true }; - childResources = [childFolder, childFile]; + childResources = [ + { resource: URI.parse('file:///test/folder1/'), isDirectory: true }, + { resource: URI.parse('file:///test/file1.txt'), isFile: true }, + ]; }); - test('|', async () => { + test('| should return root-level completions', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///test'), foldersRequested: true, pathSeparator }; const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 1, provider); - assert.deepEqual(result, [{ - label: `.${pathSeparator}folder1${pathSeparator}`, - provider, - kind: TerminalCompletionItemKind.Folder, - isDirectory: true, - isFile: false, - replacementIndex: 1, - replacementLength: 0 - }]); + + assertCompletions(result, [ + { label: '.', detail: '/test/' }, + { label: './folder1/' }, + { label: '../', detail: '/' }, + ], { replacementIndex: 1, replacementLength: 0 }); }); - test('.|', async () => { + + test('./| should return folder completions', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///test'), foldersRequested: true, - pathSeparator + pathSeparator, + shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '.', 2, provider); - assert.deepEqual(result, [{ - label: `.${pathSeparator}folder1${pathSeparator}`, - provider, - kind: TerminalCompletionItemKind.Folder, - isDirectory: true, - isFile: false, - replacementIndex: 1, - replacementLength: 1 - }]); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 3, provider); + + assertCompletions(result, [ + { label: './', detail: '/test/' }, + { label: './folder1/' }, + { label: './../', detail: '/' }, + ], { replacementIndex: 1, replacementLength: 2 }); }); - test('./|', async () => { + + test('cd ./| should return folder completions', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///test'), foldersRequested: true, - pathSeparator + pathSeparator, + shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 3, provider); - assert.deepEqual(result, [{ - label: `.${pathSeparator}folder1${pathSeparator}`, - provider, - kind: TerminalCompletionItemKind.Folder, - isDirectory: true, - isFile: false, - replacementIndex: 1, - replacementLength: 2 - }]); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./', 5, provider); + + assertCompletions(result, [ + { label: './', detail: '/test/' }, + { label: './folder1/' }, + { label: './../', detail: '/' }, + ], { replacementIndex: 3, replacementLength: 2 }); }); - test('cd |', async () => { + test('cd ./f| should return folder completions', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///test'), foldersRequested: true, - pathSeparator + pathSeparator, + shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider); - assert.deepEqual(result, [{ - label: `.${pathSeparator}folder1${pathSeparator}`, - provider, - kind: TerminalCompletionItemKind.Folder, - isDirectory: true, - isFile: false, - replacementIndex: 3, - replacementLength: 0 - }]); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./f', 6, provider); + + assertCompletions(result, [ + { label: './', detail: '/test/' }, + { label: './folder1/' }, + { label: './../', detail: '/' }, + ], { replacementIndex: 3, replacementLength: 3 }); + }); + }); + + suite('resolveResources should handle file and folder completion requests correctly', () => { + setup(() => { + validResources = [URI.parse('file:///test')]; + childResources = [ + { resource: URI.parse('file:///test/.hiddenFile'), isFile: true }, + { resource: URI.parse('file:///test/.hiddenFolder/'), isDirectory: true }, + { resource: URI.parse('file:///test/folder1/'), isDirectory: true }, + { resource: URI.parse('file:///test/file1.txt'), isFile: true }, + ]; }); - test('cd .|', async () => { + + test('./| should handle hidden files and folders', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///test'), foldersRequested: true, - pathSeparator + filesRequested: true, + pathSeparator, + shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd .', 4, provider); - assert.deepEqual(result, [{ - label: `.${pathSeparator}folder1${pathSeparator}`, - provider, - kind: TerminalCompletionItemKind.Folder, - isDirectory: true, - isFile: false, - replacementIndex: 3, - replacementLength: 1 // replacing . - }]); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider); + + assertCompletions(result, [ + { label: './', detail: '/test/' }, + { label: './.hiddenFile', kind: TerminalCompletionItemKind.File }, + { label: './.hiddenFolder/' }, + { label: './folder1/' }, + { label: './file1.txt', kind: TerminalCompletionItemKind.File }, + { label: './../', detail: '/' }, + ], { replacementIndex: 0, replacementLength: 2 }); }); - test('cd ./|', async () => { + + test('./h| should handle hidden files and folders', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///test'), foldersRequested: true, - pathSeparator + filesRequested: true, + pathSeparator, + shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./', 5, provider); - assert.deepEqual(result, [{ - label: `.${pathSeparator}folder1${pathSeparator}`, - provider, - kind: TerminalCompletionItemKind.Folder, - isDirectory: true, - isFile: false, - replacementIndex: 3, - replacementLength: 2 // replacing ./ - }]); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './h', 3, provider); + + assertCompletions(result, [ + { label: './', detail: '/test/' }, + { label: './.hiddenFile', kind: TerminalCompletionItemKind.File }, + { label: './.hiddenFolder/' }, + { label: './folder1/' }, + { label: './file1.txt', kind: TerminalCompletionItemKind.File }, + { label: './../', detail: '/' }, + ], { replacementIndex: 0, replacementLength: 3 }); + }); + }); + suite('resolveResources edge cases and advanced scenarios', () => { + setup(() => { + validResources = []; + childResources = []; }); - test('cd ./f|', async () => { + + if (!isWindows) { + test('/usr/| Missing . should show correct results', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///'), + foldersRequested: true, + pathSeparator, + shouldNormalizePrefix: true + }; + validResources = [URI.parse('file:///usr')]; + childResources = []; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '/usr/', 5, provider); + + assertCompletions(result, [ + { label: '/usr/', detail: '/' }, + ], { replacementIndex: 0, replacementLength: 5 }); + }); + } + if (isWindows) { + test('.\\folder | Case insensitivity should resolve correctly on Windows', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///C:/test'), + foldersRequested: true, + pathSeparator: '\\', + shouldNormalizePrefix: true + }; + + validResources = [URI.parse('file:///C:/test')]; + childResources = [ + { resource: URI.parse('file:///C:/test/FolderA/'), isDirectory: true }, + { resource: URI.parse('file:///C:/test/anotherFolder/'), isDirectory: true } + ]; + + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '.\\folder', 8, provider); + + assertCompletions(result, [ + { label: '.\\', detail: 'C:\\test\\' }, + { label: '.\\FolderA\\' }, + { label: '.\\anotherFolder\\' }, + { label: '.\\..\\', detail: 'C:\\' }, + ], { replacementIndex: 0, replacementLength: 8 }); + }); + } else { + test('./folder | Case sensitivity should resolve correctly on Mac/Unix', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///test'), + foldersRequested: true, + pathSeparator: '/', + shouldNormalizePrefix: true + }; + validResources = [URI.parse('file:///test')]; + childResources = [ + { resource: URI.parse('file:///test/FolderA/'), isDirectory: true }, + { resource: URI.parse('file:///test/foldera/'), isDirectory: true } + ]; + + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder', 8, provider); + + assertCompletions(result, [ + { label: './', detail: '/test/' }, + { label: './FolderA/' }, + { label: './foldera/' }, + { label: './../', detail: '/' } + ], { replacementIndex: 0, replacementLength: 8 }); + }); + + } + test('| Empty input should resolve to current directory', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///test'), foldersRequested: true, - pathSeparator + pathSeparator, + shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./f', 6, provider); - assert.deepEqual(result, [{ - label: `.${pathSeparator}folder1${pathSeparator}`, - provider, - kind: TerminalCompletionItemKind.Folder, - isDirectory: true, - isFile: false, - replacementIndex: 3, - replacementLength: 3 // replacing ./f - }]); + validResources = [URI.parse('file:///test')]; + childResources = [ + { resource: URI.parse('file:///test/folder1/'), isDirectory: true }, + { resource: URI.parse('file:///test/folder2/'), isDirectory: true } + ]; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 0, provider); + + assertCompletions(result, [ + { label: '.', detail: '/test/' }, + { label: './folder1/' }, + { label: './folder2/' }, + { label: '../', detail: '/' } + ], { replacementIndex: 0, replacementLength: 0 }); + }); + + test('./| Large directory should handle many results gracefully', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///test'), + foldersRequested: true, + pathSeparator, + shouldNormalizePrefix: true + }; + validResources = [URI.parse('file:///test')]; + childResources = Array.from({ length: 1000 }, (_, i) => ({ + resource: URI.parse(`file:///test/folder${i}/`), + isDirectory: true + })); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider); + + assert(result); + // includes the 1000 folders + ./ and ./../ + assert.strictEqual(result?.length, 1002); + assert.strictEqual(result[0].label, `.${pathSeparator}`); + assert.strictEqual(result.at(-1)?.label, `.${pathSeparator}..${pathSeparator}`); + }); + + test('./folder| Folders should be resolved even if the trailing / is missing', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///test'), + foldersRequested: true, + pathSeparator, + shouldNormalizePrefix: true + }; + validResources = [URI.parse('file:///test')]; + childResources = [ + { resource: URI.parse('file:///test/folder1/'), isDirectory: true }, + { resource: URI.parse('file:///test/folder2/'), isDirectory: true } + ]; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder1', 10, provider); + + assertCompletions(result, [ + { label: './', detail: '/test/' }, + { label: './folder1/' }, + { label: './folder2/' }, + { label: './../', detail: '/' } + ], { replacementIndex: 1, replacementLength: 9 }); }); }); }); diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts index c80732669d75..e7b52f603436 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts @@ -208,6 +208,10 @@ export class SimpleCompletionModel { // Then by file extension length ascending score = a.fileExtLow.length - b.fileExtLow.length; } + if (score === 0 || fileExtScore(a.fileExtLow) === 0 && fileExtScore(b.fileExtLow) === 0) { + // both files or directories, sort alphabetically + score = a.completion.label.localeCompare(b.completion.label); + } return score; }); this._refilterKind = Refilter.Nothing; From ab1b9f6b3fb06b0d324152a2552d33f50cdfa066 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 15 Jan 2025 15:54:54 -0600 Subject: [PATCH 0581/3587] get terminal suggest details to show, make them toggleable (#238002) --- .../browser/terminal.suggest.contribution.ts | 18 ++++++++++++++- .../suggest/browser/terminalSuggestAddon.ts | 4 ++++ .../suggest/browser/simpleSuggestWidget.ts | 22 ++++++++++++++----- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index ddaed199002a..b98c64fc879c 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -27,6 +27,7 @@ import { InstantiationType, registerSingleton } from '../../../../../platform/in import { SuggestAddon } from './terminalSuggestAddon.js'; import { TerminalClipboardContribution } from '../../clipboard/browser/terminal.clipboard.contribution.js'; import { PwshCompletionProviderAddon } from './pwshCompletionProviderAddon.js'; +import { SimpleSuggestContext } from '../../../../services/suggest/browser/simpleSuggestWidget.js'; registerSingleton(ITerminalCompletionService, TerminalCompletionService, InstantiationType.Delayed); @@ -199,7 +200,7 @@ registerActiveInstanceAction({ primary: KeyMod.CtrlCmd | KeyCode.Space, mac: { primary: KeyMod.WinCtrl | KeyCode.Space }, weight: KeybindingWeight.WorkbenchContrib + 1, - when: ContextKeyExpr.and(TerminalContextKeys.focus, ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.Enabled}`, true)) + when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.suggestWidgetVisible.negate(), ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.Enabled}`, true)) }, run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.requestCompletions(true) }); @@ -262,6 +263,21 @@ registerActiveInstanceAction({ run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.toggleExplainMode() }); +registerActiveInstanceAction({ + id: 'terminalSuggestToggleDetails', + title: localize2('workbench.action.terminal.suggestToggleDetils', 'Suggest Toggle Details'), + f1: false, + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.focus, TerminalContextKeys.isOpen, TerminalContextKeys.suggestWidgetVisible, SimpleSuggestContext.HasFocusedSuggestion), + keybinding: { + // HACK: Force weight to be higher than that to start terminal chat + weight: KeybindingWeight.ExternalExtension + 2, + primary: KeyMod.CtrlCmd | KeyCode.Space, + secondary: [KeyMod.CtrlCmd | KeyCode.KeyI], + mac: { primary: KeyMod.WinCtrl | KeyCode.Space, secondary: [KeyMod.CtrlCmd | KeyCode.KeyI] } + }, + run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.toggleSuggestionDetails() +}); + registerActiveInstanceAction({ id: TerminalSuggestCommandId.SelectNextPageSuggestion, title: localize2('workbench.action.terminal.selectNextPageSuggestion', 'Select the Next Page Suggestion'), diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index ce5cf56a980b..6ed3e16864e1 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -230,6 +230,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._suggestWidget?.toggleExplainMode(); } + toggleSuggestionDetails(): void { + this._suggestWidget?.toggleDetails(); + } + resetWidgetSize(): void { this._suggestWidget?.resetWidgetSize(); } diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index 69f00e2c463d..f397073a7b86 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -21,7 +21,8 @@ import { SuggestWidgetStatus } from '../../../../editor/contrib/suggest/browser/ import { MenuId } from '../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { SimpleSuggestDetailsOverlay, SimpleSuggestDetailsWidget } from './simpleSuggestWidgetDetails.js'; +import { canExpandCompletionItem, SimpleSuggestDetailsOverlay, SimpleSuggestDetailsWidget } from './simpleSuggestWidgetDetails.js'; +import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; const $ = dom.$; @@ -51,6 +52,10 @@ const enum WidgetPositionPreference { Below } +export const SimpleSuggestContext = { + HasFocusedSuggestion: new RawContextKey('simpleSuggestWidgetHasFocusedSuggestion', false, localize('simpleSuggestWidgetHasFocusedSuggestion', "Whether any simple suggestion is focused")), +}; + export interface IWorkbenchSuggestWidgetOptions { /** * The {@link MenuId} to use for the status bar. Items on the menu must use the groups `'left'` @@ -96,6 +101,8 @@ export class SimpleSuggestWidget extends Disposable { get list(): List { return this._list; } + private readonly _ctxSuggestWidgetHasFocusedSuggestion: IContextKey; + constructor( private readonly _container: HTMLElement, private readonly _persistedSize: IPersistedWidgetSizeDelegate, @@ -104,6 +111,7 @@ export class SimpleSuggestWidget extends Disposable { @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IStorageService private readonly _storageService: IStorageService, + @IContextKeyService _contextKeyService: IContextKeyService ) { super(); @@ -111,6 +119,8 @@ export class SimpleSuggestWidget extends Disposable { this.element.domNode.classList.add('workbench-suggest-widget'); this._container.appendChild(this.element.domNode); + this._ctxSuggestWidgetHasFocusedSuggestion = SimpleSuggestContext.HasFocusedSuggestion.bindTo(_contextKeyService); + class ResizeState { constructor( readonly persistedSize: dom.Dimension | undefined, @@ -246,6 +256,7 @@ export class SimpleSuggestWidget extends Disposable { this._currentSuggestionDetails.cancel(); this._currentSuggestionDetails = undefined; this._focusedItem = undefined; + this._ctxSuggestWidgetHasFocusedSuggestion.set(false); } this._clearAriaActiveDescendant(); return; @@ -255,7 +266,7 @@ export class SimpleSuggestWidget extends Disposable { return; } - // this._ctxSuggestWidgetHasFocusedSuggestion.set(true); + this._ctxSuggestWidgetHasFocusedSuggestion.set(true); const item = e.elements[0]; const index = e.indexes[0]; @@ -422,7 +433,7 @@ export class SimpleSuggestWidget extends Disposable { // this._contentWidget.hide(); // this._ctxSuggestWidgetVisible.reset(); // this._ctxSuggestWidgetMultipleSuggestions.reset(); - // this._ctxSuggestWidgetHasFocusedSuggestion.reset(); + this._ctxSuggestWidgetHasFocusedSuggestion.reset(); this._showTimeout.cancel(); this.element.domNode.classList.remove('visible'); this._list.splice(0, this._list.length); @@ -521,13 +532,15 @@ export class SimpleSuggestWidget extends Disposable { // hide details widget this._pendingShowDetails.clear(); // this._ctxSuggestWidgetDetailsVisible.set(false); + this._setDetailsVisible(false); this._details.hide(); this.element.domNode.classList.remove('shows-details'); - } else if ((this._explainMode) && (this._state === State.Open || this._state === State.Details || this._state === State.Frozen)) { + } else if ((canExpandCompletionItem(this._list.getFocusedElements()[0]) || this._explainMode) && (this._state === State.Open || this._state === State.Details || this._state === State.Frozen)) { // show details widget (iff possible) // this._ctxSuggestWidgetDetailsVisible.set(true); + this._setDetailsVisible(true); this._showDetails(false, focused); } @@ -570,7 +583,6 @@ export class SimpleSuggestWidget extends Disposable { } } - hide(): void { this._pendingLayout.clear(); this._pendingShowDetails.clear(); From aebff8887de82eb183c08955cbb89b4e73ec87f8 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Wed, 15 Jan 2025 14:37:56 -0800 Subject: [PATCH 0582/3587] Trigger overflowing widget to reposition on notebook list scrolling (#238006) --- .../contrib/notebook/browser/notebookEditorWidget.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index b528b21b2a6b..0099df4fb2bc 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -103,6 +103,7 @@ import { PreventDefaultContextMenuItemsContextKeyName } from '../../webview/brow import { NotebookAccessibilityProvider } from './notebookAccessibilityProvider.js'; import { NotebookHorizontalTracker } from './viewParts/notebookHorizontalTracker.js'; import { NotebookCellEditorPool } from './view/notebookCellEditorPool.js'; +import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; const $ = DOM.$; @@ -2040,6 +2041,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD CopyPasteController.get(editor)?.clearWidgets(); } }); + + this._renderedEditors.forEach((editor, cell) => { + const controller = InlineCompletionsController.get(editor); + if (controller?.model.get()?.inlineEditState.get()) { + editor.render(true); + } + }); } private editorHasDomFocus(): boolean { From b785c3bebc21a286e9bd1ca6dfed79281ef4b193 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 15 Jan 2025 23:51:58 +0100 Subject: [PATCH 0583/3587] Invalidate inline completion after timeout when editing elsewhere (#238007) Invalidate inline completion after a timeout if user is editing elsewhere --- .../browser/model/inlineCompletionsSource.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index 272646b1bc8a..f83a8dfaf005 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -342,6 +342,8 @@ export class InlineCompletionWithUpdatedRange { public _inlineEdit: ISettableObservable; public get inlineEdit() { return this._inlineEdit.get(); } + private readonly _creationTime: number = Date.now(); + constructor( public readonly inlineCompletion: InlineCompletionItem, public readonly decorationId: string, @@ -381,6 +383,14 @@ export class InlineCompletionWithUpdatedRange { if (!offsetEdit) { return; } + + if (this._creationTime + 4000 < Date.now()) { + // The completion has been shown for a while and the user + // has been working on a different part of the document, so invalidate it + this._inlineEdit.set(new OffsetEdit([new SingleOffsetEdit(new OffsetRange(0, 0), '')]), tx); + return; + } + const newEdits = offsetEdit.edits.map(edit => acceptTextModelChange(edit, e.changes)); const emptyEdit = newEdits.find(edit => edit.isEmpty); if (emptyEdit) { From c799d209cd4846a2a822b55dbf2ca21893008faa Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 15 Jan 2025 14:57:10 -0800 Subject: [PATCH 0584/3587] Include checking for `github-enterprise` (#238008) Insiders fix of https://github.com/microsoft/vscode/pull/237993 So that we hide the setup view for GHE.com users after they install Copilot & sign in. Note: this behavior still isn't great if they haven't yet installed Copilot, but to keep this candidate small, we're just focused on fixing the case for when they have Copilot installed already. --- .../contrib/chat/browser/chatSetup.ts | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 99154d7af917..9a2e5f5e4a84 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -72,7 +72,7 @@ const defaultChat = { skusDocumentationUrl: product.defaultChatAgent?.skusDocumentationUrl ?? '', publicCodeMatchesUrl: product.defaultChatAgent?.publicCodeMatchesUrl ?? '', upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '', - providerId: product.defaultChatAgent?.providerId ?? '', + providerIds: [product.defaultChatAgent?.providerId ?? '', 'github-enterprise'], providerName: product.defaultChatAgent?.providerName ?? '', providerScopes: product.defaultChatAgent?.providerScopes ?? [[]], entitlementUrl: product.defaultChatAgent?.entitlementUrl ?? '', @@ -368,7 +368,8 @@ class ChatSetupRequests extends Disposable { @IRequestService private readonly requestService: IRequestService, @IChatQuotasService private readonly chatQuotasService: IChatQuotasService, @IDialogService private readonly dialogService: IDialogService, - @IOpenerService private readonly openerService: IOpenerService + @IOpenerService private readonly openerService: IOpenerService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); @@ -381,19 +382,19 @@ class ChatSetupRequests extends Disposable { this._register(this.authenticationService.onDidChangeDeclaredProviders(() => this.resolve())); this._register(this.authenticationService.onDidChangeSessions(e => { - if (e.providerId === defaultChat.providerId) { + if (defaultChat.providerIds.includes(e.providerId)) { this.resolve(); } })); this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => { - if (e.id === defaultChat.providerId) { + if (defaultChat.providerIds.includes(e.id)) { this.resolve(); } })); this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => { - if (e.id === defaultChat.providerId) { + if (defaultChat.providerIds.includes(e.id)) { this.resolve(); } })); @@ -440,9 +441,20 @@ class ChatSetupRequests extends Disposable { } private async findMatchingProviderSession(token: CancellationToken): Promise { - const sessions = await this.authenticationService.getSessions(defaultChat.providerId); - if (token.isCancellationRequested) { - return undefined; + let sessions: ReadonlyArray = []; + const authProviderConfigValue = this.configurationService.getValue('github.copilot.advanced.authProvider'); + if (authProviderConfigValue) { + sessions = await this.authenticationService.getSessions(authProviderConfigValue); + } else { + for (const providerId of defaultChat.providerIds) { + if (token.isCancellationRequested) { + return undefined; + } + sessions = await this.authenticationService.getSessions(providerId); + if (sessions.length) { + break; + } + } } for (const session of sessions) { @@ -788,7 +800,7 @@ class ChatSetupController extends Disposable { } if (!session) { - session = (await this.authenticationService.getSessions(defaultChat.providerId)).at(0); + session = (await this.authenticationService.getSessions(defaultChat.providerIds[0])).at(0); if (!session) { this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signedIn: false, signUpErrorCode: undefined }); return; // unexpected @@ -827,11 +839,11 @@ class ChatSetupController extends Disposable { try { showCopilotView(this.viewsService, this.layoutService); - session = await this.authenticationService.createSession(defaultChat.providerId, defaultChat.providerScopes[0]); + session = await this.authenticationService.createSession(defaultChat.providerIds[0], defaultChat.providerScopes[0]); entitlement = await this.requests.forceResolveEntitlement(session); - this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, defaultChat.providerId, session.account); - this.authenticationExtensionsService.updateAccountPreference(defaultChat.chatExtensionId, defaultChat.providerId, session.account); + this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, defaultChat.providerIds[0], session.account); + this.authenticationExtensionsService.updateAccountPreference(defaultChat.chatExtensionId, defaultChat.providerIds[0], session.account); } catch (error) { this.logService.error(`[chat setup] signIn: error ${error}`); } From 31188fed068c5c724d73a1956c846401d4d7b01d Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 15 Jan 2025 22:50:49 -0600 Subject: [PATCH 0585/3587] add command to commands to skip shell (#238021) --- .../suggest/browser/terminal.suggest.contribution.ts | 4 ++-- .../terminalContrib/suggest/common/terminal.suggest.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index b98c64fc879c..0b1215a143fb 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -264,8 +264,8 @@ registerActiveInstanceAction({ }); registerActiveInstanceAction({ - id: 'terminalSuggestToggleDetails', - title: localize2('workbench.action.terminal.suggestToggleDetils', 'Suggest Toggle Details'), + id: TerminalSuggestCommandId.ToggleDetails, + title: localize2('workbench.action.terminal.suggestToggleDetails', 'Suggest Toggle Details'), f1: false, precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.focus, TerminalContextKeys.isOpen, TerminalContextKeys.suggestWidgetVisible, SimpleSuggestContext.HasFocusedSuggestion), keybinding: { diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts index 0b62f7901242..bff806680b42 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts @@ -14,6 +14,7 @@ export const enum TerminalSuggestCommandId { ClearSuggestCache = 'workbench.action.terminal.clearSuggestCache', RequestCompletions = 'workbench.action.terminal.requestCompletions', ResetWidgetSize = 'workbench.action.terminal.resetSuggestWidgetSize', + ToggleDetails = 'workbench.action.terminal.suggestToggleDetails' } export const defaultTerminalSuggestCommandsToSkipShell = [ @@ -26,4 +27,5 @@ export const defaultTerminalSuggestCommandsToSkipShell = [ TerminalSuggestCommandId.HideSuggestWidget, TerminalSuggestCommandId.ClearSuggestCache, TerminalSuggestCommandId.RequestCompletions, + TerminalSuggestCommandId.ToggleDetails ]; From da45c202b2b1ed34b6fdb7fd5eaba5afe9794c76 Mon Sep 17 00:00:00 2001 From: henry Date: Wed, 15 Jan 2025 22:54:34 -0600 Subject: [PATCH 0586/3587] added unix time to copyfiles for markdown --- extensions/markdown-language-features/package.nls.json | 2 +- .../src/languageFeatures/copyFiles/copyFiles.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json index 47f26b055819..be3f6db00f93 100644 --- a/extensions/markdown-language-features/package.nls.json +++ b/extensions/markdown-language-features/package.nls.json @@ -72,7 +72,7 @@ "configuration.markdown.updateLinksOnFileMove.enableForDirectories": "Enable updating links when a directory is moved or renamed in the workspace.", "configuration.markdown.occurrencesHighlight.enabled": "Enable highlighting link occurrences in the current document.", "configuration.markdown.copyFiles.destination": { - "message": "Configures the path and file name of files created by copy/paste or drag and drop. This is a map of globs that match against a Markdown document path to the destination path where the new file should be created.\n\nThe destination path may use the following variables:\n\n- `${documentDirName}` — Absolute parent directory path of the Markdown document, e.g. `/Users/me/myProject/docs`.\n- `${documentRelativeDirName}` — Relative parent directory path of the Markdown document, e.g. `docs`. This is the same as `${documentDirName}` if the file is not part of a workspace.\n- `${documentFileName}` — The full filename of the Markdown document, e.g. `README.md`.\n- `${documentBaseName}` — The basename of the Markdown document, e.g. `README`.\n- `${documentExtName}` — The extension of the Markdown document, e.g. `md`.\n- `${documentFilePath}` — Absolute path of the Markdown document, e.g. `/Users/me/myProject/docs/README.md`.\n- `${documentRelativeFilePath}` — Relative path of the Markdown document, e.g. `docs/README.md`. This is the same as `${documentFilePath}` if the file is not part of a workspace.\n- `${documentWorkspaceFolder}` — The workspace folder for the Markdown document, e.g. `/Users/me/myProject`. This is the same as `${documentDirName}` if the file is not part of a workspace.\n- `${fileName}` — The file name of the dropped file, e.g. `image.png`.\n- `${fileExtName}` — The extension of the dropped file, e.g. `png`.", + "message": "Configures the path and file name of files created by copy/paste or drag and drop. This is a map of globs that match against a Markdown document path to the destination path where the new file should be created.\n\nThe destination path may use the following variables:\n\n- `${documentDirName}` — Absolute parent directory path of the Markdown document, e.g. `/Users/me/myProject/docs`.\n- `${documentRelativeDirName}` — Relative parent directory path of the Markdown document, e.g. `docs`. This is the same as `${documentDirName}` if the file is not part of a workspace.\n- `${documentFileName}` — The full filename of the Markdown document, e.g. `README.md`.\n- `${documentBaseName}` — The basename of the Markdown document, e.g. `README`.\n- `${documentExtName}` — The extension of the Markdown document, e.g. `md`.\n- `${documentFilePath}` — Absolute path of the Markdown document, e.g. `/Users/me/myProject/docs/README.md`.\n- `${documentRelativeFilePath}` — Relative path of the Markdown document, e.g. `docs/README.md`. This is the same as `${documentFilePath}` if the file is not part of a workspace.\n- `${documentWorkspaceFolder}` — The workspace folder for the Markdown document, e.g. `/Users/me/myProject`. This is the same as `${documentDirName}` if the file is not part of a workspace.\n- `${fileName}` — The file name of the dropped file, e.g. `image.png`.\n- `${fileExtName}` — The extension of the dropped file, e.g. `png`.\n- `${unixTime}` — The current Unix timestamp in seconds.", "comment": [ "This setting is use the user drops or pastes image data into the editor. In this case, VS Code automatically creates a new image file in the workspace containing the dropped/pasted image.", "It's easier to explain this setting with an example. For example, let's say the setting value was:", diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts index dd41b5fa9248..26f8186dbc37 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts @@ -96,6 +96,7 @@ function resolveCopyDestinationSetting(documentUri: vscode.Uri, fileName: string // File ['fileName', fileName], // The file name of the dropped file, e.g. `image.png`. ['fileExtName', path.extname(fileName).replace('.', '')], // The extension of the dropped file, e.g. `png`. + ['unixTime', Math.floor(Date.now() / 1000).toString()], // The current Unix timestamp in seconds. ]); return outDest.replaceAll(/(?\\\$)|(?\w+)(?:\/(?(?:\\\/|[^\}\/])+)\/(?(?:\\\/|[^\}\/])*)\/)?\}/g, (match, _escape, name, pattern, replacement, _offset, _str, groups) => { From a68e5d595499540a964d5c9d07f28c096fa25eb3 Mon Sep 17 00:00:00 2001 From: numbermaniac <5206120+numbermaniac@users.noreply.github.com> Date: Thu, 16 Jan 2025 19:26:01 +1100 Subject: [PATCH 0587/3587] Fix typo in InlayHintKind docs --- src/vscode-dts/vscode.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index f306830ba121..0109505b4566 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -5496,7 +5496,7 @@ declare module 'vscode' { */ export enum InlayHintKind { /** - * An inlay hint that for a type annotation. + * An inlay hint that is for a type annotation. */ Type = 1, /** From a2eb5cbbb183f300ac17a4c099ed65a303d13b12 Mon Sep 17 00:00:00 2001 From: isidorn Date: Thu, 16 Jan 2025 10:48:30 +0100 Subject: [PATCH 0588/3587] update distro pointer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 06931133711d..9e5044bb8b5b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.97.0", - "distro": "a408bc8c56673f8240be72eeeb7d29908e0e3511", + "distro": "03bcc2befd4c31b21f1b2fb56c56d767e3db462b", "author": { "name": "Microsoft Corporation" }, From eaba97f9956fea702056c5eefa42c5d655d5372b Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:37:27 +0100 Subject: [PATCH 0589/3587] Git - extract history item detail provider (#238041) * Initial refactor of hover commands * Delete old code for hover commands * More refactoring --- extensions/git-base/src/api/api1.ts | 12 +---- extensions/git-base/src/api/git-base.d.ts | 4 -- extensions/git-base/src/remoteSource.ts | 28 +---------- extensions/git/src/api/api1.ts | 6 ++- extensions/git/src/api/git.d.ts | 8 ++- extensions/git/src/blame.ts | 21 ++++---- .../git/src/historyItemDetailProvider.ts | 46 +++++++++++++++++ extensions/git/src/historyProvider.ts | 11 ++-- extensions/git/src/model.ts | 17 +++++-- extensions/git/src/remoteSource.ts | 35 ------------- extensions/git/src/repository.ts | 4 +- extensions/git/src/timelineProvider.ts | 8 +-- extensions/git/src/typings/git-base.d.ts | 3 -- extensions/github/src/commands.ts | 2 +- extensions/github/src/extension.ts | 2 + .../github/src/historyItemDetailProvider.ts | 50 +++++++++++++++++++ extensions/github/src/remoteSourceProvider.ts | 42 +--------------- extensions/github/src/typings/git-base.d.ts | 4 -- extensions/github/src/typings/git.d.ts | 8 ++- extensions/github/src/util.ts | 21 +++++++- 20 files changed, 182 insertions(+), 150 deletions(-) create mode 100644 extensions/git/src/historyItemDetailProvider.ts create mode 100644 extensions/github/src/historyItemDetailProvider.ts diff --git a/extensions/git-base/src/api/api1.ts b/extensions/git-base/src/api/api1.ts index 2c6689452111..005a79303563 100644 --- a/extensions/git-base/src/api/api1.ts +++ b/extensions/git-base/src/api/api1.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Command, Disposable, commands } from 'vscode'; +import { Disposable, commands } from 'vscode'; import { Model } from '../model'; -import { getRemoteSourceActions, getRemoteSourceControlHistoryItemCommands, pickRemoteSource, provideRemoteSourceLinks } from '../remoteSource'; +import { getRemoteSourceActions, pickRemoteSource } from '../remoteSource'; import { GitBaseExtensionImpl } from './extension'; import { API, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction, RemoteSourceProvider } from './git-base'; @@ -21,14 +21,6 @@ export class ApiImpl implements API { return getRemoteSourceActions(this._model, url); } - getRemoteSourceControlHistoryItemCommands(url: string): Promise { - return getRemoteSourceControlHistoryItemCommands(this._model, url); - } - - provideRemoteSourceLinks(url: string, content: string): Promise { - return provideRemoteSourceLinks(this._model, url, content); - } - registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable { return this._model.registerRemoteSourceProvider(provider); } diff --git a/extensions/git-base/src/api/git-base.d.ts b/extensions/git-base/src/api/git-base.d.ts index 540aa5831460..d4ec49df47dc 100644 --- a/extensions/git-base/src/api/git-base.d.ts +++ b/extensions/git-base/src/api/git-base.d.ts @@ -9,8 +9,6 @@ export { ProviderResult } from 'vscode'; export interface API { registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; getRemoteSourceActions(url: string): Promise; - getRemoteSourceControlHistoryItemCommands(url: string): Promise; - provideRemoteSourceLinks(url: string, content: string): Promise; pickRemoteSource(options: PickRemoteSourceOptions): Promise; } @@ -83,8 +81,6 @@ export interface RemoteSourceProvider { getBranches?(url: string): ProviderResult; getRemoteSourceActions?(url: string): ProviderResult; - getRemoteSourceControlHistoryItemCommands?(url: string): ProviderResult; getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; - provideRemoteSourceLinks?(url: string, content: string): Promise; } diff --git a/extensions/git-base/src/remoteSource.ts b/extensions/git-base/src/remoteSource.ts index cf570e3aa006..eb86b27367aa 100644 --- a/extensions/git-base/src/remoteSource.ts +++ b/extensions/git-base/src/remoteSource.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { QuickPickItem, window, QuickPick, QuickPickItemKind, l10n, Disposable, Command } from 'vscode'; +import { QuickPickItem, window, QuickPick, QuickPickItemKind, l10n, Disposable } from 'vscode'; import { RemoteSourceProvider, RemoteSource, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction } from './api/git-base'; import { Model } from './model'; import { throttle, debounce } from './decorators'; @@ -123,32 +123,6 @@ export async function getRemoteSourceActions(model: Model, url: string): Promise return remoteSourceActions; } -export async function getRemoteSourceControlHistoryItemCommands(model: Model, url: string): Promise { - const providers = model.getRemoteProviders(); - - const remoteSourceCommands = []; - for (const provider of providers) { - remoteSourceCommands.push(...(await provider.getRemoteSourceControlHistoryItemCommands?.(url) ?? [])); - } - - return remoteSourceCommands.length > 0 ? remoteSourceCommands : undefined; -} - -export async function provideRemoteSourceLinks(model: Model, url: string, content: string): Promise { - const providers = model.getRemoteProviders(); - - for (const provider of providers) { - const parsedContent = await provider.provideRemoteSourceLinks?.(url, content); - if (!parsedContent) { - continue; - } - - content = parsedContent; - } - - return content; -} - export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise; export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch: true }): Promise; export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise { diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 518269c41628..55cd31a1acf1 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -7,7 +7,7 @@ import { Model } from '../model'; import { Repository as BaseRepository, Resource } from '../repository'; -import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions } from './git'; +import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailProvider } from './git'; import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode'; import { combinedDisposable, filterEvent, mapEvent } from '../util'; import { toGitUri } from '../uri'; @@ -414,6 +414,10 @@ export class ApiImpl implements API { return this.#model.registerPushErrorHandler(handler); } + registerSourceControlHistoryItemDetailProvider(provider: SourceControlHistoryItemDetailProvider): Disposable { + return this.#model.registerSourceControlHistoryItemDetailProvider(provider); + } + registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable { return this.#model.registerBranchProtectionProvider(root, provider); } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index ea78ac4d99a7..622c8a80b160 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Uri, Event, Disposable, ProviderResult, Command, CancellationToken } from 'vscode'; +import { Uri, Event, Disposable, ProviderResult, Command, CancellationToken, SourceControlHistoryItem } from 'vscode'; export { ProviderResult } from 'vscode'; export interface Git { @@ -326,6 +326,11 @@ export interface BranchProtectionProvider { provideBranchProtection(): BranchProtection[]; } +export interface SourceControlHistoryItemDetailProvider { + provideHoverCommands(repository: Repository): ProviderResult; + provideMessageLinks(repository: Repository, message: string): ProviderResult; +} + export type APIState = 'uninitialized' | 'initialized'; export interface PublishEvent { @@ -353,6 +358,7 @@ export interface API { registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable; registerPushErrorHandler(handler: PushErrorHandler): Disposable; registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable; + registerSourceControlHistoryItemDetailProvider(provider: SourceControlHistoryItemDetailProvider): Disposable; } export interface GitExtension { diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 6f9d0cc88e4f..7265a61d879d 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -12,7 +12,7 @@ import { BlameInformation, Commit } from './git'; import { fromGitUri, isGitUri } from './uri'; import { emojify, ensureEmojis } from './emoji'; import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation } from './staging'; -import { getRemoteSourceControlHistoryItemCommands, provideRemoteSourceLinks } from './remoteSource'; +import { provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailProvider'; function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean { return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive); @@ -204,9 +204,9 @@ export class GitBlameController { } async getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation, includeCommitDetails = false): Promise { + const remoteHoverCommands: Command[] = []; let commitInformation: Commit | undefined; let commitMessageWithLinks: string | undefined; - const remoteSourceCommands: Command[] = []; const repository = this._model.getRepository(documentUri); if (repository) { @@ -217,16 +217,15 @@ export class GitBlameController { } catch { } } - // Remote commands + // Remote hover commands const unpublishedCommits = await repository.getUnpublishedCommits(); if (!unpublishedCommits.has(blameInformation.hash)) { - remoteSourceCommands.push(...await getRemoteSourceControlHistoryItemCommands(repository)); + remoteHoverCommands.push(...await provideSourceControlHistoryItemHoverCommands(this._model, repository) ?? []); } - // Link provider - commitMessageWithLinks = await provideRemoteSourceLinks( - repository, - commitInformation?.message ?? blameInformation.subject ?? ''); + // Message links + commitMessageWithLinks = await provideSourceControlHistoryItemMessageLinks( + this._model, repository, commitInformation?.message ?? blameInformation.subject ?? ''); } const markdownString = new MarkdownString(); @@ -289,11 +288,11 @@ export class GitBlameController { markdownString.appendMarkdown(' '); markdownString.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`); - // Remote commands - if (remoteSourceCommands.length > 0) { + // Remote hover commands + if (remoteHoverCommands.length > 0) { markdownString.appendMarkdown('  |  '); - const remoteCommandsMarkdown = remoteSourceCommands + const remoteCommandsMarkdown = remoteHoverCommands .map(command => `[${command.title}](command:${command.command}?${encodeURIComponent(JSON.stringify([...command.arguments ?? [], hash]))} "${command.tooltip}")`); markdownString.appendMarkdown(remoteCommandsMarkdown.join(' ')); } diff --git a/extensions/git/src/historyItemDetailProvider.ts b/extensions/git/src/historyItemDetailProvider.ts new file mode 100644 index 000000000000..af717a703f13 --- /dev/null +++ b/extensions/git/src/historyItemDetailProvider.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Command, Disposable } from 'vscode'; +import { SourceControlHistoryItemDetailProvider } from './api/git'; +import { Repository } from './repository'; +import { ApiRepository } from './api/api1'; + +export interface ISourceControlHistoryItemDetailProviderRegistry { + registerSourceControlHistoryItemDetailProvider(provider: SourceControlHistoryItemDetailProvider): Disposable; + getSourceControlHistoryItemDetailProviders(): SourceControlHistoryItemDetailProvider[]; +} + +export async function provideSourceControlHistoryItemHoverCommands( + registry: ISourceControlHistoryItemDetailProviderRegistry, + repository: Repository +): Promise { + for (const provider of registry.getSourceControlHistoryItemDetailProviders()) { + const result = await provider.provideHoverCommands(new ApiRepository(repository)); + + if (result) { + return result; + } + } + + return undefined; +} + +export async function provideSourceControlHistoryItemMessageLinks( + registry: ISourceControlHistoryItemDetailProviderRegistry, + repository: Repository, + message: string +): Promise { + for (const provider of registry.getSourceControlHistoryItemDetailProviders()) { + const result = await provider.provideMessageLinks( + new ApiRepository(repository), message); + + if (result) { + return result; + } + } + + return undefined; +} diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index bda106f46d58..430819977b54 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -12,7 +12,7 @@ import { Branch, LogOptions, Ref, RefType } from './api/git'; import { emojify, ensureEmojis } from './emoji'; import { Commit } from './git'; import { OperationKind, OperationResult } from './operation'; -import { provideRemoteSourceLinks } from './remoteSource'; +import { ISourceControlHistoryItemDetailProviderRegistry, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailProvider'; function toSourceControlHistoryItemRef(repository: Repository, ref: Ref): SourceControlHistoryItemRef { const rootUri = Uri.file(repository.root); @@ -97,7 +97,11 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec private disposables: Disposable[] = []; - constructor(protected readonly repository: Repository, private readonly logger: LogOutputChannel) { + constructor( + private historyItemDetailProviderRegistry: ISourceControlHistoryItemDetailProviderRegistry, + private readonly repository: Repository, + private readonly logger: LogOutputChannel + ) { const onDidRunWriteOperation = filterEvent(repository.onDidRunOperation, e => !e.operation.readOnly); this.disposables.push(onDidRunWriteOperation(this.onDidRunWriteOperation, this)); @@ -268,7 +272,8 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec const historyItems: SourceControlHistoryItem[] = []; for (const commit of commits) { const message = emojify(commit.message); - const messageWithLinks = await provideRemoteSourceLinks(this.repository, message) ?? message; + const messageWithLinks = await provideSourceControlHistoryItemMessageLinks( + this.historyItemDetailProviderRegistry, this.repository, message) ?? message; const newLineIndex = message.indexOf('\n'); const subject = newLineIndex !== -1 diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index 142d073914f3..c94df8d5a50f 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -12,13 +12,14 @@ import { Git } from './git'; import * as path from 'path'; import * as fs from 'fs'; import { fromGitUri } from './uri'; -import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider, BranchProtectionProvider } from './api/git'; +import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider, BranchProtectionProvider, SourceControlHistoryItemDetailProvider } from './api/git'; import { Askpass } from './askpass'; import { IPushErrorHandlerRegistry } from './pushError'; import { ApiRepository } from './api/api1'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { IPostCommitCommandsProviderRegistry } from './postCommitCommands'; import { IBranchProtectionProviderRegistry } from './branchProtection'; +import { ISourceControlHistoryItemDetailProviderRegistry } from './historyItemDetailProvider'; class RepositoryPick implements QuickPickItem { @memoize get label(): string { @@ -170,7 +171,7 @@ class UnsafeRepositoriesManager { } } -export class Model implements IRepositoryResolver, IBranchProtectionProviderRegistry, IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry { +export class Model implements IRepositoryResolver, IBranchProtectionProviderRegistry, IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry, ISourceControlHistoryItemDetailProviderRegistry { private _onDidOpenRepository = new EventEmitter(); readonly onDidOpenRepository: Event = this._onDidOpenRepository.event; @@ -236,6 +237,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi readonly onDidChangeBranchProtectionProviders = this._onDidChangeBranchProtectionProviders.event; private pushErrorHandlers = new Set(); + private historyItemDetailProviders = new Set(); private _unsafeRepositoriesManager: UnsafeRepositoriesManager; get unsafeRepositories(): string[] { @@ -633,7 +635,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi // Open repository const [dotGit, repositoryRootRealPath] = await Promise.all([this.git.getRepositoryDotGit(repositoryRoot), this.getRepositoryRootRealPath(repositoryRoot)]); - const repository = new Repository(this.git.open(repositoryRoot, repositoryRootRealPath, dotGit, this.logger), this, this, this, this, this, this.globalState, this.logger, this.telemetryReporter); + const repository = new Repository(this.git.open(repositoryRoot, repositoryRootRealPath, dotGit, this.logger), this, this, this, this, this, this, this.globalState, this.logger, this.telemetryReporter); this.open(repository); this._closedRepositoriesManager.deleteRepository(repository.root); @@ -1002,6 +1004,15 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi return [...this.pushErrorHandlers]; } + registerSourceControlHistoryItemDetailProvider(provider: SourceControlHistoryItemDetailProvider): Disposable { + this.historyItemDetailProviders.add(provider); + return toDisposable(() => this.historyItemDetailProviders.delete(provider)); + } + + getSourceControlHistoryItemDetailProviders(): SourceControlHistoryItemDetailProvider[] { + return [...this.historyItemDetailProviders]; + } + getUnsafeRepositoryPath(repository: string): string | undefined { return this._unsafeRepositoriesManager.getRepositoryPath(repository); } diff --git a/extensions/git/src/remoteSource.ts b/extensions/git/src/remoteSource.ts index 86daccedf952..eb63e5db81f6 100644 --- a/extensions/git/src/remoteSource.ts +++ b/extensions/git/src/remoteSource.ts @@ -3,10 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Command } from 'vscode'; import { PickRemoteSourceOptions, PickRemoteSourceResult } from './typings/git-base'; import { GitBaseApi } from './git-base'; -import { Repository } from './repository'; export async function pickRemoteSource(options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise; export async function pickRemoteSource(options: PickRemoteSourceOptions & { branch: true }): Promise; @@ -17,36 +15,3 @@ export async function pickRemoteSource(options: PickRemoteSourceOptions = {}): P export async function getRemoteSourceActions(url: string) { return GitBaseApi.getAPI().getRemoteSourceActions(url); } - -export async function getRemoteSourceControlHistoryItemCommands(repository: Repository): Promise { - if (repository.remotes.length === 0) { - return []; - } - - const getCommands = async (repository: Repository, remoteName: string): Promise => { - const remote = repository.remotes.find(r => r.name === remoteName && r.fetchUrl); - return remote ? GitBaseApi.getAPI().getRemoteSourceControlHistoryItemCommands(remote.fetchUrl!) : undefined; - }; - - // upstream -> origin -> first - return await getCommands(repository, 'upstream') - ?? await getCommands(repository, 'origin') - ?? await getCommands(repository, repository.remotes[0].name) - ?? []; -} - -export async function provideRemoteSourceLinks(repository: Repository, content: string): Promise { - if (repository.remotes.length === 0) { - return undefined; - } - - const getDocumentLinks = async (repository: Repository, remoteName: string): Promise => { - const remote = repository.remotes.find(r => r.name === remoteName && r.fetchUrl); - return remote ? GitBaseApi.getAPI().provideRemoteSourceLinks(remote.fetchUrl!, content) : undefined; - }; - - // upstream -> origin -> first - return await getDocumentLinks(repository, 'upstream') - ?? await getDocumentLinks(repository, 'origin') - ?? await getDocumentLinks(repository, repository.remotes[0].name); -} diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 9ad8c2916c20..d7c371899579 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -26,6 +26,7 @@ import { toGitUri } from './uri'; import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; import { detectEncoding } from './encoding'; +import { ISourceControlHistoryItemDetailProviderRegistry } from './historyItemDetailProvider'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -855,6 +856,7 @@ export class Repository implements Disposable { remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry, postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry, private readonly branchProtectionProviderRegistry: IBranchProtectionProviderRegistry, + historyItemDetailProviderRegistry: ISourceControlHistoryItemDetailProviderRegistry, globalState: Memento, private readonly logger: LogOutputChannel, private telemetryReporter: TelemetryReporter @@ -893,7 +895,7 @@ export class Repository implements Disposable { this._sourceControl.quickDiffProvider = this; - this._historyProvider = new GitHistoryProvider(this, logger); + this._historyProvider = new GitHistoryProvider(historyItemDetailProviderRegistry, this, logger); this._sourceControl.historyProvider = this._historyProvider; this.disposables.push(this._historyProvider); diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 90d202cc4152..6b9085eb8215 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -12,7 +12,7 @@ import { CommandCenter } from './commands'; import { OperationKind, OperationResult } from './operation'; import { getCommitShortHash } from './util'; import { CommitShortStat } from './git'; -import { getRemoteSourceControlHistoryItemCommands, provideRemoteSourceLinks } from './remoteSource'; +import { provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailProvider'; export class GitTimelineItem extends TimelineItem { static is(item: TimelineItem): item is GitTimelineItem { @@ -216,7 +216,7 @@ export class GitTimelineProvider implements TimelineProvider { const openComparison = l10n.t('Open Comparison'); const unpublishedCommits = await repo.getUnpublishedCommits(); - const remoteSourceCommands = await getRemoteSourceControlHistoryItemCommands(repo); + const remoteHoverCommands = await provideSourceControlHistoryItemHoverCommands(this.model, repo); const items: GitTimelineItem[] = []; for (let index = 0; index < commits.length; index++) { @@ -232,8 +232,8 @@ export class GitTimelineProvider implements TimelineProvider { item.description = c.authorName; } - const commitRemoteSourceCommands = !unpublishedCommits.has(c.hash) ? remoteSourceCommands : []; - const messageWithLinks = await provideRemoteSourceLinks(repo, message) ?? message; + const commitRemoteSourceCommands = !unpublishedCommits.has(c.hash) ? remoteHoverCommands : []; + const messageWithLinks = await provideSourceControlHistoryItemMessageLinks(this.model, repo, message) ?? message; item.setItemDetails(uri, c.hash, c.authorName!, c.authorEmail, dateFormatter.format(date), messageWithLinks, c.shortStat, commitRemoteSourceCommands); diff --git a/extensions/git/src/typings/git-base.d.ts b/extensions/git/src/typings/git-base.d.ts index 3b61341d8063..d4ec49df47dc 100644 --- a/extensions/git/src/typings/git-base.d.ts +++ b/extensions/git/src/typings/git-base.d.ts @@ -9,9 +9,7 @@ export { ProviderResult } from 'vscode'; export interface API { registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; getRemoteSourceActions(url: string): Promise; - getRemoteSourceControlHistoryItemCommands(url: string): Promise; pickRemoteSource(options: PickRemoteSourceOptions): Promise; - provideRemoteSourceLinks(url: string, content: string): Promise; } export interface GitBaseExtension { @@ -83,7 +81,6 @@ export interface RemoteSourceProvider { getBranches?(url: string): ProviderResult; getRemoteSourceActions?(url: string): ProviderResult; - getRemoteSourceControlHistoryItemCommands?(url: string): ProviderResult; getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; } diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 47b6aea454b3..805d91c7296b 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -85,7 +85,7 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { return; } - // Default remote (upstream -> origin -> first) + // upstream -> origin -> first const remote = remotes.find(r => r.name === 'upstream') ?? remotes.find(r => r.name === 'origin') ?? remotes[0]; diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts index 1827768312e5..968630e1852a 100644 --- a/extensions/github/src/extension.ts +++ b/extensions/github/src/extension.ts @@ -16,6 +16,7 @@ import { GithubRemoteSourcePublisher } from './remoteSourcePublisher'; import { GithubBranchProtectionProviderManager } from './branchProtection'; import { GitHubCanonicalUriProvider } from './canonicalUriProvider'; import { VscodeDevShareProvider } from './shareProviders'; +import { GitHubSourceControlHistoryItemDetailProvider } from './historyItemDetailProvider'; export function activate(context: ExtensionContext): void { const disposables: Disposable[] = []; @@ -100,6 +101,7 @@ function initializeGitExtension(context: ExtensionContext, telemetryReporter: Te disposables.add(new GithubBranchProtectionProviderManager(gitAPI, context.globalState, logger, telemetryReporter)); disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler(telemetryReporter))); disposables.add(gitAPI.registerRemoteSourcePublisher(new GithubRemoteSourcePublisher(gitAPI))); + disposables.add(gitAPI.registerSourceControlHistoryItemDetailProvider(new GitHubSourceControlHistoryItemDetailProvider())); disposables.add(new GitHubCanonicalUriProvider(gitAPI)); disposables.add(new VscodeDevShareProvider(gitAPI)); setGitHubContext(gitAPI, disposables); diff --git a/extensions/github/src/historyItemDetailProvider.ts b/extensions/github/src/historyItemDetailProvider.ts new file mode 100644 index 000000000000..bf1bdded391b --- /dev/null +++ b/extensions/github/src/historyItemDetailProvider.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Command, l10n } from 'vscode'; +import { Repository, SourceControlHistoryItemDetailProvider } from './typings/git'; +import { getRepositoryDefaultRemote, getRepositoryDefaultRemoteUrl } from './util'; + +const ISSUE_EXPRESSION = /(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/g; + +export class GitHubSourceControlHistoryItemDetailProvider implements SourceControlHistoryItemDetailProvider { + async provideHoverCommands(repository: Repository): Promise { + const url = getRepositoryDefaultRemoteUrl(repository); + if (!url) { + return undefined; + } + + return [{ + title: l10n.t('{0} Open on GitHub', '$(github)'), + tooltip: l10n.t('Open on GitHub'), + command: 'github.openOnGitHub', + arguments: [url] + }]; + } + + async provideMessageLinks(repository: Repository, message: string): Promise { + const descriptor = getRepositoryDefaultRemote(repository); + if (!descriptor) { + return undefined; + } + + return message.replace( + ISSUE_EXPRESSION, + (match, _group1, owner: string | undefined, repo: string | undefined, _group2, number: string | undefined) => { + if (!number || Number.isNaN(parseInt(number))) { + return match; + } + + const label = owner && repo + ? `${owner}/${repo}#${number}` + : `#${number}`; + + owner = owner ?? descriptor.owner; + repo = repo ?? descriptor.repo; + + return `[${label}](https://github.com/${owner}/${repo}/issues/${number})`; + }); + } +} diff --git a/extensions/github/src/remoteSourceProvider.ts b/extensions/github/src/remoteSourceProvider.ts index e79931c74155..0d8b93406953 100644 --- a/extensions/github/src/remoteSourceProvider.ts +++ b/extensions/github/src/remoteSourceProvider.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Command, Uri, env, l10n, workspace } from 'vscode'; +import { Uri, env, l10n, workspace } from 'vscode'; import { RemoteSourceProvider, RemoteSource, RemoteSourceAction } from './typings/git-base'; import { getOctokit } from './auth'; import { Octokit } from '@octokit/rest'; -import { getRepositoryFromQuery, getRepositoryFromUrl, ISSUE_EXPRESSION } from './util'; +import { getRepositoryFromQuery, getRepositoryFromUrl } from './util'; import { getBranchLink, getVscodeDevHost } from './links'; function asRemoteSource(raw: any): RemoteSource { @@ -136,42 +136,4 @@ export class GithubRemoteSourceProvider implements RemoteSourceProvider { } }]; } - - async getRemoteSourceControlHistoryItemCommands(url: string): Promise { - const repository = getRepositoryFromUrl(url); - if (!repository) { - return undefined; - } - - return [{ - title: l10n.t('{0} Open on GitHub', '$(github)'), - tooltip: l10n.t('Open on GitHub'), - command: 'github.openOnGitHub', - arguments: [url] - }]; - } - - provideRemoteSourceLinks(url: string, content: string): string | undefined { - const repository = getRepositoryFromUrl(url); - if (!repository) { - return undefined; - } - - return content.replace( - ISSUE_EXPRESSION, - (match, _group1, owner: string | undefined, repo: string | undefined, _group2, number: string | undefined) => { - if (!number || Number.isNaN(parseInt(number))) { - return match; - } - - const label = owner && repo - ? `${owner}/${repo}#${number}` - : `#${number}`; - - owner = owner ?? repository.owner; - repo = repo ?? repository.repo; - - return `[${label}](https://github.com/${owner}/${repo}/issues/${number})`; - }); - } } diff --git a/extensions/github/src/typings/git-base.d.ts b/extensions/github/src/typings/git-base.d.ts index 548369b1f0f2..d4ec49df47dc 100644 --- a/extensions/github/src/typings/git-base.d.ts +++ b/extensions/github/src/typings/git-base.d.ts @@ -9,9 +9,7 @@ export { ProviderResult } from 'vscode'; export interface API { registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; getRemoteSourceActions(url: string): Promise; - getRemoteSourceControlHistoryItemCommands(url: string): Promise; pickRemoteSource(options: PickRemoteSourceOptions): Promise; - provideRemoteSourceLinks(url: string, content: string): ProviderResult; } export interface GitBaseExtension { @@ -83,8 +81,6 @@ export interface RemoteSourceProvider { getBranches?(url: string): ProviderResult; getRemoteSourceActions?(url: string): ProviderResult; - getRemoteSourceControlHistoryItemCommands?(url: string): ProviderResult; getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; - provideRemoteSourceLinks?(url: string, content: string): ProviderResult; } diff --git a/extensions/github/src/typings/git.d.ts b/extensions/github/src/typings/git.d.ts index 7ac67937a47b..318152e82f66 100644 --- a/extensions/github/src/typings/git.d.ts +++ b/extensions/github/src/typings/git.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Uri, Event, Disposable, ProviderResult, Command } from 'vscode'; +import { Uri, Event, Disposable, ProviderResult, Command, SourceControlHistoryItem } from 'vscode'; export { ProviderResult } from 'vscode'; export interface Git { @@ -289,6 +289,11 @@ export interface BranchProtectionProvider { provideBranchProtection(): BranchProtection[]; } +export interface SourceControlHistoryItemDetailProvider { + provideHoverCommands(repository: Repository): Promise; + provideMessageLinks(repository: Repository, message: string): Promise; +} + export type APIState = 'uninitialized' | 'initialized'; export interface PublishEvent { @@ -316,6 +321,7 @@ export interface API { registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable; registerPushErrorHandler(handler: PushErrorHandler): Disposable; registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable; + registerSourceControlHistoryItemDetailProvider(provider: SourceControlHistoryItemDetailProvider): Disposable; } export interface GitExtension { diff --git a/extensions/github/src/util.ts b/extensions/github/src/util.ts index 5289bb931814..eba3bced698a 100644 --- a/extensions/github/src/util.ts +++ b/extensions/github/src/util.ts @@ -38,4 +38,23 @@ export function repositoryHasGitHubRemote(repository: Repository) { return !!repository.state.remotes.find(remote => remote.fetchUrl ? getRepositoryFromUrl(remote.fetchUrl) : undefined); } -export const ISSUE_EXPRESSION = /(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/g; +export function getRepositoryDefaultRemoteUrl(repository: Repository): string | undefined { + const remotes = repository.state.remotes + .filter(remote => remote.fetchUrl && getRepositoryFromUrl(remote.fetchUrl)); + + if (remotes.length === 0) { + return undefined; + } + + // upstream -> origin -> first + const remote = remotes.find(remote => remote.name === 'upstream') + ?? remotes.find(remote => remote.name === 'origin') + ?? remotes[0]; + + return remote.fetchUrl; +} + +export function getRepositoryDefaultRemote(repository: Repository): { owner: string; repo: string } | undefined { + const fetchUrl = getRepositoryDefaultRemoteUrl(repository); + return fetchUrl ? getRepositoryFromUrl(fetchUrl) : undefined; +} From 4d4466891769b8040fe1ca4ec6a06e36fe2285be Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 16 Jan 2025 11:51:56 +0100 Subject: [PATCH 0590/3587] auto accept changes (#238043) * add setting to auto apply AI changes * fix rounding error * tweak animation --- .../contrib/chat/browser/chat.contribution.ts | 6 ++ .../chatEditingModifiedFileEntry.ts | 57 ++++++++++- .../contrib/chat/browser/chatEditorActions.ts | 54 ++++++++++- .../chat/browser/chatEditorController.ts | 95 +++++++++++-------- .../contrib/chat/browser/chatEditorOverlay.ts | 36 ++++--- .../chat/browser/media/chatEditorOverlay.css | 19 ++++ .../contrib/chat/common/chatEditingService.ts | 4 + .../chatEdit/notebookChatActionsOverlay.ts | 3 +- 8 files changed, 212 insertions(+), 62 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 88fb0c5ba8e5..db49afe10e63 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -128,6 +128,12 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('chat.editing.alwaysSaveWithGeneratedChanges', "Whether files that have changes made by chat can be saved without confirmation."), default: false, }, + 'chat.editing.automaticallyAcceptChanges': { + type: 'boolean', + scope: ConfigurationScope.APPLICATION, + markdownDescription: nls.localize('chat.editing.automaticallyAcceptChanges', "Whether changes made by chat are automatically accepted without prior review."), + default: false, + }, 'chat.editing.confirmEditRequestRemoval': { type: 'boolean', scope: ConfigurationScope.APPLICATION, diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 604bf523b600..200f64bce6dc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -7,7 +7,7 @@ import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Disposable, IReference, toDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; -import { autorun, IObservable, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { autorun, derived, IObservable, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; import { themeColorFromId } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { EditOperation, ISingleEditOperation } from '../../../../../editor/common/core/editOperation.js'; @@ -91,6 +91,12 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie return this._maxLineNumberObs; } + private readonly _reviewModeTempObs = observableValue(this, undefined); + readonly reviewMode: IObservable; + + private readonly _autoAcceptCountdown = observableValue(this, undefined); + readonly autoAcceptCountdown: IObservable = this._autoAcceptCountdown; + private _isFirstEditAfterStartOrSnapshot: boolean = true; private _edit: OffsetEdit = OffsetEdit.empty; private _isEditFromUs: boolean = false; @@ -199,6 +205,14 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie this._diffTrimWhitespace.read(r); this._updateDiffInfoSeq(); })); + + // review mode depends on setting and temporary override + const autoAccept = observableConfigValue('chat.editing.automaticallyAcceptChanges', false, configService); + this.reviewMode = derived(r => { + const configuredValue = !autoAccept.read(r); + const tempValue = this._reviewModeTempObs.read(r); + return tempValue || configuredValue; + }); } override dispose(): void { @@ -212,6 +226,22 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie return this; } + enableReviewModeUntilSettled(): void { + + this._reviewModeTempObs.set(true, undefined); + + const cleanup = autorun(r => { + // reset config when settled + const resetConfig = this.state.read(r) !== WorkingSetEntryState.Modified; + if (resetConfig) { + this._store.delete(cleanup); + this._reviewModeTempObs.set(undefined, undefined); + } + }); + + this._store.add(cleanup); + } + private _clearCurrentEditLineDecoration() { this._editDecorations = this.doc.deltaDecorations(this._editDecorations, []); } @@ -258,6 +288,31 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie this._isCurrentlyBeingModifiedObs.set(false, tx); this._rewriteRatioObs.set(0, tx); this._clearCurrentEditLineDecoration(); + + // AUTO accept mode + if (!this.reviewMode.get()) { + + const future = Date.now() + 10000; // 10secs + const update = () => { + + const reviewMode = this.reviewMode.get(); + if (reviewMode) { + // switched back to review mode + this._autoAcceptCountdown.set(undefined, undefined); + return; + } + + const remain = Math.round((future - Date.now()) / 1000); + if (remain <= 0) { + this.accept(undefined); + this._autoAcceptCountdown.set(undefined, undefined); + } else { + this._autoAcceptCountdown.set(remain, undefined); + setTimeout(update, 1000); + } + }; + update(); + } } private _mirrorEdits(event: IModelContentChangedEvent) { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts index ebd21d41a0bb..0c6b0f4dee73 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; -import { localize2 } from '../../../../nls.js'; +import { localize, localize2 } from '../../../../nls.js'; import { EditorAction2, ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { CHAT_CATEGORY } from './actions/chatActions.js'; -import { ChatEditorController, ctxHasEditorModification, ctxHasRequestInProgress } from './chatEditorController.js'; +import { ChatEditorController, ctxHasEditorModification, ctxReviewModeEnabled } from './chatEditorController.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js'; @@ -45,6 +45,7 @@ abstract class NavigateAction extends Action2 { id: MenuId.ChatEditingEditorContent, group: 'navigate', order: !next ? 2 : 3, + when: ctxReviewModeEnabled } }); } @@ -130,7 +131,7 @@ abstract class AcceptDiscardAction extends Action2 { ? localize2('accept2', 'Accept') : localize2('discard2', 'Discard'), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(ctxHasRequestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), + precondition: ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey), icon: accept ? Codicon.check : Codicon.discard, @@ -146,6 +147,7 @@ abstract class AcceptDiscardAction extends Action2 { id: MenuId.ChatEditingEditorContent, group: 'a_resolve', order: accept ? 0 : 1, + when: !accept ? ctxReviewModeEnabled : undefined } }); } @@ -272,6 +274,7 @@ class OpenDiffAction extends EditorAction2 { id: MenuId.ChatEditingEditorContent, group: 'a_resolve', order: 2, + when: ctxReviewModeEnabled }] }); } @@ -281,12 +284,55 @@ class OpenDiffAction extends EditorAction2 { } } +export class ReviewChangesAction extends EditorAction2 { + + constructor() { + super({ + id: 'chatEditor.action.reviewChanges', + title: localize2('review', "Review"), + menu: [{ + id: MenuId.ChatEditingEditorContent, + group: 'a_resolve', + order: 3, + when: ctxReviewModeEnabled.negate(), + }] + }); + } + + override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor) { + const chatEditingService = accessor.get(IChatEditingService); + + if (!editor.hasModel()) { + return; + } + + const session = chatEditingService.editingSessionsObs.get().find(session => session.getEntry(editor.getModel().uri)); + const entry = session?.getEntry(editor.getModel().uri); + entry?.enableReviewModeUntilSettled(); + } +} + export function registerChatEditorActions() { registerAction2(class NextAction extends NavigateAction { constructor() { super(true); } }); registerAction2(class PrevAction extends NavigateAction { constructor() { super(false); } }); + registerAction2(ReviewChangesAction); registerAction2(AcceptAction); registerAction2(AcceptHunkAction); registerAction2(RejectAction); registerAction2(RejectHunkAction); registerAction2(OpenDiffAction); + } + +export const navigationBearingFakeActionId = 'chatEditor.navigation.bearings'; + +MenuRegistry.appendMenuItem(MenuId.ChatEditingEditorContent, { + command: { + id: navigationBearingFakeActionId, + title: localize('label', "Navigation Status"), + precondition: ContextKeyExpr.false(), + }, + group: 'navigate', + order: -1, + when: ctxReviewModeEnabled, +}); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index f6f4e917c80f..48b5fe9ff4da 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -39,6 +39,7 @@ import { ChatEditorOverlayWidget } from './chatEditorOverlay.js'; export const ctxHasEditorModification = new RawContextKey('chat.hasEditorModifications', undefined, localize('chat.hasEditorModifications', "The current editor contains chat modifications")); export const ctxHasRequestInProgress = new RawContextKey('chat.ctxHasRequestInProgress', false, localize('chat.ctxHasRequestInProgress', "The current editor shows a file from an edit session which is still in progress")); +export const ctxReviewModeEnabled = new RawContextKey('chat.ctxReviewModeEnabled', true, localize('chat.ctxReviewModeEnabled', "Review mode for chat changes is enabled")); export class ChatEditorController extends Disposable implements IEditorContribution { @@ -57,6 +58,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut private readonly _ctxHasEditorModification: IContextKey; private readonly _ctxRequestInProgress: IContextKey; + private readonly _ctxReviewModelEnabled: IContextKey; static get(editor: ICodeEditor): ChatEditorController | null { const controller = editor.getContribution(ChatEditorController.ID); @@ -84,13 +86,13 @@ export class ChatEditorController extends Disposable implements IEditorContribut this._overlayWidget = _instantiationService.createInstance(ChatEditorOverlayWidget, _editor); this._ctxHasEditorModification = ctxHasEditorModification.bindTo(contextKeyService); this._ctxRequestInProgress = ctxHasRequestInProgress.bindTo(contextKeyService); + this._ctxReviewModelEnabled = ctxReviewModeEnabled.bindTo(contextKeyService); const editorObs = observableCodeEditor(this._editor); const fontInfoObs = editorObs.getOption(EditorOption.fontInfo); const lineHeightObs = editorObs.getOption(EditorOption.lineHeight); const modelObs = editorObs.model; - this._store.add(autorun(r => { let isStreamingEdits = false; for (const session of _chatEditingService.editingSessionsObs.read(r)) { @@ -139,11 +141,13 @@ export class ChatEditorController extends Disposable implements IEditorContribut const { session, entries, idx, entry } = currentEditorEntry; + this._ctxReviewModelEnabled.set(entry.reviewMode.read(r)); + // context this._currentEntryIndex.set(idx, undefined); // overlay widget - if (entry.state.read(r) === WorkingSetEntryState.Accepted || entry.state.read(r) === WorkingSetEntryState.Rejected) { + if (entry.state.read(r) !== WorkingSetEntryState.Modified) { this._overlayWidget.hide(); } else { this._overlayWidget.show(session, entry, entries[(idx + 1) % entries.length]); @@ -172,9 +176,11 @@ export class ChatEditorController extends Disposable implements IEditorContribut // Add line decorations (just markers, no UI) for diff navigation this._updateDiffLineDecorations(diff); + const reviewMode = entry.reviewMode.read(r); + // Add diff decoration to the UI (unless in diff editor) if (!this._editor.getOption(EditorOption.inDiffEditor)) { - this._updateDiffRendering(entry, diff); + this._updateDiffRendering(entry, diff, reviewMode); } else { this._clearDiffRendering(); } @@ -246,6 +252,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut this._currentChangeIndex.set(undefined, undefined); this._currentEntryIndex.set(undefined, undefined); this._ctxHasEditorModification.reset(); + this._ctxReviewModelEnabled.reset(); } private _clearDiffRendering() { @@ -260,7 +267,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut this._scrollLock = false; } - private _updateDiffRendering(entry: IModifiedFileEntry, diff: IDocumentDiff): void { + private _updateDiffRendering(entry: IModifiedFileEntry, diff: IDocumentDiff, reviewMode: boolean): void { const originalModel = entry.originalModel; @@ -309,18 +316,21 @@ export class ChatEditorController extends Disposable implements IEditorContribut mightContainRTL, ); const decorations: InlineDecoration[] = []; - for (const i of diffEntry.innerChanges || []) { - decorations.push(new InlineDecoration( - i.originalRange.delta(-(diffEntry.original.startLineNumber - 1)), - diffDeleteDecoration.className!, - InlineDecorationType.Regular - )); - - // If the original range is empty, the start line number is 1 and the new range spans the entire file, don't draw an Added decoration - if (!(i.originalRange.isEmpty() && i.originalRange.startLineNumber === 1 && i.modifiedRange.endLineNumber === editorLineCount) && !i.modifiedRange.isEmpty()) { - modifiedVisualDecorations.push({ - range: i.modifiedRange, options: chatDiffAddDecoration - }); + + if (reviewMode) { + for (const i of diffEntry.innerChanges || []) { + decorations.push(new InlineDecoration( + i.originalRange.delta(-(diffEntry.original.startLineNumber - 1)), + diffDeleteDecoration.className!, + InlineDecorationType.Regular + )); + + // If the original range is empty, the start line number is 1 and the new range spans the entire file, don't draw an Added decoration + if (!(i.originalRange.isEmpty() && i.originalRange.startLineNumber === 1 && i.modifiedRange.endLineNumber === editorLineCount) && !i.modifiedRange.isEmpty()) { + modifiedVisualDecorations.push({ + range: i.modifiedRange, options: chatDiffAddDecoration + }); + } } } @@ -354,33 +364,38 @@ export class ChatEditorController extends Disposable implements IEditorContribut options: modifiedDecoration }); } - const domNode = document.createElement('div'); - domNode.className = 'chat-editing-original-zone view-lines line-delete monaco-mouse-cursor-text'; - const result = renderLines(source, renderOptions, decorations, domNode); - - if (!isCreatedContent) { - const viewZoneData: IViewZone = { - afterLineNumber: diffEntry.modified.startLineNumber - 1, - heightInLines: result.heightInLines, - domNode, - ordinal: 50000 + 2 // more than https://github.com/microsoft/vscode/blob/bf52a5cfb2c75a7327c9adeaefbddc06d529dcad/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts#L42 - }; - - this._viewZones.push(viewZoneChangeAccessor.addZone(viewZoneData)); - } - // Add content widget for each diff change - const widget = this._instantiationService.createInstance(DiffHunkWidget, entry, diffEntry, this._editor.getModel()!.getVersionId(), this._editor, isCreatedContent ? 0 : result.heightInLines); - widget.layout(diffEntry.modified.startLineNumber); + if (reviewMode) { + const domNode = document.createElement('div'); + domNode.className = 'chat-editing-original-zone view-lines line-delete monaco-mouse-cursor-text'; + const result = renderLines(source, renderOptions, decorations, domNode); + + if (!isCreatedContent) { + + const viewZoneData: IViewZone = { + afterLineNumber: diffEntry.modified.startLineNumber - 1, + heightInLines: result.heightInLines, + domNode, + ordinal: 50000 + 2 // more than https://github.com/microsoft/vscode/blob/bf52a5cfb2c75a7327c9adeaefbddc06d529dcad/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts#L42 + }; - this._diffHunkWidgets.push(widget); - diffHunkDecorations.push({ - range: diffEntry.modified.toInclusiveRange() ?? new Range(diffEntry.modified.startLineNumber, 1, diffEntry.modified.startLineNumber, Number.MAX_SAFE_INTEGER), - options: { - description: 'diff-hunk-widget', - stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges + this._viewZones.push(viewZoneChangeAccessor.addZone(viewZoneData)); } - }); + + + // Add content widget for each diff change + const widget = this._instantiationService.createInstance(DiffHunkWidget, entry, diffEntry, this._editor.getModel()!.getVersionId(), this._editor, isCreatedContent ? 0 : result.heightInLines); + widget.layout(diffEntry.modified.startLineNumber); + + this._diffHunkWidgets.push(widget); + diffHunkDecorations.push({ + range: diffEntry.modified.toInclusiveRange() ?? new Range(diffEntry.modified.startLineNumber, 1, diffEntry.modified.startLineNumber, Number.MAX_SAFE_INTEGER), + options: { + description: 'diff-hunk-widget', + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges + } + }); + } } this._diffVisualDecorations.set(modifiedVisualDecorations); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index 304a02f01cb0..f24e81fd641b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -9,7 +9,7 @@ import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPosit import { HiddenItemStrategy, MenuWorkbenchToolBar, WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IChatEditingSession, IModifiedFileEntry } from '../common/chatEditingService.js'; -import { MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; +import { MenuId } from '../../../../platform/actions/common/actions.js'; import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js'; import { Range } from '../../../../editor/common/core/range.js'; @@ -20,8 +20,7 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { assertType } from '../../../../base/common/types.js'; import { localize } from '../../../../nls.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { AcceptAction, RejectAction } from './chatEditorActions.js'; +import { AcceptAction, navigationBearingFakeActionId, RejectAction } from './chatEditorActions.js'; import { ChatEditorController } from './chatEditorController.js'; import './media/chatEditorOverlay.css'; import { findDiffEditorContainingCodeEditor } from '../../../../editor/browser/widget/diffEditor/commands.js'; @@ -123,6 +122,25 @@ export class ChatEditorOverlayWidget implements IOverlayWidget { constructor() { super(undefined, action, { ...options, icon: false, label: true, keybindingNotRenderedWithLabel: true }); } + + override render(container: HTMLElement): void { + super.render(container); + + if (action.id === AcceptAction.ID) { + assertType(this.label); + assertType(this.element); + + this._store.add(autorun(r => { + const value = that._entry.read(r)?.entry.autoAcceptCountdown.read(r); + this.label!.innerText = value === undefined + ? this.action.label + : localize('pattern', "{0} ({1})", this.action.label, value); + + this.element?.classList.toggle('auto', !!value); + })); + } + } + override set actionRunner(actionRunner: IActionRunner) { super.actionRunner = actionRunner; @@ -258,15 +276,3 @@ export class ChatEditorOverlayWidget implements IOverlayWidget { } } } - -export const navigationBearingFakeActionId = 'chatEditor.navigation.bearings'; - -MenuRegistry.appendMenuItem(MenuId.ChatEditingEditorContent, { - command: { - id: navigationBearingFakeActionId, - title: localize('label', "Navigation Status"), - precondition: ContextKeyExpr.false(), - }, - group: 'navigate', - order: -1 -}); diff --git a/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css index 153da41fe6ee..dabc35cdafa9 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css @@ -89,3 +89,22 @@ color: var(--vscode-button-foreground); opacity: 1; } + +.chat-editor-overlay-widget .action-item.auto > .action-label { + animation: textShift 5s linear infinite alternate; + background: linear-gradient(90deg, + var(--vscode-button-foreground) 0%, + var(--vscode-button-foreground) 45%, + var(--vscode-editorGutter-addedBackground) 50%, + var(--vscode-button-foreground) 55%, + var(--vscode-button-foreground) 100%); + background-size: 200% 100%; + color: transparent; + -webkit-background-clip: text; + background-clip: text; +} + +@keyframes textShift { + 0% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index d4db6b9695e8..560bb4709810 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -138,6 +138,10 @@ export interface IModifiedFileEntry { readonly lastModifyingRequestId: string; accept(transaction: ITransaction | undefined): Promise; reject(transaction: ITransaction | undefined): Promise; + + reviewMode: IObservable; + autoAcceptCountdown: IObservable; + enableReviewModeUntilSettled(): void; } export interface IChatEditingSessionStream { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts index 7ac61fbcb303..f58810283e32 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts @@ -18,8 +18,7 @@ import { autorun, autorunWithStore, IObservable, ISettableObservable, observable import { isEqual } from '../../../../../../base/common/resources.js'; import { CellDiffInfo } from '../../diff/notebookDiffViewModel.js'; import { INotebookDeletedCellDecorator } from './notebookCellDecorators.js'; -import { AcceptAction, RejectAction } from '../../../../chat/browser/chatEditorActions.js'; -import { navigationBearingFakeActionId } from '../../../../chat/browser/chatEditorOverlay.js'; +import { AcceptAction, navigationBearingFakeActionId, RejectAction } from '../../../../chat/browser/chatEditorActions.js'; export class NotebookChatActionsOverlayController extends Disposable { constructor( From 118e6f53734b679dfb6174c1413f1a41337e930e Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Wed, 15 Jan 2025 19:18:34 +0100 Subject: [PATCH 0591/3587] Local and remote proxy settings (microsoft/vscode-copilot-release#3821) --- .../src/singlefolder-tests/proxy.test.ts | 58 ++++++ src/vs/base/parts/ipc/common/ipc.ts | 2 +- src/vs/code/node/cliProcessMain.ts | 2 +- src/vs/platform/request/common/request.ts | 186 ++++++++++-------- .../electron-utility/requestService.ts | 11 ++ .../platform/request/node/requestService.ts | 25 +-- .../windows/electron-main/windowImpl.ts | 3 +- .../node/remoteExtensionHostAgentCli.ts | 2 +- src/vs/server/node/serverServices.ts | 2 +- src/vs/workbench/api/node/proxyResolver.ts | 53 +++-- .../test/node/colorRegistry.releaseTest.ts | 2 +- .../common/abstractExtensionService.ts | 2 +- .../nativeExtensionService.ts | 9 +- .../request/browser/requestService.ts | 2 +- .../electron-sandbox/requestService.ts | 2 +- 15 files changed, 237 insertions(+), 124 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts index 9e29c8e6f282..1537d89dbaa6 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts @@ -11,6 +11,7 @@ import { AddressInfo } from 'net'; import { resetCaches } from '@vscode/proxy-agent'; import * as vscode from 'vscode'; import { middleware, Straightforward } from 'straightforward'; +import assert from 'assert'; (vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('vscode API - network proxy support', () => { @@ -166,4 +167,61 @@ import { middleware, Straightforward } from 'straightforward'; await vscode.workspace.getConfiguration().update('integration-test.http.proxyAuth', undefined, vscode.ConfigurationTarget.Global); } }); + + (vscode.env.remoteName ? test : test.skip)('separate local / remote proxy settings', async () => { + // Assumes test resolver runs with `--use-host-proxy`. + const localProxy = 'http://localhost:1234'; + const remoteProxy = 'http://localhost:4321'; + + const actualLocalProxy1 = vscode.workspace.getConfiguration().get('http.proxy'); + + const p1 = waitForConfigChange('http.proxy'); + await vscode.workspace.getConfiguration().update('http.proxy', localProxy, vscode.ConfigurationTarget.Global); + await p1; + const actualLocalProxy2 = vscode.workspace.getConfiguration().get('http.proxy'); + + const p2 = waitForConfigChange('http.useLocalProxyConfiguration'); + await vscode.workspace.getConfiguration().update('http.useLocalProxyConfiguration', false, vscode.ConfigurationTarget.Global); + await p2; + const actualRemoteProxy1 = vscode.workspace.getConfiguration().get('http.proxy'); + + const p3 = waitForConfigChange('http.proxy'); + await vscode.workspace.getConfiguration().update('http.proxy', remoteProxy, vscode.ConfigurationTarget.Global); + await p3; + const actualRemoteProxy2 = vscode.workspace.getConfiguration().get('http.proxy'); + + const p4 = waitForConfigChange('http.proxy'); + await vscode.workspace.getConfiguration().update('http.proxy', undefined, vscode.ConfigurationTarget.Global); + await p4; + const actualRemoteProxy3 = vscode.workspace.getConfiguration().get('http.proxy'); + + const p5 = waitForConfigChange('http.proxy'); + await vscode.workspace.getConfiguration().update('http.useLocalProxyConfiguration', true, vscode.ConfigurationTarget.Global); + await p5; + const actualLocalProxy3 = vscode.workspace.getConfiguration().get('http.proxy'); + + const p6 = waitForConfigChange('http.proxy'); + await vscode.workspace.getConfiguration().update('http.proxy', undefined, vscode.ConfigurationTarget.Global); + await p6; + const actualLocalProxy4 = vscode.workspace.getConfiguration().get('http.proxy'); + + assert.strictEqual(actualLocalProxy1, ''); + assert.strictEqual(actualLocalProxy2, localProxy); + assert.strictEqual(actualRemoteProxy1, ''); + assert.strictEqual(actualRemoteProxy2, remoteProxy); + assert.strictEqual(actualRemoteProxy3, ''); + assert.strictEqual(actualLocalProxy3, localProxy); + assert.strictEqual(actualLocalProxy4, ''); + + function waitForConfigChange(key: string) { + return new Promise(resolve => { + const s = vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(key)) { + s.dispose(); + resolve(); + } + }); + }); + } + }); }); diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 5b51cd2ae90d..92bb92c78c67 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -649,7 +649,7 @@ export class ChannelClient implements IChannelClient, IDisposable { }); return result.finally(() => { - disposable.dispose(); + disposable?.dispose(); // Seen as undefined in tests. this.activeRequests.delete(disposableWithRequestCancel); }); } diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index e6f25b20b530..aff3b2da5794 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -196,7 +196,7 @@ class CliMain extends Disposable { services.set(IUriIdentityService, new UriIdentityService(fileService)); // Request - const requestService = new RequestService(configurationService, environmentService, logService); + const requestService = new RequestService('local', configurationService, environmentService, logService); services.set(IRequestService, requestService); // Download Service diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index 2c85ab8c043a..64818e059859 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -134,91 +134,117 @@ export async function asJson(context: IRequestContext): Promise(Extensions.Configuration); const oldProxyConfiguration = proxyConfiguration; - proxyConfiguration = { - id: 'http', - order: 15, - title: localize('httpConfigurationTitle', "HTTP"), - type: 'object', - scope, - properties: { - 'http.proxy': { - type: 'string', - pattern: '^(https?|socks|socks4a?|socks5h?)://([^:]*(:[^@]*)?@)?([^:]+|\\[[:0-9a-fA-F]+\\])(:\\d+)?/?$|^$', - markdownDescription: localize('proxy', "The proxy setting to use. If not set, will be inherited from the `http_proxy` and `https_proxy` environment variables."), - restricted: true - }, - 'http.proxyStrictSSL': { - type: 'boolean', - default: true, - description: localize('strictSSL', "Controls whether the proxy server certificate should be verified against the list of supplied CAs."), - restricted: true - }, - 'http.proxyKerberosServicePrincipal': { - type: 'string', - markdownDescription: localize('proxyKerberosServicePrincipal', "Overrides the principal service name for Kerberos authentication with the HTTP proxy. A default based on the proxy hostname is used when this is not set."), - restricted: true - }, - 'http.noProxy': { - type: 'array', - items: { type: 'string' }, - markdownDescription: localize('noProxy', "Specifies domain names for which proxy settings should be ignored for HTTP/HTTPS requests."), - restricted: true - }, - 'http.proxyAuthorization': { - type: ['null', 'string'], - default: null, - markdownDescription: localize('proxyAuthorization', "The value to send as the `Proxy-Authorization` header for every network request."), - restricted: true - }, - 'http.proxySupport': { - type: 'string', - enum: ['off', 'on', 'fallback', 'override'], - enumDescriptions: [ - localize('proxySupportOff', "Disable proxy support for extensions."), - localize('proxySupportOn', "Enable proxy support for extensions."), - localize('proxySupportFallback', "Enable proxy support for extensions, fall back to request options, when no proxy found."), - localize('proxySupportOverride', "Enable proxy support for extensions, override request options."), - ], - default: 'override', - description: localize('proxySupport', "Use the proxy support for extensions."), - restricted: true - }, - 'http.systemCertificates': { - type: 'boolean', - default: true, - description: localize('systemCertificates', "Controls whether CA certificates should be loaded from the OS. (On Windows and macOS, a reload of the window is required after turning this off.)"), - restricted: true - }, - 'http.experimental.systemCertificatesV2': { - type: 'boolean', - tags: ['experimental'], - default: false, - description: localize('systemCertificatesV2', "Controls whether experimental loading of CA certificates from the OS should be enabled. This uses a more general approach than the default implementation."), - restricted: true - }, - 'http.electronFetch': { - type: 'boolean', - default: false, - description: localize('electronFetch', "Controls whether use of Electron's fetch implementation instead of Node.js' should be enabled. All local extensions will get Electron's fetch implementation for the global fetch API."), - restricted: true - }, - 'http.fetchAdditionalSupport': { - type: 'boolean', - default: true, - markdownDescription: localize('fetchAdditionalSupport', "Controls whether Node.js' fetch implementation should be extended with additional support. Currently proxy support ({0}) and system certificates ({1}) are added when the corresponding settings are enabled.", '`#http.proxySupport#`', '`#http.systemCertificates#`'), - restricted: true + proxyConfiguration = [ + { + id: 'http', + order: 15, + title: localize('httpConfigurationTitle', "HTTP"), + type: 'object', + scope: ConfigurationScope.MACHINE, + properties: { + 'http.useLocalProxyConfiguration': { + type: 'boolean', + default: useHostProxyDefault, + markdownDescription: localize('useLocalProxy', "Controls whether in the remote extension host the local proxy configuration should be used. This setting only applies as a remote setting during [remote development](https://aka.ms/vscode-remote)."), + restricted: true + }, + } + }, + { + id: 'http', + order: 15, + title: localize('httpConfigurationTitle', "HTTP"), + type: 'object', + scope: ConfigurationScope.APPLICATION, + properties: { + 'http.electronFetch': { + type: 'boolean', + default: false, + description: localize('electronFetch', "Controls whether use of Electron's fetch implementation instead of Node.js' should be enabled. All local extensions will get Electron's fetch implementation for the global fetch API."), + restricted: true + }, + } + }, + { + id: 'http', + order: 15, + title: localize('httpConfigurationTitle', "HTTP"), + type: 'object', + scope: useHostProxy ? ConfigurationScope.APPLICATION : ConfigurationScope.MACHINE, + properties: { + 'http.proxy': { + type: 'string', + pattern: '^(https?|socks|socks4a?|socks5h?)://([^:]*(:[^@]*)?@)?([^:]+|\\[[:0-9a-fA-F]+\\])(:\\d+)?/?$|^$', + markdownDescription: localize('proxy', "The proxy setting to use. If not set, will be inherited from the `http_proxy` and `https_proxy` environment variables. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), + restricted: true + }, + 'http.proxyStrictSSL': { + type: 'boolean', + default: true, + markdownDescription: localize('strictSSL', "Controls whether the proxy server certificate should be verified against the list of supplied CAs. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), + restricted: true + }, + 'http.proxyKerberosServicePrincipal': { + type: 'string', + markdownDescription: localize('proxyKerberosServicePrincipal', "Overrides the principal service name for Kerberos authentication with the HTTP proxy. A default based on the proxy hostname is used when this is not set. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), + restricted: true + }, + 'http.noProxy': { + type: 'array', + items: { type: 'string' }, + markdownDescription: localize('noProxy', "Specifies domain names for which proxy settings should be ignored for HTTP/HTTPS requests. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), + restricted: true + }, + 'http.proxyAuthorization': { + type: ['null', 'string'], + default: null, + markdownDescription: localize('proxyAuthorization', "The value to send as the `Proxy-Authorization` header for every network request. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), + restricted: true + }, + 'http.proxySupport': { + type: 'string', + enum: ['off', 'on', 'fallback', 'override'], + enumDescriptions: [ + localize('proxySupportOff', "Disable proxy support for extensions."), + localize('proxySupportOn', "Enable proxy support for extensions."), + localize('proxySupportFallback', "Enable proxy support for extensions, fall back to request options, when no proxy found."), + localize('proxySupportOverride', "Enable proxy support for extensions, override request options."), + ], + default: 'override', + markdownDescription: localize('proxySupport', "Use the proxy support for extensions. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), + restricted: true + }, + 'http.systemCertificates': { + type: 'boolean', + default: true, + markdownDescription: localize('systemCertificates', "Controls whether CA certificates should be loaded from the OS. On Windows and macOS, a reload of the window is required after turning this off. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), + restricted: true + }, + 'http.experimental.systemCertificatesV2': { + type: 'boolean', + tags: ['experimental'], + default: false, + markdownDescription: localize('systemCertificatesV2', "Controls whether experimental loading of CA certificates from the OS should be enabled. This uses a more general approach than the default implementation. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'), + restricted: true + }, + 'http.fetchAdditionalSupport': { + type: 'boolean', + default: true, + markdownDescription: localize('fetchAdditionalSupport', "Controls whether Node.js' fetch implementation should be extended with additional support. Currently proxy support ({1}) and system certificates ({2}) are added when the corresponding settings are enabled. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`', '`#http.proxySupport#`', '`#http.systemCertificates#`'), + restricted: true + } } } - }; - configurationRegistry.updateConfigurations({ add: [proxyConfiguration], remove: oldProxyConfiguration ? [oldProxyConfiguration] : [] }); + ]; + configurationRegistry.updateConfigurations({ add: proxyConfiguration, remove: oldProxyConfiguration }); } -registerProxyConfigurations(ConfigurationScope.APPLICATION); +registerProxyConfigurations(); diff --git a/src/vs/platform/request/electron-utility/requestService.ts b/src/vs/platform/request/electron-utility/requestService.ts index f92a2450c3e6..eb3097b2e949 100644 --- a/src/vs/platform/request/electron-utility/requestService.ts +++ b/src/vs/platform/request/electron-utility/requestService.ts @@ -7,6 +7,9 @@ import { net } from 'electron'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { IRequestContext, IRequestOptions } from '../../../base/parts/request/common/request.js'; import { IRawRequestFunction, RequestService as NodeRequestService } from '../node/requestService.js'; +import { IConfigurationService } from '../../configuration/common/configuration.js'; +import { INativeEnvironmentService } from '../../environment/common/environment.js'; +import { ILogService } from '../../log/common/log.js'; function getRawRequest(options: IRequestOptions): IRawRequestFunction { return net.request as any as IRawRequestFunction; @@ -14,6 +17,14 @@ function getRawRequest(options: IRequestOptions): IRawRequestFunction { export class RequestService extends NodeRequestService { + constructor( + @IConfigurationService configurationService: IConfigurationService, + @INativeEnvironmentService environmentService: INativeEnvironmentService, + @ILogService logService: ILogService, + ) { + super('local', configurationService, environmentService, logService); + } + override request(options: IRequestOptions, token: CancellationToken): Promise { return super.request({ ...(options || {}), getRawRequest, isChromiumNetwork: true }, token); } diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index 38a582b044e9..333705f23e22 100644 --- a/src/vs/platform/request/node/requestService.ts +++ b/src/vs/platform/request/node/requestService.ts @@ -21,12 +21,6 @@ import { AbstractRequestService, AuthInfo, Credentials, IRequestService } from ' import { Agent, getProxyAgent } from './proxy.js'; import { createGunzip } from 'zlib'; -interface IHTTPConfiguration { - proxy?: string; - proxyStrictSSL?: boolean; - proxyAuthorization?: string; -} - export interface IRawRequestFunction { (options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void): http.ClientRequest; } @@ -52,6 +46,7 @@ export class RequestService extends AbstractRequestService implements IRequestSe private shellEnvErrorLogged?: boolean; constructor( + private readonly machine: 'local' | 'remote', @IConfigurationService private readonly configurationService: IConfigurationService, @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, @ILogService logService: ILogService, @@ -66,11 +61,9 @@ export class RequestService extends AbstractRequestService implements IRequestSe } private configure() { - const config = this.configurationService.getValue('http'); - - this.proxyUrl = config?.proxy; - this.strictSSL = !!config?.proxyStrictSSL; - this.authorization = config?.proxyAuthorization; + this.proxyUrl = this.getConfigValue('http.proxy'); + this.strictSSL = !!this.getConfigValue('http.proxyStrictSSL'); + this.authorization = this.getConfigValue('http.proxyAuthorization'); } async request(options: NodeRequestOptions, token: CancellationToken): Promise { @@ -115,7 +108,7 @@ export class RequestService extends AbstractRequestService implements IRequestSe async lookupKerberosAuthorization(urlStr: string): Promise { try { - const spnConfig = this.configurationService.getValue('http.proxyKerberosServicePrincipal'); + const spnConfig = this.getConfigValue('http.proxyKerberosServicePrincipal'); const response = await lookupKerberosAuthorization(urlStr, spnConfig, this.logService, 'RequestService#lookupKerberosAuthorization'); return 'Negotiate ' + response; } catch (err) { @@ -128,6 +121,14 @@ export class RequestService extends AbstractRequestService implements IRequestSe const proxyAgent = await import('@vscode/proxy-agent'); return proxyAgent.loadSystemCertificates({ log: this.logService }); } + + private getConfigValue(key: string): T | undefined { + if (this.machine === 'remote') { + return this.configurationService.getValue(key); + } + const values = this.configurationService.inspect(key); + return values.userLocalValue || values.defaultValue; + } } export async function lookupKerberosAuthorization(urlStr: string, spnConfig: string | undefined, logService: ILogService, logPrefix: string) { diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index ab629987eae8..6664f2b502c9 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -923,7 +923,8 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { // Proxy if (!e || e.affectsConfiguration('http.proxy') || e.affectsConfiguration('http.noProxy')) { - let newHttpProxy = (this.configurationService.getValue('http.proxy') || '').trim() + const inspect = this.configurationService.inspect('http.proxy'); + let newHttpProxy = (inspect.userLocalValue || '').trim() || (process.env['https_proxy'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['HTTP_PROXY'] || '').trim() // Not standardized. || undefined; diff --git a/src/vs/server/node/remoteExtensionHostAgentCli.ts b/src/vs/server/node/remoteExtensionHostAgentCli.ts index 87ceb0788c9a..df33289a964c 100644 --- a/src/vs/server/node/remoteExtensionHostAgentCli.ts +++ b/src/vs/server/node/remoteExtensionHostAgentCli.ts @@ -129,7 +129,7 @@ class CliMain extends Disposable { userDataProfilesService.init() ]); - services.set(IRequestService, new SyncDescriptor(RequestService)); + services.set(IRequestService, new SyncDescriptor(RequestService, ['remote'])); services.set(IDownloadService, new SyncDescriptor(DownloadService)); services.set(ITelemetryService, NullTelemetryService); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService)); diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index 930626375a68..fb973bd49858 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -149,7 +149,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken services.set(IExtensionHostStatusService, extensionHostStatusService); // Request - const requestService = new RequestService(configurationService, environmentService, logService); + const requestService = new RequestService('remote', configurationService, environmentService, logService); services.set(IRequestService, requestService); let oneDsAppender: ITelemetryAppender = NullAppender; diff --git a/src/vs/workbench/api/node/proxyResolver.ts b/src/vs/workbench/api/node/proxyResolver.ts index a4e4952e34aa..9c85d7b980e2 100644 --- a/src/vs/workbench/api/node/proxyResolver.ts +++ b/src/vs/workbench/api/node/proxyResolver.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IExtHostWorkspaceProvider } from '../common/extHostWorkspace.js'; -import { ExtHostConfigProvider } from '../common/extHostConfiguration.js'; +import { ConfigurationInspect, ExtHostConfigProvider } from '../common/extHostConfiguration.js'; import { MainThreadTelemetryShape } from '../common/extHost.protocol.js'; import { IExtensionHostInitData } from '../../services/extensions/common/extensionHostProtocol.js'; import { ExtHostExtensionService } from './extHostExtensionService.js'; @@ -39,17 +39,20 @@ export function connectProxyResolver( disposables: DisposableStore, ) { - const useHostProxy = initData.environment.useHostProxy; - const doUseHostProxy = typeof useHostProxy === 'boolean' ? useHostProxy : !initData.remote.isRemote; + const isRemote = initData.remote.isRemote; + const useHostProxyDefault = initData.environment.useHostProxy ?? !isRemote; + const fallbackToLocalKerberos = useHostProxyDefault; + const loadLocalCertificates = useHostProxyDefault; + const isUseHostProxyEnabled = () => configProvider.getConfiguration('http').get('useLocalProxyConfiguration', useHostProxyDefault); const params: ProxyAgentParams = { resolveProxy: url => extHostWorkspace.resolveProxy(url), - lookupProxyAuthorization: lookupProxyAuthorization.bind(undefined, extHostWorkspace, extHostLogService, mainThreadTelemetry, configProvider, {}, {}, initData.remote.isRemote, doUseHostProxy), - getProxyURL: () => configProvider.getConfiguration('http').get('proxy'), - getProxySupport: () => configProvider.getConfiguration('http').get('proxySupport') || 'off', - getNoProxyConfig: () => configProvider.getConfiguration('http').get('noProxy') || [], - isAdditionalFetchSupportEnabled: () => configProvider.getConfiguration('http').get('fetchAdditionalSupport', true), - addCertificatesV1: () => certSettingV1(configProvider), - addCertificatesV2: () => certSettingV2(configProvider), + lookupProxyAuthorization: lookupProxyAuthorization.bind(undefined, extHostWorkspace, extHostLogService, mainThreadTelemetry, configProvider, {}, {}, initData.remote.isRemote, fallbackToLocalKerberos), + getProxyURL: () => getExtHostConfigValue(configProvider, isRemote, 'http.proxy'), + getProxySupport: () => getExtHostConfigValue(configProvider, isRemote, 'http.proxySupport') || 'off', + getNoProxyConfig: () => getExtHostConfigValue(configProvider, isRemote, 'http.noProxy') || [], + isAdditionalFetchSupportEnabled: () => getExtHostConfigValue(configProvider, isRemote, 'http.fetchAdditionalSupport', true), + addCertificatesV1: () => certSettingV1(configProvider, isRemote), + addCertificatesV2: () => certSettingV2(configProvider, isRemote), log: extHostLogService, getLogLevel: () => { const level = extHostLogService.getLevel(); @@ -68,13 +71,13 @@ export function connectProxyResolver( } }, proxyResolveTelemetry: () => { }, - useHostProxy: doUseHostProxy, + useHostProxy: isUseHostProxyEnabled(), // TODO: can change at runtime now loadAdditionalCertificates: async () => { const promises: Promise[] = []; if (initData.remote.isRemote) { promises.push(loadSystemCertificates({ log: extHostLogService })); } - if (doUseHostProxy) { + if (loadLocalCertificates) { extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loading certificates from main process'); const certs = extHostWorkspace.loadCertificates(); // Loading from main process to share cache. certs.then(certs => extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loaded certificates from main process', certs.length)); @@ -249,14 +252,12 @@ function createPatchedModules(params: ProxyAgentParams, resolveProxy: ResolvePro }; } -function certSettingV1(configProvider: ExtHostConfigProvider) { - const http = configProvider.getConfiguration('http'); - return !http.get('experimental.systemCertificatesV2', systemCertificatesV2Default) && !!http.get('systemCertificates'); +function certSettingV1(configProvider: ExtHostConfigProvider, isRemote: boolean) { + return !getExtHostConfigValue(configProvider, isRemote, 'http.experimental.systemCertificatesV2', systemCertificatesV2Default) && !!getExtHostConfigValue(configProvider, isRemote, 'http.systemCertificates'); } -function certSettingV2(configProvider: ExtHostConfigProvider) { - const http = configProvider.getConfiguration('http'); - return !!http.get('experimental.systemCertificatesV2', systemCertificatesV2Default) && !!http.get('systemCertificates'); +function certSettingV2(configProvider: ExtHostConfigProvider, isRemote: boolean) { + return !!getExtHostConfigValue(configProvider, isRemote, 'http.experimental.systemCertificatesV2', systemCertificatesV2Default) && !!getExtHostConfigValue(configProvider, isRemote, 'http.systemCertificates'); } const modulesCache = new Map(); @@ -306,7 +307,7 @@ async function lookupProxyAuthorization( proxyAuthenticateCache: Record, basicAuthCache: Record, isRemote: boolean, - useHostProxy: boolean, + fallbackToLocalKerberos: boolean, proxyURL: string, proxyAuthenticate: string | string[] | undefined, state: { kerberosRequested?: boolean; basicAuthCacheUsed?: boolean; basicAuthAttempt?: number } @@ -323,14 +324,14 @@ async function lookupProxyAuthorization( state.kerberosRequested = true; try { - const spnConfig = configProvider.getConfiguration('http').get('proxyKerberosServicePrincipal'); + const spnConfig = getExtHostConfigValue(configProvider, isRemote, 'http.proxyKerberosServicePrincipal'); const response = await lookupKerberosAuthorization(proxyURL, spnConfig, extHostLogService, 'ProxyResolver#lookupProxyAuthorization'); return 'Negotiate ' + response; } catch (err) { extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Kerberos authentication failed', err); } - if (isRemote && useHostProxy) { + if (isRemote && fallbackToLocalKerberos) { extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Kerberos authentication lookup on host', `proxyURL:${proxyURL}`); const auth = await extHostWorkspace.lookupKerberosAuthorization(proxyURL); if (auth) { @@ -405,3 +406,13 @@ function sendTelemetry(mainThreadTelemetry: MainThreadTelemetryShape, authentica extensionHostType: isRemote ? 'remote' : 'local', }); } + +function getExtHostConfigValue(configProvider: ExtHostConfigProvider, isRemote: boolean, key: string, fallback: T): T; +function getExtHostConfigValue(configProvider: ExtHostConfigProvider, isRemote: boolean, key: string): T | undefined; +function getExtHostConfigValue(configProvider: ExtHostConfigProvider, isRemote: boolean, key: string, fallback?: T): T | undefined { + if (isRemote) { + return configProvider.getConfiguration().get(key) ?? fallback; + } + const values: ConfigurationInspect | undefined = configProvider.getConfiguration().inspect(key); + return values?.globalLocalValue ?? values?.defaultValue ?? fallback; +} diff --git a/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts b/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts index 50a4b4b68379..02079b12c3f8 100644 --- a/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts +++ b/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts @@ -91,7 +91,7 @@ suite('Color Registry', function () { const docUrl = 'https://raw.githubusercontent.com/microsoft/vscode-docs/main/api/references/theme-color.md'; - const reqContext = await new RequestService(new TestConfigurationService(), environmentService, new NullLogService()).request({ url: docUrl }, CancellationToken.None); + const reqContext = await new RequestService('local', new TestConfigurationService(), environmentService, new NullLogService()).request({ url: docUrl }, CancellationToken.None); const content = (await asTextOrError(reqContext))!; const expression = /-\s*\`([\w\.]+)\`: (.*)/g; diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 8e63847fb9b6..786153b46b67 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -111,7 +111,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx @IProductService protected readonly _productService: IProductService, @IWorkbenchExtensionManagementService protected readonly _extensionManagementService: IWorkbenchExtensionManagementService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, - @IConfigurationService private readonly _configurationService: IConfigurationService, + @IConfigurationService protected readonly _configurationService: IConfigurationService, @IExtensionManifestPropertiesService private readonly _extensionManifestPropertiesService: IExtensionManifestPropertiesService, @ILogService protected readonly _logService: ILogService, @IRemoteAgentService protected readonly _remoteAgentService: IRemoteAgentService, diff --git a/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts b/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts index 33eabcc36ddf..a552fa153333 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts @@ -15,7 +15,6 @@ import { Categories } from '../../../../platform/action/common/actionCommonCateg import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { ExtensionKind } from '../../../../platform/environment/common/environment.js'; import { IExtensionGalleryService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; @@ -414,7 +413,13 @@ export class NativeExtensionService extends AbstractExtensionService implements return this._startLocalExtensionHost(emitter); } - updateProxyConfigurationsScope(remoteEnv.useHostProxy ? ConfigurationScope.APPLICATION : ConfigurationScope.MACHINE); + const useHostProxyDefault = remoteEnv.useHostProxy; + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('http.useLocalProxyConfiguration')) { + updateProxyConfigurationsScope(this._configurationService.getValue('http.useLocalProxyConfiguration'), useHostProxyDefault); + } + })); + updateProxyConfigurationsScope(this._configurationService.getValue('http.useLocalProxyConfiguration'), useHostProxyDefault); } else { this._remoteAuthorityResolverService._setCanonicalURIProvider(async (uri) => uri); diff --git a/src/vs/workbench/services/request/browser/requestService.ts b/src/vs/workbench/services/request/browser/requestService.ts index 63d46d34d092..b90bcbdd0729 100644 --- a/src/vs/workbench/services/request/browser/requestService.ts +++ b/src/vs/workbench/services/request/browser/requestService.ts @@ -29,7 +29,7 @@ export class BrowserRequestService extends AbstractRequestService implements IRe async request(options: IRequestOptions, token: CancellationToken): Promise { try { if (!options.proxyAuthorization) { - options.proxyAuthorization = this.configurationService.getValue('http.proxyAuthorization'); + options.proxyAuthorization = this.configurationService.inspect('http.proxyAuthorization').userLocalValue; } const context = await this.logAndRequest(options, () => request(options, token, () => navigator.onLine)); diff --git a/src/vs/workbench/services/request/electron-sandbox/requestService.ts b/src/vs/workbench/services/request/electron-sandbox/requestService.ts index 9cfba42f54fe..421d421e573c 100644 --- a/src/vs/workbench/services/request/electron-sandbox/requestService.ts +++ b/src/vs/workbench/services/request/electron-sandbox/requestService.ts @@ -26,7 +26,7 @@ export class NativeRequestService extends AbstractRequestService implements IReq async request(options: IRequestOptions, token: CancellationToken): Promise { if (!options.proxyAuthorization) { - options.proxyAuthorization = this.configurationService.getValue('http.proxyAuthorization'); + options.proxyAuthorization = this.configurationService.inspect('http.proxyAuthorization').userLocalValue; } return this.logAndRequest(options, () => request(options, token, () => navigator.onLine)); } From 7610cc1d19491b9d32bf68eacbc5f38f9d729d0a Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:25:19 +0100 Subject: [PATCH 0592/3587] =?UTF-8?q?Git=20-=20=F0=9F=92=84=20fix=20histor?= =?UTF-8?q?y=20item=20details=20provider=20name=20(#238045)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/git/src/api/api1.ts | 6 +++--- extensions/git/src/api/git.d.ts | 4 ++-- extensions/git/src/blame.ts | 2 +- ...ovider.ts => historyItemDetailsProvider.ts} | 16 ++++++++-------- extensions/git/src/historyProvider.ts | 4 ++-- extensions/git/src/model.ts | 18 +++++++++--------- extensions/git/src/repository.ts | 4 ++-- extensions/git/src/timelineProvider.ts | 2 +- extensions/github/src/extension.ts | 4 ++-- ...ovider.ts => historyItemDetailsProvider.ts} | 4 ++-- extensions/github/src/typings/git.d.ts | 4 ++-- 11 files changed, 34 insertions(+), 34 deletions(-) rename extensions/git/src/{historyItemDetailProvider.ts => historyItemDetailsProvider.ts} (70%) rename extensions/github/src/{historyItemDetailProvider.ts => historyItemDetailsProvider.ts} (89%) diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 55cd31a1acf1..d715c535a501 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -7,7 +7,7 @@ import { Model } from '../model'; import { Repository as BaseRepository, Resource } from '../repository'; -import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailProvider } from './git'; +import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailsProvider } from './git'; import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode'; import { combinedDisposable, filterEvent, mapEvent } from '../util'; import { toGitUri } from '../uri'; @@ -414,8 +414,8 @@ export class ApiImpl implements API { return this.#model.registerPushErrorHandler(handler); } - registerSourceControlHistoryItemDetailProvider(provider: SourceControlHistoryItemDetailProvider): Disposable { - return this.#model.registerSourceControlHistoryItemDetailProvider(provider); + registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable { + return this.#model.registerSourceControlHistoryItemDetailsProvider(provider); } registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable { diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 622c8a80b160..58299ae68260 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -326,7 +326,7 @@ export interface BranchProtectionProvider { provideBranchProtection(): BranchProtection[]; } -export interface SourceControlHistoryItemDetailProvider { +export interface SourceControlHistoryItemDetailsProvider { provideHoverCommands(repository: Repository): ProviderResult; provideMessageLinks(repository: Repository, message: string): ProviderResult; } @@ -358,7 +358,7 @@ export interface API { registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable; registerPushErrorHandler(handler: PushErrorHandler): Disposable; registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable; - registerSourceControlHistoryItemDetailProvider(provider: SourceControlHistoryItemDetailProvider): Disposable; + registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable; } export interface GitExtension { diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 7265a61d879d..f472116e0e41 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -12,7 +12,7 @@ import { BlameInformation, Commit } from './git'; import { fromGitUri, isGitUri } from './uri'; import { emojify, ensureEmojis } from './emoji'; import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation } from './staging'; -import { provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailProvider'; +import { provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean { return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive); diff --git a/extensions/git/src/historyItemDetailProvider.ts b/extensions/git/src/historyItemDetailsProvider.ts similarity index 70% rename from extensions/git/src/historyItemDetailProvider.ts rename to extensions/git/src/historyItemDetailsProvider.ts index af717a703f13..26d6bac30dd9 100644 --- a/extensions/git/src/historyItemDetailProvider.ts +++ b/extensions/git/src/historyItemDetailsProvider.ts @@ -4,20 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import { Command, Disposable } from 'vscode'; -import { SourceControlHistoryItemDetailProvider } from './api/git'; +import { SourceControlHistoryItemDetailsProvider } from './api/git'; import { Repository } from './repository'; import { ApiRepository } from './api/api1'; -export interface ISourceControlHistoryItemDetailProviderRegistry { - registerSourceControlHistoryItemDetailProvider(provider: SourceControlHistoryItemDetailProvider): Disposable; - getSourceControlHistoryItemDetailProviders(): SourceControlHistoryItemDetailProvider[]; +export interface ISourceControlHistoryItemDetailsProviderRegistry { + registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable; + getSourceControlHistoryItemDetailsProviders(): SourceControlHistoryItemDetailsProvider[]; } export async function provideSourceControlHistoryItemHoverCommands( - registry: ISourceControlHistoryItemDetailProviderRegistry, + registry: ISourceControlHistoryItemDetailsProviderRegistry, repository: Repository ): Promise { - for (const provider of registry.getSourceControlHistoryItemDetailProviders()) { + for (const provider of registry.getSourceControlHistoryItemDetailsProviders()) { const result = await provider.provideHoverCommands(new ApiRepository(repository)); if (result) { @@ -29,11 +29,11 @@ export async function provideSourceControlHistoryItemHoverCommands( } export async function provideSourceControlHistoryItemMessageLinks( - registry: ISourceControlHistoryItemDetailProviderRegistry, + registry: ISourceControlHistoryItemDetailsProviderRegistry, repository: Repository, message: string ): Promise { - for (const provider of registry.getSourceControlHistoryItemDetailProviders()) { + for (const provider of registry.getSourceControlHistoryItemDetailsProviders()) { const result = await provider.provideMessageLinks( new ApiRepository(repository), message); diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 430819977b54..6322a025a3f7 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -12,7 +12,7 @@ import { Branch, LogOptions, Ref, RefType } from './api/git'; import { emojify, ensureEmojis } from './emoji'; import { Commit } from './git'; import { OperationKind, OperationResult } from './operation'; -import { ISourceControlHistoryItemDetailProviderRegistry, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailProvider'; +import { ISourceControlHistoryItemDetailsProviderRegistry, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; function toSourceControlHistoryItemRef(repository: Repository, ref: Ref): SourceControlHistoryItemRef { const rootUri = Uri.file(repository.root); @@ -98,7 +98,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec private disposables: Disposable[] = []; constructor( - private historyItemDetailProviderRegistry: ISourceControlHistoryItemDetailProviderRegistry, + private historyItemDetailProviderRegistry: ISourceControlHistoryItemDetailsProviderRegistry, private readonly repository: Repository, private readonly logger: LogOutputChannel ) { diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index c94df8d5a50f..f64528275d09 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -12,14 +12,14 @@ import { Git } from './git'; import * as path from 'path'; import * as fs from 'fs'; import { fromGitUri } from './uri'; -import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider, BranchProtectionProvider, SourceControlHistoryItemDetailProvider } from './api/git'; +import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider, BranchProtectionProvider, SourceControlHistoryItemDetailsProvider } from './api/git'; import { Askpass } from './askpass'; import { IPushErrorHandlerRegistry } from './pushError'; import { ApiRepository } from './api/api1'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { IPostCommitCommandsProviderRegistry } from './postCommitCommands'; import { IBranchProtectionProviderRegistry } from './branchProtection'; -import { ISourceControlHistoryItemDetailProviderRegistry } from './historyItemDetailProvider'; +import { ISourceControlHistoryItemDetailsProviderRegistry } from './historyItemDetailsProvider'; class RepositoryPick implements QuickPickItem { @memoize get label(): string { @@ -171,7 +171,7 @@ class UnsafeRepositoriesManager { } } -export class Model implements IRepositoryResolver, IBranchProtectionProviderRegistry, IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry, ISourceControlHistoryItemDetailProviderRegistry { +export class Model implements IRepositoryResolver, IBranchProtectionProviderRegistry, IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry, ISourceControlHistoryItemDetailsProviderRegistry { private _onDidOpenRepository = new EventEmitter(); readonly onDidOpenRepository: Event = this._onDidOpenRepository.event; @@ -237,7 +237,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi readonly onDidChangeBranchProtectionProviders = this._onDidChangeBranchProtectionProviders.event; private pushErrorHandlers = new Set(); - private historyItemDetailProviders = new Set(); + private historyItemDetailsProviders = new Set(); private _unsafeRepositoriesManager: UnsafeRepositoriesManager; get unsafeRepositories(): string[] { @@ -1004,13 +1004,13 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi return [...this.pushErrorHandlers]; } - registerSourceControlHistoryItemDetailProvider(provider: SourceControlHistoryItemDetailProvider): Disposable { - this.historyItemDetailProviders.add(provider); - return toDisposable(() => this.historyItemDetailProviders.delete(provider)); + registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable { + this.historyItemDetailsProviders.add(provider); + return toDisposable(() => this.historyItemDetailsProviders.delete(provider)); } - getSourceControlHistoryItemDetailProviders(): SourceControlHistoryItemDetailProvider[] { - return [...this.historyItemDetailProviders]; + getSourceControlHistoryItemDetailsProviders(): SourceControlHistoryItemDetailsProvider[] { + return [...this.historyItemDetailsProviders]; } getUnsafeRepositoryPath(repository: string): string | undefined { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index d7c371899579..79b8379426e8 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -26,7 +26,7 @@ import { toGitUri } from './uri'; import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; import { detectEncoding } from './encoding'; -import { ISourceControlHistoryItemDetailProviderRegistry } from './historyItemDetailProvider'; +import { ISourceControlHistoryItemDetailsProviderRegistry } from './historyItemDetailsProvider'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -856,7 +856,7 @@ export class Repository implements Disposable { remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry, postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry, private readonly branchProtectionProviderRegistry: IBranchProtectionProviderRegistry, - historyItemDetailProviderRegistry: ISourceControlHistoryItemDetailProviderRegistry, + historyItemDetailProviderRegistry: ISourceControlHistoryItemDetailsProviderRegistry, globalState: Memento, private readonly logger: LogOutputChannel, private telemetryReporter: TelemetryReporter diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 6b9085eb8215..20daaebe2634 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -12,7 +12,7 @@ import { CommandCenter } from './commands'; import { OperationKind, OperationResult } from './operation'; import { getCommitShortHash } from './util'; import { CommitShortStat } from './git'; -import { provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailProvider'; +import { provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; export class GitTimelineItem extends TimelineItem { static is(item: TimelineItem): item is GitTimelineItem { diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts index 968630e1852a..5ca0d1cb6d53 100644 --- a/extensions/github/src/extension.ts +++ b/extensions/github/src/extension.ts @@ -16,7 +16,7 @@ import { GithubRemoteSourcePublisher } from './remoteSourcePublisher'; import { GithubBranchProtectionProviderManager } from './branchProtection'; import { GitHubCanonicalUriProvider } from './canonicalUriProvider'; import { VscodeDevShareProvider } from './shareProviders'; -import { GitHubSourceControlHistoryItemDetailProvider } from './historyItemDetailProvider'; +import { GitHubSourceControlHistoryItemDetailsProvider } from './historyItemDetailsProvider'; export function activate(context: ExtensionContext): void { const disposables: Disposable[] = []; @@ -101,7 +101,7 @@ function initializeGitExtension(context: ExtensionContext, telemetryReporter: Te disposables.add(new GithubBranchProtectionProviderManager(gitAPI, context.globalState, logger, telemetryReporter)); disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler(telemetryReporter))); disposables.add(gitAPI.registerRemoteSourcePublisher(new GithubRemoteSourcePublisher(gitAPI))); - disposables.add(gitAPI.registerSourceControlHistoryItemDetailProvider(new GitHubSourceControlHistoryItemDetailProvider())); + disposables.add(gitAPI.registerSourceControlHistoryItemDetailsProvider(new GitHubSourceControlHistoryItemDetailsProvider())); disposables.add(new GitHubCanonicalUriProvider(gitAPI)); disposables.add(new VscodeDevShareProvider(gitAPI)); setGitHubContext(gitAPI, disposables); diff --git a/extensions/github/src/historyItemDetailProvider.ts b/extensions/github/src/historyItemDetailsProvider.ts similarity index 89% rename from extensions/github/src/historyItemDetailProvider.ts rename to extensions/github/src/historyItemDetailsProvider.ts index bf1bdded391b..4f7d0120ced6 100644 --- a/extensions/github/src/historyItemDetailProvider.ts +++ b/extensions/github/src/historyItemDetailsProvider.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { Command, l10n } from 'vscode'; -import { Repository, SourceControlHistoryItemDetailProvider } from './typings/git'; +import { Repository, SourceControlHistoryItemDetailsProvider } from './typings/git'; import { getRepositoryDefaultRemote, getRepositoryDefaultRemoteUrl } from './util'; const ISSUE_EXPRESSION = /(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/g; -export class GitHubSourceControlHistoryItemDetailProvider implements SourceControlHistoryItemDetailProvider { +export class GitHubSourceControlHistoryItemDetailsProvider implements SourceControlHistoryItemDetailsProvider { async provideHoverCommands(repository: Repository): Promise { const url = getRepositoryDefaultRemoteUrl(repository); if (!url) { diff --git a/extensions/github/src/typings/git.d.ts b/extensions/github/src/typings/git.d.ts index 318152e82f66..714474533e0d 100644 --- a/extensions/github/src/typings/git.d.ts +++ b/extensions/github/src/typings/git.d.ts @@ -289,7 +289,7 @@ export interface BranchProtectionProvider { provideBranchProtection(): BranchProtection[]; } -export interface SourceControlHistoryItemDetailProvider { +export interface SourceControlHistoryItemDetailsProvider { provideHoverCommands(repository: Repository): Promise; provideMessageLinks(repository: Repository, message: string): Promise; } @@ -321,7 +321,7 @@ export interface API { registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable; registerPushErrorHandler(handler: PushErrorHandler): Disposable; registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable; - registerSourceControlHistoryItemDetailProvider(provider: SourceControlHistoryItemDetailProvider): Disposable; + registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable; } export interface GitExtension { From df4ffeb3477d7bb3d539511ab63d3451ead6ff6c Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 16 Jan 2025 15:49:05 +0100 Subject: [PATCH 0593/3587] `automaticallyAcceptChanges` doesn't need to be app-scoped (#238054) --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index db49afe10e63..66894e42135b 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -130,7 +130,6 @@ configurationRegistry.registerConfiguration({ }, 'chat.editing.automaticallyAcceptChanges': { type: 'boolean', - scope: ConfigurationScope.APPLICATION, markdownDescription: nls.localize('chat.editing.automaticallyAcceptChanges', "Whether changes made by chat are automatically accepted without prior review."), default: false, }, From 2dff698dfbb5d747c086848a366a3bf629d2e5f1 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 16 Jan 2025 10:37:02 -0500 Subject: [PATCH 0594/3587] pass in shellPath for when platform() is win32 too --- .../src/singlefolder-tests/terminal.shellIntegration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts index e644b0cf8824..86fc52cf6653 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts @@ -41,7 +41,7 @@ import { assertNoRpc } from '../utils'; } })); const terminal = platform() === 'win32' - ? window.createTerminal() + ? window.createTerminal({ shellPath }) : window.createTerminal({ shellPath: shellPath ?? '/bin/bash' }); terminal.show(); }); From 81880711959321280ed8c228cf07e50ca46fa48c Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 16 Jan 2025 10:47:20 -0500 Subject: [PATCH 0595/3587] get $__vsc_stable flag from terminalEnvironment.ts --- .../terminal/node/terminalEnvironment.ts | 1 + .../common/scripts/shellIntegration-rc.zsh | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index 3688900dc2bc..71398d253731 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -224,6 +224,7 @@ export function getShellIntegrationInjection( source: path.join(appRoot, 'out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh'), dest: path.join(zdotdir, '.zlogin') }); + envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; return { newArgs, envMixin, filesToCopy }; } } diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index 9e85f892b28c..31885e517952 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -99,6 +99,10 @@ __vsc_current_command="" __vsc_nonce="$VSCODE_NONCE" unset VSCODE_NONCE +# Some features should only work in Insiders +__vsc_stable="$VSCODE_STABLE" +unset VSCODE_STABLE + __vsc_prompt_start() { builtin printf '\e]633;A\a' } @@ -150,8 +154,10 @@ __vsc_command_complete() { builtin printf '\e]633;D;%s\a' "$__vsc_status" fi __vsc_update_cwd - # Is there stable/insider flag in zsh? - __vsc_update_env + + if [[ "$__vsc_stable" == "0" ]]; then + __vsc_update_env + fi } if [[ -o NOUNSET ]]; then @@ -186,8 +192,10 @@ __vsc_precmd() { # non null __vsc_update_prompt fi - # TODO: Is there stable/insider flag in zsh? - __vsc_update_env + + if [[ "$__vsc_stable" == "0" ]]; then + __vsc_update_env + fi } __vsc_preexec() { From 523486d2d1e495a67671202e7914979a9c29b5b1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 16 Jan 2025 07:59:28 -0800 Subject: [PATCH 0596/3587] Use a Intl.Segmenter to find emoji in a line Part of #237897 --- .../browser/gpu/fullFileRenderStrategy.ts | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index ccf57a7f4070..ce101adad38e 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -23,6 +23,7 @@ import { quadVertices } from './gpuUtils.js'; import { GlyphRasterizer } from './raster/glyphRasterizer.js'; import { ViewGpuContext } from './viewGpuContext.js'; import { Color } from '../../../base/common/color.js'; +import type { IntlWordSegmentData } from '../../common/core/wordCharacterClassifier.js'; const enum Constants { IndicesPerCell = 6, @@ -269,6 +270,9 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend let tokens: IViewLineTokens; const dpr = getActiveWindow().devicePixelRatio; + const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' }); + let segmenterIndex = 0; + let segmentedContent: Intl.SegmentData[]; if (!this._scrollInitialized) { this.onScrollChanged(); @@ -344,6 +348,11 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend content = lineData.content; xOffset = 0; + // Don't need to use the segmenter for non-ASCII content + if (!lineData.isBasicASCII) { + segmentedContent = Array.from(segmenter.segment(content)); + } + tokens = lineData.tokens; tokenStartIndex = lineData.minColumn - 1; tokenEndIndex = 0; @@ -356,12 +365,29 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend tokenMetadata = tokens.getMetadata(tokenIndex); + segmenterIndex = 0; for (x = tokenStartIndex; x < tokenEndIndex; x++) { // TODO: This needs to move to a dynamic long line rendering strategy if (x > this._viewGpuContext.maxGpuCols) { break; } - chars = content.charAt(x); + + if (lineData.isBasicASCII) { + chars = content.charAt(x); + } else { + const segment = segmentedContent![segmenterIndex]; + + // No more segments in the string (eg. an emoji is the last segment) + if (!segment) { + break; + } + if (segment.index !== x) { + continue; + } + segmenterIndex++; + chars = segment.segment; + } + decorationStyleSetColor = undefined; decorationStyleSetBold = undefined; decorationStyleSetOpacity = undefined; From a5d836544d28742186c19a5fa1718c2c976622a1 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 16 Jan 2025 17:01:26 +0100 Subject: [PATCH 0597/3587] tweak auto-accept flow (#238057) * make setting a number which is the delay in seconds * no count-down in btn, but just an simple animation * stop auto-accept when hovering over the btn https://github.com/microsoft/vscode-copilot/issues/11747 --- .../lib/stylelint/vscode-known-variables.json | 3 +- .../contrib/chat/browser/chat.contribution.ts | 8 ++-- .../chatEditingModifiedFileEntry.ts | 42 ++++++++++++++----- .../contrib/chat/browser/chatEditorOverlay.ts | 29 +++++++++---- .../chat/browser/media/chatEditorOverlay.css | 40 ++++++++++-------- .../contrib/chat/common/chatEditingService.ts | 2 +- 6 files changed, 83 insertions(+), 41 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index d6eb99d2f398..9cadf39caa39 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -912,6 +912,7 @@ "--zoom-factor", "--test-bar-width", "--widget-color", - "--text-link-decoration" + "--text-link-decoration", + "--vscode-action-item-auto-timeout" ] } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 66894e42135b..9006925683d1 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -128,10 +128,10 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('chat.editing.alwaysSaveWithGeneratedChanges', "Whether files that have changes made by chat can be saved without confirmation."), default: false, }, - 'chat.editing.automaticallyAcceptChanges': { - type: 'boolean', - markdownDescription: nls.localize('chat.editing.automaticallyAcceptChanges', "Whether changes made by chat are automatically accepted without prior review."), - default: false, + 'chat.editing.autoAcceptDelay': { + type: 'number', + markdownDescription: nls.localize('chat.editing.autoAcceptDelay', "Delay after which changes made by chat are automatically accepted. Values are in seconds, `0` means disabled and `100` seconds is the maximum."), + default: 0, }, 'chat.editing.confirmEditRequestRemoval': { type: 'boolean', diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 200f64bce6dc..91821810a13b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -4,9 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { RunOnceScheduler } from '../../../../../base/common/async.js'; +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Disposable, IReference, toDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; +import { clamp } from '../../../../../base/common/numbers.js'; import { autorun, derived, IObservable, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; import { themeColorFromId } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -38,6 +40,13 @@ import { ChatEditKind, IModifiedFileEntry, WorkingSetEntryState } from '../../co import { IChatService } from '../../common/chatService.js'; import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; +class AutoAcceptControl { + constructor( + readonly remaining: number, + readonly cancel: () => void + ) { } +} + export class ChatEditingModifiedFileEntry extends Disposable implements IModifiedFileEntry { public static readonly scheme = 'modified-file-entry'; @@ -94,8 +103,8 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie private readonly _reviewModeTempObs = observableValue(this, undefined); readonly reviewMode: IObservable; - private readonly _autoAcceptCountdown = observableValue(this, undefined); - readonly autoAcceptCountdown: IObservable = this._autoAcceptCountdown; + private readonly _autoAcceptCtrl = observableValue(this, undefined); + readonly autoAcceptController: IObservable<{ remaining: number; cancel(): void } | undefined> = this._autoAcceptCtrl; private _isFirstEditAfterStartOrSnapshot: boolean = true; private _edit: OffsetEdit = OffsetEdit.empty; @@ -142,6 +151,8 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie private _refCounter: number = 1; + private readonly _autoAcceptTimeout: IObservable; + constructor( resourceRef: IReference, private readonly _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void }, @@ -207,11 +218,15 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie })); // review mode depends on setting and temporary override - const autoAccept = observableConfigValue('chat.editing.automaticallyAcceptChanges', false, configService); + const autoAcceptRaw = observableConfigValue('chat.editing.autoAcceptDelay', 0, configService); + this._autoAcceptTimeout = derived(r => { + const value = autoAcceptRaw.read(r); + return clamp(value, 0, 100); + }); this.reviewMode = derived(r => { - const configuredValue = !autoAccept.read(r); + const configuredValue = this._autoAcceptTimeout.read(r); const tempValue = this._reviewModeTempObs.read(r); - return tempValue || configuredValue; + return tempValue ?? configuredValue === 0; }); } @@ -292,23 +307,28 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie // AUTO accept mode if (!this.reviewMode.get()) { - const future = Date.now() + 10000; // 10secs + const future = Date.now() + (this._autoAcceptTimeout.get() * 1000); + const cts = new CancellationTokenSource(); const update = () => { const reviewMode = this.reviewMode.get(); if (reviewMode) { // switched back to review mode - this._autoAcceptCountdown.set(undefined, undefined); + this._autoAcceptCtrl.set(undefined, undefined); + return; + } + + if (cts.token.isCancellationRequested) { + this._autoAcceptCtrl.set(undefined, undefined); return; } const remain = Math.round((future - Date.now()) / 1000); if (remain <= 0) { this.accept(undefined); - this._autoAcceptCountdown.set(undefined, undefined); } else { - this._autoAcceptCountdown.set(remain, undefined); - setTimeout(update, 1000); + this._autoAcceptCtrl.set(new AutoAcceptControl(remain, () => cts.cancel()), undefined); + setTimeout(update, 100); } }; update(); @@ -523,6 +543,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie this._diffInfo.set(nullDocumentDiff, transaction); this._edit = OffsetEdit.empty; this._stateObs.set(WorkingSetEntryState.Accepted, transaction); + this._autoAcceptCtrl.set(undefined, transaction); await this.collapse(transaction); this._notifyAction('accepted'); } @@ -547,6 +568,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie await this.collapse(transaction); } this._stateObs.set(WorkingSetEntryState.Rejected, transaction); + this._autoAcceptCtrl.set(undefined, transaction); this._notifyAction('rejected'); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index f24e81fd641b..c37d9a4d4109 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -14,7 +14,7 @@ import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionView import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js'; import { Range } from '../../../../editor/common/core/range.js'; import { IActionRunner } from '../../../../base/common/actions.js'; -import { $, append, EventLike, reset } from '../../../../base/browser/dom.js'; +import { $, addDisposableGenericMouseMoveListener, append, EventLike, reset } from '../../../../base/browser/dom.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { Codicon } from '../../../../base/common/codicons.js'; @@ -127,16 +127,29 @@ export class ChatEditorOverlayWidget implements IOverlayWidget { super.render(container); if (action.id === AcceptAction.ID) { - assertType(this.label); - assertType(this.element); + const listener = this._store.add(new MutableDisposable()); + + let timeIsSet = false; this._store.add(autorun(r => { - const value = that._entry.read(r)?.entry.autoAcceptCountdown.read(r); - this.label!.innerText = value === undefined - ? this.action.label - : localize('pattern', "{0} ({1})", this.action.label, value); - this.element?.classList.toggle('auto', !!value); + assertType(this.label); + assertType(this.element); + + const ctrl = that._entry.read(r)?.entry.autoAcceptController.read(r); + if (ctrl) { + + if (!timeIsSet) { + this.element.style.setProperty('--vscode-action-item-auto-timeout', `${ctrl.remaining}s`); + timeIsSet = true; + } + + this.element.classList.toggle('auto', true); + listener.value = addDisposableGenericMouseMoveListener(this.element, () => ctrl.cancel()); + } else { + this.element.classList.toggle('auto', false); + listener.clear(); + } })); } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css index dabc35cdafa9..eb1603f43d1b 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css @@ -90,21 +90,27 @@ opacity: 1; } -.chat-editor-overlay-widget .action-item.auto > .action-label { - animation: textShift 5s linear infinite alternate; - background: linear-gradient(90deg, - var(--vscode-button-foreground) 0%, - var(--vscode-button-foreground) 45%, - var(--vscode-editorGutter-addedBackground) 50%, - var(--vscode-button-foreground) 55%, - var(--vscode-button-foreground) 100%); - background-size: 200% 100%; - color: transparent; - -webkit-background-clip: text; - background-clip: text; -} - -@keyframes textShift { - 0% { background-position: 100% 50%; } - 100% { background-position: 0% 50%; } +.chat-editor-overlay-widget .action-item.auto { + position: relative; + overflow: hidden; +} + +.chat-editor-overlay-widget .action-item.auto::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background-color: var(--vscode-toolbar-hoverBackground); + animation: slideRight var(--vscode-action-item-auto-timeout) linear forwards; +} + +@keyframes slideRight { + 0% { + left: -100%; + } + 100% { + left: 0%; + } } diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 560bb4709810..71a610aacc3f 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -140,7 +140,7 @@ export interface IModifiedFileEntry { reject(transaction: ITransaction | undefined): Promise; reviewMode: IObservable; - autoAcceptCountdown: IObservable; + autoAcceptController: IObservable<{ remaining: number; cancel(): void } | undefined>; enableReviewModeUntilSettled(): void; } From c8d16eb08c3e802cf6cb04e7c1e311d60c81d80e Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 16 Jan 2025 17:09:01 +0100 Subject: [PATCH 0598/3587] workaround for https://github.com/microsoft/vscode-copilot/issues/11639 (#238058) --- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 031fa500a951..7cac98241cf9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -1314,7 +1314,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } if (currentChatEditingState === ChatEditingSessionState.StreamingEdits || chatWidget?.viewModel?.requestInProgress) { - this._chatEditsProgress ??= new ProgressBar(innerContainer); + // this._chatEditsProgress ??= new ProgressBar(innerContainer); this._chatEditsProgress?.infinite().show(500); } From 8961ff9f680d04110cc19e62384ba1f72554bc41 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Thu, 16 Jan 2025 17:23:10 +0100 Subject: [PATCH 0599/3587] Making verbosity icons clip (#238049) * adding code * setting position to relative --- .../hover/browser/contentHoverWidget.ts | 7 ++++ .../browser/contentHoverWidgetWrapper.ts | 3 ++ src/vs/editor/contrib/hover/browser/hover.css | 14 +++++-- .../contrib/hover/browser/hoverTypes.ts | 2 + .../hover/browser/markdownHoverParticipant.ts | 41 ++++++++++++++++--- 5 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts b/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts index fdfc464bc048..1b30ad9357a7 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts @@ -17,6 +17,7 @@ import { getHoverAccessibleViewHint, HoverWidget } from '../../../../base/browse import { PositionAffinity } from '../../../common/model.js'; import { Emitter } from '../../../../base/common/event.js'; import { RenderedContentHover } from './contentHoverRendered.js'; +import { ScrollEvent } from '../../../../base/common/scrollable.js'; const HORIZONTAL_SCROLLING_BY = 30; @@ -37,6 +38,9 @@ export class ContentHoverWidget extends ResizableContentWidget { private readonly _onDidResize = this._register(new Emitter()); public readonly onDidResize = this._onDidResize.event; + private readonly _onDidScroll = this._register(new Emitter()); + public readonly onDidScroll = this._onDidScroll.event; + public get isVisibleFromKeyboard(): boolean { return (this._renderedHover?.source === HoverStartSource.Keyboard); } @@ -86,6 +90,9 @@ export class ContentHoverWidget extends ResizableContentWidget { this._register(focusTracker.onDidBlur(() => { this._hoverFocusedKey.set(false); })); + this._register(this._hover.scrollbar.onScroll((e) => { + this._onDidScroll.fire(e); + })); this._setRenderedHover(undefined); this._editor.addContentWidget(this); } diff --git a/src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.ts b/src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.ts index ad9a99209670..1fa7e2dc9e47 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.ts @@ -58,6 +58,9 @@ export class ContentHoverWidgetWrapper extends Disposable implements IHoverWidge this._register(this._contentHoverWidget.onDidResize(() => { this._participants.forEach(participant => participant.handleResize?.()); })); + this._register(this._contentHoverWidget.onDidScroll((e) => { + this._participants.forEach(participant => participant.handleScroll?.(e)); + })); return participants; } diff --git a/src/vs/editor/contrib/hover/browser/hover.css b/src/vs/editor/contrib/hover/browser/hover.css index 1314484424b6..2f5c23527133 100644 --- a/src/vs/editor/contrib/hover/browser/hover.css +++ b/src/vs/editor/contrib/hover/browser/hover.css @@ -44,24 +44,30 @@ } .monaco-editor .monaco-hover .hover-row .verbosity-actions { + border-right: 1px solid var(--vscode-editorHoverWidget-border); + width: 22px; + overflow: hidden; +} + +.monaco-editor .monaco-hover .hover-row .verbosity-actions-inner { display: flex; flex-direction: column; padding-left: 5px; padding-right: 5px; justify-content: flex-end; - border-right: 1px solid var(--vscode-editorHoverWidget-border); + position: relative; } -.monaco-editor .monaco-hover .hover-row .verbosity-actions .codicon { +.monaco-editor .monaco-hover .hover-row .verbosity-actions-inner .codicon { cursor: pointer; font-size: 11px; } -.monaco-editor .monaco-hover .hover-row .verbosity-actions .codicon.enabled { +.monaco-editor .monaco-hover .hover-row .verbosity-actions-inner .codicon.enabled { color: var(--vscode-textLink-foreground); } -.monaco-editor .monaco-hover .hover-row .verbosity-actions .codicon.disabled { +.monaco-editor .monaco-hover .hover-row .verbosity-actions-inner .codicon.disabled { opacity: 0.6; } diff --git a/src/vs/editor/contrib/hover/browser/hoverTypes.ts b/src/vs/editor/contrib/hover/browser/hoverTypes.ts index ad5442f1fa23..9e85daf3eb60 100644 --- a/src/vs/editor/contrib/hover/browser/hoverTypes.ts +++ b/src/vs/editor/contrib/hover/browser/hoverTypes.ts @@ -13,6 +13,7 @@ import { Range } from '../../../common/core/range.js'; import { IModelDecoration } from '../../../common/model.js'; import { BrandedService, IConstructorSignature } from '../../../../platform/instantiation/common/instantiation.js'; import { HoverStartSource } from './hoverOperation.js'; +import { ScrollEvent } from '../../../../base/common/scrollable.js'; export interface IHoverPart { /** @@ -167,6 +168,7 @@ export interface IEditorHoverParticipant { getAccessibleContent(hoverPart: T): string; handleResize?(): void; handleHide?(): void; + handleScroll?(e: ScrollEvent): void; } export type IEditorHoverParticipantCtor = IConstructorSignature; diff --git a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts index 3fd5876cfe40..bc663ef46bb6 100644 --- a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts @@ -35,6 +35,7 @@ import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry import { getHoverProviderResultsAsAsyncIterable } from './getHover.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { HoverStartSource } from './hoverOperation.js'; +import { ScrollEvent } from '../../../../base/common/scrollable.js'; const $ = dom.$; const increaseHoverVerbosityIcon = registerIcon('hover-increase-verbosity', Codicon.add, nls.localize('increaseHoverVerbosity', 'Icon for increaseing hover verbosity.')); @@ -196,6 +197,10 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant { public readonly hoverPart: MarkdownHover, public readonly hoverElement: HTMLElement, public readonly disposables: DisposableStore, + public readonly actionsContainer?: HTMLElement ) { } get hoverAccessibleContent(): string { @@ -295,10 +301,11 @@ class MarkdownRenderedHoverParts implements IRenderedHoverParts { const actionsContainer = $('div.verbosity-actions'); renderedMarkdownElement.prepend(actionsContainer); - - disposables.add(this._renderHoverExpansionAction(actionsContainer, HoverVerbosityAction.Increase, canIncreaseVerbosity)); - disposables.add(this._renderHoverExpansionAction(actionsContainer, HoverVerbosityAction.Decrease, canDecreaseVerbosity)); - return new RenderedMarkdownHoverPart(hoverPart, renderedMarkdownElement, disposables); + const actionsContainerInner = $('div.verbosity-actions-inner'); + actionsContainer.append(actionsContainerInner); + disposables.add(this._renderHoverExpansionAction(actionsContainerInner, HoverVerbosityAction.Increase, canIncreaseVerbosity)); + disposables.add(this._renderHoverExpansionAction(actionsContainerInner, HoverVerbosityAction.Decrease, canDecreaseVerbosity)); + return new RenderedMarkdownHoverPart(hoverPart, renderedMarkdownElement, disposables, actionsContainerInner); } private _renderMarkdownHover( @@ -333,6 +340,29 @@ class MarkdownRenderedHoverParts implements IRenderedHoverParts { return store; } + public handleScroll(e: ScrollEvent): void { + this.renderedHoverParts.forEach(renderedHoverPart => { + const actionsContainerInner = renderedHoverPart.actionsContainer; + if (!actionsContainerInner) { + return; + } + const hoverElement = renderedHoverPart.hoverElement; + const topOfHoverScrollPosition = e.scrollTop; + const bottomOfHoverScrollPosition = topOfHoverScrollPosition + e.height; + const topOfRenderedPart = hoverElement.offsetTop; + const hoverElementHeight = hoverElement.clientHeight; + const bottomOfRenderedPart = topOfRenderedPart + hoverElementHeight; + const iconsHeight = 22; + let top: number; + if (bottomOfRenderedPart <= bottomOfHoverScrollPosition || topOfRenderedPart >= bottomOfHoverScrollPosition) { + top = hoverElementHeight - iconsHeight; + } else { + top = bottomOfHoverScrollPosition - topOfRenderedPart - iconsHeight; + } + actionsContainerInner.style.top = `${top}px`; + }); + } + public async updateMarkdownHoverPartVerbosityLevel(action: HoverVerbosityAction, index: number): Promise<{ hoverPart: MarkdownHover; hoverElement: HTMLElement } | undefined> { const model = this._editor.getModel(); if (!model) { @@ -425,7 +455,8 @@ class MarkdownRenderedHoverParts implements IRenderedHoverParts { const newRenderedHoverPart = new RenderedMarkdownHoverPart( hoverPart, currentRenderedMarkdown, - renderedHoverPart.disposables + renderedHoverPart.disposables, + renderedHoverPart.actionsContainer ); currentRenderedHoverPart.dispose(); this.renderedHoverParts[index] = newRenderedHoverPart; From 27544c5f463f0d22cbd5400f60efff75b611a0e5 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Thu, 16 Jan 2025 17:30:30 +0100 Subject: [PATCH 0600/3587] Adding method `isValidRange`, using it in native edit context (#238061) adding method `isValidRange` --- .../controller/editContext/native/nativeEditContext.ts | 8 +++++++- src/vs/editor/common/model.ts | 5 +++++ src/vs/editor/common/model/textModel.ts | 4 ++++ src/vs/monaco.d.ts | 4 ++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 116364099d0b..06d66e9ce033 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -290,6 +290,9 @@ export class NativeEditContext extends AbstractEditContext { private _updateEditContext(): void { const editContextState = this._getNewEditContextState(); + if (!editContextState) { + return; + } this._editContext.updateText(0, Number.MAX_SAFE_INTEGER, editContextState.text); this._editContext.updateSelection(editContextState.selectionStartOffset, editContextState.selectionEndOffset); this._textStartPositionWithinEditor = editContextState.textStartPositionWithinEditor; @@ -348,8 +351,11 @@ export class NativeEditContext extends AbstractEditContext { } } - private _getNewEditContextState(): { text: string; selectionStartOffset: number; selectionEndOffset: number; textStartPositionWithinEditor: Position } { + private _getNewEditContextState(): { text: string; selectionStartOffset: number; selectionEndOffset: number; textStartPositionWithinEditor: Position } | undefined { const model = this._context.viewModel.model; + if (!model.isValidRange(this._primarySelection)) { + return; + } const primarySelectionStartLine = this._primarySelection.startLineNumber; const primarySelectionEndLine = this._primarySelection.endLineNumber; const endColumnOfEndLineNumber = model.getLineMaxColumn(primarySelectionEndLine); diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 6700e53da2c9..146c9830e049 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -865,6 +865,11 @@ export interface ITextModel { */ validateRange(range: IRange): Range; + /** + * Verifies the range is valid. + */ + isValidRange(range: IRange): boolean; + /** * Converts the position to a zero-based offset. * diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 0313e5c208a9..704afa2acf4e 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1034,6 +1034,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return this._validatePosition(position.lineNumber, position.column, validationType); } + public isValidRange(range: Range): boolean { + return this._isValidRange(range, StringOffsetValidationType.SurrogatePairs); + } + private _isValidRange(range: Range, validationType: StringOffsetValidationType): boolean { const startLineNumber = range.startLineNumber; const startColumn = range.startColumn; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 461852b5196d..3da36e313354 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2114,6 +2114,10 @@ declare namespace monaco.editor { * Create a valid range. */ validateRange(range: IRange): Range; + /** + * Verifies the range is valid. + */ + isValidRange(range: IRange): boolean; /** * Converts the position to a zero-based offset. * From 566a4ca5c83cc0126045abd073f69968109b40db Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Thu, 16 Jan 2025 17:25:26 +0100 Subject: [PATCH 0601/3587] useHostProxy > isUseHostProxyEnabled() (microsoft/vscode-copilot-release#3821) --- package-lock.json | 8 ++++---- package.json | 2 +- remote/package-lock.json | 8 ++++---- remote/package.json | 2 +- src/vs/workbench/api/node/proxyResolver.ts | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index caa4903a0c97..99a6c490a60d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.8", - "@vscode/proxy-agent": "^0.29.0", + "@vscode/proxy-agent": "^0.30.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", @@ -2849,9 +2849,9 @@ } }, "node_modules/@vscode/proxy-agent": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.29.0.tgz", - "integrity": "sha512-zwpDvm5rwtJjXZv4TC8IXFRDDOU+fUNRe3asmls92Tz0dM0AJ8/WVfNgki5YOKxQMjVzWHAt0w53ZJxXj567EQ==", + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.30.0.tgz", + "integrity": "sha512-3MicZnHWbJna0WIHFDgrCFMgT8cWrzXe9VXKrJQTay8jWC+7Xi3+FTYgK9kC7RWAf7GVcwkjJh5UGTIbEDW7Fg==", "license": "MIT", "dependencies": { "@tootallnate/once": "^3.0.0", diff --git a/package.json b/package.json index 9e5044bb8b5b..2a792905f9b0 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.8", - "@vscode/proxy-agent": "^0.29.0", + "@vscode/proxy-agent": "^0.30.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", diff --git a/remote/package-lock.json b/remote/package-lock.json index 675fdc5b7b24..975417a80b2e 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -13,7 +13,7 @@ "@parcel/watcher": "2.5.0", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.29.0", + "@vscode/proxy-agent": "^0.30.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/tree-sitter-wasm": "^0.0.5", @@ -419,9 +419,9 @@ "integrity": "sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg==" }, "node_modules/@vscode/proxy-agent": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.29.0.tgz", - "integrity": "sha512-zwpDvm5rwtJjXZv4TC8IXFRDDOU+fUNRe3asmls92Tz0dM0AJ8/WVfNgki5YOKxQMjVzWHAt0w53ZJxXj567EQ==", + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.30.0.tgz", + "integrity": "sha512-3MicZnHWbJna0WIHFDgrCFMgT8cWrzXe9VXKrJQTay8jWC+7Xi3+FTYgK9kC7RWAf7GVcwkjJh5UGTIbEDW7Fg==", "license": "MIT", "dependencies": { "@tootallnate/once": "^3.0.0", diff --git a/remote/package.json b/remote/package.json index 00c008e51e5e..cffdccb4ff3a 100644 --- a/remote/package.json +++ b/remote/package.json @@ -8,7 +8,7 @@ "@parcel/watcher": "2.5.0", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.29.0", + "@vscode/proxy-agent": "^0.30.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/tree-sitter-wasm": "^0.0.5", diff --git a/src/vs/workbench/api/node/proxyResolver.ts b/src/vs/workbench/api/node/proxyResolver.ts index 9c85d7b980e2..533144660e47 100644 --- a/src/vs/workbench/api/node/proxyResolver.ts +++ b/src/vs/workbench/api/node/proxyResolver.ts @@ -43,7 +43,7 @@ export function connectProxyResolver( const useHostProxyDefault = initData.environment.useHostProxy ?? !isRemote; const fallbackToLocalKerberos = useHostProxyDefault; const loadLocalCertificates = useHostProxyDefault; - const isUseHostProxyEnabled = () => configProvider.getConfiguration('http').get('useLocalProxyConfiguration', useHostProxyDefault); + const isUseHostProxyEnabled = () => !isRemote || configProvider.getConfiguration('http').get('useLocalProxyConfiguration', useHostProxyDefault); const params: ProxyAgentParams = { resolveProxy: url => extHostWorkspace.resolveProxy(url), lookupProxyAuthorization: lookupProxyAuthorization.bind(undefined, extHostWorkspace, extHostLogService, mainThreadTelemetry, configProvider, {}, {}, initData.remote.isRemote, fallbackToLocalKerberos), @@ -71,7 +71,7 @@ export function connectProxyResolver( } }, proxyResolveTelemetry: () => { }, - useHostProxy: isUseHostProxyEnabled(), // TODO: can change at runtime now + isUseHostProxyEnabled, loadAdditionalCertificates: async () => { const promises: Promise[] = []; if (initData.remote.isRemote) { From d707be90c9d8fb77f61aac7829e188bdbdfac19e Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 16 Jan 2025 17:51:56 +0100 Subject: [PATCH 0602/3587] Add deletion view (#238064) Add deletion view --- .../browser/view/inlineEdits/deletionView.ts | 177 ++++++++++++++++++ .../view/inlineEdits/sideBySideDiff.ts | 1 - .../browser/view/inlineEdits/utils.ts | 64 +++++++ .../browser/view/inlineEdits/view.ts | 51 +++-- 4 files changed, 277 insertions(+), 16 deletions(-) create mode 100644 src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts new file mode 100644 index 000000000000..8150d454061a --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { IObservable, constObservable, derived, derivedObservableWithCache } from '../../../../../../base/common/observable.js'; +import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; +import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; +import { Point } from '../../../../../browser/point.js'; +import { EditorOption } from '../../../../../common/config/editorOptions.js'; +import { LineRange } from '../../../../../common/core/lineRange.js'; +import { Position } from '../../../../../common/core/position.js'; +import { createRectangle, mapOutFalsy, maxContentWidthInRange, n } from './utils.js'; +import { InlineEditWithChanges } from './viewAndDiffProducer.js'; + +export class InlineEditsDeletionView extends Disposable { + private readonly _editorObs = observableCodeEditor(this._editor); + + constructor( + private readonly _editor: ICodeEditor, + private readonly _edit: IObservable, + private readonly _uiState: IObservable<{ + edit: InlineEditWithChanges; + originalDisplayRange: LineRange; + widgetStartColumn: number; + } | undefined>, + ) { + super(); + + this._register(this._editorObs.createOverlayWidget({ + domNode: this._nonOverflowView.element, + position: constObservable(null), + allowEditorOverflow: false, + minContentWidthInPx: derived(reader => { + const info = this._editorLayoutInfo.read(reader); + if (info === null) { return 0; } + return info.code1.x - info.codeStart1.x; + }), + })); + } + + private readonly _display = derived(this, reader => !!this._uiState.read(reader) ? 'block' : 'none'); + + private readonly previewRef = n.ref(); + private readonly toolbarRef = n.ref(); + + private readonly _editorContainer = n.div({ + class: ['editorContainer', this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !v.edits.experimental.useGutterIndicator && 'showHover')], + style: { position: 'absolute' }, + }, [ + n.div({ class: 'preview', style: {}, ref: this.previewRef }), + n.div({ class: 'toolbar', style: {}, ref: this.toolbarRef }), + ]).keepUpdated(this._store); + + private readonly _originalStartPosition = derived(this, (reader) => { + const inlineEdit = this._edit.read(reader); + return inlineEdit ? new Position(inlineEdit.originalLineRange.startLineNumber, 1) : null; + }); + + private readonly _originalEndPosition = derived(this, (reader) => { + const inlineEdit = this._edit.read(reader); + return inlineEdit ? new Position(inlineEdit.originalLineRange.endLineNumberExclusive, 1) : null; + }); + + private readonly _originalVerticalStartPosition = this._editorObs.observePosition(this._originalStartPosition, this._store).map(p => p?.y); + private readonly _originalVerticalEndPosition = this._editorObs.observePosition(this._originalEndPosition, this._store).map(p => p?.y); + + private readonly _originalDisplayRange = this._uiState.map(s => s?.originalDisplayRange); + private readonly _editorMaxContentWidthInRange = derived(this, reader => { + const originalDisplayRange = this._originalDisplayRange.read(reader); + if (!originalDisplayRange) { + return constObservable(0); + } + this._editorObs.versionId.read(reader); + + // Take the max value that we observed. + // Reset when either the edit changes or the editor text version. + return derivedObservableWithCache(this, (reader, lastValue) => { + const maxWidth = maxContentWidthInRange(this._editorObs, originalDisplayRange, reader); + return Math.max(maxWidth, lastValue ?? 0); + }); + }).map((v, r) => v.read(r)); + + private readonly _editorLayoutInfo = derived(this, (reader) => { + const inlineEdit = this._edit.read(reader); + if (!inlineEdit) { + return null; + } + const state = this._uiState.read(reader); + if (!state) { + return null; + } + + const w = this._editorObs.getOption(EditorOption.fontInfo).read(reader).typicalHalfwidthCharacterWidth; + const editorLayout = this._editorObs.layoutInfo.read(reader); + const horizontalScrollOffset = this._editorObs.scrollLeft.read(reader); + + const left = editorLayout.contentLeft + this._editorMaxContentWidthInRange.read(reader) - horizontalScrollOffset; + + const range = inlineEdit.originalLineRange; + const selectionTop = this._originalVerticalStartPosition.read(reader) ?? this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); + const selectionBottom = this._originalVerticalEndPosition.read(reader) ?? this._editor.getTopForLineNumber(range.endLineNumberExclusive) - this._editorObs.scrollTop.read(reader); + + const codeLeft = editorLayout.contentLeft + state.widgetStartColumn * w; + + if (left <= codeLeft) { + return null; + } + + const code1 = new Point(left, selectionTop); + const codeStart1 = new Point(codeLeft, selectionTop); + const code2 = new Point(left, selectionBottom); + const codeStart2 = new Point(codeLeft, selectionBottom); + const codeHeight = selectionBottom - selectionTop; + + return { + code1, + codeStart1, + code2, + codeStart2, + codeHeight, + horizontalScrollOffset, + padding: 3, + borderRadius: 4, + }; + }).recomputeInitiallyAndOnChange(this._store); + + private readonly _foregroundSvg = n.svg({ + transform: 'translate(-0.5 -0.5)', + style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, + }, derived(reader => { + const layoutInfoObs = mapOutFalsy(this._editorLayoutInfo).read(reader); + if (!layoutInfoObs) { return undefined; } + + const layoutInfo = layoutInfoObs.read(reader); + + // TODO: look into why 1px offset is needed + const rectangleOverlay = createRectangle( + { + topLeft: layoutInfo.codeStart1, + topRight: layoutInfo.code1.deltaX(1), + bottomLeft: layoutInfo.codeStart2.deltaY(1), + bottomRight: layoutInfo.code2.deltaX(1).deltaY(1), + }, + layoutInfo.padding, + layoutInfo.borderRadius, + { hideLeft: layoutInfo.horizontalScrollOffset !== 0 } + ); + + return [ + n.svgElem('path', { + class: 'originalOverlay', + d: rectangleOverlay, + style: { + fill: 'var(--vscode-inlineEdit-originalBackground, transparent)', + stroke: 'var(--vscode-inlineEdit-originalBorder)', + strokeWidth: '1px', + } + }), + ]; + })).keepUpdated(this._store); + + private readonly _nonOverflowView = n.div({ + class: 'inline-edits-view', + style: { + position: 'absolute', + overflow: 'visible', + top: '0px', + left: '0px', + zIndex: '0', + display: this._display, + }, + }, [ + [this._editorContainer, this._foregroundSvg], + ]).keepUpdated(this._store); + +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index 1894f06327bd..314bc2d8f144 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -505,7 +505,6 @@ export class InlineEditsSideBySideDiff extends Disposable { if (!layoutInfo) { return undefined; } const width = layoutInfo.previewEditorWidth + layoutInfo.padding; - const topLeft = layoutInfo.edit1; const topRight = layoutInfo.edit1.deltaX(width); const topRightBefore = topRight.deltaX(-layoutInfo.borderRadius); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts index a3b822d875d8..f9734fa18b63 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts @@ -163,6 +163,70 @@ export class PathBuilder { } } +export function createRectangle( + corners: { topLeft: Point; topRight: Point; bottomLeft: Point; bottomRight: Point }, + padding: number | { top: number; right: number; bottom: number; left: number }, + borderRadius: number, + options: { hideLeft?: boolean; hideRight?: boolean; hideTop?: boolean; hideBottom?: boolean } = {} +): string { + const { top: paddingTop, bottom: paddingBottom, left: paddingLeft, right: paddingRight } = typeof padding === 'number' ? + { top: padding, bottom: padding, left: padding, right: padding } + : padding; + + // The path is drawn from bottom left at the end of the rounded corner in a clockwise direction + // Before: before the rounded corner + // After: after the rounded corner + const topLeft = corners.topLeft.deltaX(-paddingLeft).deltaY(-paddingTop); + const topRight = corners.topRight.deltaX(paddingRight).deltaY(-paddingTop); + const topLeftBefore = topLeft.deltaY(borderRadius); + const topLeftAfter = topLeft.deltaX(borderRadius); + const topRightBefore = topRight.deltaX(-borderRadius); + const topRightAfter = topRight.deltaY(borderRadius); + + const bottomLeft = corners.bottomLeft.deltaX(-paddingLeft).deltaY(paddingBottom); + const bottomRight = corners.bottomRight.deltaX(paddingRight).deltaY(paddingBottom); + const bottomLeftBefore = bottomLeft.deltaX(borderRadius); + const bottomLeftAfter = bottomLeft.deltaY(-borderRadius); + const bottomRightBefore = bottomRight.deltaY(-borderRadius); + const bottomRightAfter = bottomRight.deltaX(-borderRadius); + + const path = new PathBuilder(); + + if (!options.hideLeft) { + path.moveTo(bottomLeftAfter).lineTo(topLeftBefore); + } + + if (!options.hideLeft && !options.hideTop) { + path.curveTo(topLeft, topLeftAfter); + } + + if (!options.hideTop) { + path.moveTo(topLeftAfter).lineTo(topRightBefore); + } + + if (!options.hideTop && !options.hideRight) { + path.curveTo(topRight, topRightAfter); + } + + if (!options.hideRight) { + path.moveTo(topRightAfter).lineTo(bottomRightBefore); + } + + if (!options.hideRight && !options.hideBottom) { + path.curveTo(bottomRight, bottomRightAfter); + } + + if (!options.hideBottom) { + path.moveTo(bottomRightAfter).lineTo(bottomLeftBefore); + } + + if (!options.hideBottom && !options.hideLeft) { + path.curveTo(bottomLeft, bottomLeftAfter); + } + + return path.build(); +} + type Value = T | IObservable; type ValueOrList = Value | ValueOrList[]; type ValueOrList2 = ValueOrList | ValueOrList>; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index c632f5e67c06..8f8286d43a8d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -17,6 +17,7 @@ import { TextLength } from '../../../../../common/core/textLength.js'; import { DetailedLineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../../../../../common/diff/rangeMapping.js'; import { TextModel } from '../../../../../common/model/textModel.js'; import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; +import { InlineEditsDeletionView } from './deletionView.js'; import { InlineEditsGutterIndicator } from './gutterIndicatorView.js'; import { IInlineEditsIndicatorState, InlineEditsIndicator } from './indicatorView.js'; import { IOriginalEditorInlineDiffViewState, OriginalEditorInlineDiffView } from './inlineDiffView.js'; @@ -111,6 +112,16 @@ export class InlineEditsView extends Disposable { }) : undefined), )); + protected readonly _deletion = this._register(this._instantiationService.createInstance(InlineEditsDeletionView, + this._editor, + this._edit, + this._uiState.map(s => s && s.state.kind === 'deletion' ? ({ + edit: s.edit, + originalDisplayRange: s.originalDisplayRange, + widgetStartColumn: s.state.widgetStartColumn, + }) : undefined), + )); + private readonly _inlineDiffViewState = derived(this, reader => { const e = this._uiState.read(reader); if (!e) { return undefined; } @@ -120,7 +131,7 @@ export class InlineEditsView extends Disposable { return { modifiedText: new StringText(e.newText), diff: e.diff, - mode: e.state.kind === 'collapsed' ? 'sideBySide' : e.state.kind, + mode: e.state.kind === 'collapsed' || e.state.kind === 'deletion' ? 'sideBySide' : e.state.kind, modifiedCodeEditor: this._sideBySide.previewEditor, }; }); @@ -171,16 +182,26 @@ export class InlineEditsView extends Disposable { return { kind: 'collapsed' as const }; } + const inner = diff.flatMap(d => d.innerChanges ?? []); + const isSingleInnerEdit = inner.length === 1; + if ( - this._useMixedLinesDiff.read(reader) === 'forStableInsertions' - && isSingleLineInsertionAfterPosition(diff, edit.cursorPosition) - || isSingleLineDeletion(diff) + isSingleInnerEdit && ( + this._useMixedLinesDiff.read(reader) === 'forStableInsertions' + && isSingleLineInsertionAfterPosition(diff, edit.cursorPosition) + || isSingleLineDeletion(diff) + ) ) { return { kind: 'ghostText' as const }; } + if (inner.every(m => newText.getValueOfRange(m.modifiedRange).trim() === '')) { + const trimLength = getPrefixTrimLength(edit.originalText.getLineAt(edit.originalLineRange.startLineNumber), newText.getLineAt(edit.modifiedLineRange.startLineNumber)); + const widgetStartColumn = Math.min(trimLength, ...inner.map(m => m.originalRange.startLineNumber !== m.originalRange.endLineNumber ? 0 : m.originalRange.startColumn - 1)); + return { kind: 'deletion' as const, widgetStartColumn }; + } + if (diff.length === 1 && diff[0].original.length === 1 && diff[0].modified.length === 1) { - const inner = diff.flatMap(d => d.innerChanges!); const canUseWordReplacementView = inner.every(m => ( m.originalRange.isEmpty() && this._useWordInsertionView.read(reader) === 'whenPossible' || !m.originalRange.isEmpty() && this._useWordReplacementView.read(reader) === 'whenPossible' @@ -190,9 +211,9 @@ export class InlineEditsView extends Disposable { const allInnerModifiedTexts = inner.map(m => newText.getValueOfRange(m.modifiedRange)); const allInnerOriginalTexts = inner.map(m => edit.originalText.getValueOfRange(m.originalRange)); const allInnerEditsAreTheSame = allInnerModifiedTexts.every(text => text === allInnerModifiedTexts[0]) && allInnerOriginalTexts.every(text => text === allInnerOriginalTexts[0]); - const allInnerChangesNotToLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); + const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); - if (allInnerEditsAreTheSame && allInnerChangesNotToLong) { + if (allInnerChangesNotTooLong && isSingleInnerEdit && allInnerEditsAreTheSame) { return { kind: 'wordReplacements' as const, replacements: inner.map(i => @@ -200,14 +221,6 @@ export class InlineEditsView extends Disposable { ) }; } else { - function getPrefixTrimLength(originalLine: string, editedLine: string) { - let startTrim = 0; - while (originalLine[startTrim] === editedLine[startTrim] && originalLine[startTrim] === ' ') { - startTrim++; - } - return startTrim; - } - const replacements = inner.map((m, i) => new SingleTextEdit(m.originalRange, allInnerModifiedTexts[i])); const originalLine = edit.originalText.getLineAt(edit.originalLineRange.startLineNumber); @@ -285,3 +298,11 @@ function isSingleLineDeletion(diff: DetailedLineRangeMapping[]): boolean { return true; } } + +function getPrefixTrimLength(originalLine: string, editedLine: string) { + let startTrim = 0; + while (originalLine[startTrim] === editedLine[startTrim] && (originalLine[startTrim] === ' ' || originalLine[startTrim] === '\t')) { + startTrim++; + } + return startTrim; +} From 2718b079efb4aa5754e413d6a777dab33f176e2a Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 16 Jan 2025 12:50:00 -0500 Subject: [PATCH 0603/3587] try not using stable flag just in zsh. Leave it in for .ts --- .../terminal/common/scripts/shellIntegration-rc.zsh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index 31885e517952..131e8afecb22 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -155,9 +155,9 @@ __vsc_command_complete() { fi __vsc_update_cwd - if [[ "$__vsc_stable" == "0" ]]; then - __vsc_update_env - fi + # if [[ "$__vsc_stable" == "0" ]]; then + __vsc_update_env + # fi } if [[ -o NOUNSET ]]; then @@ -193,9 +193,9 @@ __vsc_precmd() { __vsc_update_prompt fi - if [[ "$__vsc_stable" == "0" ]]; then - __vsc_update_env - fi + # if [[ "$__vsc_stable" == "0" ]]; then + __vsc_update_env + # fi } __vsc_preexec() { From 7caedf655beab64374e01875c1496124b689735d Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Thu, 16 Jan 2025 09:54:40 -0800 Subject: [PATCH 0604/3587] Fix context change handling in Getting Started page to ensure current walkthrough is active (#238070) --- .../contrib/welcomeGettingStarted/browser/gettingStarted.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 7e0597591871..7719d18b7a5f 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -1511,7 +1511,7 @@ export class GettingStartedPage extends EditorPane { buildStepList(); this.detailsPageDisposables.add(this.contextService.onDidChangeContext(e => { - if (e.affectsSome(contextKeysToWatch)) { + if (e.affectsSome(contextKeysToWatch) && this.currentWalkthrough) { buildStepList(); this.registerDispatchListeners(); this.selectStep(this.editorInput.selectedStep, false); From 0a8a8b416b6f5acdd6442b75d7eb24529bd7e15d Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 16 Jan 2025 13:19:47 -0500 Subject: [PATCH 0605/3587] comment out envMixin VSCODE_STABLE --- src/vs/platform/terminal/node/terminalEnvironment.ts | 2 +- .../contrib/terminal/common/scripts/shellIntegration-rc.zsh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index 71398d253731..d89d8c9277d2 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -224,7 +224,7 @@ export function getShellIntegrationInjection( source: path.join(appRoot, 'out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh'), dest: path.join(zdotdir, '.zlogin') }); - envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; + // envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; return { newArgs, envMixin, filesToCopy }; } } diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index 131e8afecb22..51f3a45d9622 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -100,8 +100,8 @@ __vsc_nonce="$VSCODE_NONCE" unset VSCODE_NONCE # Some features should only work in Insiders -__vsc_stable="$VSCODE_STABLE" -unset VSCODE_STABLE +# __vsc_stable="$VSCODE_STABLE" +# unset VSCODE_STABLE __vsc_prompt_start() { builtin printf '\e]633;A\a' From 483705ee04bf2ed551d090b2f86ceb191b9fd6ae Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:55:59 -0800 Subject: [PATCH 0606/3587] Move content segmenter logic into own file --- src/vs/editor/browser/gpu/contentSegmenter.ts | 68 +++++++++++++++++++ .../browser/gpu/fullFileRenderStrategy.ts | 35 +++------- 2 files changed, 77 insertions(+), 26 deletions(-) create mode 100644 src/vs/editor/browser/gpu/contentSegmenter.ts diff --git a/src/vs/editor/browser/gpu/contentSegmenter.ts b/src/vs/editor/browser/gpu/contentSegmenter.ts new file mode 100644 index 000000000000..22a5c72a518e --- /dev/null +++ b/src/vs/editor/browser/gpu/contentSegmenter.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { ViewLineRenderingData } from '../../common/viewModel.js'; + +export interface IContentSegmenter { + /** + * Gets the content segment at an index within the line data's contents. This will be undefined + * when the index should not be rendered, ie. when it's part of an earlier segment like the tail + * end of an emoji, or when the line is not that long. + * @param index The index within the line data's content string. + */ + getSegmentAtIndex(index: number): string | undefined; +} + +export function createContentSegmenter(lineData: ViewLineRenderingData): IContentSegmenter { + if (lineData.isBasicASCII) { + return new AsciiContentSegmenter(lineData); + } + return new GraphemeContentSegmenter(lineData); +} + +class AsciiContentSegmenter implements IContentSegmenter { + private readonly _content: string; + + constructor(lineData: ViewLineRenderingData) { + this._content = lineData.content; + } + + getSegmentAtIndex(column: number): string { + return this._content[column]; + } +} + +class GraphemeContentSegmenter implements IContentSegmenter { + private readonly _segments: (string | undefined)[] = []; + + constructor(lineData: ViewLineRenderingData) { + const content = lineData.content; + const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' }); + const segmentedContent = Array.from(segmenter.segment(content)); + let segmenterIndex = 0; + + for (let x = 0; x < content.length; x++) { + const segment = segmentedContent[segmenterIndex]; + + // No more segments in the string (eg. an emoji is the last segment) + if (!segment) { + break; + } + + // The segment isn't renderable (eg. the tail end of an emoji) + if (segment.index !== x) { + this._segments.push(undefined); + continue; + } + + segmenterIndex++; + this._segments.push(segment.segment); + } + } + + getSegmentAtIndex(index: number): string | undefined { + return this._segments.length > index ? this._segments[index] : ''; + } +} diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index ce101adad38e..509b581acffe 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -23,7 +23,7 @@ import { quadVertices } from './gpuUtils.js'; import { GlyphRasterizer } from './raster/glyphRasterizer.js'; import { ViewGpuContext } from './viewGpuContext.js'; import { Color } from '../../../base/common/color.js'; -import type { IntlWordSegmentData } from '../../common/core/wordCharacterClassifier.js'; +import { createContentSegmenter, type IContentSegmenter } from './contentSegmenter.js'; const enum Constants { IndicesPerCell = 6, @@ -52,6 +52,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend readonly wgsl: string = fullFileRenderStrategyWgsl; private readonly _glyphRasterizer: MandatoryMutableDisposable; + get glyphRasterizer() { return this._glyphRasterizer.value; } private _cellBindBuffer!: GPUBuffer; @@ -263,16 +264,13 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend let lineData: ViewLineRenderingData; let decoration: InlineDecoration; - let content: string = ''; let fillStartIndex = 0; let fillEndIndex = 0; let tokens: IViewLineTokens; const dpr = getActiveWindow().devicePixelRatio; - const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' }); - let segmenterIndex = 0; - let segmentedContent: Intl.SegmentData[]; + let contentSegmenter: IContentSegmenter; if (!this._scrollInitialized) { this.onScrollChanged(); @@ -345,13 +343,9 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend dirtyLineEnd = Math.max(dirtyLineEnd, y); lineData = viewportData.getViewLineRenderingData(y); - content = lineData.content; xOffset = 0; - // Don't need to use the segmenter for non-ASCII content - if (!lineData.isBasicASCII) { - segmentedContent = Array.from(segmenter.segment(content)); - } + contentSegmenter = createContentSegmenter(lineData); tokens = lineData.tokens; tokenStartIndex = lineData.minColumn - 1; @@ -365,28 +359,17 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend tokenMetadata = tokens.getMetadata(tokenIndex); - segmenterIndex = 0; + // segmenterIndex = 0; for (x = tokenStartIndex; x < tokenEndIndex; x++) { // TODO: This needs to move to a dynamic long line rendering strategy if (x > this._viewGpuContext.maxGpuCols) { break; } - - if (lineData.isBasicASCII) { - chars = content.charAt(x); - } else { - const segment = segmentedContent![segmenterIndex]; - - // No more segments in the string (eg. an emoji is the last segment) - if (!segment) { - break; - } - if (segment.index !== x) { - continue; - } - segmenterIndex++; - chars = segment.segment; + const s = contentSegmenter.getSegmentAtIndex(x); + if (s === undefined) { + continue; } + chars = s; decorationStyleSetColor = undefined; decorationStyleSetBold = undefined; From 7b8757404e1227b55d2f902d7a1c0ae7258bd201 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 16 Jan 2025 14:04:10 -0500 Subject: [PATCH 0607/3587] at least uncomment ts side --- src/vs/platform/terminal/node/terminalEnvironment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index d89d8c9277d2..71398d253731 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -224,7 +224,7 @@ export function getShellIntegrationInjection( source: path.join(appRoot, 'out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh'), dest: path.join(zdotdir, '.zlogin') }); - // envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; + envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; return { newArgs, envMixin, filesToCopy }; } } From e1d37fbb23637786859fb92f99f6ba94f20f6851 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:44:28 -0800 Subject: [PATCH 0608/3587] Mostly support non-standard character widths --- .../browser/gpu/fullFileRenderStrategy.ts | 28 ++++++++++++++----- src/vs/editor/browser/gpu/gpu.ts | 2 ++ .../browser/gpu/raster/glyphRasterizer.ts | 4 +++ src/vs/editor/browser/gpu/raster/raster.ts | 2 ++ 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 509b581acffe..3a69d67b5705 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -246,11 +246,12 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend // dropped frames. let chars = ''; + let charWidth = 0; let y = 0; let x = 0; let absoluteOffsetX = 0; let absoluteOffsetY = 0; - let xOffset = 0; + let tabXOffset = 0; let glyph: Readonly; let cellIndex = 0; @@ -343,9 +344,11 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend dirtyLineEnd = Math.max(dirtyLineEnd, y); lineData = viewportData.getViewLineRenderingData(y); - xOffset = 0; + tabXOffset = 0; contentSegmenter = createContentSegmenter(lineData); + charWidth = viewLineOptions.spaceWidth * dpr; + absoluteOffsetX = 0; tokens = lineData.tokens; tokenStartIndex = lineData.minColumn - 1; @@ -359,7 +362,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend tokenMetadata = tokens.getMetadata(tokenIndex); - // segmenterIndex = 0; for (x = tokenStartIndex; x < tokenEndIndex; x++) { // TODO: This needs to move to a dynamic long line rendering strategy if (x > this._viewGpuContext.maxGpuCols) { @@ -371,6 +373,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } chars = s; + if (!lineData.isBasicASCII) { + charWidth = this._glyphRasterizer.value.getTextMetrics(chars).width; + } + decorationStyleSetColor = undefined; decorationStyleSetBold = undefined; decorationStyleSetOpacity = undefined; @@ -430,7 +436,14 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend cellBuffer.fill(0, cellIndex, cellIndex + CellBufferInfo.FloatsPerEntry); // Adjust xOffset for tab stops if (chars === '\t') { - xOffset = CursorColumns.nextRenderTabStop(x + xOffset, lineData.tabSize) - x - 1; + // Find the pixel offset between the current position and the next tab stop + const offsetBefore = x + tabXOffset; + tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize); + absoluteOffsetX += charWidth * (tabXOffset - offsetBefore); + // Convert back to offset excluding x and the current character + tabXOffset -= x + 1; + } else { + absoluteOffsetX += charWidth; } continue; } @@ -438,8 +451,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity); glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer.value, chars, tokenMetadata, decorationStyleSetId); - // TODO: Support non-standard character widths - absoluteOffsetX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * dpr); absoluteOffsetY = Math.round( // Top of layout box (includes line height) viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] * dpr + @@ -454,10 +465,13 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend ); cellIndex = ((y - 1) * this._viewGpuContext.maxGpuCols + x) * Constants.IndicesPerCell; - cellBuffer[cellIndex + CellBufferInfo.Offset_X] = absoluteOffsetX; + cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.round(absoluteOffsetX); cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = absoluteOffsetY; cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex; cellBuffer[cellIndex + CellBufferInfo.TextureIndex] = glyph.pageIndex; + + // Adjust the x pixel offset for the next character + absoluteOffsetX += charWidth; } tokenStartIndex = tokenEndIndex; diff --git a/src/vs/editor/browser/gpu/gpu.ts b/src/vs/editor/browser/gpu/gpu.ts index cbd292a1b3d3..b5d4d72e70af 100644 --- a/src/vs/editor/browser/gpu/gpu.ts +++ b/src/vs/editor/browser/gpu/gpu.ts @@ -6,6 +6,7 @@ import type { ViewConfigurationChangedEvent, ViewLinesChangedEvent, ViewLinesDeletedEvent, ViewLinesInsertedEvent, ViewScrollChangedEvent, ViewTokensChangedEvent } from '../../common/viewEvents.js'; import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js'; +import type { IGlyphRasterizer } from './raster/raster.js'; export const enum BindingId { GlyphInfo, @@ -20,6 +21,7 @@ export const enum BindingId { export interface IGpuRenderStrategy { readonly wgsl: string; readonly bindGroupEntries: GPUBindGroupEntry[]; + readonly glyphRasterizer: IGlyphRasterizer; onLinesDeleted(e: ViewLinesDeletedEvent): boolean; onConfigurationChanged(e: ViewConfigurationChangedEvent): boolean; diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index d7f39647d82a..1a8fe6ac4d95 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -268,4 +268,8 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { } } } + + public getTextMetrics(text: string): TextMetrics { + return this._ctx.measureText(text); + } } diff --git a/src/vs/editor/browser/gpu/raster/raster.ts b/src/vs/editor/browser/gpu/raster/raster.ts index b93d8748978f..eb6c56f1ffd3 100644 --- a/src/vs/editor/browser/gpu/raster/raster.ts +++ b/src/vs/editor/browser/gpu/raster/raster.ts @@ -32,6 +32,8 @@ export interface IGlyphRasterizer { decorationStyleSetId: number, colorMap: string[], ): Readonly; + + getTextMetrics(text: string): TextMetrics; } /** From abecfae1ec3cf8f29b6cb0a654a8053995108e79 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 16 Jan 2025 21:33:08 +0100 Subject: [PATCH 0609/3587] Git - add avatar resolution to git blame editor decoration hover (#238083) * Initial implementation * Add logging * Improve avatar resolution --- extensions/git/src/api/git.d.ts | 1 + extensions/git/src/blame.ts | 15 +- .../git/src/historyItemDetailsProvider.ts | 18 ++ extensions/github/src/extension.ts | 2 +- .../github/src/historyItemDetailsProvider.ts | 228 +++++++++++++++++- extensions/github/src/typings/git.d.ts | 1 + extensions/github/src/util.ts | 34 +++ 7 files changed, 290 insertions(+), 9 deletions(-) diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 58299ae68260..c36879fd6fed 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -327,6 +327,7 @@ export interface BranchProtectionProvider { } export interface SourceControlHistoryItemDetailsProvider { + provideAvatar(repository: Repository, commit: string, authorName?: string, authorEmail?: string): ProviderResult; provideHoverCommands(repository: Repository): ProviderResult; provideMessageLinks(repository: Repository, message: string): ProviderResult; } diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index f472116e0e41..ac7bd402081c 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -12,7 +12,7 @@ import { BlameInformation, Commit } from './git'; import { fromGitUri, isGitUri } from './uri'; import { emojify, ensureEmojis } from './emoji'; import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation } from './staging'; -import { provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; +import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean { return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive); @@ -205,6 +205,7 @@ export class GitBlameController { async getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation, includeCommitDetails = false): Promise { const remoteHoverCommands: Command[] = []; + let commitAvatar: string | undefined; let commitInformation: Commit | undefined; let commitMessageWithLinks: string | undefined; @@ -214,6 +215,10 @@ export class GitBlameController { if (includeCommitDetails) { try { commitInformation = await repository.getCommit(blameInformation.hash); + + // Avatar + commitAvatar = await provideSourceControlHistoryItemAvatar( + this._model, repository, blameInformation.hash, blameInformation.authorName, blameInformation.authorEmail); } catch { } } @@ -234,16 +239,18 @@ export class GitBlameController { markdownString.supportThemeIcons = true; // Author, date + const hash = commitInformation?.hash ?? blameInformation.hash; const authorName = commitInformation?.authorName ?? blameInformation.authorName; const authorEmail = commitInformation?.authorEmail ?? blameInformation.authorEmail; const authorDate = commitInformation?.authorDate ?? blameInformation.authorDate; + const avatar = commitAvatar ? `![${authorName}](${commitAvatar})` : '$(account)'; if (authorName) { if (authorEmail) { const emailTitle = l10n.t('Email'); - markdownString.appendMarkdown(`$(account) [**${authorName}**](mailto:${authorEmail} "${emailTitle} ${authorName}")`); + markdownString.appendMarkdown(`${avatar} [**${authorName}**](mailto:${authorEmail} "${emailTitle} ${authorName}")`); } else { - markdownString.appendMarkdown(`$(account) **${authorName}**`); + markdownString.appendMarkdown(`${avatar} **${authorName}**`); } if (authorDate) { @@ -282,8 +289,6 @@ export class GitBlameController { } // Commands - const hash = commitInformation?.hash ?? blameInformation.hash; - markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, hash]))} "${l10n.t('Open Commit')}")`); markdownString.appendMarkdown(' '); markdownString.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`); diff --git a/extensions/git/src/historyItemDetailsProvider.ts b/extensions/git/src/historyItemDetailsProvider.ts index 26d6bac30dd9..33b887430f0d 100644 --- a/extensions/git/src/historyItemDetailsProvider.ts +++ b/extensions/git/src/historyItemDetailsProvider.ts @@ -13,6 +13,24 @@ export interface ISourceControlHistoryItemDetailsProviderRegistry { getSourceControlHistoryItemDetailsProviders(): SourceControlHistoryItemDetailsProvider[]; } +export async function provideSourceControlHistoryItemAvatar( + registry: ISourceControlHistoryItemDetailsProviderRegistry, + repository: Repository, + commit: string, + authorName?: string, + authorEmail?: string +): Promise { + for (const provider of registry.getSourceControlHistoryItemDetailsProviders()) { + const result = await provider.provideAvatar(new ApiRepository(repository), commit, authorName, authorEmail); + + if (result) { + return result; + } + } + + return undefined; +} + export async function provideSourceControlHistoryItemHoverCommands( registry: ISourceControlHistoryItemDetailsProviderRegistry, repository: Repository diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts index 5ca0d1cb6d53..72a9111326e7 100644 --- a/extensions/github/src/extension.ts +++ b/extensions/github/src/extension.ts @@ -101,7 +101,7 @@ function initializeGitExtension(context: ExtensionContext, telemetryReporter: Te disposables.add(new GithubBranchProtectionProviderManager(gitAPI, context.globalState, logger, telemetryReporter)); disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler(telemetryReporter))); disposables.add(gitAPI.registerRemoteSourcePublisher(new GithubRemoteSourcePublisher(gitAPI))); - disposables.add(gitAPI.registerSourceControlHistoryItemDetailsProvider(new GitHubSourceControlHistoryItemDetailsProvider())); + disposables.add(gitAPI.registerSourceControlHistoryItemDetailsProvider(new GitHubSourceControlHistoryItemDetailsProvider(gitAPI, logger))); disposables.add(new GitHubCanonicalUriProvider(gitAPI)); disposables.add(new VscodeDevShareProvider(gitAPI)); setGitHubContext(gitAPI, disposables); diff --git a/extensions/github/src/historyItemDetailsProvider.ts b/extensions/github/src/historyItemDetailsProvider.ts index 4f7d0120ced6..c3e7899015a5 100644 --- a/extensions/github/src/historyItemDetailsProvider.ts +++ b/extensions/github/src/historyItemDetailsProvider.ts @@ -3,13 +3,142 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Command, l10n } from 'vscode'; -import { Repository, SourceControlHistoryItemDetailsProvider } from './typings/git'; -import { getRepositoryDefaultRemote, getRepositoryDefaultRemoteUrl } from './util'; +import { authentication, Command, l10n, LogOutputChannel } from 'vscode'; +import { Commit, Repository as GitHubRepository, Maybe } from '@octokit/graphql-schema'; +import { API, Repository, SourceControlHistoryItemDetailsProvider } from './typings/git'; +import { DisposableStore, getRepositoryDefaultRemote, getRepositoryDefaultRemoteUrl, getRepositoryFromUrl, sequentialize } from './util'; +import { AuthenticationError, getOctokitGraphql } from './auth'; + +const AVATAR_SIZE = 20; const ISSUE_EXPRESSION = /(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/g; +const ASSIGNABLE_USERS_QUERY = ` + query assignableUsers($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + assignableUsers(first: 100) { + nodes { + id + login + name + email + avatarUrl(size: ${AVATAR_SIZE}) + } + } + } + } +`; + +const COMMIT_AUTHOR_QUERY = ` + query commitAuthor($owner: String!, $repo: String!, $commit: String!) { + repository(owner: $owner, name: $repo) { + object(expression: $commit) { + ... on Commit { + author { + name + email + avatarUrl(size: ${AVATAR_SIZE}) + user { + id + login + } + } + } + } + } + } +`; + +interface GitHubRepositoryStore { + readonly users: GitHubUser[]; + readonly commits: Set; +} + +interface GitHubUser { + readonly id: string; + readonly login: string; + readonly name?: Maybe; + readonly email: string; + readonly avatarUrl: string; +} + export class GitHubSourceControlHistoryItemDetailsProvider implements SourceControlHistoryItemDetailsProvider { + private _enabled = true; + private readonly _store = new Map(); + private readonly _disposables = new DisposableStore(); + + constructor(private readonly _gitAPI: API, private readonly _logger: LogOutputChannel) { + this._disposables.add(this._gitAPI.onDidCloseRepository(this._onDidCloseRepository)); + + this._disposables.add(authentication.onDidChangeSessions(e => { + if (e.provider.id === 'github') { + this._enabled = true; + } + })); + } + + async provideAvatar(repository: Repository, commit: string, authorName?: string, authorEmail?: string): Promise { + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution for ${commit} in ${repository.rootUri.fsPath}.`); + + if (!this._enabled) { + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution is disabled.`); + return undefined; + } + + const descriptor = getRepositoryDefaultRemote(repository); + if (!descriptor) { + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Repository does not have a GitHub remote.`); + return undefined; + } + + try { + // Get the first page of the assignable users + await this._loadAssignableUsers(descriptor); + + const repositoryStore = this._store.get(this._getRepositoryKey(descriptor)); + if (!repositoryStore) { + return undefined; + } + + // Lookup the user in the cache + const avatarUrl = repositoryStore.users.find( + user => user.email === authorEmail || user.name === authorName)?.avatarUrl; + if (avatarUrl) { + return this._getAvatarUrl(avatarUrl, AVATAR_SIZE); + } + + // Check the commit against the list of known commits + // that are known to have incomplte author information + if (repositoryStore.commits.has(commit)) { + return undefined; + } + + // Get the commit details + const commitAuthor = await this._getCommitAuthor(descriptor, commit); + if (!commitAuthor) { + // The commit has incomplete author information, + // so we should not try to query the authors details + // again + repositoryStore.commits.add(commit); + return undefined; + } + + // Save the user to the cache + repositoryStore.users.push(commitAuthor); + + return this._getAvatarUrl(commitAuthor.avatarUrl, AVATAR_SIZE); + } catch (err) { + // A GitHub authentication session could be missing if the user has not yet + // signed in with their GitHub account or they have signed out. Disable the + // avatar resolution until the user signes in with their GitHub account. + if (err instanceof AuthenticationError) { + this._enabled = false; + } + } + + return undefined; + } + async provideHoverCommands(repository: Repository): Promise { const url = getRepositoryDefaultRemoteUrl(repository); if (!url) { @@ -47,4 +176,97 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont return `[${label}](https://github.com/${owner}/${repo}/issues/${number})`; }); } + + private _onDidCloseRepository(repository: Repository) { + for (const remote of repository.state.remotes) { + if (!remote.fetchUrl) { + continue; + } + + const repository = getRepositoryFromUrl(remote.fetchUrl); + if (!repository) { + continue; + } + + this._store.delete(this._getRepositoryKey(repository)); + } + } + + @sequentialize + private async _loadAssignableUsers(descriptor: { owner: string; repo: string }): Promise { + if (this._store.has(this._getRepositoryKey(descriptor))) { + return; + } + + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][_loadAssignableUsers] Querying assignable user(s) for ${descriptor.owner}/${descriptor.repo}.`); + + try { + const graphql = await getOctokitGraphql(); + const { repository } = await graphql<{ repository: GitHubRepository }>(ASSIGNABLE_USERS_QUERY, descriptor); + + const users: GitHubUser[] = []; + for (const node of repository.assignableUsers.nodes ?? []) { + if (!node) { + continue; + } + + users.push({ + id: node.id, + login: node.login, + name: node.name, + email: node.email, + avatarUrl: node.avatarUrl, + } satisfies GitHubUser); + } + + this._store.set(this._getRepositoryKey(descriptor), { users, commits: new Set() }); + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][_loadAssignableUsers] Successfully queried assignable user(s) for ${descriptor.owner}/${descriptor.repo}: ${users.length} user(s).`); + } catch (err) { + this._logger.warn(`[GitHubSourceControlHistoryItemDetailsProvider][_loadAssignableUsers] Failed to load assignable user(s) for ${descriptor.owner}/${descriptor.repo}: ${err}`); + throw err; + } + } + + private async _getCommitAuthor(descriptor: { owner: string; repo: string }, commit: string): Promise { + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][_getCommitAuthor] Querying commit author for ${descriptor.owner}/${descriptor.repo}/${commit}.`); + + try { + const graphql = await getOctokitGraphql(); + const { repository } = await graphql<{ repository: GitHubRepository }>(COMMIT_AUTHOR_QUERY, { ...descriptor, commit }); + + const commitAuthor = (repository.object as Commit).author; + if (!commitAuthor?.user?.id || !commitAuthor.user?.login || + !commitAuthor?.name || !commitAuthor?.email || !commitAuthor?.avatarUrl) { + this._logger.info(`[GitHubSourceControlHistoryItemDetailsProvider][_getCommitAuthor] Incomplete commit author for ${descriptor.owner}/${descriptor.repo}/${commit}: ${JSON.stringify(repository.object)}`); + + return undefined; + } + + const user = { + id: commitAuthor.user.id, + login: commitAuthor.user.login, + name: commitAuthor.name, + email: commitAuthor.email, + avatarUrl: commitAuthor.avatarUrl, + } satisfies GitHubUser; + + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][_getCommitAuthor] Successfully queried commit author for ${descriptor.owner}/${descriptor.repo}/${commit}: ${user.login}.`); + return user; + } catch (err) { + this._logger.warn(`[GitHubSourceControlHistoryItemDetailsProvider][_getCommitAuthor] Failed to get commit author for ${descriptor.owner}/${descriptor.repo}/${commit}: ${err}`); + throw err; + } + } + + private _getAvatarUrl(url: string, size: number): string { + return `${url}|height=${size},width=${size}`; + } + + private _getRepositoryKey(descriptor: { owner: string; repo: string }): string { + return `${descriptor.owner}/${descriptor.repo}`; + } + + dispose(): void { + this._disposables.dispose(); + } } diff --git a/extensions/github/src/typings/git.d.ts b/extensions/github/src/typings/git.d.ts index 714474533e0d..59dfa09aa836 100644 --- a/extensions/github/src/typings/git.d.ts +++ b/extensions/github/src/typings/git.d.ts @@ -290,6 +290,7 @@ export interface BranchProtectionProvider { } export interface SourceControlHistoryItemDetailsProvider { + provideAvatar(repository: Repository, commit: string, authorName?: string, authorEmail?: string): Promise; provideHoverCommands(repository: Repository): Promise; provideMessageLinks(repository: Repository, message: string): Promise; } diff --git a/extensions/github/src/util.ts b/extensions/github/src/util.ts index eba3bced698a..979b912d7b45 100644 --- a/extensions/github/src/util.ts +++ b/extensions/github/src/util.ts @@ -23,6 +23,40 @@ export class DisposableStore { } } +function decorate(decorator: (fn: Function, key: string) => Function): Function { + return (_target: any, key: string, descriptor: any) => { + let fnKey: string | null = null; + let fn: Function | null = null; + + if (typeof descriptor.value === 'function') { + fnKey = 'value'; + fn = descriptor.value; + } else if (typeof descriptor.get === 'function') { + fnKey = 'get'; + fn = descriptor.get; + } + + if (!fn || !fnKey) { + throw new Error('not supported'); + } + + descriptor[fnKey] = decorator(fn, key); + }; +} + +function _sequentialize(fn: Function, key: string): Function { + const currentKey = `__$sequence$${key}`; + + return function (this: any, ...args: any[]) { + const currentPromise = this[currentKey] as Promise || Promise.resolve(null); + const run = async () => await fn.apply(this, args); + this[currentKey] = currentPromise.then(run, run); + return this[currentKey]; + }; +} + +export const sequentialize = decorate(_sequentialize); + export function getRepositoryFromUrl(url: string): { owner: string; repo: string } | undefined { const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(\.git)?$/i.exec(url) || /^git@github\.com:([^/]+)\/([^/]+?)(\.git)?$/i.exec(url); From ebf1b9e952022d40787250286eb9ee81f6626bed Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:34:19 -0800 Subject: [PATCH 0610/3587] Include emoji when converting mouse coords to positions --- src/vs/editor/browser/gpu/contentSegmenter.ts | 19 ++-- .../viewParts/viewLinesGpu/viewLinesGpu.ts | 90 +++++++++++++++---- .../browser/gpu/atlas/textureAtlas.test.ts | 3 + 3 files changed, 89 insertions(+), 23 deletions(-) diff --git a/src/vs/editor/browser/gpu/contentSegmenter.ts b/src/vs/editor/browser/gpu/contentSegmenter.ts index 22a5c72a518e..455127677dfd 100644 --- a/src/vs/editor/browser/gpu/contentSegmenter.ts +++ b/src/vs/editor/browser/gpu/contentSegmenter.ts @@ -13,6 +13,7 @@ export interface IContentSegmenter { * @param index The index within the line data's content string. */ getSegmentAtIndex(index: number): string | undefined; + getSegmentData(index: number): Intl.SegmentData | undefined; } export function createContentSegmenter(lineData: ViewLineRenderingData): IContentSegmenter { @@ -29,13 +30,17 @@ class AsciiContentSegmenter implements IContentSegmenter { this._content = lineData.content; } - getSegmentAtIndex(column: number): string { - return this._content[column]; + getSegmentAtIndex(index: number): string { + return this._content[index]; + } + + getSegmentData(index: number): Intl.SegmentData | undefined { + return undefined; } } class GraphemeContentSegmenter implements IContentSegmenter { - private readonly _segments: (string | undefined)[] = []; + private readonly _segments: (Intl.SegmentData | undefined)[] = []; constructor(lineData: ViewLineRenderingData) { const content = lineData.content; @@ -58,11 +63,15 @@ class GraphemeContentSegmenter implements IContentSegmenter { } segmenterIndex++; - this._segments.push(segment.segment); + this._segments.push(segment); } } getSegmentAtIndex(index: number): string | undefined { - return this._segments.length > index ? this._segments[index] : ''; + return this._segments[index]?.segment; + } + + getSegmentData(index: number): Intl.SegmentData | undefined { + return this._segments[index]; } } diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index 7be81a4dc999..f70c07860e25 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -25,6 +25,7 @@ import { ViewLineOptions } from '../viewLines/viewLineOptions.js'; import type * as viewEvents from '../../../common/viewEvents.js'; import { CursorColumns } from '../../../common/core/cursorColumns.js'; import { TextureAtlas } from '../../gpu/atlas/textureAtlas.js'; +import { createContentSegmenter, type IContentSegmenter } from '../../gpu/contentSegmenter.js'; const enum GlyphStorageBufferInfo { FloatsPerEntry = 2 + 2 + 2, @@ -520,17 +521,45 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { // Resolve tab widths for this line const lineData = viewportData.getViewLineRenderingData(lineNumber); const content = lineData.content; + + let contentSegmenter: IContentSegmenter | undefined; + if (!lineData.isBasicASCII) { + contentSegmenter = createContentSegmenter(lineData); + } + + let chars: string | undefined = ''; + let resolvedStartColumn = 0; + let resolvedStartCssPixelOffset = 0; for (let x = 0; x < startColumn - 1; x++) { - if (content[x] === '\t') { + if (lineData.isBasicASCII) { + chars = content.charAt(x); + } else { + chars = contentSegmenter!.getSegmentAtIndex(x); + if (chars === undefined) { + continue; + } + resolvedStartCssPixelOffset += (this._renderStrategy.glyphRasterizer.getTextMetrics(chars).width / getActiveWindow().devicePixelRatio) - viewLineOptions.spaceWidth; + } + if (chars === '\t') { resolvedStartColumn = CursorColumns.nextRenderTabStop(resolvedStartColumn, lineData.tabSize); } else { resolvedStartColumn++; } } let resolvedEndColumn = resolvedStartColumn; + let resolvedEndCssPixelOffset = 0; for (let x = startColumn - 1; x < endColumn - 1; x++) { - if (content[x] === '\t') { + if (lineData.isBasicASCII) { + chars = content.charAt(x); + } else { + chars = contentSegmenter!.getSegmentAtIndex(x); + if (chars === undefined) { + continue; + } + resolvedEndCssPixelOffset += (this._renderStrategy.glyphRasterizer.getTextMetrics(chars).width / getActiveWindow().devicePixelRatio) - viewLineOptions.spaceWidth; + } + if (chars === '\t') { resolvedEndColumn = CursorColumns.nextRenderTabStop(resolvedEndColumn, lineData.tabSize); } else { resolvedEndColumn++; @@ -539,8 +568,8 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { // Visible horizontal range in _scaled_ pixels const result = new VisibleRanges(false, [new FloatHorizontalRange( - resolvedStartColumn * viewLineOptions.spaceWidth, - (resolvedEndColumn - resolvedStartColumn) * viewLineOptions.spaceWidth) + resolvedStartColumn * viewLineOptions.spaceWidth + resolvedStartCssPixelOffset, + (resolvedEndColumn - resolvedStartColumn) * viewLineOptions.spaceWidth + resolvedEndCssPixelOffset) ]); return result; @@ -581,24 +610,49 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { } const lineData = this._lastViewportData.getViewLineRenderingData(lineNumber); const content = lineData.content; - let visualColumnTarget = Math.round(mouseContentHorizontalOffset / this._lastViewLineOptions.spaceWidth); - let contentColumn = 0; - let contentColumnWithTabStops = 0; - while (visualColumnTarget > 0) { - let columnWithTabStopsSize = 0; - if (content[contentColumn] === '\t') { - const tabStop = CursorColumns.nextRenderTabStop(contentColumnWithTabStops, lineData.tabSize); - columnWithTabStopsSize = tabStop - contentColumnWithTabStops; + const dpr = getActiveWindow().devicePixelRatio; + const mouseContentHorizontalOffsetDevicePixels = mouseContentHorizontalOffset * dpr; + const spaceWidthDevicePixels = this._lastViewLineOptions.spaceWidth * dpr; + const contentSegmenter = createContentSegmenter(lineData); + + let widthSoFar = 0; + let charWidth = 0; + let tabXOffset = 0; + let column = 0; + for (let x = 0; x < content.length; x++) { + const chars = contentSegmenter.getSegmentAtIndex(x); + + // Part of an earlier segment + if (chars === undefined) { + column++; + continue; + } + + // Get the width of the character + if (lineData.isBasicASCII) { + charWidth = spaceWidthDevicePixels; } else { - columnWithTabStopsSize = 1; + charWidth = this._renderStrategy.glyphRasterizer.getTextMetrics(chars).width; } - if (visualColumnTarget - columnWithTabStopsSize / 2 < 0) { + + // Adjust for tabs + if (chars === '\t') { + // Find the pixel offset between the current position and the next tab stop + const offsetBefore = x + tabXOffset; + tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize); + charWidth = spaceWidthDevicePixels * (tabXOffset - offsetBefore); + // Convert back to offset excluding x and the current character + tabXOffset -= x + 1; + } + + if (mouseContentHorizontalOffsetDevicePixels < widthSoFar + charWidth / 2) { break; } - visualColumnTarget -= columnWithTabStopsSize; - contentColumn++; - contentColumnWithTabStops += columnWithTabStopsSize; + + widthSoFar += charWidth; + column++; } - return new Position(lineNumber, Math.floor(contentColumn) + 1); + + return new Position(lineNumber, column + 1); } } diff --git a/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts index 5b2c44367c05..816fa17cf811 100644 --- a/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts @@ -60,6 +60,9 @@ class TestGlyphRasterizer implements IGlyphRasterizer { fontBoundingBoxDescent: 0, }; } + getTextMetrics(text: string): TextMetrics { + return null!; + } } suite('TextureAtlas', () => { From cc263f80ee25db162d37bdf8d0a66e0f0c51aac6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:36:27 -0800 Subject: [PATCH 0611/3587] Improve variable name and share instance --- src/vs/editor/browser/gpu/fullFileRenderStrategy.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 3a69d67b5705..2690248277c5 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -246,6 +246,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend // dropped frames. let chars = ''; + let segment: string | undefined; let charWidth = 0; let y = 0; let x = 0; @@ -367,11 +368,11 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend if (x > this._viewGpuContext.maxGpuCols) { break; } - const s = contentSegmenter.getSegmentAtIndex(x); - if (s === undefined) { + segment = contentSegmenter.getSegmentAtIndex(x); + if (segment === undefined) { continue; } - chars = s; + chars = segment; if (!lineData.isBasicASCII) { charWidth = this._glyphRasterizer.value.getTextMetrics(chars).width; From cb34f37e6032a7a542a2f54b46675ddbe1b17de8 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 16 Jan 2025 15:46:03 -0500 Subject: [PATCH 0612/3587] is this real? --- src/vs/platform/terminal/test/node/terminalEnvironment.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts index 6248f83003d4..28c2072f0a15 100644 --- a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts @@ -132,7 +132,7 @@ suite('platform - terminalEnvironment', () => { /.+\/out\/vs\/workbench\/contrib\/terminal\/common\/scripts\/shellIntegration-login.zsh/ ]; function assertIsEnabled(result: IShellIntegrationConfigInjection, globalZdotdir = homedir()) { - strictEqual(Object.keys(result.envMixin!).length, 3); + strictEqual(Object.keys(result.envMixin!).length, 4); ok(result.envMixin!['ZDOTDIR']?.match(expectedDir)); strictEqual(result.envMixin!['USER_ZDOTDIR'], globalZdotdir); ok(result.envMixin!['VSCODE_INJECTION']?.match('1')); From 30b45136b08164d88436cd58f395d34b787efc19 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 16 Jan 2025 16:04:34 -0500 Subject: [PATCH 0613/3587] re-enable __vsc_stable --- .../test/node/terminalEnvironment.test.ts | 1 - .../common/scripts/shellIntegration-rc.zsh | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts index 28c2072f0a15..0829f838db63 100644 --- a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts @@ -132,7 +132,6 @@ suite('platform - terminalEnvironment', () => { /.+\/out\/vs\/workbench\/contrib\/terminal\/common\/scripts\/shellIntegration-login.zsh/ ]; function assertIsEnabled(result: IShellIntegrationConfigInjection, globalZdotdir = homedir()) { - strictEqual(Object.keys(result.envMixin!).length, 4); ok(result.envMixin!['ZDOTDIR']?.match(expectedDir)); strictEqual(result.envMixin!['USER_ZDOTDIR'], globalZdotdir); ok(result.envMixin!['VSCODE_INJECTION']?.match('1')); diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index 51f3a45d9622..31885e517952 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -100,8 +100,8 @@ __vsc_nonce="$VSCODE_NONCE" unset VSCODE_NONCE # Some features should only work in Insiders -# __vsc_stable="$VSCODE_STABLE" -# unset VSCODE_STABLE +__vsc_stable="$VSCODE_STABLE" +unset VSCODE_STABLE __vsc_prompt_start() { builtin printf '\e]633;A\a' @@ -155,9 +155,9 @@ __vsc_command_complete() { fi __vsc_update_cwd - # if [[ "$__vsc_stable" == "0" ]]; then - __vsc_update_env - # fi + if [[ "$__vsc_stable" == "0" ]]; then + __vsc_update_env + fi } if [[ -o NOUNSET ]]; then @@ -193,9 +193,9 @@ __vsc_precmd() { __vsc_update_prompt fi - # if [[ "$__vsc_stable" == "0" ]]; then - __vsc_update_env - # fi + if [[ "$__vsc_stable" == "0" ]]; then + __vsc_update_env + fi } __vsc_preexec() { From 87a0d07c364639ea521263dd6263af2ab9fd6ec8 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 16 Jan 2025 15:15:12 -0800 Subject: [PATCH 0614/3587] Go back to simpler sample of editFile instead of JSON block (#238086) Which might lead it to write a JSON block instead of calling the tool --- .../workbench/contrib/chat/browser/tools/tools.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/tools.ts b/src/vs/workbench/contrib/chat/browser/tools/tools.ts index 448b262887a4..146fe081abaa 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/tools.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/tools.ts @@ -51,11 +51,14 @@ Avoid repeating existing code, instead use comments to represent regions of unch { changed code } // ...existing code... -Here is an example of how you should use vscode_editFile to edit an existing Person class: -{ - "explanation": "Add an age property to the Person class", - "filePath": "/folder/person.ts", - "code": "// ...existing code...\\n class Person {\\n // ...existing code...\\n age: number;\\n // ...existing code...\\n getAge() {\\n return this.age;\\n }\n // ...existing code...\n }" +Here is an example of how you should use format an edit to an existing Person class: +class Person { + // ...existing code... + age: number; + // ...existing code... + getAge() { + return this.age; + } } `; From ab8fe3f37387ee36ee3f2dddc0bea31a66572eb1 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 17 Jan 2025 00:31:26 +0100 Subject: [PATCH 0615/3587] support default compound logs (#238089) --- src/vs/platform/log/common/log.ts | 22 +- .../api/browser/mainThreadOutputService.ts | 2 +- .../contrib/logs/common/logs.contribution.ts | 66 +++--- .../contrib/logs/common/logsActions.ts | 10 +- .../output/browser/output.contribution.ts | 209 ++++++++++++------ .../contrib/output/browser/outputServices.ts | 93 ++++++-- .../contrib/output/browser/outputView.ts | 25 ++- .../output/common/outputChannelModel.ts | 166 ++++++++++---- .../common/outputChannelModelService.ts | 21 +- .../services/output/common/output.ts | 58 ++++- 10 files changed, 479 insertions(+), 193 deletions(-) diff --git a/src/vs/platform/log/common/log.ts b/src/vs/platform/log/common/log.ts index 7ce341461a48..b5780a161348 100644 --- a/src/vs/platform/log/common/log.ts +++ b/src/vs/platform/log/common/log.ts @@ -91,6 +91,11 @@ function format(args: any, verbose: boolean = false): string { return result; } +export type LoggerGroup = { + readonly id: string; + readonly name: string; +}; + export interface ILogService extends ILogger { readonly _serviceBrand: undefined; } @@ -136,6 +141,11 @@ export interface ILoggerOptions { * Id of the extension that created this logger. */ extensionId?: string; + + /** + * Group of the logger. + */ + group?: LoggerGroup; } export interface ILoggerResource { @@ -146,6 +156,7 @@ export interface ILoggerResource { readonly hidden?: boolean; readonly when?: string; readonly extensionId?: string; + readonly group?: LoggerGroup; } export type DidChangeLoggersEvent = { @@ -616,7 +627,16 @@ export abstract class AbstractLoggerService extends Disposable implements ILogge } const loggerEntry: LoggerEntry = { logger, - info: { resource, id, logLevel, name: options?.name, hidden: options?.hidden, extensionId: options?.extensionId, when: options?.when } + info: { + resource, + id, + logLevel, + name: options?.name, + hidden: options?.hidden, + group: options?.group, + extensionId: options?.extensionId, + when: options?.when + } }; this.registerLogger(loggerEntry.info); // TODO: @sandy081 Remove this once registerLogger can take ILogger diff --git a/src/vs/workbench/api/browser/mainThreadOutputService.ts b/src/vs/workbench/api/browser/mainThreadOutputService.ts index 0bb3d04b1354..063ed1901e71 100644 --- a/src/vs/workbench/api/browser/mainThreadOutputService.ts +++ b/src/vs/workbench/api/browser/mainThreadOutputService.ts @@ -59,7 +59,7 @@ export class MainThreadOutputService extends Disposable implements MainThreadOut const id = `extension-output-${extensionId}-#${idCounter}-${label}`; const resource = URI.revive(file); - Registry.as(Extensions.OutputChannels).registerChannel({ id, label, files: [resource], log: false, languageId, extensionId }); + Registry.as(Extensions.OutputChannels).registerChannel({ id, label, source: { resource }, log: false, languageId, extensionId }); this._register(toDisposable(() => this.$dispose(id))); return id; } diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 90fae135d50a..0d9082b7a747 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -9,15 +9,13 @@ import { Categories } from '../../../../platform/action/common/actionCommonCateg import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { SetLogLevelAction } from './logsActions.js'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../common/contributions.js'; -import { IFileService, whenProviderRegistered } from '../../../../platform/files/common/files.js'; -import { IOutputChannelRegistry, IOutputService, Extensions } from '../../../services/output/common/output.js'; -import { Disposable, DisposableMap, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { IOutputChannelRegistry, IOutputService, Extensions, isMultiSourceOutputChannelDescriptor, isSingleSourceOutputChannelDescriptor } from '../../../services/output/common/output.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; import { CONTEXT_LOG_LEVEL, ILoggerResource, ILoggerService, LogLevel, LogLevelToString, isLogLevel } from '../../../../platform/log/common/log.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { Event } from '../../../../base/common/event.js'; import { windowLogId, showWindowLogActionId } from '../../../services/log/common/logConstants.js'; -import { createCancelablePromise } from '../../../../base/common/async.js'; import { IDefaultLogLevelsService } from './defaultLogLevels.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { CounterSet } from '../../../../base/common/map.js'; @@ -55,12 +53,10 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { private readonly contextKeys = new CounterSet(); private readonly outputChannelRegistry = Registry.as(Extensions.OutputChannels); - private readonly loggerDisposables = this._register(new DisposableMap()); constructor( @ILoggerService private readonly loggerService: ILoggerService, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IFileService private readonly fileService: IFileService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, ) { super(); @@ -138,36 +134,48 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { } private registerLogChannel(logger: ILoggerResource): void { + if (logger.group) { + this.registerCompoundLogChannel(logger.group.id, logger.group.name, logger); + return; + } + const channel = this.outputChannelRegistry.getChannel(logger.id); - if (channel?.files?.length === 1 && this.uriIdentityService.extUri.isEqual(channel.files[0], logger.resource)) { + if (channel && isSingleSourceOutputChannelDescriptor(channel) && this.uriIdentityService.extUri.isEqual(channel.source.resource, logger.resource)) { return; } - const disposables = new DisposableStore(); - const promise = createCancelablePromise(async token => { - await whenProviderRegistered(logger.resource, this.fileService); - if (token.isCancellationRequested) { - return; - } - const existingChannel = this.outputChannelRegistry.getChannel(logger.id); - const remoteLogger = existingChannel?.files?.[0].scheme === Schemas.vscodeRemote ? this.loggerService.getRegisteredLogger(existingChannel.files[0]) : undefined; - if (remoteLogger) { - this.deregisterLogChannel(remoteLogger); - } - const hasToAppendRemote = existingChannel && logger.resource.scheme === Schemas.vscodeRemote; - const id = hasToAppendRemote ? `${logger.id}.remote` : logger.id; - const label = hasToAppendRemote ? nls.localize('remote name', "{0} (Remote)", logger.name ?? logger.id) : logger.name ?? logger.id; - this.outputChannelRegistry.registerChannel({ id, label, files: [logger.resource], log: true, extensionId: logger.extensionId }); - disposables.add(toDisposable(() => this.outputChannelRegistry.removeChannel(id))); - if (remoteLogger) { - this.registerLogChannel(remoteLogger); + + const existingChannel = this.outputChannelRegistry.getChannel(logger.id); + const remoteLogger = existingChannel && isSingleSourceOutputChannelDescriptor(existingChannel) && existingChannel.source.resource.scheme === Schemas.vscodeRemote ? this.loggerService.getRegisteredLogger(existingChannel.source.resource) : undefined; + if (remoteLogger) { + this.deregisterLogChannel(remoteLogger); + } + const hasToAppendRemote = existingChannel && logger.resource.scheme === Schemas.vscodeRemote; + const id = hasToAppendRemote ? `${logger.id}.remote` : logger.id; + const label = hasToAppendRemote ? nls.localize('remote name', "{0} (Remote)", logger.name ?? logger.id) : logger.name ?? logger.id; + this.outputChannelRegistry.registerChannel({ id, label, source: { resource: logger.resource }, log: true, extensionId: logger.extensionId }); + } + + private registerCompoundLogChannel(id: string, name: string, logger: ILoggerResource): void { + const channel = this.outputChannelRegistry.getChannel(id); + const source = { resource: logger.resource, name: logger.name ?? logger.id }; + if (channel) { + if (isMultiSourceOutputChannelDescriptor(channel) && !channel.source.some(({ resource }) => this.uriIdentityService.extUri.isEqual(resource, logger.resource))) { + this.outputChannelRegistry.updateChannelSources(id, [...channel.source, source]); } - }); - disposables.add(toDisposable(() => promise.cancel())); - this.loggerDisposables.set(logger.resource.toString(), disposables); + } else { + this.outputChannelRegistry.registerChannel({ id, label: name, log: true, source: [source] }); + } } private deregisterLogChannel(logger: ILoggerResource): void { - this.loggerDisposables.deleteAndDispose(logger.resource.toString()); + if (logger.group) { + const channel = this.outputChannelRegistry.getChannel(logger.group.id); + if (channel && isMultiSourceOutputChannelDescriptor(channel)) { + this.outputChannelRegistry.updateChannelSources(logger.group.id, channel.source.filter(({ resource }) => !this.uriIdentityService.extUri.isEqual(resource, logger.resource))); + } + } else { + this.outputChannelRegistry.removeChannel(logger.id); + } } private registerShowWindowLogAction(): void { diff --git a/src/vs/workbench/contrib/logs/common/logsActions.ts b/src/vs/workbench/contrib/logs/common/logsActions.ts index 26514a1cb5b4..4f94dffcd072 100644 --- a/src/vs/workbench/contrib/logs/common/logsActions.ts +++ b/src/vs/workbench/contrib/logs/common/logsActions.ts @@ -12,7 +12,7 @@ import { IFileService } from '../../../../platform/files/common/files.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { dirname, basename, isEqual } from '../../../../base/common/resources.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { IOutputChannelDescriptor, IOutputService } from '../../../services/output/common/output.js'; +import { IOutputChannelDescriptor, IOutputService, isSingleSourceOutputChannelDescriptor } from '../../../services/output/common/output.js'; import { extensionTelemetryLogChannelId, telemetryLogId } from '../../../../platform/telemetry/common/telemetryUtils.js'; import { IDefaultLogLevelsService } from './defaultLogLevels.js'; import { Codicon } from '../../../../base/common/codicons.js'; @@ -52,11 +52,11 @@ export class SetLogLevelAction extends Action { const extensionLogs: LogChannelQuickPickItem[] = [], logs: LogChannelQuickPickItem[] = []; const logLevel = this.loggerService.getLogLevel(); for (const channel of this.outputService.getChannelDescriptors()) { - if (!SetLogLevelAction.isLevelSettable(channel) || channel.files?.length !== 1) { + if (!isSingleSourceOutputChannelDescriptor(channel) || !SetLogLevelAction.isLevelSettable(channel)) { continue; } - const channelLogLevel = this.loggerService.getLogLevel(channel.files[0]) ?? logLevel; - const item: LogChannelQuickPickItem = { id: channel.id, resource: channel.files[0], label: channel.label, description: channelLogLevel !== logLevel ? this.getLabel(channelLogLevel) : undefined, extensionId: channel.extensionId }; + const channelLogLevel = this.loggerService.getLogLevel(channel.source.resource) ?? logLevel; + const item: LogChannelQuickPickItem = { id: channel.id, resource: channel.source.resource, label: channel.label, description: channelLogLevel !== logLevel ? this.getLabel(channelLogLevel) : undefined, extensionId: channel.extensionId }; if (channel.extensionId) { extensionLogs.push(item); } else { @@ -97,7 +97,7 @@ export class SetLogLevelAction extends Action { } static isLevelSettable(channel: IOutputChannelDescriptor): boolean { - return channel.log && channel.files?.length === 1 && channel.id !== telemetryLogId && channel.id !== extensionTelemetryLogChannelId; + return channel.log && isSingleSourceOutputChannelDescriptor(channel) && channel.id !== telemetryLogId && channel.id !== extensionTelemetryLogChannelId; } private async setLogLevelForChannel(logChannel: LogChannelQuickPickItem): Promise { diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 58d802b533d2..8a56c1677051 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -10,7 +10,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuId, registerAction2, Action2, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { OutputService } from './outputServices.js'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, OUTPUT_FILTER_FOCUS_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT } from '../../../services/output/common/output.js'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, OUTPUT_FILTER_FOCUS_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT, isSingleSourceOutputChannelDescriptor, ISingleSourceOutputChannelDescriptor, isMultiSourceOutputChannelDescriptor, HIDE_SOURCE_FILTER_CONTEXT } from '../../../services/output/common/output.js'; import { OutputViewPane } from './outputView.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; @@ -22,13 +22,11 @@ import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContaine import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IQuickPickItem, IQuickInputService, IQuickPickSeparator, QuickPickInput } from '../../../../platform/quickinput/common/quickInput.js'; import { AUX_WINDOW_GROUP, AUX_WINDOW_GROUP_TYPE, IEditorService } from '../../../services/editor/common/editorService.js'; -import { assertIsDefined } from '../../../../base/common/types.js'; import { ContextKeyExpr, ContextKeyExpression } from '../../../../platform/contextkey/common/contextkey.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; -import { Disposable, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; +import { Disposable, DisposableMap, DisposableStore, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { ILoggerService, LogLevel, LogLevelToLocalizedString, LogLevelToString } from '../../../../platform/log/common/log.js'; import { IDefaultLogLevelsService } from '../../logs/common/defaultLogLevels.js'; @@ -43,6 +41,7 @@ import { ViewAction } from '../../../browser/parts/views/viewPane.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { basename } from '../../../../base/common/resources.js'; +import { escapeRegExpCharacters } from '../../../../base/common/strings.js'; const IMPORTED_LOG_ID_PREFIX = 'importedLog.'; @@ -106,6 +105,8 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { private registerActions(): void { this.registerSwitchOutputAction(); + this.registerAddCompoundLogAction(); + this.registerRemoveLogAction(); this.registerShowOutputChannelsAction(); this.registerClearOutputAction(); this.registerToggleAutoScrollAction(); @@ -115,7 +116,9 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { this.registerShowLogsAction(); this.registerOpenLogFileAction(); this.registerConfigureActiveOutputLogLevelAction(); - this.registerFilterActions(); + this.registerLogLevelFilterActions(); + this.registerSourceFilterAction(); + this.registerClearFilterActions(); this.registerExportLogsAction(); this.registerImportLogAction(); } @@ -148,7 +151,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { const registerOutputChannels = (channels: IOutputChannelDescriptor[]) => { for (const channel of channels) { const title = channel.label; - const group = (channel.files?.length && channel.files.length > 1) || channel.id.startsWith(IMPORTED_LOG_ID_PREFIX) ? '2_custom_logs' : channel.extensionId ? '0_ext_outputchannels' : '1_core_outputchannels'; + const group = channel.user ? '2_user_outputchannels' : channel.extensionId ? '0_ext_outputchannels' : '1_core_outputchannels'; registeredChannels.set(channel.id, registerAction2(class extends Action2 { constructor() { super({ @@ -179,7 +182,9 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { registeredChannels.get(e.id)?.dispose(); registeredChannels.delete(e.id); })); + } + private registerAddCompoundLogAction(): void { this._register(registerAction2(class extends Action2 { constructor() { super({ @@ -198,18 +203,17 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { const outputService = accessor.get(IOutputService); const quickInputService = accessor.get(IQuickInputService); - const extensionLogs: IFileOutputChannelDescriptor[] = [], logs: IFileOutputChannelDescriptor[] = []; + const extensionLogs: ISingleSourceOutputChannelDescriptor[] = [], logs: ISingleSourceOutputChannelDescriptor[] = []; for (const channel of outputService.getChannelDescriptors()) { - if (channel.log && channel.files?.length === 1) { - const fileChannel = channel; + if (channel.log && isSingleSourceOutputChannelDescriptor(channel)) { if (channel.extensionId) { - extensionLogs.push(fileChannel); + extensionLogs.push(channel); } else { - logs.push(fileChannel); + logs.push(channel); } } } - const entries: Array = []; + const entries: Array = []; for (const log of logs.sort((a, b) => a.label.localeCompare(b.label))) { entries.push(log); } @@ -225,12 +229,14 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { } } })); + } + private registerRemoveLogAction(): void { this._register(registerAction2(class extends Action2 { constructor() { super({ - id: 'workbench.action.output.removeCompoundLog', - title: nls.localize2('removeCompoundLog', "Remove Compound Log..."), + id: 'workbench.action.output.remove', + title: nls.localize2('removeLog', "Remove Output..."), category: Categories.Developer, f1: true }); @@ -239,13 +245,17 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { const outputService = accessor.get(IOutputService); const quickInputService = accessor.get(IQuickInputService); const notificationService = accessor.get(INotificationService); - const entries: Array = outputService.getChannelDescriptors().filter(channel => channel.files && channel.files?.length > 1); + const entries: Array = outputService.getChannelDescriptors().filter(channel => channel.user); if (entries.length === 0) { - notificationService.info(nls.localize('noCompoundLogs', "No compound logs to remove.")); + notificationService.info(nls.localize('nocustumoutput', "No custom outputs to remove.")); return; } const result = await quickInputService.pick(entries, { placeHolder: nls.localize('selectlog', "Select Log"), canPickMany: true }); - for (const channel of result ?? []) { + if (!result?.length) { + return; + } + const outputChannelRegistry = Registry.as(Extensions.OutputChannels); + for (const channel of result) { outputChannelRegistry.removeChannel(channel.id); } } @@ -399,7 +409,6 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { } private registerSaveActiveOutputAsAction(): void { - const that = this; this._register(registerAction2(class extends Action2 { constructor() { super({ @@ -415,9 +424,12 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { } async run(accessor: ServicesAccessor): Promise { const outputService = accessor.get(IOutputService); - const fileOutputChannelDescriptor = that.getFileOutputChannelDescriptor(); - if (fileOutputChannelDescriptor) { - await outputService.saveOutputAs(fileOutputChannelDescriptor); + const channel = outputService.getActiveChannel(); + if (channel) { + const descriptor = outputService.getChannelDescriptors().find(c => c.id === channel.id); + if (descriptor) { + await outputService.saveOutputAs(descriptor); + } } } })); @@ -435,19 +447,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { } } - private getFileOutputChannelDescriptor(): IFileOutputChannelDescriptor | null { - const channel = this.outputService.getActiveChannel(); - if (channel) { - const descriptor = this.outputService.getChannelDescriptors().filter(c => c.id === channel.id)[0]; - if (descriptor?.files) { - return descriptor; - } - } - return null; - } - private registerConfigureActiveOutputLogLevelAction(): void { - const that = this; const logLevelMenu = new MenuId('workbench.output.menu.logLevel'); this._register(MenuRegistry.appendMenuItem(MenuId.ViewTitle, { submenu: logLevelMenu, @@ -474,11 +474,13 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { }); } async run(accessor: ServicesAccessor): Promise { - const channel = that.outputService.getActiveChannel(); + const loggerService = accessor.get(ILoggerService); + const outputService = accessor.get(IOutputService); + const channel = outputService.getActiveChannel(); if (channel) { - const channelDescriptor = that.outputService.getChannelDescriptor(channel.id); - if (channelDescriptor?.log && channelDescriptor.files?.length === 1) { - return accessor.get(ILoggerService).setLogLevel(channelDescriptor.files[0], logLevel); + const channelDescriptor = outputService.getChannelDescriptor(channel.id); + if (channelDescriptor && isSingleSourceOutputChannelDescriptor(channelDescriptor)) { + return loggerService.setLogLevel(channelDescriptor.source.resource, logLevel); } } } @@ -506,12 +508,15 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { }); } async run(accessor: ServicesAccessor): Promise { - const channel = that.outputService.getActiveChannel(); + const outputService = accessor.get(IOutputService); + const loggerService = accessor.get(ILoggerService); + const defaultLogLevelsService = accessor.get(IDefaultLogLevelsService); + const channel = outputService.getActiveChannel(); if (channel) { - const channelDescriptor = that.outputService.getChannelDescriptor(channel.id); - if (channelDescriptor?.log && channelDescriptor.files?.length === 1) { - const logLevel = accessor.get(ILoggerService).getLogLevel(channelDescriptor.files[0]); - return await accessor.get(IDefaultLogLevelsService).setDefaultLogLevel(logLevel, channelDescriptor.extensionId); + const channelDescriptor = outputService.getChannelDescriptor(channel.id); + if (channelDescriptor && isSingleSourceOutputChannelDescriptor(channelDescriptor)) { + const logLevel = loggerService.getLogLevel(channelDescriptor.source.resource); + return await defaultLogLevelsService.setDefaultLogLevel(logLevel, channelDescriptor.extensionId); } } } @@ -562,14 +567,11 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { } private registerOpenLogFileAction(): void { - interface IOutputChannelQuickPickItem extends IQuickPickItem { - channel: IOutputChannelDescriptor; - } this._register(registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.openLogFile', - title: nls.localize2('openLogFile', "Open Log File..."), + title: nls.localize2('openLogFile', "Open Log..."), category: Categories.Developer, menu: { id: MenuId.CommandPalette, @@ -590,15 +592,13 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { const outputService = accessor.get(IOutputService); const quickInputService = accessor.get(IQuickInputService); const editorService = accessor.get(IEditorService); - const fileConfigurationService = accessor.get(IFilesConfigurationService); - - let entry: IOutputChannelQuickPickItem | undefined; + let entry: IQuickPickItem | undefined; const argName = args && typeof args === 'string' ? args : undefined; - const extensionChannels: IOutputChannelQuickPickItem[] = []; - const coreChannels: IOutputChannelQuickPickItem[] = []; + const extensionChannels: IQuickPickItem[] = []; + const coreChannels: IQuickPickItem[] = []; for (const c of outputService.getChannelDescriptors()) { - if (c.files?.length === 1 && c.log) { - const e = { id: c.id, label: c.label, channel: c }; + if (c.log) { + const e = { id: c.id, label: c.label }; if (c.extensionId) { extensionChannels.push(e); } else { @@ -615,23 +615,24 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { entries.push({ type: 'separator' }); entries.push(...coreChannels.sort((a, b) => a.label.localeCompare(b.label))); } - entry = await quickInputService.pick(entries, { placeHolder: nls.localize('selectlogFile', "Select Log File") }); + entry = await quickInputService.pick(entries, { placeHolder: nls.localize('selectlogFile', "Select Log File") }); } - if (entry) { - const resource = assertIsDefined(entry.channel.files?.[0]); - await fileConfigurationService.updateReadonly(resource, true); - await editorService.openEditor({ - resource, - options: { - pinned: true, - } - }); + if (entry?.id) { + const channel = outputService.getChannel(entry.id); + if (channel) { + await editorService.openEditor({ + resource: channel.uri, + options: { + pinned: true, + } + }); + } } } })); } - private registerFilterActions(): void { + private registerLogLevelFilterActions(): void { let order = 0; const registerLogLevel = (logLevel: LogLevel, toggled: ContextKeyExpression) => { this._register(registerAction2(class extends ViewAction { @@ -645,7 +646,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { toggled, menu: { id: viewFilterSubmenu, - group: '1_filter', + group: '2_log_filter', when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), CONTEXT_ACTIVE_LOG_FILE_OUTPUT), order: order++ }, @@ -682,7 +683,60 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { registerLogLevel(LogLevel.Info, SHOW_INFO_FILTER_CONTEXT); registerLogLevel(LogLevel.Warning, SHOW_WARNING_FILTER_CONTEXT); registerLogLevel(LogLevel.Error, SHOW_ERROR_FILTER_CONTEXT); + } + private registerSourceFilterAction(): void { + const registeredChannels = new DisposableMap(); + this._register(toDisposable(() => dispose(registeredChannels.values()))); + const registerOutputChannels = (channels: IOutputChannelDescriptor[]) => { + for (const channel of channels) { + if (!isMultiSourceOutputChannelDescriptor(channel)) { + continue; + } + const disposables = new DisposableStore(); + registeredChannels.set(channel.id, disposables); + for (const source of channel.source) { + if (!source.name) { + continue; + } + const sourceFilter = `${channel.id}-${source.name}`; + disposables.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.${OUTPUT_VIEW_ID}.toggle.${source.name}`, + title: source.name!, + toggled: ContextKeyExpr.regex(HIDE_SOURCE_FILTER_CONTEXT.key, new RegExp(`.*,${escapeRegExpCharacters(sourceFilter)},.*`)).negate(), + menu: { + id: viewFilterSubmenu, + group: '1_source_filter', + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), ACTIVE_OUTPUT_CHANNEL_CONTEXT.isEqualTo(channel.id)), + } + }); + } + async run(accessor: ServicesAccessor): Promise { + const outputService = accessor.get(IOutputService); + outputService.filters.toggleSource(sourceFilter); + } + })); + } + } + }; + registerOutputChannels(this.outputService.getChannelDescriptors()); + const outputChannelRegistry = Registry.as(Extensions.OutputChannels); + this._register(outputChannelRegistry.onDidRegisterChannel(e => { + const channel = this.outputService.getChannelDescriptor(e); + if (channel) { + registerOutputChannels([channel]); + } + })); + this._register(outputChannelRegistry.onDidUpdateChannelSources(e => { + registeredChannels.deleteAndDispose(e.id); + registerOutputChannels([e]); + })); + this._register(outputChannelRegistry.onDidRemoveChannel(e => registeredChannels.deleteAndDispose(e.id))); + } + + private registerClearFilterActions(): void { this._register(registerAction2(class extends ViewAction { constructor() { super({ @@ -721,18 +775,19 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { async run(accessor: ServicesAccessor): Promise { const outputService = accessor.get(IOutputService); const quickInputService = accessor.get(IQuickInputService); - const extensionLogs: IFileOutputChannelDescriptor[] = [], logs: IFileOutputChannelDescriptor[] = []; + const extensionLogs: IOutputChannelDescriptor[] = [], logs: IOutputChannelDescriptor[] = [], userLogs: IOutputChannelDescriptor[] = []; for (const channel of outputService.getChannelDescriptors()) { - if (channel.log && channel.files?.length === 1) { - const fileChannel = channel; + if (channel.log) { if (channel.extensionId) { - extensionLogs.push(fileChannel); + extensionLogs.push(channel); + } else if (channel.user) { + userLogs.push(channel); } else { - logs.push(fileChannel); + logs.push(channel); } } } - const entries: Array = []; + const entries: Array = []; for (const log of logs.sort((a, b) => a.label.localeCompare(b.label))) { entries.push(log); } @@ -742,6 +797,12 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { for (const log of extensionLogs.sort((a, b) => a.label.localeCompare(b.label))) { entries.push(log); } + if (userLogs.length && (extensionLogs.length || logs.length)) { + entries.push({ type: 'separator', label: nls.localize('userLogs', "User Logs") }); + } + for (const log of userLogs.sort((a, b) => a.label.localeCompare(b.label))) { + entries.push(log); + } const result = await quickInputService.pick(entries, { placeHolder: nls.localize('selectlog', "Select Log"), canPickMany: true }); if (result?.length) { await outputService.saveOutputAs(...result); @@ -788,8 +849,10 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { id: channelId, label: channelName, log: true, - files: result, - fileNames: result.map(r => basename(r).split('.')[0]) + user: true, + source: result.length === 1 + ? { resource: result[0] } + : result.map(resource => ({ resource, name: basename(resource).split('.')[0] })) }); outputService.showChannel(channelId); } diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 37a028ad4c54..f3aaea6abcba 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -6,11 +6,11 @@ import { Event, Emitter } from '../../../../base/common/event.js'; import { Schemas } from '../../../../base/common/network.js'; import { URI } from '../../../../base/common/uri.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; -import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, IOutputViewFilters, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT, IFileOutputChannelDescriptor } from '../../../services/output/common/output.js'; +import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, IOutputViewFilters, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT, IMultiSourceOutputChannelDescriptor, isSingleSourceOutputChannelDescriptor, HIDE_SOURCE_FILTER_CONTEXT, isMultiSourceOutputChannelDescriptor } from '../../../services/output/common/output.js'; import { OutputLinkProvider } from './outputLinkProvider.js'; import { ITextModelService, ITextModelContentProvider } from '../../../../editor/common/services/resolverService.js'; import { ITextModel } from '../../../../editor/common/model.js'; @@ -53,7 +53,7 @@ class OutputChannel extends Disposable implements IOutputChannel { this.id, this.uri, outputChannelDescriptor.languageId ? languageService.createById(outputChannelDescriptor.languageId) : languageService.createByMimeType(outputChannelDescriptor.log ? LOG_MIME : OUTPUT_MIME), - outputChannelDescriptor.files ? outputChannelDescriptor.files.map((file, index) => ({ file, name: outputChannelDescriptor.fileNames?.[index] ?? '' })) : undefined) + outputChannelDescriptor.source) ); } @@ -81,6 +81,7 @@ interface IOutputFilterOptions { info: boolean; warning: boolean; error: boolean; + sources: string; } class OutputViewFilters extends Disposable implements IOutputViewFilters { @@ -99,6 +100,7 @@ class OutputViewFilters extends Disposable implements IOutputViewFilters { this._info.set(options.info); this._warning.set(options.warning); this._error.set(options.error); + this._sources.set(options.sources); this.filterHistory = options.filterHistory; } @@ -170,13 +172,38 @@ class OutputViewFilters extends Disposable implements IOutputViewFilters { this._onDidChange.fire(); } } + + private readonly _sources = HIDE_SOURCE_FILTER_CONTEXT.bindTo(this.contextKeyService); + get sources(): string { + return this._sources.get() || ','; + } + set sources(sources: string) { + this._sources.set(sources); + this._onDidChange.fire(); + } + + toggleSource(source: string): void { + const sources = this.sources; + if (this.hasSource(source)) { + this.sources = sources.replace(`,${source},`, ','); + } else { + this.sources = `${sources}${source},`; + } + } + + hasSource(source: string): boolean { + if (source === ',') { + return false; + } + return this.sources.includes(`,${source},`); + } } export class OutputService extends Disposable implements IOutputService, ITextModelContentProvider { declare readonly _serviceBrand: undefined; - private channels: Map = new Map(); + private readonly channels = this._register(new DisposableMap()); private activeChannelIdInStorage: string; private activeChannel?: OutputChannel; @@ -227,6 +254,8 @@ export class OutputService extends Disposable implements IOutputService, ITextMo this.onDidRegisterChannel(channelIdentifier.id); } this._register(registry.onDidRegisterChannel(id => this.onDidRegisterChannel(id))); + this._register(registry.onDidUpdateChannelSources(channel => this.onDidUpdateChannelSources(channel))); + this._register(registry.onDidRemoveChannel(channel => this.onDidRemoveChannel(channel))); // Set active channel to first channel if not set if (!this.activeChannel) { @@ -256,7 +285,8 @@ export class OutputService extends Disposable implements IOutputService, ITextMo debug: true, info: true, warning: true, - error: true + error: true, + sources: '', }, contextKeyService)); } @@ -296,23 +326,35 @@ export class OutputService extends Disposable implements IOutputService, ITextMo return this.activeChannel; } - registerCompoundLogChannel(channels: IFileOutputChannelDescriptor[]): string { + registerCompoundLogChannel(descriptors: IOutputChannelDescriptor[]): string { const outputChannelRegistry = Registry.as(Extensions.OutputChannels); - channels.sort((a, b) => a.label.localeCompare(b.label)); - const id = channels.map(r => r.id.toLowerCase()).join('-'); + descriptors.sort((a, b) => a.label.localeCompare(b.label)); + const id = descriptors.map(r => r.id.toLowerCase()).join('-'); if (!outputChannelRegistry.getChannel(id)) { outputChannelRegistry.registerChannel({ id, - label: channels.map(r => r.label).join(', '), - log: channels.some(r => r.log), - files: channels.map(r => r.files[0]), - fileNames: channels.map(r => r.label) + label: descriptors.map(r => r.label).join(', '), + log: descriptors.some(r => r.log), + user: true, + source: descriptors.map(descriptor => { + if (isSingleSourceOutputChannelDescriptor(descriptor)) { + return [{ resource: descriptor.source.resource, name: descriptor.source.name ?? descriptor.label }]; + } + if (isMultiSourceOutputChannelDescriptor(descriptor)) { + return descriptor.source; + } + const channel = this.getChannel(descriptor.id); + if (channel) { + return channel.model.source; + } + return []; + }).flat(), }); } return id; } - async saveOutputAs(...channels: IFileOutputChannelDescriptor[]): Promise { + async saveOutputAs(...channels: IOutputChannelDescriptor[]): Promise { let channel: IOutputChannel | undefined; if (channels.length > 1) { const compoundChannelId = this.registerCompoundLogChannel(channels); @@ -367,6 +409,23 @@ export class OutputService extends Disposable implements IOutputService, ITextMo } } + private onDidUpdateChannelSources(channel: IMultiSourceOutputChannelDescriptor): void { + const outputChannel = this.channels.get(channel.id); + if (outputChannel) { + outputChannel.model.updateChannelSources(channel.source); + } + } + + private onDidRemoveChannel(channel: IOutputChannelDescriptor): void { + if (this.activeChannel?.id === channel.id) { + const channels = this.getChannelDescriptors(); + if (channels[0]) { + this.showChannel(channels[0].id); + } + } + this.channels.deleteAndDispose(channel.id); + } + private createChannel(id: string): OutputChannel { const channel = this.instantiateChannel(id); this._register(Event.once(channel.model.onDispose)(() => { @@ -396,14 +455,14 @@ export class OutputService extends Disposable implements IOutputService, ITextMo private setLevelContext(): void { const descriptor = this.activeChannel?.outputChannelDescriptor; - const channelLogLevel = descriptor?.log && descriptor.files?.length === 1 ? this.loggerService.getLogLevel(descriptor.files[0]) : undefined; + const channelLogLevel = descriptor?.log && isSingleSourceOutputChannelDescriptor(descriptor) ? this.loggerService.getLogLevel(descriptor.source.resource) : undefined; this.activeOutputChannelLevelContext.set(channelLogLevel !== undefined ? LogLevelToString(channelLogLevel) : ''); } private async setLevelIsDefaultContext(): Promise { const descriptor = this.activeChannel?.outputChannelDescriptor; - if (descriptor?.log && descriptor.files?.length === 1) { - const channelLogLevel = this.loggerService.getLogLevel(descriptor.files[0]); + if (descriptor?.log && isSingleSourceOutputChannelDescriptor(descriptor)) { + const channelLogLevel = this.loggerService.getLogLevel(descriptor.source.resource); const channelDefaultLogLevel = await this.defaultLogLevelsService.getDefaultLogLevel(descriptor.extensionId); this.activeOutputChannelLevelIsDefaultContext.set(channelDefaultLogLevel === channelLogLevel); } else { @@ -414,7 +473,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo private setActiveChannel(channel: OutputChannel | undefined): void { this.activeChannel = channel; const descriptor = channel?.outputChannelDescriptor; - this.activeFileOutputChannelContext.set(descriptor?.files?.length === 1); + this.activeFileOutputChannelContext.set(!!descriptor && isSingleSourceOutputChannelDescriptor(descriptor)); this.activeLogOutputChannelContext.set(!!descriptor?.log); this.activeOutputChannelLevelSettableContext.set(descriptor !== undefined && SetLogLevelAction.isLevelSettable(descriptor)); this.setLevelIsDefaultContext(); diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 00b34b22af79..d389eb166bdb 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -96,6 +96,7 @@ export class OutputViewPane extends FilterViewPane { filters.info = this.panelState['showInfo'] ?? true; filters.warning = this.panelState['showWarning'] ?? true; filters.error = this.panelState['showError'] ?? true; + filters.sources = this.panelState['sourcesFilter'] ?? ''; this.scrollLockContextKey = CONTEXT_OUTPUT_SCROLL_LOCK.bindTo(this.contextKeyService); @@ -172,6 +173,7 @@ export class OutputViewPane extends FilterViewPane { private setInput(channel: IOutputChannel): void { this.channelId = channel.id; + this.checkMoreFilters(); const input = this.createInput(channel); if (!this.editor.input || !input.matches(this.editor.input)) { @@ -182,9 +184,9 @@ export class OutputViewPane extends FilterViewPane { } - public checkMoreFilters(): void { + private checkMoreFilters(): void { const filters = this.outputService.filters; - this.filterWidget.checkMoreFilters(!filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error); + this.filterWidget.checkMoreFilters(!filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error || (!!this.channelId && filters.sources.includes(this.channelId))); } private clearInput(): void { @@ -205,6 +207,7 @@ export class OutputViewPane extends FilterViewPane { this.panelState['showInfo'] = filters.info; this.panelState['showWarning'] = filters.warning; this.panelState['showError'] = filters.error; + this.panelState['sourcesFilter'] = filters.sources; this.memento.saveMemento(); super.saveState(); @@ -419,14 +422,19 @@ export class FilterController extends Disposable implements IEditorContribution private filterIncremental(model: ITextModel, from: number): void { const filters = this.outputService.filters; + const activeChannelId = this.outputService.getActiveChannel()?.id ?? ''; const findMatchesDecorations: IModelDeltaDecoration[] = []; if (this.logEntries) { const hasLogLevelFilter = !filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error; - if (hasLogLevelFilter || filters.text) { + if (hasLogLevelFilter || filters.text || filters.sources.includes(activeChannelId)) { for (let i = from; i < this.logEntries.length; i++) { const entry = this.logEntries[i]; - if (hasLogLevelFilter && !this.shouldShowEntry(entry, filters)) { + if (hasLogLevelFilter && !this.shouldShowLogLevel(entry, filters)) { + this.hiddenAreas.push(entry.range); + continue; + } + if (!this.shouldShowSource(activeChannelId, entry, filters)) { this.hiddenAreas.push(entry.range); continue; } @@ -465,7 +473,7 @@ export class FilterController extends Disposable implements IEditorContribution } } - private shouldShowEntry(entry: ILogEntry, filters: IOutputViewFilters): boolean { + private shouldShowLogLevel(entry: ILogEntry, filters: IOutputViewFilters): boolean { switch (entry.logLevel) { case LogLevel.Trace: return filters.trace; @@ -480,4 +488,11 @@ export class FilterController extends Disposable implements IEditorContribution } return true; } + + private shouldShowSource(activeChannelId: string, entry: ILogEntry, filters: IOutputViewFilters): boolean { + if (!entry.source) { + return true; + } + return !filters.hasSource(`${activeChannelId}-${entry.source}`); + } } diff --git a/src/vs/workbench/contrib/output/common/outputChannelModel.ts b/src/vs/workbench/contrib/output/common/outputChannelModel.ts index 96a3e02d518d..51ae036df70f 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModel.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModel.ts @@ -13,7 +13,7 @@ import { Promises, ThrottledDelayer } from '../../../../base/common/async.js'; import { FileOperationResult, IFileService, toFileOperationResult } from '../../../../platform/files/common/files.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { ILanguageSelection } from '../../../../editor/common/languages/language.js'; -import { Disposable, toDisposable, IDisposable, MutableDisposable, DisposableStore, combinedDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, toDisposable, IDisposable, MutableDisposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { isNumber } from '../../../../base/common/types.js'; import { EditOperation, ISingleEditOperation } from '../../../../editor/common/core/editOperation.js'; import { Position } from '../../../../editor/common/core/position.js'; @@ -21,30 +21,28 @@ import { Range } from '../../../../editor/common/core/range.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { ILogger, ILoggerService, ILogService } from '../../../../platform/log/common/log.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { ILogEntry, LOG_MIME, logEntryIterator, OutputChannelUpdateMode } from '../../../services/output/common/output.js'; +import { ILogEntry, IOutputContentSource, LOG_MIME, logEntryIterator, OutputChannelUpdateMode } from '../../../services/output/common/output.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { TextModel } from '../../../../editor/common/model/textModel.js'; -import { binarySearch } from '../../../../base/common/arrays.js'; +import { binarySearch, sortedDiff } from '../../../../base/common/arrays.js'; export interface IOutputChannelModel extends IDisposable { readonly onDispose: Event; + readonly source: IOutputContentSource | ReadonlyArray; append(output: string): void; update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void; + updateChannelSources(sources: ReadonlyArray): void; loadModel(): Promise; clear(): void; replace(value: string): void; } -export interface IOutputChannelFileInfo { - readonly name: string; - readonly file: URI; -} - interface IContentProvider { readonly onDidAppend: Event; readonly onDidReset: Event; reset(): void; - watch(): IDisposable; + watch(): void; + unwatch(): void; getContent(): Promise<{ readonly content: string; readonly consume: () => void }>; } @@ -63,18 +61,18 @@ class FileContentProvider extends Disposable implements IContentProvider { private startOffset: number = 0; private endOffset: number = 0; - private readonly file: URI; - private readonly name: string; + readonly resource: URI; + readonly name: string; constructor( - { name, file }: IOutputChannelFileInfo, + { name, resource }: IOutputContentSource, @IFileService private readonly fileService: IFileService, @ILogService private readonly logService: ILogService, ) { super(); - this.name = name; - this.file = file; + this.name = name ?? ''; + this.resource = resource; this.syncDelayer = new ThrottledDelayer(500); this._register(toDisposable(() => this.unwatch())); } @@ -87,20 +85,19 @@ class FileContentProvider extends Disposable implements IContentProvider { this.startOffset = this.endOffset; } - watch(): IDisposable { + watch(): void { if (!this.watching) { - this.logService.trace('Started polling', this.file.toString()); + this.logService.trace('Started polling', this.resource.toString()); this.poll(); this.watching = true; } - return toDisposable(() => this.unwatch()); } - private unwatch(): void { + unwatch(): void { if (this.watching) { this.syncDelayer.cancel(); this.watching = false; - this.logService.trace('Stopped polling', this.file.toString()); + this.logService.trace('Stopped polling', this.resource.toString()); } } @@ -115,7 +112,10 @@ class FileContentProvider extends Disposable implements IContentProvider { private async doWatch(): Promise { try { - const stat = await this.fileService.stat(this.file); + if (!this.fileService.hasProvider(this.resource)) { + return; + } + const stat = await this.fileService.stat(this.resource); if (stat.etag !== this.etag) { this.etag = stat.etag; if (isNumber(stat.size) && this.endOffset > stat.size) { @@ -134,7 +134,14 @@ class FileContentProvider extends Disposable implements IContentProvider { async getContent(): Promise<{ readonly name: string; readonly content: string; readonly consume: () => void }> { try { - const content = await this.fileService.readFile(this.file, { position: this.endOffset }); + if (!this.fileService.hasProvider(this.resource)) { + return { + name: this.name, + content: '', + consume: () => { /* No Op */ } + }; + } + const content = await this.fileService.readFile(this.resource, { position: this.endOffset }); let consumed = false; return { name: this.name, @@ -162,40 +169,90 @@ class FileContentProvider extends Disposable implements IContentProvider { class MultiFileContentProvider extends Disposable implements IContentProvider { - readonly onDidAppend: Event; + private readonly _onDidAppend = this._register(new Emitter()); + readonly onDidAppend = this._onDidAppend.event; readonly onDidReset = Event.None; - private readonly fileOutputs: ReadonlyArray = []; + private readonly fileContentProviderItems: [FileContentProvider, DisposableStore][] = []; + + private watching: boolean = false; constructor( - filesInfos: IOutputChannelFileInfo[], + filesInfos: IOutputContentSource[], @IInstantiationService private readonly instantiationService: IInstantiationService, - @IFileService fileService: IFileService, - @ILogService logService: ILogService, + @IFileService private readonly fileService: IFileService, + @ILogService private readonly logService: ILogService, ) { super(); - this.fileOutputs = filesInfos.map(file => this._register(new FileContentProvider(file, fileService, logService))); - this.onDidAppend = Event.any(...this.fileOutputs.map(output => output.onDidAppend)); + for (const file of filesInfos) { + this.fileContentProviderItems.push(this.createFileContentProvider(file)); + } + this._register(toDisposable(() => { + for (const [, disposables] of this.fileContentProviderItems) { + disposables.dispose(); + } + })); + } + + private createFileContentProvider(file: IOutputContentSource): [FileContentProvider, DisposableStore] { + const disposables = new DisposableStore(); + const fileOutput = disposables.add(new FileContentProvider(file, this.fileService, this.logService)); + disposables.add(fileOutput.onDidAppend(() => this._onDidAppend.fire())); + return [fileOutput, disposables]; + } + + watch(): void { + if (!this.watching) { + this.watching = true; + for (const [output] of this.fileContentProviderItems) { + output.watch(); + } + } } - watch(): IDisposable { - return combinedDisposable(...this.fileOutputs.map(output => output.watch())); + unwatch(): void { + if (this.watching) { + this.watching = false; + for (const [output] of this.fileContentProviderItems) { + output.unwatch(); + } + } + } + + updateFiles(files: IOutputContentSource[]): void { + const wasWatching = this.watching; + if (wasWatching) { + this.unwatch(); + } + + const result = sortedDiff(this.fileContentProviderItems.map(([output]) => output), files, (a, b) => resources.extUri.compare(a.resource, b.resource)); + for (const { start, deleteCount, toInsert } of result) { + const outputs = toInsert.map(file => this.createFileContentProvider(file)); + const outputsToRemove = this.fileContentProviderItems.splice(start, deleteCount, ...outputs); + for (const [, disposables] of outputsToRemove) { + disposables.dispose(); + } + } + + if (wasWatching) { + this.watch(); + } } reset(): void { - for (const output of this.fileOutputs) { + for (const [output] of this.fileContentProviderItems) { output.reset(); } } resetToEnd(): void { - for (const output of this.fileOutputs) { + for (const [output] of this.fileContentProviderItems) { output.resetToEnd(); } } async getContent(): Promise<{ readonly content: string; readonly consume: () => void }> { - const outputs = await Promise.all(this.fileOutputs.map(output => output.getContent())); + const outputs = await Promise.all(this.fileContentProviderItems.map(([output]) => output.getContent())); const content = this.combineLogEntries(outputs); return { content, @@ -298,6 +355,8 @@ export abstract class AbstractFileOutputChannelModel extends Disposable implemen private readonly appendThrottler = this._register(new ThrottledDelayer(300)); private replacePromise: Promise | undefined; + abstract readonly source: IOutputContentSource | ReadonlyArray; + constructor( private readonly modelUri: URI, private readonly language: ILanguageSelection, @@ -318,7 +377,8 @@ export abstract class AbstractFileOutputChannelModel extends Disposable implemen consume(); this.modelDisposable.value.add(this.outputContentProvider.onDidReset(() => this.onDidContentChange(true, true))); this.modelDisposable.value.add(this.outputContentProvider.onDidAppend(() => this.onDidContentChange(false, false))); - this.modelDisposable.value.add(this.outputContentProvider.watch()); + this.outputContentProvider.watch(); + this.modelDisposable.value.add(toDisposable(() => this.outputContentProvider.unwatch())); this.modelDisposable.value.add(this.model.onWillDispose(() => { this.outputContentProvider.reset(); this.modelDisposable.value = undefined; @@ -470,6 +530,7 @@ export abstract class AbstractFileOutputChannelModel extends Disposable implemen abstract clear(): void; abstract update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void; + abstract updateChannelSources(files: IOutputContentSource[]): void; } export class FileOutputChannelModel extends AbstractFileOutputChannelModel implements IOutputChannelModel { @@ -479,13 +540,13 @@ export class FileOutputChannelModel extends AbstractFileOutputChannelModel imple constructor( modelUri: URI, language: ILanguageSelection, - fileInfo: IOutputChannelFileInfo, + readonly source: IOutputContentSource, @IFileService fileService: IFileService, @IModelService modelService: IModelService, @ILogService logService: ILogService, @IEditorWorkerService editorWorkerService: IEditorWorkerService, ) { - const fileOutput = new FileContentProvider(fileInfo, fileService, logService); + const fileOutput = new FileContentProvider(source, fileService, logService); super(modelUri, language, fileOutput, modelService, editorWorkerService); this.fileOutput = this._register(fileOutput); } @@ -508,6 +569,7 @@ export class FileOutputChannelModel extends AbstractFileOutputChannelModel imple }); } + override updateChannelSources(files: IOutputContentSource[]): void { throw new Error('Not supported'); } } export class MultiFileOutputChannelModel extends AbstractFileOutputChannelModel implements IOutputChannelModel { @@ -517,18 +579,28 @@ export class MultiFileOutputChannelModel extends AbstractFileOutputChannelModel constructor( modelUri: URI, language: ILanguageSelection, - filesInfos: IOutputChannelFileInfo[], + readonly source: IOutputContentSource[], @IFileService fileService: IFileService, @IModelService modelService: IModelService, @ILogService logService: ILogService, @IEditorWorkerService editorWorkerService: IEditorWorkerService, @IInstantiationService instantiationService: IInstantiationService, ) { - const multifileOutput = new MultiFileContentProvider(filesInfos, instantiationService, fileService, logService); + const multifileOutput = new MultiFileContentProvider(source, instantiationService, fileService, logService); super(modelUri, language, multifileOutput, modelService, editorWorkerService); this.multifileOutput = this._register(multifileOutput); } + override updateChannelSources(files: IOutputContentSource[]): void { + this.multifileOutput.unwatch(); + this.multifileOutput.updateFiles(files); + this.multifileOutput.reset(); + this.doUpdate(OutputChannelUpdateMode.Replace, true); + if (this.isVisible()) { + this.multifileOutput.watch(); + } + } + override clear(): void { const loadModelPromise: Promise = this.loadModelPromise ? this.loadModelPromise : Promise.resolve(); loadModelPromise.then(() => { @@ -556,7 +628,7 @@ class OutputChannelBackedByFile extends FileOutputChannelModel implements IOutpu @ILogService logService: ILogService, @IEditorWorkerService editorWorkerService: IEditorWorkerService ) { - super(modelUri, language, { file, name: '' }, fileService, modelService, logService, editorWorkerService); + super(modelUri, language, { resource: file, name: '' }, fileService, modelService, logService, editorWorkerService); // Donot rotate to check for the file reset this.logger = loggerService.createLogger(file, { logLevel: 'always', donotRotate: true, donotUseFormatters: true, hidden: true }); @@ -590,21 +662,25 @@ export class DelegatedOutputChannelModel extends Disposable implements IOutputCh readonly onDispose: Event = this._onDispose.event; private readonly outputChannelModel: Promise; + readonly source: IOutputContentSource; constructor( id: string, modelUri: URI, language: ILanguageSelection, - outputDir: Promise, + outputDir: URI, + outputDirCreationPromise: Promise, @IInstantiationService private readonly instantiationService: IInstantiationService, @IFileService private readonly fileService: IFileService, ) { super(); - this.outputChannelModel = this.createOutputChannelModel(id, modelUri, language, outputDir); + this.outputChannelModel = this.createOutputChannelModel(id, modelUri, language, outputDir, outputDirCreationPromise); + const resource = resources.joinPath(outputDir, `${id.replace(/[\\/:\*\?"<>\|]/g, '')}.log`); + this.source = { resource }; } - private async createOutputChannelModel(id: string, modelUri: URI, language: ILanguageSelection, outputDirPromise: Promise): Promise { - const outputDir = await outputDirPromise; + private async createOutputChannelModel(id: string, modelUri: URI, language: ILanguageSelection, outputDir: URI, outputDirPromise: Promise): Promise { + await outputDirPromise; const file = resources.joinPath(outputDir, `${id.replace(/[\\/:\*\?"<>\|]/g, '')}.log`); await this.fileService.createFile(file); const outputChannelModel = this._register(this.instantiationService.createInstance(OutputChannelBackedByFile, id, modelUri, language, file)); @@ -631,4 +707,8 @@ export class DelegatedOutputChannelModel extends Disposable implements IOutputCh replace(value: string): void { this.outputChannelModel.then(outputChannelModel => outputChannelModel.replace(value)); } + + updateChannelSources(files: IOutputContentSource[]): void { + this.outputChannelModel.then(outputChannelModel => outputChannelModel.updateChannelSources(files)); + } } diff --git a/src/vs/workbench/contrib/output/common/outputChannelModelService.ts b/src/vs/workbench/contrib/output/common/outputChannelModelService.ts index f527202b532f..c3388fe8b721 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModelService.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModelService.ts @@ -9,16 +9,17 @@ import { createDecorator, IInstantiationService } from '../../../../platform/ins import { IFileService } from '../../../../platform/files/common/files.js'; import { toLocalISOString } from '../../../../base/common/date.js'; import { joinPath } from '../../../../base/common/resources.js'; -import { DelegatedOutputChannelModel, FileOutputChannelModel, IOutputChannelFileInfo, IOutputChannelModel, MultiFileOutputChannelModel } from './outputChannelModel.js'; +import { DelegatedOutputChannelModel, FileOutputChannelModel, IOutputChannelModel, MultiFileOutputChannelModel } from './outputChannelModel.js'; import { URI } from '../../../../base/common/uri.js'; import { ILanguageSelection } from '../../../../editor/common/languages/language.js'; +import { IOutputContentSource } from '../../../services/output/common/output.js'; export const IOutputChannelModelService = createDecorator('outputChannelModelService'); export interface IOutputChannelModelService { readonly _serviceBrand: undefined; - createOutputChannelModel(id: string, modelUri: URI, language: ILanguageSelection, files?: IOutputChannelFileInfo[]): IOutputChannelModel; + createOutputChannelModel(id: string, modelUri: URI, language: ILanguageSelection, source?: IOutputContentSource | ReadonlyArray): IOutputChannelModel; } @@ -36,17 +37,17 @@ export class OutputChannelModelService implements IOutputChannelModelService { this.outputLocation = joinPath(environmentService.windowLogsPath, `output_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); } - createOutputChannelModel(id: string, modelUri: URI, language: ILanguageSelection, files?: IOutputChannelFileInfo[]): IOutputChannelModel { - return files?.length ? - files.length === 1 ? this.instantiationService.createInstance(FileOutputChannelModel, modelUri, language, files[0]) - : this.instantiationService.createInstance(MultiFileOutputChannelModel, modelUri, language, files) - : this.instantiationService.createInstance(DelegatedOutputChannelModel, id, modelUri, language, this.outputDir); + createOutputChannelModel(id: string, modelUri: URI, language: ILanguageSelection, source?: IOutputContentSource | IOutputContentSource[]): IOutputChannelModel { + return source ? + Array.isArray(source) ? this.instantiationService.createInstance(MultiFileOutputChannelModel, modelUri, language, source) + : this.instantiationService.createInstance(FileOutputChannelModel, modelUri, language, source) + : this.instantiationService.createInstance(DelegatedOutputChannelModel, id, modelUri, language, this.outputLocation, this.outputDirPromise); } - private _outputDir: Promise | null = null; - private get outputDir(): Promise { + private _outputDir: Promise | null = null; + private get outputDirPromise(): Promise { if (!this._outputDir) { - this._outputDir = this.fileService.createFolder(this.outputLocation).then(() => this.outputLocation); + this._outputDir = this.fileService.createFolder(this.outputLocation).then(() => undefined); } return this._outputDir; } diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index 0de5b386b368..3be1c1eb3d49 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -51,6 +51,7 @@ export const SHOW_INFO_FILTER_CONTEXT = new RawContextKey('output.filte export const SHOW_WARNING_FILTER_CONTEXT = new RawContextKey('output.filter.warning', true); export const SHOW_ERROR_FILTER_CONTEXT = new RawContextKey('output.filter.error', true); export const OUTPUT_FILTER_FOCUS_CONTEXT = new RawContextKey('outputFilterFocus', false); +export const HIDE_SOURCE_FILTER_CONTEXT = new RawContextKey('output.filter.sources', ''); export interface IOutputViewFilters { readonly onDidChange: Event; @@ -60,6 +61,9 @@ export interface IOutputViewFilters { info: boolean; warning: boolean; error: boolean; + sources: string; + toggleSource(source: string): void; + hasSource(source: string): boolean; } export const IOutputService = createDecorator('outputService'); @@ -110,12 +114,12 @@ export interface IOutputService { /** * Register a compound log channel with the given channels. */ - registerCompoundLogChannel(channels: IFileOutputChannelDescriptor[]): string; + registerCompoundLogChannel(channels: IOutputChannelDescriptor[]): string; /** * Save the logs to a file. */ - saveOutputAs(...channels: IFileOutputChannelDescriptor[]): Promise; + saveOutputAs(...channels: IOutputChannelDescriptor[]): Promise; } export enum OutputChannelUpdateMode { @@ -177,25 +181,48 @@ export interface IOutputChannelDescriptor { label: string; log: boolean; languageId?: string; - files?: URI[]; - fileNames?: string[]; + source?: IOutputContentSource | ReadonlyArray; extensionId?: string; + user?: boolean; } -export interface IFileOutputChannelDescriptor extends IOutputChannelDescriptor { - files: URI[]; +export interface ISingleSourceOutputChannelDescriptor extends IOutputChannelDescriptor { + source: IOutputContentSource; +} + +export interface IMultiSourceOutputChannelDescriptor extends IOutputChannelDescriptor { + source: ReadonlyArray; +} + +export function isSingleSourceOutputChannelDescriptor(descriptor: IOutputChannelDescriptor): descriptor is ISingleSourceOutputChannelDescriptor { + return !!descriptor.source && !Array.isArray(descriptor.source); +} + +export function isMultiSourceOutputChannelDescriptor(descriptor: IOutputChannelDescriptor): descriptor is IMultiSourceOutputChannelDescriptor { + return Array.isArray(descriptor.source); +} + +export interface IOutputContentSource { + readonly name?: string; + readonly resource: URI; } export interface IOutputChannelRegistry { readonly onDidRegisterChannel: Event; readonly onDidRemoveChannel: Event; + readonly onDidUpdateChannelSources: Event; /** * Make an output channel known to the output world. */ registerChannel(descriptor: IOutputChannelDescriptor): void; + /** + * Update the files for the given output channel. + */ + updateChannelSources(id: string, sources: IOutputContentSource[]): void; + /** * Returns the list of channels known to the output world. */ @@ -221,6 +248,9 @@ class OutputChannelRegistry implements IOutputChannelRegistry { private readonly _onDidRemoveChannel = new Emitter(); readonly onDidRemoveChannel = this._onDidRemoveChannel.event; + private readonly _onDidUpdateChannelFiles = new Emitter(); + readonly onDidUpdateChannelSources = this._onDidUpdateChannelFiles.event; + public registerChannel(descriptor: IOutputChannelDescriptor): void { if (!this.channels.has(descriptor.id)) { this.channels.set(descriptor.id, descriptor); @@ -238,6 +268,14 @@ class OutputChannelRegistry implements IOutputChannelRegistry { return this.channels.get(id); } + public updateChannelSources(id: string, sources: IOutputContentSource[]): void { + const channel = this.channels.get(id); + if (channel && isMultiSourceOutputChannelDescriptor(channel)) { + channel.source = sources; + this._onDidUpdateChannelFiles.fire(channel); + } + } + public removeChannel(id: string): void { const channel = this.channels.get(id); if (channel) { @@ -249,10 +287,11 @@ class OutputChannelRegistry implements IOutputChannelRegistry { Registry.add(Extensions.OutputChannels, new OutputChannelRegistry()); -const LOG_ENTRY_REGEX = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s(?:\[(?!info|trace|debug|error|warning).*?\]\s)?(\[(info|trace|debug|error|warning)\])/; +const LOG_ENTRY_REGEX = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s(?:\[((?!info|trace|debug|error|warning).*?)\]\s)?(\[(info|trace|debug|error|warning)\])/; export interface ILogEntry { readonly timestamp: number; + readonly source?: string; readonly logLevel: LogLevel; readonly timestampRange: Range; readonly range: Range; @@ -296,7 +335,8 @@ export function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntr if (match) { const timestamp = new Date(match[1]).getTime(); const timestampRange = new Range(lineNumber, 1, lineNumber, match[1].length + 1); - const logLevel = parseLogLevel(match[3]); + const source = match[2]; + const logLevel = parseLogLevel(match[4]); const startLine = lineNumber; let endLine = lineNumber; @@ -307,7 +347,7 @@ export function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntr } endLine++; } - return { timestamp, logLevel, range: new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)), timestampRange }; + return { timestamp, logLevel, source, range: new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)), timestampRange }; } return null; } From a74aabd9bb87c33b047c822aa79d265bc5f5543e Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 16 Jan 2025 17:59:21 -0800 Subject: [PATCH 0616/3587] Don't prompt to save files when in agent mode (#238093) --- .../browser/chatEditing/chatEditingSession.ts | 7 ++ .../contrib/chat/browser/chatEditorSaving.ts | 3 +- .../contrib/chat/browser/tools/tools.ts | 68 ++++++++++--------- .../contrib/chat/common/chatEditingService.ts | 1 + 4 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 32afe69b4f0d..27a7bebfb4f1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -160,6 +160,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return Boolean(this._editorPane && this._editorPane.isVisible()); } + private _isToolsAgentSession = false; + get isToolsAgentSession(): boolean { + return this._isToolsAgentSession; + } + constructor( public readonly chatSessionId: string, private editingSessionFileLimitPromise: Promise, @@ -560,6 +565,8 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return; } + this._isToolsAgentSession = !!responseModel.agent?.isToolsAgent; + // ensure that the edits are processed sequentially this._sequencer.queue(() => this._acceptTextEdits(resource, textEdits, isLastEdits, responseModel)); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts b/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts index 378f23630857..d93c639d9d1b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts @@ -223,7 +223,8 @@ export class ChatEditorSaving extends Disposable implements IWorkbenchContributi } const session = await chatEditingService.getOrRestoreEditingSession(); - if (!session) { + if (!session || session.isToolsAgentSession) { + // For now, don't prompt when in agent mode return; } const entry = session.getEntry(workingCopy.resource); diff --git a/src/vs/workbench/contrib/chat/browser/tools/tools.ts b/src/vs/workbench/contrib/chat/browser/tools/tools.ts index 146fe081abaa..5d8bc3d0f89f 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/tools.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/tools.ts @@ -5,20 +5,20 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { IJSONSchema } from '../../../../../base/common/jsonSchema.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; +import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { IChatEditingService } from '../../common/chatEditingService.js'; import { ChatModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; -import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js'; +import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; export class BuiltinToolsContribution extends Disposable implements IWorkbenchContribution { @@ -31,8 +31,8 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo super(); const editTool = instantiationService.createInstance(EditTool); - this._register(toolsService.registerToolData(editTool)); - this._register(toolsService.registerToolImplementation(editTool.id, editTool)); + this._register(toolsService.registerToolData(editToolData)); + this._register(toolsService.registerToolImplementation(editToolData.id, editTool)); } } @@ -62,39 +62,41 @@ class Person { } `; -class EditTool implements IToolData, IToolImpl { - readonly id = 'vscode_editFile'; - readonly tags = ['vscode_editing']; - readonly displayName = localize('chat.tools.editFile', "Edit File"); - readonly modelDescription = `Edit a file in the workspace. Use this tool once per file that needs to be modified, even if there are multiple changes for a file. Generate the "explanation" property first. ${codeInstructions}`; - readonly inputSchema: IJSONSchema; +const editToolData: IToolData = { + id: 'vscode_editFile', + tags: ['vscode_editing'], + displayName: localize('chat.tools.editFile', "Edit File"), + modelDescription: `Edit a file in the workspace. Use this tool once per file that needs to be modified, even if there are multiple changes for a file. Generate the "explanation" property first. ${codeInstructions}`, + inputSchema: { + type: 'object', + properties: { + explanation: { + type: 'string', + description: 'A short explanation of the edit being made. Can be the same as the explanation you showed to the user.', + }, + filePath: { + type: 'string', + description: 'An absolute path to the file to edit', + }, + code: { + type: 'string', + description: 'The code change to apply to the file. ' + codeInstructions + } + }, + required: ['explanation', 'filePath', 'code'] + } +}; + +class EditTool implements IToolImpl { constructor( @IChatService private readonly chatService: IChatService, @IChatEditingService private readonly chatEditingService: IChatEditingService, @ICodeMapperService private readonly codeMapperService: ICodeMapperService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @ILanguageModelIgnoredFilesService private readonly ignoredFilesService: ILanguageModelIgnoredFilesService - ) { - this.inputSchema = { - type: 'object', - properties: { - explanation: { - type: 'string', - description: 'A short explanation of the edit being made. Can be the same as the explanation you showed to the user.', - }, - filePath: { - type: 'string', - description: 'An absolute path to the file to edit', - }, - code: { - type: 'string', - description: 'The code change to apply to the file. ' + codeInstructions - } - }, - required: ['explanation', 'filePath', 'code'] - }; - } + @ILanguageModelIgnoredFilesService private readonly ignoredFilesService: ILanguageModelIgnoredFilesService, + @ITextFileService private readonly textFileService: ITextFileService, + ) { } async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { if (!invocation.context) { @@ -160,6 +162,8 @@ class EditTool implements IToolData, IToolImpl { }); }); + await this.textFileService.save(uri); + return { content: [{ kind: 'text', value: 'Success' }] }; diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 71a610aacc3f..71f168eb0973 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -83,6 +83,7 @@ export interface IChatEditingSession { readonly entries: IObservable; readonly workingSet: ResourceMap; readonly isVisible: boolean; + readonly isToolsAgentSession: boolean; addFileToWorkingSet(uri: URI, description?: string, kind?: WorkingSetEntryState.Transient | WorkingSetEntryState.Suggested): void; show(): Promise; remove(reason: WorkingSetEntryRemovalReason, ...uris: URI[]): void; From e3c12a8c2bf2d53ac51fe3a61031c1e1b3d4313e Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 17 Jan 2025 01:24:25 -0500 Subject: [PATCH 0617/3587] handle edge case, from more rigorous testing --- .../terminal/common/scripts/shellIntegration-rc.zsh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index 31885e517952..089474c2849c 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -116,14 +116,19 @@ __vsc_update_cwd() { } __vsc_update_env() { - builtin printf '\e]633;EnvStart;%s;\a' $__vsc_nonce + builtin printf '\e]633;EnvSingleStart;%s;\a' $__vsc_nonce for var in ${(k)parameters}; do if printenv "$var" >/dev/null 2>&1; then value=$(builtin printf '%s' "${(P)var}") - builtin printf '\e]633;EnvEntry;%s;%s;%s\a' "$var" "$(__vsc_escape_value "$value")" $__vsc_nonce + # Skip variables with values that start with a hypen + # Things like -q can lead to __vsc_escape_value bad option + if [[ "$value" == -* ]]; then + continue + fi + builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$var" "$(__vsc_escape_value "$value")" $__vsc_nonce fi done - builtin printf '\e]633;EnvEnd;%s;\a' $__vsc_nonce + builtin printf '\e]633;EnvSingleEnd;%s;\a' $__vsc_nonce } __vsc_command_output_start() { From 6a6cfadea2c2cc7c2e960b13139eea0dc409fbfc Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 17 Jan 2025 01:29:03 -0500 Subject: [PATCH 0618/3587] also skip when variable starts with hypen --- .../contrib/terminal/common/scripts/shellIntegration-rc.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index 089474c2849c..013aef5dedf1 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -122,7 +122,7 @@ __vsc_update_env() { value=$(builtin printf '%s' "${(P)var}") # Skip variables with values that start with a hypen # Things like -q can lead to __vsc_escape_value bad option - if [[ "$value" == -* ]]; then + if [[ "$value" == -* || "$var" == -* ]]; then continue fi builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$var" "$(__vsc_escape_value "$value")" $__vsc_nonce From 9013dde56de0ded5e8b6387a0a0e63615efce2d6 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 17 Jan 2025 01:31:21 -0500 Subject: [PATCH 0619/3587] handle edge case better with hypens --- .../contrib/terminal/common/scripts/shellIntegration-rc.zsh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index 013aef5dedf1..737eca2804a7 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -120,9 +120,9 @@ __vsc_update_env() { for var in ${(k)parameters}; do if printenv "$var" >/dev/null 2>&1; then value=$(builtin printf '%s' "${(P)var}") - # Skip variables with values that start with a hypen - # Things like -q can lead to __vsc_escape_value bad option - if [[ "$value" == -* || "$var" == -* ]]; then + # It is not valid to have hypen in variable name + # It is not valid to have hypen to start variable value + if [[ "$value" == -* || "$var" == *-* ]]; then continue fi builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$var" "$(__vsc_escape_value "$value")" $__vsc_nonce From ab0a511b34b571096c0753e1fa3c9bfe4cc3ae83 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 17 Jan 2025 01:48:38 -0500 Subject: [PATCH 0620/3587] remove extra line --- .../src/singlefolder-tests/terminal.shellIntegration.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts index 86fc52cf6653..291d3d354b5d 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.shellIntegration.test.ts @@ -118,7 +118,6 @@ import { assertNoRpc } from '../utils'; }); } - test('execution events should fire in order when a command runs', async () => { const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration(); const events: string[] = []; From 434fed40321aae2bc5a64045d6ef7a2c82a722a2 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 17 Jan 2025 09:47:24 +0100 Subject: [PATCH 0621/3587] remove from i18n --- build/lib/i18n.resources.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index fb732ae1eaf8..2b5107578554 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -58,10 +58,6 @@ "name": "vs/workbench/contrib/commands", "project": "vscode-workbench" }, - { - "name": "vs/workbench/contrib/mappedEdits", - "project": "vscode-workbench" - }, { "name": "vs/workbench/contrib/markdown", "project": "vscode-workbench" From 833bb3e8c96cd4e8c3b6c1a2e3177b5b2384989b Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 17 Jan 2025 09:18:34 +0100 Subject: [PATCH 0622/3587] Show proxy settings in local and remote tabs (microsoft/vscode-copilot-release#3821) --- src/vs/platform/request/common/request.ts | 12 ++++++++++++ .../preferences/browser/settingsTreeModels.ts | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index 64818e059859..af71380553ae 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -138,6 +138,18 @@ export function updateProxyConfigurationsScope(useHostProxy: boolean, useHostPro registerProxyConfigurations(useHostProxy, useHostProxyDefault); } +export const USER_LOCAL_AND_REMOTE_SETTINGS = [ + 'http.proxy', + 'http.proxyStrictSSL', + 'http.proxyKerberosServicePrincipal', + 'http.noProxy', + 'http.proxyAuthorization', + 'http.proxySupport', + 'http.systemCertificates', + 'http.experimental.systemCertificatesV2', + 'http.fetchAdditionalSupport', +]; + let proxyConfiguration: IConfigurationNode[] = []; function registerProxyConfigurations(useHostProxy = true, useHostProxyDefault = true): void { const configurationRegistry = Registry.as(Extensions.Configuration); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index 87ec480c25a2..646164d1c841 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -22,6 +22,7 @@ import { ILanguageService } from '../../../../editor/common/languages/language.j import { Registry } from '../../../../platform/registry/common/platform.js'; import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; +import { USER_LOCAL_AND_REMOTE_SETTINGS } from '../../../../platform/request/common/request.js'; export const ONLINE_SERVICES_SETTING_TAG = 'usesOnlineServices'; @@ -425,12 +426,12 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { } if (configTarget === ConfigurationTarget.USER_REMOTE) { - return REMOTE_MACHINE_SCOPES.includes(this.setting.scope); + return REMOTE_MACHINE_SCOPES.includes(this.setting.scope) || USER_LOCAL_AND_REMOTE_SETTINGS.includes(this.setting.key); } if (configTarget === ConfigurationTarget.USER_LOCAL) { if (isRemote) { - return LOCAL_MACHINE_SCOPES.includes(this.setting.scope); + return LOCAL_MACHINE_SCOPES.includes(this.setting.scope) || USER_LOCAL_AND_REMOTE_SETTINGS.includes(this.setting.key); } } From b7ffd704f7eacd8ce566f6dd8d0292fc87095467 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 17 Jan 2025 10:41:55 +0100 Subject: [PATCH 0623/3587] Disabling edit context for screen reader users (#238112) disabling edit context for screen reader users --- src/vs/editor/browser/view.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 10ecd41bb166..1e883bfcd2b2 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -63,6 +63,7 @@ import { NativeEditContext } from './controller/editContext/native/nativeEditCon import { RulersGpu } from './viewParts/rulersGpu/rulersGpu.js'; import { EditContext } from './controller/editContext/native/editContextFactory.js'; import { GpuMarkOverlay } from './viewParts/gpuMark/gpuMark.js'; +import { AccessibilitySupport } from '../../platform/accessibility/common/accessibility.js'; export interface IContentWidgetData { @@ -101,6 +102,7 @@ export class View extends ViewEventHandler { private readonly _viewController: ViewController; private _experimentalEditContextEnabled: boolean; + private _accessibilitySupport: AccessibilitySupport; private _editContext: AbstractEditContext; private readonly _pointerHandler: PointerHandler; @@ -145,7 +147,8 @@ export class View extends ViewEventHandler { // Keyboard handler this._experimentalEditContextEnabled = this._context.configuration.options.get(EditorOption.experimentalEditContextEnabled); - this._editContext = this._instantiateEditContext(this._experimentalEditContextEnabled); + this._accessibilitySupport = this._context.configuration.options.get(EditorOption.accessibilitySupport); + this._editContext = this._instantiateEditContext(this._experimentalEditContextEnabled, this._accessibilitySupport); this._viewParts.push(this._editContext); @@ -274,10 +277,10 @@ export class View extends ViewEventHandler { this._pointerHandler = this._register(new PointerHandler(this._context, this._viewController, this._createPointerHandlerHelper())); } - private _instantiateEditContext(experimentalEditContextEnabled: boolean): AbstractEditContext { + private _instantiateEditContext(experimentalEditContextEnabled: boolean, accessibilitySupport: AccessibilitySupport): AbstractEditContext { const domNode = dom.getWindow(this._overflowGuardContainer.domNode); const isEditContextSupported = EditContext.supported(domNode); - if (experimentalEditContextEnabled && isEditContextSupported) { + if (experimentalEditContextEnabled && isEditContextSupported && accessibilitySupport !== AccessibilitySupport.Enabled) { return this._instantiationService.createInstance(NativeEditContext, this._ownerID, this._context, this._overflowGuardContainer, this._viewController, this._createTextAreaHandlerHelper()); } else { return this._instantiationService.createInstance(TextAreaEditContext, this._context, this._overflowGuardContainer, this._viewController, this._createTextAreaHandlerHelper()); @@ -286,14 +289,16 @@ export class View extends ViewEventHandler { private _updateEditContext(): void { const experimentalEditContextEnabled = this._context.configuration.options.get(EditorOption.experimentalEditContextEnabled); - if (this._experimentalEditContextEnabled === experimentalEditContextEnabled) { + const accessibilitySupport = this._context.configuration.options.get(EditorOption.accessibilitySupport); + if (this._experimentalEditContextEnabled === experimentalEditContextEnabled && this._accessibilitySupport === accessibilitySupport) { return; } this._experimentalEditContextEnabled = experimentalEditContextEnabled; + this._accessibilitySupport = accessibilitySupport; const isEditContextFocused = this._editContext.isFocused(); const indexOfEditContext = this._viewParts.indexOf(this._editContext); this._editContext.dispose(); - this._editContext = this._instantiateEditContext(experimentalEditContextEnabled); + this._editContext = this._instantiateEditContext(experimentalEditContextEnabled, accessibilitySupport); if (isEditContextFocused) { this._editContext.focus(); } From 0e71d44de075767429bf1c2daeed7445ebf12a6b Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 17 Jan 2025 10:50:55 +0100 Subject: [PATCH 0624/3587] remove mappedEdits.contribution.js reference --- src/vs/workbench/workbench.common.main.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 054f3e75a6d4..90894ea4fac1 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -246,9 +246,6 @@ import './contrib/mergeEditor/browser/mergeEditor.contribution.js'; // Multi Diff Editor import './contrib/multiDiffEditor/browser/multiDiffEditor.contribution.js'; -// Mapped Edits -import './contrib/mappedEdits/common/mappedEdits.contribution.js'; - // Commands import './contrib/commands/common/commands.contribution.js'; From 5e399f306963a724e8913a43bf827be86f08b419 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:56:49 +0100 Subject: [PATCH 0625/3587] GitHub - add support for batch avatar query (#238114) --- extensions/git/src/api/git.d.ts | 8 +- extensions/git/src/blame.ts | 11 ++- .../git/src/historyItemDetailsProvider.ts | 10 +- .../github/src/historyItemDetailsProvider.ts | 98 +++++++++++++------ extensions/github/src/typings/git.d.ts | 8 +- extensions/github/src/util.ts | 14 +++ 6 files changed, 109 insertions(+), 40 deletions(-) diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index c36879fd6fed..81e778dfa3a6 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -326,8 +326,14 @@ export interface BranchProtectionProvider { provideBranchProtection(): BranchProtection[]; } +export interface AvatarQuery { + readonly commit: string; + readonly authorName?: string; + readonly authorEmail?: string; +} + export interface SourceControlHistoryItemDetailsProvider { - provideAvatar(repository: Repository, commit: string, authorName?: string, authorEmail?: string): ProviderResult; + provideAvatar(repository: Repository, query: AvatarQuery[]): ProviderResult>; provideHoverCommands(repository: Repository): ProviderResult; provideMessageLinks(repository: Repository, message: string): ProviderResult; } diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index ac7bd402081c..b407dbfc4e70 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -13,6 +13,7 @@ import { fromGitUri, isGitUri } from './uri'; import { emojify, ensureEmojis } from './emoji'; import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation } from './staging'; import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; +import { AvatarQuery } from './api/git'; function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean { return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive); @@ -217,8 +218,14 @@ export class GitBlameController { commitInformation = await repository.getCommit(blameInformation.hash); // Avatar - commitAvatar = await provideSourceControlHistoryItemAvatar( - this._model, repository, blameInformation.hash, blameInformation.authorName, blameInformation.authorEmail); + const avatarQuery = { + commit: blameInformation.hash, + authorName: blameInformation.authorName, + authorEmail: blameInformation.authorEmail + } satisfies AvatarQuery; + + const avatarResult = await provideSourceControlHistoryItemAvatar(this._model, repository, [avatarQuery]); + commitAvatar = avatarResult?.get(avatarQuery.commit); } catch { } } diff --git a/extensions/git/src/historyItemDetailsProvider.ts b/extensions/git/src/historyItemDetailsProvider.ts index 33b887430f0d..c9928b824b9b 100644 --- a/extensions/git/src/historyItemDetailsProvider.ts +++ b/extensions/git/src/historyItemDetailsProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Command, Disposable } from 'vscode'; -import { SourceControlHistoryItemDetailsProvider } from './api/git'; +import { AvatarQuery, SourceControlHistoryItemDetailsProvider } from './api/git'; import { Repository } from './repository'; import { ApiRepository } from './api/api1'; @@ -16,12 +16,10 @@ export interface ISourceControlHistoryItemDetailsProviderRegistry { export async function provideSourceControlHistoryItemAvatar( registry: ISourceControlHistoryItemDetailsProviderRegistry, repository: Repository, - commit: string, - authorName?: string, - authorEmail?: string -): Promise { + query: AvatarQuery[] +): Promise | undefined> { for (const provider of registry.getSourceControlHistoryItemDetailsProviders()) { - const result = await provider.provideAvatar(new ApiRepository(repository), commit, authorName, authorEmail); + const result = await provider.provideAvatar(new ApiRepository(repository), query); if (result) { return result; diff --git a/extensions/github/src/historyItemDetailsProvider.ts b/extensions/github/src/historyItemDetailsProvider.ts index c3e7899015a5..6c4f5aa8adfb 100644 --- a/extensions/github/src/historyItemDetailsProvider.ts +++ b/extensions/github/src/historyItemDetailsProvider.ts @@ -5,8 +5,8 @@ import { authentication, Command, l10n, LogOutputChannel } from 'vscode'; import { Commit, Repository as GitHubRepository, Maybe } from '@octokit/graphql-schema'; -import { API, Repository, SourceControlHistoryItemDetailsProvider } from './typings/git'; -import { DisposableStore, getRepositoryDefaultRemote, getRepositoryDefaultRemoteUrl, getRepositoryFromUrl, sequentialize } from './util'; +import { API, AvatarQuery, Repository, SourceControlHistoryItemDetailsProvider } from './typings/git'; +import { DisposableStore, getRepositoryDefaultRemote, getRepositoryDefaultRemoteUrl, getRepositoryFromUrl, groupBy, sequentialize } from './util'; import { AuthenticationError, getOctokitGraphql } from './auth'; const AVATAR_SIZE = 20; @@ -62,6 +62,17 @@ interface GitHubUser { readonly avatarUrl: string; } +function compareAvatarQuery(a: AvatarQuery, b: AvatarQuery): number { + // Email + const emailComparison = (a.authorEmail ?? '').localeCompare(b.authorEmail ?? ''); + if (emailComparison !== 0) { + return emailComparison; + } + + // Name + return (a.authorName ?? '').localeCompare(b.authorName ?? ''); +} + export class GitHubSourceControlHistoryItemDetailsProvider implements SourceControlHistoryItemDetailsProvider { private _enabled = true; private readonly _store = new Map(); @@ -77,8 +88,8 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont })); } - async provideAvatar(repository: Repository, commit: string, authorName?: string, authorEmail?: string): Promise { - this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution for ${commit} in ${repository.rootUri.fsPath}.`); + async provideAvatar(repository: Repository, query: AvatarQuery[]): Promise | undefined> { + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution for ${query.length} commit(s) in ${repository.rootUri.fsPath}.`); if (!this._enabled) { this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution is disabled.`); @@ -92,7 +103,8 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont } try { - // Get the first page of the assignable users + // Warm up the in-memory cache with the first page + // (100 users) from this list of assignable users await this._loadAssignableUsers(descriptor); const repositoryStore = this._store.get(this._getRepositoryKey(descriptor)); @@ -100,33 +112,59 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont return undefined; } - // Lookup the user in the cache - const avatarUrl = repositoryStore.users.find( - user => user.email === authorEmail || user.name === authorName)?.avatarUrl; - if (avatarUrl) { - return this._getAvatarUrl(avatarUrl, AVATAR_SIZE); - } + // Group the query by author + const authorQuery = groupBy(query, compareAvatarQuery); - // Check the commit against the list of known commits - // that are known to have incomplte author information - if (repositoryStore.commits.has(commit)) { - return undefined; - } + const results = new Map(); + await Promise.all(authorQuery.map(async q => { + if (q.length === 0) { + return; + } - // Get the commit details - const commitAuthor = await this._getCommitAuthor(descriptor, commit); - if (!commitAuthor) { - // The commit has incomplete author information, - // so we should not try to query the authors details - // again - repositoryStore.commits.add(commit); - return undefined; - } + // Query the in-memory cache for the user + const avatarUrl = repositoryStore.users.find( + user => user.email === q[0].authorEmail || user.name === q[0].authorName)?.avatarUrl; + + // Cache hit + if (avatarUrl) { + // Add avatar for each commit + for (const { commit } of q) { + results.set(commit, this._getAvatarUrl(avatarUrl, AVATAR_SIZE)); + } + return; + } + + // Check if any of the commit are being tracked in the list + // of known commits that have incomplte author information + if (q.some(({ commit }) => repositoryStore.commits.has(commit))) { + for (const { commit } of q) { + results.set(commit, undefined); + } + return; + } + + // Get the commit details + const commitAuthor = await this._getCommitAuthor(descriptor, q[0].commit); + if (!commitAuthor) { + // The commit has incomplete author information, so + // we should not try to query the authors details again + for (const { commit } of q) { + repositoryStore.commits.add(commit); + results.set(commit, undefined); + } + return; + } + + // Save the user to the cache + repositoryStore.users.push(commitAuthor); - // Save the user to the cache - repositoryStore.users.push(commitAuthor); + // Add avatar for each commit + for (const { commit } of q) { + results.set(commit, this._getAvatarUrl(commitAuthor.avatarUrl, AVATAR_SIZE)); + } + })); - return this._getAvatarUrl(commitAuthor.avatarUrl, AVATAR_SIZE); + return results; } catch (err) { // A GitHub authentication session could be missing if the user has not yet // signed in with their GitHub account or they have signed out. Disable the @@ -134,9 +172,9 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont if (err instanceof AuthenticationError) { this._enabled = false; } - } - return undefined; + return undefined; + } } async provideHoverCommands(repository: Repository): Promise { diff --git a/extensions/github/src/typings/git.d.ts b/extensions/github/src/typings/git.d.ts index 59dfa09aa836..7ad60e954001 100644 --- a/extensions/github/src/typings/git.d.ts +++ b/extensions/github/src/typings/git.d.ts @@ -289,8 +289,14 @@ export interface BranchProtectionProvider { provideBranchProtection(): BranchProtection[]; } +export interface AvatarQuery { + readonly commit: string; + readonly authorName?: string; + readonly authorEmail?: string; +} + export interface SourceControlHistoryItemDetailsProvider { - provideAvatar(repository: Repository, commit: string, authorName?: string, authorEmail?: string): Promise; + provideAvatar(repository: Repository, query: AvatarQuery[]): Promise | undefined>; provideHoverCommands(repository: Repository): Promise; provideMessageLinks(repository: Repository, message: string): Promise; } diff --git a/extensions/github/src/util.ts b/extensions/github/src/util.ts index 979b912d7b45..4c8a032405d9 100644 --- a/extensions/github/src/util.ts +++ b/extensions/github/src/util.ts @@ -57,6 +57,20 @@ function _sequentialize(fn: Function, key: string): Function { export const sequentialize = decorate(_sequentialize); +export function groupBy(data: ReadonlyArray, compare: (a: T, b: T) => number): T[][] { + const result: T[][] = []; + let currentGroup: T[] | undefined = undefined; + for (const element of data.slice(0).sort(compare)) { + if (!currentGroup || compare(currentGroup[0], element) !== 0) { + currentGroup = [element]; + result.push(currentGroup); + } else { + currentGroup.push(element); + } + } + return result; +} + export function getRepositoryFromUrl(url: string): { owner: string; repo: string } | undefined { const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(\.git)?$/i.exec(url) || /^git@github\.com:([^/]+)\/([^/]+?)(\.git)?$/i.exec(url); From 2f00c5955f0a8f8aaea5d62453ef0a7c24d9e7ba Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 17 Jan 2025 10:57:33 +0100 Subject: [PATCH 0626/3587] fix edits doesn't reveal change (#238113) scroll immediate because when edits are done because the settings update (restore readonly etc) will also reset the scroll top --- .../chat/browser/chatEditorController.ts | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index 48b5fe9ff4da..66127bd38a7c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -11,7 +11,7 @@ import { themeColorFromId } from '../../../../base/common/themables.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, IViewZone, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; import { LineSource, renderLines, RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; import { diffAddDecoration, diffDeleteDecoration, diffWholeLineAddDecoration } from '../../../../editor/browser/widget/diffEditor/registrations.contribution.js'; -import { EditorOption, IEditorStickyScrollOptions } from '../../../../editor/common/config/editorOptions.js'; +import { EditorOption, IEditorOptions } from '../../../../editor/common/config/editorOptions.js'; import { Range } from '../../../../editor/common/core/range.js'; import { IDocumentDiff } from '../../../../editor/common/diff/documentDiffProvider.js'; import { IEditorContribution, ScrollType } from '../../../../editor/common/editorCommon.js'; @@ -187,7 +187,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut if (!didReval && !diff.identical) { didReval = true; - this.revealNext(); + this._reveal(true, false, ScrollType.Immediate); } } })); @@ -209,16 +209,17 @@ export class ChatEditorController extends Disposable implements IEditorContribut }); - let actualReadonly: boolean | undefined; - let actualDeco: 'off' | 'editable' | 'on' | undefined; - let actualStickyScroll: IEditorStickyScrollOptions | undefined; + let actualOptions: IEditorOptions | undefined; this._register(autorun(r => { const value = shouldBeReadOnly.read(r); if (value) { - actualReadonly ??= this._editor.getOption(EditorOption.readOnly); - actualDeco ??= this._editor.getOption(EditorOption.renderValidationDecorations); - actualStickyScroll ??= this._editor.getOption(EditorOption.stickyScroll); + + actualOptions ??= { + readOnly: this._editor.getOption(EditorOption.readOnly), + renderValidationDecorations: this._editor.getOption(EditorOption.renderValidationDecorations), + stickyScroll: this._editor.getOption(EditorOption.stickyScroll) + }; this._editor.updateOptions({ readOnly: true, @@ -226,15 +227,9 @@ export class ChatEditorController extends Disposable implements IEditorContribut stickyScroll: { enabled: false } }); } else { - if (actualReadonly !== undefined && actualDeco !== undefined && actualStickyScroll !== undefined) { - this._editor.updateOptions({ - readOnly: actualReadonly, - renderValidationDecorations: actualDeco, - stickyScroll: actualStickyScroll - }); - actualReadonly = undefined; - actualDeco = undefined; - actualStickyScroll = undefined; + if (actualOptions !== undefined) { + this._editor.updateOptions(actualOptions); + actualOptions = undefined; } } })); @@ -505,7 +500,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut return this._reveal(false, strict); } - private _reveal(next: boolean, strict: boolean): boolean { + private _reveal(next: boolean, strict: boolean, scrollType = ScrollType.Smooth): boolean { const position = this._editor.getPosition(); if (!position) { this._currentChangeIndex.set(undefined, undefined); @@ -544,7 +539,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut const targetPosition = next ? decorations[target].getStartPosition() : decorations[target].getEndPosition(); this._editor.setPosition(targetPosition); - this._editor.revealPositionInCenter(targetPosition, ScrollType.Smooth); + this._editor.revealPositionInCenter(targetPosition, scrollType); this._editor.focus(); return true; From 30858d2ab1915c98a03766be833d5a8b808da529 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 17 Jan 2025 11:22:52 +0100 Subject: [PATCH 0627/3587] chore - remove arrays#rail, rename tail2 to tail (#238115) --- src/vs/base/browser/ui/grid/grid.ts | 2 +- src/vs/base/browser/ui/grid/gridview.ts | 2 +- src/vs/base/browser/ui/tree/indexTreeModel.ts | 4 ++-- src/vs/base/common/arrays.ts | 16 ++++++++-------- src/vs/base/test/common/event.test.ts | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index 60d79ed61559..cd7aabf6cfba 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IBoundarySashes, Orientation } from '../sash/sash.js'; -import { equals, tail2 as tail } from '../../../common/arrays.js'; +import { equals, tail } from '../../../common/arrays.js'; import { Event } from '../../../common/event.js'; import { Disposable } from '../../../common/lifecycle.js'; import './gridview.css'; diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 3dc5d8f6a561..09452c14992f 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -6,7 +6,7 @@ import { $ } from '../../dom.js'; import { IBoundarySashes, Orientation, Sash } from '../sash/sash.js'; import { DistributeSizing, ISplitViewStyles, IView as ISplitView, LayoutPriority, Sizing, AutoSizing, SplitView } from '../splitview/splitview.js'; -import { equals as arrayEquals, tail2 as tail } from '../../../common/arrays.js'; +import { equals as arrayEquals, tail } from '../../../common/arrays.js'; import { Color } from '../../../common/color.js'; import { Emitter, Event, Relay } from '../../../common/event.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../common/lifecycle.js'; diff --git a/src/vs/base/browser/ui/tree/indexTreeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts index c263eecddf6f..adbab9de3f34 100644 --- a/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -5,7 +5,7 @@ import { IIdentityProvider } from '../list/list.js'; import { ICollapseStateChangeEvent, ITreeElement, ITreeFilter, ITreeFilterDataResult, ITreeListSpliceData, ITreeModel, ITreeModelSpliceEvent, ITreeNode, TreeError, TreeVisibility } from './tree.js'; -import { splice, tail2 } from '../../../common/arrays.js'; +import { splice, tail } from '../../../common/arrays.js'; import { Delayer } from '../../../common/async.js'; import { MicrotaskDelay } from '../../../common/symbols.js'; import { LcsDiff } from '../../../common/diff/diff.js'; @@ -762,7 +762,7 @@ export class IndexTreeModel, TFilterData = voi } else if (location.length === 1) { return []; } else { - return tail2(location)[0]; + return tail(location)[0]; } } diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 9035ffd9f182..dcc6aa5406d6 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -9,15 +9,15 @@ import { CancellationError } from './errors.js'; import { ISplice } from './sequence.js'; /** - * Returns the last element of an array. - * @param array The array. - * @param n Which element from the end (default is zero). + * Returns the last entry and the initial N-1 entries of the array, as a tuple of [rest, last]. + * + * The array must have at least one element. + * + * @param arr The input array + * @returns A tuple of [rest, last] where rest is all but the last element and last is the last element + * @throws Error if the array is empty */ -export function tail(array: ArrayLike, n: number = 0): T | undefined { - return array[array.length - (1 + n)]; -} - -export function tail2(arr: T[]): [T[], T] { +export function tail(arr: T[]): [T[], T] { if (arr.length === 0) { throw new Error('Invalid tail call'); } diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index 9e754ca833b1..3e3986752729 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; import { stub } from 'sinon'; -import { tail2 } from '../../common/arrays.js'; import { DeferredPromise, timeout } from '../../common/async.js'; import { CancellationToken } from '../../common/cancellation.js'; import { errorHandler, setUnexpectedErrorHandler } from '../../common/errors.js'; @@ -14,6 +13,7 @@ import { observableValue, transaction } from '../../common/observable.js'; import { MicrotaskDelay } from '../../common/symbols.js'; import { runWithFakedTimers } from './timeTravelScheduler.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; +import { tail } from '../../common/arrays.js'; namespace Samples { @@ -405,8 +405,8 @@ suite('Event', function () { } assert.deepStrictEqual(allError.length, 5); - const [start, tail] = tail2(allError); - assert.ok(tail instanceof ListenerRefusalError); + const [start, rest] = tail(allError); + assert.ok(rest instanceof ListenerRefusalError); for (const item of start) { assert.ok(item instanceof ListenerLeakError); From b17f0be90ea531633e70f7c14a6689cbd868dd0b Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 17 Jan 2025 11:32:49 +0100 Subject: [PATCH 0628/3587] Give negative tab index to the text-area used for the native edit context (#237880) adding tabindex which is negative to textarea --- .../browser/controller/editContext/native/nativeEditContext.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 06d66e9ce033..472a6420eba7 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -76,6 +76,7 @@ export class NativeEditContext extends AbstractEditContext { this.domNode.setClassName(`native-edit-context`); this.textArea = new FastDomNode(document.createElement('textarea')); this.textArea.setClassName('native-edit-context-textarea'); + this.textArea.setAttribute('tabindex', '-1'); this._updateDomAttributes(); From 71b1efb5f36d6bc3dd9c01f392660cdf69b5be2b Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 17 Jan 2025 12:00:43 +0100 Subject: [PATCH 0629/3587] Separate network logging into different log in window and shared process and use compound log to show network logs in window and shared logs (#238116) --- .../sharedProcess/sharedProcessMain.ts | 8 +++++--- src/vs/platform/request/common/request.ts | 2 +- src/vs/workbench/browser/web.main.ts | 6 +++--- .../output/browser/output.contribution.ts | 10 +++++----- .../output/common/outputChannelModel.ts | 4 ++-- .../services/log/common/logConstants.ts | 4 ++++ .../log/electron-sandbox/logService.ts | 5 ++--- .../workbench/services/output/common/output.ts | 18 ++++++++++-------- .../services/request/browser/requestService.ts | 11 +++++++++-- .../request/electron-sandbox/requestService.ts | 11 +++++++++-- 10 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts index 563f563455a0..98824d792188 100644 --- a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts @@ -43,7 +43,7 @@ import { InstantiationService } from '../../../platform/instantiation/common/ins import { ServiceCollection } from '../../../platform/instantiation/common/serviceCollection.js'; import { ILanguagePackService } from '../../../platform/languagePacks/common/languagePacks.js'; import { NativeLanguagePackService } from '../../../platform/languagePacks/node/languagePacks.js'; -import { ConsoleLogger, ILoggerService, ILogService } from '../../../platform/log/common/log.js'; +import { ConsoleLogger, ILoggerService, ILogService, LoggerGroup } from '../../../platform/log/common/log.js'; import { LoggerChannelClient } from '../../../platform/log/common/logIpc.js'; import product from '../../../platform/product/common/product.js'; import { IProductService } from '../../../platform/product/common/productService.js'; @@ -219,7 +219,8 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { services.set(ILoggerService, loggerService); // Log - const logger = this._register(loggerService.createLogger('sharedprocess', { name: localize('sharedLog', "Shared") })); + const sharedLogGroup: LoggerGroup = { id: 'shared', name: localize('sharedLog', "Shared") }; + const logger = this._register(loggerService.createLogger('sharedprocess', { name: localize('sharedLog', "Shared"), group: sharedLogGroup })); const consoleLogger = this._register(new ConsoleLogger(logger.getLevel())); const logService = this._register(new LogService(logger, [consoleLogger])); services.set(ILogService, logService); @@ -273,7 +274,8 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { ]); // Request - const requestService = new RequestService(configurationService, environmentService, logService); + const networkLogger = this._register(loggerService.createLogger(`network-shared`, { name: localize('networkk', "Network"), group: sharedLogGroup })); + const requestService = new RequestService(configurationService, environmentService, this._register(new LogService(networkLogger))); services.set(IRequestService, requestService); // Checksum diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index af71380553ae..9d2afcf3a5f5 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -75,7 +75,7 @@ export abstract class AbstractRequestService extends Disposable implements IRequ } protected async logAndRequest(options: IRequestOptions, request: () => Promise): Promise { - const prefix = `[network] #${++this.counter}: ${options.url}`; + const prefix = `#${++this.counter}: ${options.url}`; this.logService.trace(`${prefix} - begin`, options.type, new LoggableHeaders(options.headers ?? {})); try { const result = await request(); diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 142cfa90cd19..09bf7f6afac6 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -77,7 +77,7 @@ import { UserDataProfileService } from '../services/userDataProfile/common/userD import { IUserDataProfileService } from '../services/userDataProfile/common/userDataProfile.js'; import { BrowserUserDataProfilesService } from '../../platform/userDataProfile/browser/userDataProfile.js'; import { DeferredPromise, timeout } from '../../base/common/async.js'; -import { windowLogId } from '../services/log/common/logConstants.js'; +import { windowLogGroup, windowLogId } from '../services/log/common/logConstants.js'; import { LogService } from '../../platform/log/common/logService.js'; import { IRemoteSocketFactoryService, RemoteSocketFactoryService } from '../../platform/remote/common/remoteSocketFactoryService.js'; import { BrowserSocketFactory } from '../../platform/remote/browser/browserSocketFactory.js'; @@ -298,7 +298,7 @@ export class BrowserMain extends Disposable { if (environmentService.isExtensionDevelopment && !!environmentService.extensionTestsLocationURI) { otherLoggers.push(new ConsoleLogInAutomationLogger(loggerService.getLogLevel())); } - const logger = loggerService.createLogger(environmentService.logFile, { id: windowLogId, name: localize('rendererLog', "Window") }); + const logger = loggerService.createLogger(environmentService.logFile, { id: windowLogId, name: windowLogGroup.name, group: windowLogGroup }); const logService = new LogService(logger, otherLoggers); serviceCollection.set(ILogService, logService); @@ -397,7 +397,7 @@ export class BrowserMain extends Disposable { this._register(workspaceTrustManagementService.onDidChangeTrust(() => configurationService.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted()))); // Request Service - const requestService = new BrowserRequestService(remoteAgentService, configurationService, logService); + const requestService = new BrowserRequestService(remoteAgentService, configurationService, loggerService); serviceCollection.set(IRequestService, requestService); // Userdata Sync Store Management Service diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 8a56c1677051..c9e1ccb8e35c 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -10,7 +10,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuId, registerAction2, Action2, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { OutputService } from './outputServices.js'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, OUTPUT_FILTER_FOCUS_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT, isSingleSourceOutputChannelDescriptor, ISingleSourceOutputChannelDescriptor, isMultiSourceOutputChannelDescriptor, HIDE_SOURCE_FILTER_CONTEXT } from '../../../services/output/common/output.js'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, OUTPUT_FILTER_FOCUS_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT, isSingleSourceOutputChannelDescriptor, isMultiSourceOutputChannelDescriptor, HIDE_SOURCE_FILTER_CONTEXT } from '../../../services/output/common/output.js'; import { OutputViewPane } from './outputView.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; @@ -203,9 +203,9 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { const outputService = accessor.get(IOutputService); const quickInputService = accessor.get(IQuickInputService); - const extensionLogs: ISingleSourceOutputChannelDescriptor[] = [], logs: ISingleSourceOutputChannelDescriptor[] = []; + const extensionLogs: IOutputChannelDescriptor[] = [], logs: IOutputChannelDescriptor[] = []; for (const channel of outputService.getChannelDescriptors()) { - if (channel.log && isSingleSourceOutputChannelDescriptor(channel)) { + if (channel.log && !channel.user) { if (channel.extensionId) { extensionLogs.push(channel); } else { @@ -213,7 +213,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { } } } - const entries: Array = []; + const entries: Array = []; for (const log of logs.sort((a, b) => a.label.localeCompare(b.label))) { entries.push(log); } @@ -703,7 +703,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { disposables.add(registerAction2(class extends Action2 { constructor() { super({ - id: `workbench.actions.${OUTPUT_VIEW_ID}.toggle.${source.name}`, + id: `workbench.actions.${OUTPUT_VIEW_ID}.toggle.${sourceFilter}`, title: source.name!, toggled: ContextKeyExpr.regex(HIDE_SOURCE_FILTER_CONTEXT.key, new RegExp(`.*,${escapeRegExpCharacters(sourceFilter)},.*`)).negate(), menu: { diff --git a/src/vs/workbench/contrib/output/common/outputChannelModel.ts b/src/vs/workbench/contrib/output/common/outputChannelModel.ts index 51ae036df70f..ed44b86a3c06 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModel.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModel.ts @@ -271,8 +271,8 @@ class MultiFileContentProvider extends Disposable implements IContentProvider { const timestamps: number[] = []; const contents: string[] = []; const process = (model: ITextModel, logEntry: ILogEntry, name: string): [number, string] => { - const lineContent = model.getLineContent(logEntry.range.endLineNumber); - const content = `${lineContent.substring(0, logEntry.timestampRange.endColumn - 1)} [${name}]${lineContent.substring(logEntry.timestampRange.endColumn - 1)}`; + const lineContent = model.getValueInRange(logEntry.range); + const content = name ? `${lineContent.substring(0, logEntry.logLevelRange.endColumn)} [${name}]${lineContent.substring(logEntry.logLevelRange.endColumn)}` : lineContent; return [logEntry.timestamp, content]; }; diff --git a/src/vs/workbench/services/log/common/logConstants.ts b/src/vs/workbench/services/log/common/logConstants.ts index 688a1b2c006a..81ea16db9818 100644 --- a/src/vs/workbench/services/log/common/logConstants.ts +++ b/src/vs/workbench/services/log/common/logConstants.ts @@ -3,5 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from '../../../../nls.js'; +import { LoggerGroup } from '../../../../platform/log/common/log.js'; + export const windowLogId = 'rendererLog'; +export const windowLogGroup: LoggerGroup = { id: windowLogId, name: localize('window', "Window") }; export const showWindowLogActionId = 'workbench.action.showWindowLog'; diff --git a/src/vs/workbench/services/log/electron-sandbox/logService.ts b/src/vs/workbench/services/log/electron-sandbox/logService.ts index 31cd8e8b5b00..34245d85ff09 100644 --- a/src/vs/workbench/services/log/electron-sandbox/logService.ts +++ b/src/vs/workbench/services/log/electron-sandbox/logService.ts @@ -7,8 +7,7 @@ import { ConsoleLogger, ILogger } from '../../../../platform/log/common/log.js'; import { INativeWorkbenchEnvironmentService } from '../../environment/electron-sandbox/environmentService.js'; import { LoggerChannelClient } from '../../../../platform/log/common/logIpc.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { localize } from '../../../../nls.js'; -import { windowLogId } from '../common/logConstants.js'; +import { windowLogGroup, windowLogId } from '../common/logConstants.js'; import { LogService } from '../../../../platform/log/common/logService.js'; export class NativeLogService extends LogService { @@ -17,7 +16,7 @@ export class NativeLogService extends LogService { const disposables = new DisposableStore(); - const fileLogger = disposables.add(loggerService.createLogger(environmentService.logFile, { id: windowLogId, name: localize('rendererLog', "Window") })); + const fileLogger = disposables.add(loggerService.createLogger(environmentService.logFile, { id: windowLogId, name: windowLogGroup.name, group: windowLogGroup })); let consoleLogger: ILogger; if (environmentService.isExtensionDevelopment && !!environmentService.extensionTestsLocationURI) { diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index 3be1c1eb3d49..67781e8ed168 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -287,14 +287,15 @@ class OutputChannelRegistry implements IOutputChannelRegistry { Registry.add(Extensions.OutputChannels, new OutputChannelRegistry()); -const LOG_ENTRY_REGEX = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s(?:\[((?!info|trace|debug|error|warning).*?)\]\s)?(\[(info|trace|debug|error|warning)\])/; +const LOG_ENTRY_REGEX = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s(\[(info|trace|debug|error|warning)\])\s(\[(.*?)\]\s)?/; export interface ILogEntry { + readonly range: Range; readonly timestamp: number; - readonly source?: string; - readonly logLevel: LogLevel; readonly timestampRange: Range; - readonly range: Range; + readonly logLevel: LogLevel; + readonly logLevelRange: Range; + readonly source?: string; } /** @@ -334,9 +335,10 @@ export function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntr const match = LOG_ENTRY_REGEX.exec(lineContent); if (match) { const timestamp = new Date(match[1]).getTime(); - const timestampRange = new Range(lineNumber, 1, lineNumber, match[1].length + 1); - const source = match[2]; - const logLevel = parseLogLevel(match[4]); + const timestampRange = new Range(lineNumber, 1, lineNumber, match[1].length); + const logLevel = parseLogLevel(match[3]); + const logLevelRange = new Range(lineNumber, timestampRange.endColumn + 1, lineNumber, timestampRange.endColumn + 1 + match[2].length); + const source = match[5]; const startLine = lineNumber; let endLine = lineNumber; @@ -347,7 +349,7 @@ export function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntr } endLine++; } - return { timestamp, logLevel, source, range: new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)), timestampRange }; + return { range: new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)), timestamp, timestampRange, logLevel, logLevelRange, source }; } return null; } diff --git a/src/vs/workbench/services/request/browser/requestService.ts b/src/vs/workbench/services/request/browser/requestService.ts index b90bcbdd0729..b7fd8daa9692 100644 --- a/src/vs/workbench/services/request/browser/requestService.ts +++ b/src/vs/workbench/services/request/browser/requestService.ts @@ -12,7 +12,10 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; import { AbstractRequestService, AuthInfo, Credentials, IRequestService } from '../../../../platform/request/common/request.js'; import { request } from '../../../../base/parts/request/common/requestImpl.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; +import { ILoggerService } from '../../../../platform/log/common/log.js'; +import { localize } from '../../../../nls.js'; +import { LogService } from '../../../../platform/log/common/logService.js'; +import { windowLogGroup } from '../../log/common/logConstants.js'; export class BrowserRequestService extends AbstractRequestService implements IRequestService { @@ -21,9 +24,13 @@ export class BrowserRequestService extends AbstractRequestService implements IRe constructor( @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @IConfigurationService private readonly configurationService: IConfigurationService, - @ILogService logService: ILogService, + @ILoggerService loggerService: ILoggerService, ) { + const logger = loggerService.createLogger(`network`, { name: localize('network', "Network"), group: windowLogGroup }); + const logService = new LogService(logger); super(logService); + this._register(logger); + this._register(logService); } async request(options: IRequestOptions, token: CancellationToken): Promise { diff --git a/src/vs/workbench/services/request/electron-sandbox/requestService.ts b/src/vs/workbench/services/request/electron-sandbox/requestService.ts index 421d421e573c..5f6960784eee 100644 --- a/src/vs/workbench/services/request/electron-sandbox/requestService.ts +++ b/src/vs/workbench/services/request/electron-sandbox/requestService.ts @@ -10,7 +10,10 @@ import { INativeHostService } from '../../../../platform/native/common/native.js import { IRequestContext, IRequestOptions } from '../../../../base/parts/request/common/request.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { request } from '../../../../base/parts/request/common/requestImpl.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; +import { ILoggerService } from '../../../../platform/log/common/log.js'; +import { localize } from '../../../../nls.js'; +import { windowLogGroup } from '../../log/common/logConstants.js'; +import { LogService } from '../../../../platform/log/common/logService.js'; export class NativeRequestService extends AbstractRequestService implements IRequestService { @@ -19,9 +22,13 @@ export class NativeRequestService extends AbstractRequestService implements IReq constructor( @INativeHostService private readonly nativeHostService: INativeHostService, @IConfigurationService private readonly configurationService: IConfigurationService, - @ILogService logService: ILogService, + @ILoggerService loggerService: ILoggerService, ) { + const logger = loggerService.createLogger(`network`, { name: localize('network', "Network"), group: windowLogGroup }); + const logService = new LogService(logger); super(logService); + this._register(logger); + this._register(logService); } async request(options: IRequestOptions, token: CancellationToken): Promise { From a0af0335c87c318691199d21a9c01df5aef177a6 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 17 Jan 2025 12:10:00 +0100 Subject: [PATCH 0630/3587] chore - cleanup (#238118) --- .../contrib/inlineChat/browser/inlineChatController.ts | 2 +- .../contrib/inlineChat/browser/inlineChatStrategies.ts | 4 ---- .../contrib/inlineChat/browser/inlineChatZoneWidget.ts | 2 -- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index a68ca5c47cd6..cbd575f7fde4 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -423,7 +423,7 @@ export class InlineChatController implements IEditorContribution { this._ctxUserDidEdit.set(altVersionNow !== this._editor.getModel()?.getAlternativeVersionId()); } - if (this._session?.hunkData.ignoreTextModelNChanges || this._strategy?.hasFocus()) { + if (this._session?.hunkData.ignoreTextModelNChanges || this._ui.value.widget.hasFocus()) { return; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 2518e2f29305..1c19f39cd028 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -498,10 +498,6 @@ export class LiveStrategy { return renderHunks()?.position; } - hasFocus(): boolean { - return this._zone.widget.hasFocus(); - } - getWholeRangeDecoration(): IModelDeltaDecoration[] { // don't render the blue in live mode return []; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index 783e6f05f592..9d61a980d52a 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -16,7 +16,6 @@ import { Range } from '../../../../editor/common/core/range.js'; import { ScrollType } from '../../../../editor/common/editorCommon.js'; import { IOptions, ZoneWidget } from '../../../../editor/contrib/zoneWidget/browser/zoneWidget.js'; import { localize } from '../../../../nls.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -52,7 +51,6 @@ export class InlineChatZoneWidget extends ZoneWidget { @IInstantiationService private readonly _instaService: IInstantiationService, @ILogService private _logService: ILogService, @IContextKeyService contextKeyService: IContextKeyService, - @IConfigurationService configurationService: IConfigurationService, ) { super(editor, InlineChatZoneWidget._options); From d4f17605c35a6df22dafd13e72eec9d9f0fa854a Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 17 Jan 2025 12:43:10 +0100 Subject: [PATCH 0631/3587] Editor Hover Operation: save options for last operation (#238119) save options for last operation --- src/vs/editor/contrib/hover/browser/hoverOperation.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/editor/contrib/hover/browser/hoverOperation.ts b/src/vs/editor/contrib/hover/browser/hoverOperation.ts index daa3e06cc3bd..acfc9361a028 100644 --- a/src/vs/editor/contrib/hover/browser/hoverOperation.ts +++ b/src/vs/editor/contrib/hover/browser/hoverOperation.ts @@ -108,6 +108,7 @@ export class HoverOperation extends Disposable { } private _setState(state: HoverOperationState, options: TArgs): void { + this._options = options; this._state = state; this._fireResult(options); } From d7f1a3ce1493244a95e8fb4dc086078c7f48d2e7 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 17 Jan 2025 14:33:12 +0100 Subject: [PATCH 0632/3587] Use http.proxy setting (microsoft/vscode#225225) --- .../src/singlefolder-tests/proxy.test.ts | 58 +++++++++---------- .../electron-main/nativeHostMainService.ts | 6 -- 2 files changed, 26 insertions(+), 38 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts index 1537d89dbaa6..99c55cc2e4f1 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts @@ -78,7 +78,7 @@ import assert from 'assert'; } }); - test.skip('basic auth', async () => { + test('basic auth', async () => { const url = 'https://example.com'; // Need to use non-local URL because local URLs are excepted from proxying. const user = 'testuser'; const pass = 'testpassword'; @@ -106,27 +106,19 @@ import assert from 'assert'; await proxyListen; const proxyPort = (sf.server.address() as AddressInfo).port; - for (let i = 0; i < 3; i++) { - await vscode.workspace.getConfiguration().update('integration-test.http.proxy', `PROXY 127.0.0.1:${proxyPort}`, vscode.ConfigurationTarget.Global); - await delay(1000); // Wait for the configuration change to propagate. - try { - await new Promise((resolve, reject) => { - https.get(url, res => { - if (res.statusCode === 418) { - resolve(); - } else { - reject(new Error(`Unexpected status code (expected 418): ${res.statusCode}`)); - } - }) - .on('error', reject); - }); - break; // Exit the loop if the request is successful - } catch (err) { - if (i === 2) { - throw err; // Rethrow the error if it's the last attempt + const change = waitForConfigChange('http.proxy'); + await vscode.workspace.getConfiguration().update('http.proxy', `http://127.0.0.1:${proxyPort}`, vscode.ConfigurationTarget.Global); + await change; + await new Promise((resolve, reject) => { + https.get(url, res => { + if (res.statusCode === 418) { + resolve(); + } else { + reject(new Error(`Unexpected status code (expected 418): ${res.statusCode}`)); } - } - } + }) + .on('error', reject); + }); authEnabled = true; await new Promise((resolve, reject) => { @@ -163,7 +155,9 @@ import assert from 'assert'; } } finally { sf.close(); - await vscode.workspace.getConfiguration().update('integration-test.http.proxy', undefined, vscode.ConfigurationTarget.Global); + const change = waitForConfigChange('http.proxy'); + await vscode.workspace.getConfiguration().update('http.proxy', undefined, vscode.ConfigurationTarget.Global); + await change; await vscode.workspace.getConfiguration().update('integration-test.http.proxyAuth', undefined, vscode.ConfigurationTarget.Global); } }); @@ -212,16 +206,16 @@ import assert from 'assert'; assert.strictEqual(actualRemoteProxy3, ''); assert.strictEqual(actualLocalProxy3, localProxy); assert.strictEqual(actualLocalProxy4, ''); + }); - function waitForConfigChange(key: string) { - return new Promise(resolve => { - const s = vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - s.dispose(); - resolve(); - } - }); + function waitForConfigChange(key: string) { + return new Promise(resolve => { + const s = vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(key)) { + s.dispose(); + resolve(); + } }); - } - }); + }); + } }); diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 030b6a275061..b272b5836187 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -870,12 +870,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain //#region Connectivity async resolveProxy(windowId: number | undefined, url: string): Promise { - if (this.environmentMainService.extensionTestsLocationURI) { - const testProxy = this.configurationService.getValue('integration-test.http.proxy'); - if (testProxy) { - return testProxy; - } - } const window = this.codeWindowById(windowId); const session = window?.win?.webContents?.session; From d2172925d22084b271330700b7660dbdd8968e1a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 17 Jan 2025 05:49:40 -0800 Subject: [PATCH 0633/3587] Support node as a shell type Fixes #238126 --- src/vs/platform/terminal/common/terminal.ts | 3 ++- src/vs/platform/terminal/node/terminalProcess.ts | 1 + src/vs/platform/terminal/node/windowsShellHelper.ts | 3 +++ src/vs/workbench/contrib/terminal/browser/terminalInstance.ts | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 2accdc4b6121..13b92e6b4857 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -150,7 +150,8 @@ export const enum GeneralShellType { PowerShell = 'pwsh', Python = 'python', Julia = 'julia', - NuShell = 'nu' + NuShell = 'nu', + Node = 'node', } export type TerminalShellType = PosixShellType | WindowsShellType | GeneralShellType; diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index a57acc78b17f..1fff426753ac 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -81,6 +81,7 @@ const generalShellTypeMap = new Map([ ['python', GeneralShellType.Python], ['julia', GeneralShellType.Julia], ['nu', GeneralShellType.NuShell], + ['node', GeneralShellType.Node], ]); export class TerminalProcess extends Disposable implements ITerminalChildProcess { diff --git a/src/vs/platform/terminal/node/windowsShellHelper.ts b/src/vs/platform/terminal/node/windowsShellHelper.ts index 15edb8c02cab..ded8e456c751 100644 --- a/src/vs/platform/terminal/node/windowsShellHelper.ts +++ b/src/vs/platform/terminal/node/windowsShellHelper.ts @@ -33,6 +33,7 @@ const SHELL_EXECUTABLES = [ 'sles-12.exe', 'julia.exe', 'nu.exe', + 'node.exe', ]; const SHELL_EXECUTABLE_REGEXES = [ @@ -157,6 +158,8 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe return WindowsShellType.GitBash; case 'julia.exe': return GeneralShellType.Julia; + case 'node.exe': + return GeneralShellType.Node; case 'nu.exe': return GeneralShellType.NuShell; case 'wsl.exe': diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 15054267ff5a..35f316a4bd0c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -2686,6 +2686,7 @@ function guessShellTypeFromExecutable(os: OperatingSystem, executable: string): const exeBasename = path.basename(executable); const generalShellTypeMap: Map = new Map([ [GeneralShellType.Julia, /^julia$/], + [GeneralShellType.Node, /^node$/], [GeneralShellType.NuShell, /^nu$/], [GeneralShellType.PowerShell, /^pwsh(-preview)?|powershell$/], [GeneralShellType.Python, /^py(?:thon)?$/] From 3545d88d12582af195f2b0eaaa1ed981b98b186f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 17 Jan 2025 06:07:35 -0800 Subject: [PATCH 0634/3587] Explain GraphemeContentSegmenter relationship to GraphemeIterator Part of #237897 --- src/vs/editor/browser/gpu/contentSegmenter.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/editor/browser/gpu/contentSegmenter.ts b/src/vs/editor/browser/gpu/contentSegmenter.ts index 455127677dfd..8e916564c413 100644 --- a/src/vs/editor/browser/gpu/contentSegmenter.ts +++ b/src/vs/editor/browser/gpu/contentSegmenter.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type { GraphemeIterator } from '../../../base/common/strings.js'; import type { ViewLineRenderingData } from '../../common/viewModel.js'; export interface IContentSegmenter { @@ -39,6 +40,10 @@ class AsciiContentSegmenter implements IContentSegmenter { } } +/** + * This is a more modern version of {@link GraphemeIterator}, relying on browser APIs instead of a + * manual table approach. + */ class GraphemeContentSegmenter implements IContentSegmenter { private readonly _segments: (Intl.SegmentData | undefined)[] = []; From 9380c77339e02139824018bc2cbb17c18f508a1f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 17 Jan 2025 06:37:22 -0800 Subject: [PATCH 0635/3587] Get proportional fonts working in gpu renderer Fixes #227110 --- src/vs/editor/browser/gpu/contentSegmenter.ts | 8 ++++--- .../browser/gpu/fullFileRenderStrategy.ts | 4 ++-- .../viewParts/viewLinesGpu/viewLinesGpu.ts | 21 ++++++++----------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/vs/editor/browser/gpu/contentSegmenter.ts b/src/vs/editor/browser/gpu/contentSegmenter.ts index 455127677dfd..0de1e9faa0b8 100644 --- a/src/vs/editor/browser/gpu/contentSegmenter.ts +++ b/src/vs/editor/browser/gpu/contentSegmenter.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { safeIntl } from '../../../base/common/date.js'; import type { ViewLineRenderingData } from '../../common/viewModel.js'; +import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js'; export interface IContentSegmenter { /** @@ -16,8 +18,8 @@ export interface IContentSegmenter { getSegmentData(index: number): Intl.SegmentData | undefined; } -export function createContentSegmenter(lineData: ViewLineRenderingData): IContentSegmenter { - if (lineData.isBasicASCII) { +export function createContentSegmenter(lineData: ViewLineRenderingData, options: ViewLineOptions): IContentSegmenter { + if (lineData.isBasicASCII && options.useMonospaceOptimizations) { return new AsciiContentSegmenter(lineData); } return new GraphemeContentSegmenter(lineData); @@ -44,7 +46,7 @@ class GraphemeContentSegmenter implements IContentSegmenter { constructor(lineData: ViewLineRenderingData) { const content = lineData.content; - const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' }); + const segmenter = safeIntl.Segmenter(undefined, { granularity: 'grapheme' }); const segmentedContent = Array.from(segmenter.segment(content)); let segmenterIndex = 0; diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 2690248277c5..d0c86d3da873 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -347,7 +347,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend lineData = viewportData.getViewLineRenderingData(y); tabXOffset = 0; - contentSegmenter = createContentSegmenter(lineData); + contentSegmenter = createContentSegmenter(lineData, viewLineOptions); charWidth = viewLineOptions.spaceWidth * dpr; absoluteOffsetX = 0; @@ -374,7 +374,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } chars = segment; - if (!lineData.isBasicASCII) { + if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) { charWidth = this._glyphRasterizer.value.getTextMetrics(chars).width; } diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index f70c07860e25..62b2ff4e41b1 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -523,8 +523,8 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { const content = lineData.content; let contentSegmenter: IContentSegmenter | undefined; - if (!lineData.isBasicASCII) { - contentSegmenter = createContentSegmenter(lineData); + if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) { + contentSegmenter = createContentSegmenter(lineData, viewLineOptions); } let chars: string | undefined = ''; @@ -532,7 +532,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { let resolvedStartColumn = 0; let resolvedStartCssPixelOffset = 0; for (let x = 0; x < startColumn - 1; x++) { - if (lineData.isBasicASCII) { + if (lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations) { chars = content.charAt(x); } else { chars = contentSegmenter!.getSegmentAtIndex(x); @@ -550,7 +550,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { let resolvedEndColumn = resolvedStartColumn; let resolvedEndCssPixelOffset = 0; for (let x = startColumn - 1; x < endColumn - 1; x++) { - if (lineData.isBasicASCII) { + if (lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations) { chars = content.charAt(x); } else { chars = contentSegmenter!.getSegmentAtIndex(x); @@ -613,7 +613,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { const dpr = getActiveWindow().devicePixelRatio; const mouseContentHorizontalOffsetDevicePixels = mouseContentHorizontalOffset * dpr; const spaceWidthDevicePixels = this._lastViewLineOptions.spaceWidth * dpr; - const contentSegmenter = createContentSegmenter(lineData); + const contentSegmenter = createContentSegmenter(lineData, this._lastViewLineOptions); let widthSoFar = 0; let charWidth = 0; @@ -629,13 +629,6 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { } // Get the width of the character - if (lineData.isBasicASCII) { - charWidth = spaceWidthDevicePixels; - } else { - charWidth = this._renderStrategy.glyphRasterizer.getTextMetrics(chars).width; - } - - // Adjust for tabs if (chars === '\t') { // Find the pixel offset between the current position and the next tab stop const offsetBefore = x + tabXOffset; @@ -643,6 +636,10 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { charWidth = spaceWidthDevicePixels * (tabXOffset - offsetBefore); // Convert back to offset excluding x and the current character tabXOffset -= x + 1; + } else if (lineData.isBasicASCII && this._lastViewLineOptions) { + charWidth = spaceWidthDevicePixels; + } else { + charWidth = this._renderStrategy.glyphRasterizer.getTextMetrics(chars).width; } if (mouseContentHorizontalOffsetDevicePixels < widthSoFar + charWidth / 2) { From 15be96976a70e9ba977188b23e66d475d1dd5539 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 17 Jan 2025 06:45:20 -0800 Subject: [PATCH 0636/3587] Fix mouse target for proportional fonts --- src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index 62b2ff4e41b1..ec749ef2fdb6 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -636,7 +636,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { charWidth = spaceWidthDevicePixels * (tabXOffset - offsetBefore); // Convert back to offset excluding x and the current character tabXOffset -= x + 1; - } else if (lineData.isBasicASCII && this._lastViewLineOptions) { + } else if (lineData.isBasicASCII && this._lastViewLineOptions.useMonospaceOptimizations) { charWidth = spaceWidthDevicePixels; } else { charWidth = this._renderStrategy.glyphRasterizer.getTextMetrics(chars).width; @@ -650,6 +650,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { column++; } + console.log(lineNumber, column); return new Position(lineNumber, column + 1); } } From 97ff3e51ff425a8da52cb88c269f2e59ff2724af Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 17 Jan 2025 16:12:25 +0100 Subject: [PATCH 0637/3587] Prevent NES flickering when switching tab actions (#238133) fixes https://github.com/microsoft/vscode-copilot/issues/11743 --- .../inlineCompletions/browser/view/inlineEdits/view.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 8f8286d43a8d..37aea03afe9c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -81,7 +81,12 @@ export class InlineEditsView extends Disposable { )!; this._previewTextModel.setLanguage(this._editor.getModel()!.getLanguageId()); - this._previewTextModel.setValue(newText); + + const previousNewText = this._previewTextModel.getValue(); + if (previousNewText !== newText) { + // Only update the model if the text has changed to avoid flickering + this._previewTextModel.setValue(newText); + } return { state, From e47951d00e72efa8d3b1a0eddddb02f451daf3d9 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 17 Jan 2025 16:16:59 +0100 Subject: [PATCH 0638/3587] Timeline - expose timeline item to all extensions that have the proposal enabled (#238125) --- src/vs/workbench/api/common/extHostCommands.ts | 6 +++--- src/vs/workbench/api/common/extHostTimeline.ts | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index b1ff98172011..0c170ec48a95 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -28,7 +28,7 @@ import { VSBuffer } from '../../../base/common/buffer.js'; import { SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; import { toErrorMessage } from '../../../base/common/errorMessage.js'; import { StopWatch } from '../../../base/common/stopwatch.js'; -import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; +import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { TelemetryTrustedValue } from '../../../platform/telemetry/common/telemetryUtils.js'; import { IExtHostTelemetry } from './extHostTelemetry.js'; import { generateUuid } from '../../../base/common/uuid.js'; @@ -41,7 +41,7 @@ interface CommandHandler { } export interface ArgumentProcessor { - processArgument(arg: any, extensionId: ExtensionIdentifier | undefined): any; + processArgument(arg: any, extension: IExtensionDescription | undefined): any; } export class ExtHostCommands implements ExtHostCommandsShape { @@ -310,7 +310,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { if (!cmdHandler) { return Promise.reject(new Error(`Contributed command '${id}' does not exist.`)); } else { - args = args.map(arg => this._argumentProcessors.reduce((r, p) => p.processArgument(r, cmdHandler.extension?.identifier), arg)); + args = args.map(arg => this._argumentProcessors.reduce((r, p) => p.processArgument(r, cmdHandler.extension), arg)); return this._executeContributedCommand(id, args, true); } } diff --git a/src/vs/workbench/api/common/extHostTimeline.ts b/src/vs/workbench/api/common/extHostTimeline.ts index 4bfa096c1cee..bd631ddb10bf 100644 --- a/src/vs/workbench/api/common/extHostTimeline.ts +++ b/src/vs/workbench/api/common/extHostTimeline.ts @@ -16,6 +16,7 @@ import { MarkdownString } from './extHostTypeConverters.js'; import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; import { MarshalledId } from '../../../base/common/marshallingIds.js'; import { isString } from '../../../base/common/types.js'; +import { isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; export interface IExtHostTimeline extends ExtHostTimelineShape { readonly _serviceBrand: undefined; @@ -42,7 +43,7 @@ export class ExtHostTimeline implements IExtHostTimeline { commands.registerArgumentProcessor({ processArgument: (arg, extension) => { if (arg && arg.$mid === MarshalledId.TimelineActionContext) { - if (this._providers.get(arg.source) && ExtensionIdentifier.equals(extension, this._providers.get(arg.source)?.extension)) { + if (this._providers.get(arg.source) && extension && isProposedApiEnabled(extension, 'timeline')) { const uri = arg.uri === undefined ? undefined : URI.revive(arg.uri); return this._itemsBySourceAndUriMap.get(arg.source)?.get(getUriKey(uri))?.get(arg.handle); } else { From 862fa30bc68e5056194da44115d12a60708bc0e1 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 17 Jan 2025 16:17:20 +0100 Subject: [PATCH 0639/3587] fix source filters (#238134) --- src/vs/workbench/contrib/output/browser/output.contribution.ts | 2 +- src/vs/workbench/contrib/output/browser/outputView.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index c9e1ccb8e35c..32d2ff9404f9 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -699,7 +699,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { if (!source.name) { continue; } - const sourceFilter = `${channel.id}-${source.name}`; + const sourceFilter = `${channel.id}:${source.name}`; disposables.add(registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index d389eb166bdb..d9bd19f09007 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -186,7 +186,7 @@ export class OutputViewPane extends FilterViewPane { private checkMoreFilters(): void { const filters = this.outputService.filters; - this.filterWidget.checkMoreFilters(!filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error || (!!this.channelId && filters.sources.includes(this.channelId))); + this.filterWidget.checkMoreFilters(!filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error || (!!this.channelId && filters.sources.includes(`,${this.channelId}:`))); } private clearInput(): void { From a97fcaa5b1b45b909544aea9e70b613450f31518 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 17 Jan 2025 16:26:59 +0100 Subject: [PATCH 0640/3587] removing code so that accessibility support is not taken into account --- src/vs/editor/browser/view.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 1e883bfcd2b2..d2c80d112c68 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -280,9 +280,11 @@ export class View extends ViewEventHandler { private _instantiateEditContext(experimentalEditContextEnabled: boolean, accessibilitySupport: AccessibilitySupport): AbstractEditContext { const domNode = dom.getWindow(this._overflowGuardContainer.domNode); const isEditContextSupported = EditContext.supported(domNode); - if (experimentalEditContextEnabled && isEditContextSupported && accessibilitySupport !== AccessibilitySupport.Enabled) { + if (experimentalEditContextEnabled && isEditContextSupported) { + console.log('creating native edit context'); return this._instantiationService.createInstance(NativeEditContext, this._ownerID, this._context, this._overflowGuardContainer, this._viewController, this._createTextAreaHandlerHelper()); } else { + console.log('creating text area edit context'); return this._instantiationService.createInstance(TextAreaEditContext, this._context, this._overflowGuardContainer, this._viewController, this._createTextAreaHandlerHelper()); } } From 8159b3e574bb43fe59ba2938120c8f944d6183a7 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 17 Jan 2025 09:52:23 -0600 Subject: [PATCH 0641/3587] add toggle details focus for terminal suggest widget (#238022) --- .../browser/terminal.suggest.contribution.ts | 29 +++++++++++++++++-- .../suggest/browser/terminalSuggestAddon.ts | 28 +++++++++++++++++- .../suggest/common/terminal.suggest.ts | 6 ++-- .../suggest/browser/simpleSuggestWidget.ts | 3 ++ .../browser/simpleSuggestWidgetDetails.ts | 4 ++- 5 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 0b1215a143fb..a43271a5e609 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -28,6 +28,8 @@ import { SuggestAddon } from './terminalSuggestAddon.js'; import { TerminalClipboardContribution } from '../../clipboard/browser/terminal.clipboard.contribution.js'; import { PwshCompletionProviderAddon } from './pwshCompletionProviderAddon.js'; import { SimpleSuggestContext } from '../../../../services/suggest/browser/simpleSuggestWidget.js'; +import { SuggestDetailsClassName } from '../../../../services/suggest/browser/simpleSuggestWidgetDetails.js'; +import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; registerSingleton(ITerminalCompletionService, TerminalCompletionService, InstantiationType.Delayed); @@ -153,7 +155,16 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo addon.setContainerWithOverflow(dom.findParentWithClass(xterm.element!, 'panel')!); } addon.setScreen(xterm.element!.querySelector('.xterm-screen')!); - this.add(this._ctx.instance.onDidBlur(() => addon.hideSuggestWidget())); + + this.add(dom.addDisposableListener(this._ctx.instance.domElement, dom.EventType.FOCUS_OUT, (e) => { + const focusedElement = e.relatedTarget as HTMLElement; + if (focusedElement.className === SuggestDetailsClassName) { + // Don't hide the suggest widget if the focus is moving to the details + return; + } + addon.hideSuggestWidget(); + })); + this.add(addon.onAcceptedCompletion(async text => { this._ctx.instance.focus(); this._ctx.instance.sendText(text, false); @@ -263,11 +274,25 @@ registerActiveInstanceAction({ run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.toggleExplainMode() }); +registerActiveInstanceAction({ + id: TerminalSuggestCommandId.ToggleDetailsFocus, + title: localize2('workbench.action.terminal.suggestToggleDetailsFocus', 'Suggest Toggle Suggestion Focus'), + f1: false, + // HACK: This does not work with a precondition of `TerminalContextKeys.suggestWidgetVisible`, so make sure to not override the editor's keybinding + precondition: EditorContextKeys.textInputFocus.negate(), + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Space, + mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Space } + }, + run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.toggleSuggestionFocus() +}); + registerActiveInstanceAction({ id: TerminalSuggestCommandId.ToggleDetails, title: localize2('workbench.action.terminal.suggestToggleDetails', 'Suggest Toggle Details'), f1: false, - precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.focus, TerminalContextKeys.isOpen, TerminalContextKeys.suggestWidgetVisible, SimpleSuggestContext.HasFocusedSuggestion), + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.isOpen, TerminalContextKeys.focus, TerminalContextKeys.suggestWidgetVisible, SimpleSuggestContext.HasFocusedSuggestion), keybinding: { // HACK: Force weight to be higher than that to start terminal chat weight: KeybindingWeight.ExternalExtension + 2, diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 6ed3e16864e1..c0cb3ab00f16 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -230,6 +230,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._suggestWidget?.toggleExplainMode(); } + toggleSuggestionFocus(): void { + this._suggestWidget?.toggleDetailsFocus(); + } + toggleSuggestionDetails(): void { this._suggestWidget?.toggleDetails(); } @@ -373,6 +377,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } const suggestWidget = this._ensureSuggestWidget(this._terminal); suggestWidget.setCompletionModel(model); + this._register(suggestWidget.onDidFocus(() => this._terminal?.focus())); if (!this._promptInputModel || !explicitlyInvoked && model.items.length === 0) { return; } @@ -413,8 +418,29 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest listInactiveFocusOutline: activeContrastBorder })); this._register(this._suggestWidget.onDidSelect(async e => this.acceptSelectedSuggestion(e))); - this._register(this._suggestWidget.onDidHide(() => this._terminalSuggestWidgetVisibleContextKey.set(false))); + this._register(this._suggestWidget.onDidHide(() => this._terminalSuggestWidgetVisibleContextKey.reset())); this._register(this._suggestWidget.onDidShow(() => this._terminalSuggestWidgetVisibleContextKey.set(true))); + + const element = this._terminal?.element?.querySelector('.xterm-helper-textarea'); + if (element) { + this._register(dom.addDisposableListener(dom.getActiveDocument(), 'click', (event) => { + const target = event.target as HTMLElement; + if (this._terminal?.element?.contains(target)) { + this._suggestWidget?.hide(); + } + })); + } + + this._register(this._suggestWidget.onDidBlurDetails((e) => { + const elt = e.relatedTarget as HTMLElement; + if (this._terminal?.element?.contains(elt)) { + // Do nothing, just the terminal getting focused + // If there was a mouse click, the suggest widget will be + // hidden above + return; + } + this._suggestWidget?.hide(); + })); this._terminalSuggestWidgetVisibleContextKey.set(false); } return this._suggestWidget; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts index bff806680b42..049ce2b29768 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts @@ -14,7 +14,8 @@ export const enum TerminalSuggestCommandId { ClearSuggestCache = 'workbench.action.terminal.clearSuggestCache', RequestCompletions = 'workbench.action.terminal.requestCompletions', ResetWidgetSize = 'workbench.action.terminal.resetSuggestWidgetSize', - ToggleDetails = 'workbench.action.terminal.suggestToggleDetails' + ToggleDetails = 'workbench.action.terminal.suggestToggleDetails', + ToggleDetailsFocus = 'workbench.action.terminal.suggestToggleDetailsFocus', } export const defaultTerminalSuggestCommandsToSkipShell = [ @@ -27,5 +28,6 @@ export const defaultTerminalSuggestCommandsToSkipShell = [ TerminalSuggestCommandId.HideSuggestWidget, TerminalSuggestCommandId.ClearSuggestCache, TerminalSuggestCommandId.RequestCompletions, - TerminalSuggestCommandId.ToggleDetails + TerminalSuggestCommandId.ToggleDetails, + TerminalSuggestCommandId.ToggleDetailsFocus, ]; diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index f397073a7b86..0c925bfd2b30 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -98,6 +98,8 @@ export class SimpleSuggestWidget extends Disposable { readonly onDidShow: Event = this._onDidShow.event; private readonly _onDidFocus = new PauseableEmitter(); readonly onDidFocus: Event = this._onDidFocus.event; + private readonly _onDidBlurDetails = this._register(new Emitter()); + readonly onDidBlurDetails = this._onDidBlurDetails.event; get list(): List { return this._list; } @@ -223,6 +225,7 @@ export class SimpleSuggestWidget extends Disposable { const details: SimpleSuggestDetailsWidget = this._register(instantiationService.createInstance(SimpleSuggestDetailsWidget)); this._register(details.onDidClose(() => this.toggleDetails())); this._details = this._register(new SimpleSuggestDetailsOverlay(details, this._listElement)); + this._register(dom.addDisposableListener(this._details.widget.domNode, 'blur', (e) => this._onDidBlurDetails.fire(e))); if (options.statusBarMenuId) { this._status = this._register(instantiationService.createInstance(SuggestWidgetStatus, this.element.domNode, options.statusBarMenuId)); diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts index 595afceb0430..028169745497 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts @@ -21,6 +21,8 @@ export function canExpandCompletionItem(item: SimpleCompletionItem | undefined): return !!item && Boolean(item.completion.detail && item.completion.detail !== item.completion.label); } +export const SuggestDetailsClassName = 'suggest-details'; + export class SimpleSuggestDetailsWidget { readonly domNode: HTMLDivElement; @@ -158,7 +160,7 @@ export class SimpleSuggestDetailsWidget { this._renderDisposeable.add(renderedContents); } - // this.domNode.classList.toggle('detail-and-doc', !!documentation); + this.domNode.classList.toggle('detail-and-doc', !!detail && !!documentation); this.domNode.style.userSelect = 'text'; this.domNode.tabIndex = -1; From 6579249107664476db242a5b04d029f1de5ddca0 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 17 Jan 2025 17:02:20 +0100 Subject: [PATCH 0642/3587] Improve side-by-side view (#238131) improved side by side view --- .../browser/view/inlineEdits/deletionView.ts | 7 +- .../view/inlineEdits/sideBySideDiff.ts | 144 +++++++++++------- .../browser/view/inlineEdits/utils.ts | 50 +++--- .../browser/view/inlineEdits/view.css | 4 + 4 files changed, 126 insertions(+), 79 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts index 8150d454061a..390b07779c24 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts @@ -138,14 +138,13 @@ export class InlineEditsDeletionView extends Disposable { const rectangleOverlay = createRectangle( { topLeft: layoutInfo.codeStart1, - topRight: layoutInfo.code1.deltaX(1), - bottomLeft: layoutInfo.codeStart2.deltaY(1), - bottomRight: layoutInfo.code2.deltaX(1).deltaY(1), + width: layoutInfo.code1.x - layoutInfo.codeStart1.x + 1, + height: layoutInfo.code2.y - layoutInfo.code1.y + 1, }, layoutInfo.padding, layoutInfo.borderRadius, { hideLeft: layoutInfo.horizontalScrollOffset !== 0 } - ); + ).build(); return [ n.svgElem('path', { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index 314bc2d8f144..5d34efa6ef8d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -2,13 +2,13 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getWindow } from '../../../../../../base/browser/dom.js'; +import { $, getWindow } from '../../../../../../base/browser/dom.js'; import { ActionViewItem } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IAction } from '../../../../../../base/common/actions.js'; import { Color } from '../../../../../../base/common/color.js'; import { structuralEquals } from '../../../../../../base/common/equals.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { IObservable, autorun, constObservable, derived, derivedObservableWithCache, derivedOpts, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; +import { IObservable, autorun, constObservable, derived, derivedObservableWithCache, derivedOpts, observableFromEvent } from '../../../../../../base/common/observable.js'; import { MenuId, MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; @@ -30,7 +30,7 @@ import { ITextModel } from '../../../../../common/model.js'; import { StickyScrollController } from '../../../../stickyScroll/browser/stickyScrollController.js'; import { InlineCompletionContextKeys } from '../../controller/inlineCompletionContextKeys.js'; import { CustomizedMenuWorkbenchToolBar } from '../../hintsWidget/inlineCompletionsHintsWidget.js'; -import { PathBuilder, StatusBarViewItem, getOffsetForPos, mapOutFalsy, maxContentWidthInRange, n } from './utils.js'; +import { PathBuilder, StatusBarViewItem, createRectangle, getOffsetForPos, mapOutFalsy, maxContentWidthInRange, n } from './utils.js'; import { InlineEditWithChanges } from './viewAndDiffProducer.js'; import { localize } from '../../../../../../nls.js'; @@ -159,13 +159,13 @@ export class InlineEditsSideBySideDiff extends Disposable { return; } - const topEdit = layoutInfo.edit1; - const bottomEdit = layoutInfo.edit2; + const editorTopLeft = layoutInfo.editStart1.deltaY(layoutInfo.padding); + const editorBottomLeft = layoutInfo.editStart2.deltaY(-layoutInfo.padding); - this.previewEditor.layout({ height: bottomEdit.y - topEdit.y, width: layoutInfo.previewEditorWidth }); - this.previewEditor.updateOptions({ padding: { top: layoutInfo.padding, bottom: layoutInfo.padding } }); - this._editorContainer.element.style.top = `${topEdit.y}px`; - this._editorContainer.element.style.left = `${topEdit.x}px`; + this.previewEditor.layout({ height: editorBottomLeft.y - editorTopLeft.y, width: layoutInfo.previewEditorWidth + 15 /* Make sure editor does not scroll horizontally */ }); + this._editorContainer.element.style.top = `${editorTopLeft.y}px`; + this._editorContainer.element.style.left = `${editorTopLeft.x}px`; + this._editorContainer.element.style.width = `${layoutInfo.previewEditorWidth}px`; // Set width to clip view zone })); /*const toolbarDropdownVisible = observableFromEvent(this, this._toolbar.onDidChangeDropdownVisibility, (e) => e ?? false); @@ -182,8 +182,6 @@ export class InlineEditsSideBySideDiff extends Disposable { this._previewEditorObs.editor.setScrollLeft(layoutInfo.desiredPreviewEditorScrollLeft); })); - - this._editorContainerTopLeft.set(this._previewEditorLayoutInfo.map(i => i?.edit1), undefined); } private readonly _display = derived(this, reader => !!this._uiState.read(reader) ? 'block' : 'none'); @@ -191,11 +189,9 @@ export class InlineEditsSideBySideDiff extends Disposable { private readonly previewRef = n.ref(); private readonly toolbarRef = n.ref(); - private readonly _editorContainerTopLeft = observableValue | undefined>(this, undefined); - private readonly _editorContainer = n.div({ class: ['editorContainer', this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !v.edits.experimental.useGutterIndicator && 'showHover')], - style: { position: 'absolute' }, + style: { position: 'absolute', overflow: 'hidden' }, }, [ n.div({ class: 'preview', style: {}, ref: this.previewRef }), n.div({ class: 'toolbar', style: {}, ref: this.toolbarRef }), @@ -299,6 +295,7 @@ export class InlineEditsSideBySideDiff extends Disposable { private readonly _previewEditorObs = observableCodeEditor(this.previewEditor); + private _activeViewZones: string[] = []; private readonly _updatePreviewEditor = derived(reader => { this._editorContainer.readEffect(reader); @@ -331,6 +328,24 @@ export class InlineEditsSideBySideDiff extends Disposable { this.previewEditor.setHiddenAreas(hiddenAreas, undefined, true); + // TODO: is this the proper way to handle viewzones? + const previousViewZones = [...this._activeViewZones]; + this._activeViewZones = []; + + const reducedLinesCount = (range.endLineNumberExclusive - range.startLineNumber) - uiState.newTextLineCount; + this.previewEditor.changeViewZones((changeAccessor) => { + previousViewZones.forEach(id => changeAccessor.removeZone(id)); + + if (reducedLinesCount > 0) { + this._activeViewZones.push(changeAccessor.addZone({ + afterLineNumber: range.startLineNumber + uiState.newTextLineCount - 1, + heightInLines: reducedLinesCount, + showInHiddenAreas: true, + domNode: $('div.diagonal-fill.inline-edits-view-zone'), + })); + } + }); + }).recomputeInitiallyAndOnChange(this._store); private readonly _previewEditorWidth = derived(this, reader => { @@ -338,7 +353,7 @@ export class InlineEditsSideBySideDiff extends Disposable { if (!edit) { return 0; } this._updatePreviewEditor.read(reader); - return maxContentWidthInRange(this._previewEditorObs, edit.modifiedLineRange, reader) + 10; + return maxContentWidthInRange(this._previewEditorObs, edit.modifiedLineRange, reader); }); private readonly _cursorPosIfTouchesEdit = derived(this, reader => { @@ -409,7 +424,7 @@ export class InlineEditsSideBySideDiff extends Disposable { const cursorPos = this._cursorPosIfTouchesEdit.read(reader); const maxPreviewEditorLeft = Math.max( - // We're starting from the content area right and moving it left by IN_EDITOR_DISPLACEMENT and also by an ammount to ensure some mimum desired width + // We're starting from the content area right and moving it left by IN_EDITOR_DISPLACEMENT and also by an amount to ensure some minimum desired width editorContentAreaWidth + horizontalScrollOffset - IN_EDITOR_DISPLACEMENT - Math.max(0, desiredMinimumWidth - maximumAvailableWidth), // But we don't want that the moving left ends up covering the cursor, so this will push it to the right again Math.min( @@ -438,27 +453,50 @@ export class InlineEditsSideBySideDiff extends Disposable { const codeLeft = editorLayout.contentLeft; - const code1 = new Point(left, selectionTop); - const codeStart1 = new Point(codeLeft, selectionTop); - const code2 = new Point(left, selectionBottom); - const codeStart2 = new Point(codeLeft, selectionBottom); + let code1 = new Point(left, selectionTop); + let codeStart1 = new Point(codeLeft, selectionTop); + let code2 = new Point(left, selectionBottom); + let codeStart2 = new Point(codeLeft, selectionBottom); + + const editHeight = this._editor.getOption(EditorOption.lineHeight) * inlineEdit.modifiedLineRange.length; const codeHeight = selectionBottom - selectionTop; + const previewEditorHeight = Math.max(codeHeight, editHeight); - const codeEditDistRange = inlineEdit.modifiedLineRange.length === inlineEdit.originalLineRange.length + const editIsSameHeight = codeHeight === previewEditorHeight; + const codeEditDistRange = editIsSameHeight ? new OffsetRange(4, 61) : new OffsetRange(60, 61); const clipped = dist === 0; + const PADDING = 4; - const codeEditDist = codeEditDistRange.clip(dist); - const editHeight = this._editor.getOption(EditorOption.lineHeight) * inlineEdit.modifiedLineRange.length; + const codeEditDist = editIsSameHeight ? PADDING : codeEditDistRange.clip(dist); // TODO: Is there a better way to specify the distance? const previewEditorWidth = Math.min(previewContentWidth, remainingWidthRightOfEditor + editorLayout.width - editorLayout.contentLeft - codeEditDist); - const PADDING = 4; - - const edit1 = new Point(left + codeEditDist, selectionTop); - const edit2 = new Point(left + codeEditDist, selectionTop + editHeight + PADDING * 2); + let editStart1 = new Point(left + codeEditDist, selectionTop); + let edit1 = editStart1.deltaX(previewEditorWidth); + let editStart2 = new Point(left + codeEditDist, selectionTop + previewEditorHeight); + let edit2 = editStart2.deltaX(previewEditorWidth); + + // padding + const isInsertion = codeHeight === 0; + if (!isInsertion) { + codeStart1 = codeStart1.deltaY(-PADDING).deltaX(-PADDING); + code1 = code1.deltaY(-PADDING); + codeStart2 = codeStart2.deltaY(PADDING).deltaX(-PADDING); + code2 = code2.deltaY(PADDING); + + editStart1 = editStart1.deltaY(-PADDING); + edit1 = edit1.deltaY(-PADDING).deltaX(PADDING); + editStart2 = editStart2.deltaY(PADDING); + edit2 = edit2.deltaY(PADDING).deltaX(PADDING); + } else { + // Align top of edit with insertion line + edit1 = edit1.deltaX(PADDING); + editStart2 = editStart2.deltaY(2 * PADDING); + edit2 = edit2.deltaY(2 * PADDING).deltaX(PADDING); + } return { code1, @@ -466,8 +504,11 @@ export class InlineEditsSideBySideDiff extends Disposable { code2, codeStart2, codeHeight, + codeScrollLeft: horizontalScrollOffset, + editStart1, edit1, + editStart2, edit2, editHeight, maxContentWidth, @@ -503,31 +544,19 @@ export class InlineEditsSideBySideDiff extends Disposable { private readonly _extendedModifiedPath = derived(reader => { const layoutInfo = this._previewEditorLayoutInfo.read(reader); if (!layoutInfo) { return undefined; } - const width = layoutInfo.previewEditorWidth + layoutInfo.padding; - - const topLeft = layoutInfo.edit1; - const topRight = layoutInfo.edit1.deltaX(width); - const topRightBefore = topRight.deltaX(-layoutInfo.borderRadius); - const topRightAfter = topRight.deltaY(layoutInfo.borderRadius); - - const bottomLeft = layoutInfo.edit2; - const bottomRight = bottomLeft.deltaX(width); - const bottomRightBefore = bottomRight.deltaY(-layoutInfo.borderRadius); - const bottomRightAfter = bottomRight.deltaX(-layoutInfo.borderRadius); - - const extendedModifiedPathBuilder = new PathBuilder() - .moveTo(layoutInfo.code1) - .lineTo(topLeft) - .lineTo(topRightBefore) - .curveTo(topRight, topRightAfter) - .lineTo(bottomRightBefore) - .curveTo(bottomRight, bottomRightAfter) - .lineTo(bottomLeft); - - if (layoutInfo.edit2.y !== layoutInfo.code2.y) { - extendedModifiedPathBuilder.curveTo2(layoutInfo.edit2.deltaX(-20), layoutInfo.code2.deltaX(20), layoutInfo.code2.deltaX(0)); + + const extendedModifiedPathBuilder = createRectangle( + { topLeft: layoutInfo.editStart1, width: layoutInfo.edit1.x - layoutInfo.editStart1.x, height: layoutInfo.editStart2.y - layoutInfo.editStart1.y }, + 0, + { topLeft: 0, bottomLeft: 0, topRight: layoutInfo.borderRadius, bottomRight: layoutInfo.borderRadius }, + { hideLeft: true } + ); + + if (layoutInfo.editStart2.y !== layoutInfo.code2.y) { + extendedModifiedPathBuilder.moveTo(layoutInfo.editStart2); + extendedModifiedPathBuilder.curveTo2(layoutInfo.editStart2.deltaX(-20), layoutInfo.code2.deltaX(20), layoutInfo.code2.deltaX(0)); } - extendedModifiedPathBuilder.lineTo(layoutInfo.code2); + extendedModifiedPathBuilder.lineTo(layoutInfo.code2).moveTo(layoutInfo.code1).lineTo(layoutInfo.editStart1); return extendedModifiedPathBuilder.build(); }); @@ -601,13 +630,12 @@ export class InlineEditsSideBySideDiff extends Disposable { return [ n.svgElem('path', { class: 'originalOverlay', - d: layoutInfoObs.map(layoutInfo => new PathBuilder() - .moveTo(layoutInfo.code2) - .lineTo(layoutInfo.codeStart2) - .lineTo(layoutInfo.codeStart1) - .lineTo(layoutInfo.code1) - .build() - ), + d: layoutInfoObs.map(layoutInfo => createRectangle( + { topLeft: layoutInfo.codeStart1, width: layoutInfo.code1.x - layoutInfo.codeStart1.x, height: layoutInfo.code2.y - layoutInfo.code1.y }, + 0, + { topLeft: layoutInfo.borderRadius, bottomLeft: layoutInfo.borderRadius, topRight: 0, bottomRight: 0 }, + { hideRight: true, hideLeft: layoutInfo.codeScrollLeft !== 0 } + ).build()), style: { fill: 'var(--vscode-inlineEdit-originalBackground, transparent)', stroke: 'var(--vscode-inlineEdit-originalBorder)', diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts index f9734fa18b63..142938e1ee2b 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts @@ -163,32 +163,48 @@ export class PathBuilder { } } +// Arguments are a bit messy currently, could be improved export function createRectangle( - corners: { topLeft: Point; topRight: Point; bottomLeft: Point; bottomRight: Point }, + layout: { topLeft: Point; width: number; height: number }, padding: number | { top: number; right: number; bottom: number; left: number }, - borderRadius: number, + borderRadius: number | { topLeft: number; topRight: number; bottomLeft: number; bottomRight: number }, options: { hideLeft?: boolean; hideRight?: boolean; hideTop?: boolean; hideBottom?: boolean } = {} -): string { +): PathBuilder { + + const topLeftInner = layout.topLeft; + const topRightInner = topLeftInner.deltaX(layout.width); + const bottomLeftInner = topLeftInner.deltaY(layout.height); + const bottomRightInner = bottomLeftInner.deltaX(layout.width); + + // padding const { top: paddingTop, bottom: paddingBottom, left: paddingLeft, right: paddingRight } = typeof padding === 'number' ? { top: padding, bottom: padding, left: padding, right: padding } : padding; + // corner radius + const { topLeft: radiusTL, topRight: radiusTR, bottomLeft: radiusBL, bottomRight: radiusBR } = typeof borderRadius === 'number' ? + { topLeft: borderRadius, topRight: borderRadius, bottomLeft: borderRadius, bottomRight: borderRadius } : + borderRadius; + + const totalHeight = layout.height + paddingTop + paddingBottom; + const totalWidth = layout.width + paddingLeft + paddingRight; + // The path is drawn from bottom left at the end of the rounded corner in a clockwise direction // Before: before the rounded corner // After: after the rounded corner - const topLeft = corners.topLeft.deltaX(-paddingLeft).deltaY(-paddingTop); - const topRight = corners.topRight.deltaX(paddingRight).deltaY(-paddingTop); - const topLeftBefore = topLeft.deltaY(borderRadius); - const topLeftAfter = topLeft.deltaX(borderRadius); - const topRightBefore = topRight.deltaX(-borderRadius); - const topRightAfter = topRight.deltaY(borderRadius); - - const bottomLeft = corners.bottomLeft.deltaX(-paddingLeft).deltaY(paddingBottom); - const bottomRight = corners.bottomRight.deltaX(paddingRight).deltaY(paddingBottom); - const bottomLeftBefore = bottomLeft.deltaX(borderRadius); - const bottomLeftAfter = bottomLeft.deltaY(-borderRadius); - const bottomRightBefore = bottomRight.deltaY(-borderRadius); - const bottomRightAfter = bottomRight.deltaX(-borderRadius); + const topLeft = topLeftInner.deltaX(-paddingLeft).deltaY(-paddingTop); + const topRight = topRightInner.deltaX(paddingRight).deltaY(-paddingTop); + const topLeftBefore = topLeft.deltaY(Math.min(radiusTL, totalHeight / 2)); + const topLeftAfter = topLeft.deltaX(Math.min(radiusTL, totalWidth / 2)); + const topRightBefore = topRight.deltaX(-Math.min(radiusTR, totalWidth / 2)); + const topRightAfter = topRight.deltaY(Math.min(radiusTR, totalHeight / 2)); + + const bottomLeft = bottomLeftInner.deltaX(-paddingLeft).deltaY(paddingBottom); + const bottomRight = bottomRightInner.deltaX(paddingRight).deltaY(paddingBottom); + const bottomLeftBefore = bottomLeft.deltaX(Math.min(radiusBL, totalWidth / 2)); + const bottomLeftAfter = bottomLeft.deltaY(-Math.min(radiusBL, totalHeight / 2)); + const bottomRightBefore = bottomRight.deltaY(-Math.min(radiusBR, totalHeight / 2)); + const bottomRightAfter = bottomRight.deltaX(-Math.min(radiusBR, totalWidth / 2)); const path = new PathBuilder(); @@ -224,7 +240,7 @@ export function createRectangle( path.curveTo(bottomLeft, bottomLeftAfter); } - return path.build(); + return path; } type Value = T | IObservable; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index 46ef6e893476..6fb2883ec042 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -133,6 +133,10 @@ } } } + + .inline-edits-view-zone.diagonal-fill { + opacity: 0.5; + } } } From 748e34c02b930bd8911c5977dbb6ee84d51e01f3 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 17 Jan 2025 17:09:18 +0100 Subject: [PATCH 0643/3587] Compound view for all telemetry with supporting source filters (#238090) --- .../telemetry/common/telemetryLogAppender.ts | 11 +++++++---- src/vs/platform/telemetry/common/telemetryUtils.ts | 3 +++ .../test/common/telemetryLogAppender.test.ts | 4 ++-- src/vs/workbench/api/common/extHostTelemetry.ts | 12 ++++++++---- .../api/test/browser/extHostTelemetry.test.ts | 6 +++--- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/vs/platform/telemetry/common/telemetryLogAppender.ts b/src/vs/platform/telemetry/common/telemetryLogAppender.ts index f4a194726044..26ba89e7a4ca 100644 --- a/src/vs/platform/telemetry/common/telemetryLogAppender.ts +++ b/src/vs/platform/telemetry/common/telemetryLogAppender.ts @@ -8,7 +8,7 @@ import { localize } from '../../../nls.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; import { ILogService, ILogger, ILoggerService, LogLevel } from '../../log/common/log.js'; import { IProductService } from '../../product/common/productService.js'; -import { ITelemetryAppender, isLoggingOnly, supportsTelemetry, telemetryLogId, validateTelemetryData } from './telemetryUtils.js'; +import { ITelemetryAppender, TelemetryLogGroup, isLoggingOnly, supportsTelemetry, telemetryLogId, validateTelemetryData } from './telemetryUtils.js'; export class TelemetryLogAppender extends Disposable implements ITelemetryAppender { @@ -31,10 +31,13 @@ export class TelemetryLogAppender extends Disposable implements ITelemetryAppend const justLoggingAndNotSending = isLoggingOnly(productService, environmentService); const logSuffix = justLoggingAndNotSending ? ' (Not Sent)' : ''; const isVisible = () => supportsTelemetry(productService, environmentService) && logService.getLevel() === LogLevel.Trace; - this.logger = this._register(loggerService.createLogger(telemetryLogId, { name: localize('telemetryLog', "Telemetry{0}", logSuffix), hidden: !isVisible() })); + this.logger = this._register(loggerService.createLogger(telemetryLogId, + { + name: localize('telemetryLog', "Telemetry{0}", logSuffix), + hidden: !isVisible(), + group: TelemetryLogGroup + })); this._register(logService.onDidChangeLogLevel(() => loggerService.setVisibility(telemetryLogId, isVisible()))); - this.logger.info('Below are logs for every telemetry event sent from VS Code once the log level is set to trace.'); - this.logger.info('==========================================================='); } } diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index 4ec7bb1ec827..d491bcde6557 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -6,8 +6,10 @@ import { cloneAndChange, safeStringify } from '../../../base/common/objects.js'; import { isObject } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; +import { localize } from '../../../nls.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; +import { LoggerGroup } from '../../log/common/log.js'; import { IProductService } from '../../product/common/productService.js'; import { getRemoteName } from '../../remote/common/remoteHosts.js'; import { verifyMicrosoftInternalDomain } from './commonProperties.js'; @@ -56,6 +58,7 @@ export class NullEndpointTelemetryService implements ICustomEndpointTelemetrySer export const telemetryLogId = 'telemetry'; export const extensionTelemetryLogChannelId = 'extensionTelemetryLog'; +export const TelemetryLogGroup: LoggerGroup = { id: 'telemetry', name: localize('telemetryLogName', "Telemetry") }; export interface ITelemetryAppender { log(eventName: string, data: any): void; diff --git a/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts b/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts index eb72191f928c..34b82fb327c4 100644 --- a/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts +++ b/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts @@ -93,7 +93,7 @@ suite('TelemetryLogAdapter', () => { const testInstantiationService = new TestInstantiationService(); const testObject = new TelemetryLogAppender(new NullLogService(), testLoggerService, testInstantiationService.stub(IEnvironmentService, {}), testInstantiationService.stub(IProductService, {})); testObject.log('testEvent', { hello: 'world', isTrue: true, numberBetween1And3: 2 }); - assert.strictEqual(testLoggerService.createLogger().logs.length, 2); + assert.strictEqual(testLoggerService.createLogger().logs.length, 0); testObject.dispose(); testInstantiationService.dispose(); }); @@ -103,7 +103,7 @@ suite('TelemetryLogAdapter', () => { const testInstantiationService = new TestInstantiationService(); const testObject = new TelemetryLogAppender(new NullLogService(), testLoggerService, testInstantiationService.stub(IEnvironmentService, {}), testInstantiationService.stub(IProductService, {})); testObject.log('testEvent', { hello: 'world', isTrue: true, numberBetween1And3: 2 }); - assert.strictEqual(testLoggerService.createLogger().logs[2], 'telemetry/testEvent' + JSON.stringify([{ + assert.strictEqual(testLoggerService.createLogger().logs[0], 'telemetry/testEvent' + JSON.stringify([{ properties: { hello: 'world', }, diff --git a/src/vs/workbench/api/common/extHostTelemetry.ts b/src/vs/workbench/api/common/extHostTelemetry.ts index ffef0eeb5d85..09383009cd86 100644 --- a/src/vs/workbench/api/common/extHostTelemetry.ts +++ b/src/vs/workbench/api/common/extHostTelemetry.ts @@ -13,7 +13,7 @@ import { IExtHostInitDataService } from './extHostInitDataService.js'; import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { UIKind } from '../../services/extensions/common/extensionHostProtocol.js'; import { getRemoteName } from '../../../platform/remote/common/remoteHosts.js'; -import { cleanData, cleanRemoteAuthority, extensionTelemetryLogChannelId } from '../../../platform/telemetry/common/telemetryUtils.js'; +import { cleanData, cleanRemoteAuthority, extensionTelemetryLogChannelId, TelemetryLogGroup } from '../../../platform/telemetry/common/telemetryUtils.js'; import { mixin } from '../../../base/common/objects.js'; import { URI } from '../../../base/common/uri.js'; import { Disposable } from '../../../base/common/lifecycle.js'; @@ -46,15 +46,19 @@ export class ExtHostTelemetry extends Disposable implements ExtHostTelemetryShap super(); this.extHostTelemetryLogFile = URI.revive(this.initData.environment.extensionTelemetryLogResource); this._inLoggingOnlyMode = this.initData.environment.isExtensionTelemetryLoggingOnly; - this._outputLogger = loggerService.createLogger(this.extHostTelemetryLogFile, { id: extensionTelemetryLogChannelId, name: localize('extensionTelemetryLog', "Extension Telemetry{0}", this._inLoggingOnlyMode ? ' (Not Sent)' : ''), hidden: true }); + this._outputLogger = loggerService.createLogger(this.extHostTelemetryLogFile, + { + id: extensionTelemetryLogChannelId, + name: localize('extensionTelemetryLog', "Extension Telemetry{0}", this._inLoggingOnlyMode ? ' (Not Sent)' : ''), + hidden: true, + group: TelemetryLogGroup, + }); this._register(this._outputLogger); this._register(loggerService.onDidChangeLogLevel(arg => { if (isLogLevel(arg)) { this.updateLoggerVisibility(); } })); - this._outputLogger.info('Below are logs for extension telemetry events sent to the telemetry output channel API once the log level is set to trace.'); - this._outputLogger.info('==========================================================='); } private updateLoggerVisibility(): void { diff --git a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts index 476023a47128..a3660ca12b23 100644 --- a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts @@ -286,11 +286,11 @@ suite('ExtHostTelemetry', function () { const logger = createLogger(functionSpy, extensionTelemetry); // Ensure headers are logged on instantiation - assert.strictEqual(loggerService.createLogger().logs.length, 2); + assert.strictEqual(loggerService.createLogger().logs.length, 0); logger.logUsage('test-event', { 'test-data': 'test-data' }); // Initial header is logged then the event - assert.strictEqual(loggerService.createLogger().logs.length, 3); - assert.ok(loggerService.createLogger().logs[2].startsWith('test-extension/test-event')); + assert.strictEqual(loggerService.createLogger().logs.length, 1); + assert.ok(loggerService.createLogger().logs[0].startsWith('test-extension/test-event')); }); }); From a27b338e7bd1717be9c36134b526630d83cf7730 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 17 Jan 2025 08:17:44 -0800 Subject: [PATCH 0644/3587] Fix toggling wrapping sometimes losing track of DOM nodes On flush, the DOM renderer expects the following render to have lines and as such defers clearing the innerHTML of the line collection in order to save a little time. Since this GPU renderer may not actually render lines however, this ends up having the possibility to lose track of rendered DOM lines and they would stick around until the next flush where it actually does clear the parent out. This was mostly reproducible when toggling wrapping in markdown files, but it was somewhat difficult I believe depending on how many lines would pass canRender or not which changes at various stages of view events being fired. Since this is a relatively risky and unnecessary change to apply when GPU acceleration is off, it will only change behavior when useGpu is true. Fixes #237527 --- src/vs/editor/browser/view/viewLayer.ts | 15 +++++++++++++-- .../browser/viewParts/viewLines/viewLines.ts | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index 7162a1bb9396..ec5d8bce6f3d 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -277,9 +277,20 @@ export class VisibleLinesCollection { return false; } - public onFlushed(e: viewEvents.ViewFlushedEvent): boolean { + public onFlushed(e: viewEvents.ViewFlushedEvent, flushDom?: boolean): boolean { + // No need to clear the dom node because a full .innerHTML will occur in + // ViewLayerRenderer._render, however the the fallbakc mechanism in the + // GPU renderer may cause this to be necessary as the .innerHTML call + // may not happen depending on the new state, leaving stale DOM nodes + // around. + if (flushDom) { + const start = this._linesCollection.getStartLineNumber(); + const end = this._linesCollection.getEndLineNumber(); + for (let i = start; i <= end; i++) { + this._linesCollection.getLine(i).getDomNode()?.remove(); + } + } this._linesCollection.flush(); - // No need to clear the dom node because a full .innerHTML will occur in ViewLayerRenderer._render return true; } diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts index 6de14aedb6ae..78324773a68f 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts @@ -254,7 +254,7 @@ export class ViewLines extends ViewPart implements IViewLines { return true; } public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean { - const shouldRender = this._visibleLines.onFlushed(e); + const shouldRender = this._visibleLines.onFlushed(e, this._viewLineOptions.useGpu); this._maxLineWidth = 0; return shouldRender; } From e2207e5274d3d48ae90b3d20c0ae3080eb6d345b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 17 Jan 2025 08:20:22 -0800 Subject: [PATCH 0645/3587] Render wrapped lines on GPU Fixes #238138 --- src/vs/editor/browser/gpu/viewGpuContext.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index f33b9ee6fa11..c8e4fc8a21e9 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -163,7 +163,6 @@ export class ViewGpuContext extends Disposable { if ( data.containsRTL || data.maxColumn > GpuRenderLimits.maxGpuCols || - data.continuesWithWrappedLine || lineNumber >= GpuRenderLimits.maxGpuLines ) { return false; @@ -212,9 +211,6 @@ export class ViewGpuContext extends Disposable { if (data.maxColumn > GpuRenderLimits.maxGpuCols) { reasons.push('maxColumn > maxGpuCols'); } - if (data.continuesWithWrappedLine) { - reasons.push('continuesWithWrappedLine'); - } if (data.inlineDecorations.length > 0) { let supported = true; const problemTypes: InlineDecorationType[] = []; From 738b906a1a89b2dc511bdc79341ed9fc14c74b0f Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 17 Jan 2025 17:20:37 +0100 Subject: [PATCH 0646/3587] Expose source as getter in inline completions (#238135) expose source as getter --- .../controller/inlineCompletionsController.ts | 2 +- .../browser/model/inlineCompletionsModel.ts | 21 +++++++++++-------- .../browser/model/inlineCompletionsSource.ts | 5 ++++- .../view/inlineEdits/gutterIndicatorView.ts | 4 ++-- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index a0fbec729203..5b9811a2f036 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -292,7 +292,7 @@ export class InlineCompletionsController extends Disposable { this._register(contextKeySvcObs.bind(InlineCompletionContextKeys.suppressSuggestions, reader => { const model = this.model.read(reader); const state = model?.inlineCompletionState.read(reader); - return state?.primaryGhostText && state?.inlineCompletion ? state.inlineCompletion.inlineCompletion.source.inlineCompletions.suppressSuggestions : undefined; + return state?.primaryGhostText && state?.inlineCompletion ? state.inlineCompletion.source.inlineCompletions.suppressSuggestions : undefined; })); this._register(contextKeySvcObs.bind(InlineCompletionContextKeys.inlineSuggestionVisible, reader => { const model = this.model.read(reader); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index 6e9a3969605f..5ae0b0670124 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -258,9 +258,11 @@ export class InlineCompletionsModel extends Disposable { public stop(stopReason: 'explicitCancel' | 'automatic' = 'automatic', tx?: ITransaction): void { subtransaction(tx, tx => { if (stopReason === 'explicitCancel') { - const completion = this.state.get()?.inlineCompletion?.inlineCompletion; - if (completion && completion.source.provider.handleRejection) { - completion.source.provider.handleRejection(completion.source.inlineCompletions, completion.sourceInlineCompletion); + const inlineCompletion = this.state.get()?.inlineCompletion; + const source = inlineCompletion?.source; + const sourceInlineCompletion = inlineCompletion?.sourceInlineCompletion; + if (sourceInlineCompletion && source?.provider.handleRejection) { + source.provider.handleRejection(source.inlineCompletions, sourceInlineCompletion); } } @@ -284,7 +286,7 @@ export class InlineCompletionsModel extends Disposable { let inlineEdit: InlineCompletionWithUpdatedRange | undefined = undefined; const visibleCompletions: InlineCompletionWithUpdatedRange[] = []; for (const completion of c.inlineCompletions) { - if (!completion.inlineCompletion.sourceInlineCompletion.isInlineEdit) { + if (!completion.sourceInlineCompletion.isInlineEdit) { if (completion.isVisible(this.textModel, cursorPosition, reader)) { visibleCompletions.push(completion); } @@ -329,7 +331,7 @@ export class InlineCompletionsModel extends Disposable { }); public readonly activeCommands = derivedOpts({ owner: this, equalsFn: itemsEquals() }, - r => this.selectedInlineCompletion.read(r)?.inlineCompletion.source.inlineCompletions.commands ?? [] + r => this.selectedInlineCompletion.read(r)?.source.inlineCompletions.commands ?? [] ); public readonly lastTriggerKind: IObservable @@ -745,10 +747,11 @@ export class InlineCompletionsModel extends Disposable { const augmentedCompletion = this._computeAugmentation(itemEdit, undefined); if (!augmentedCompletion) { return; } - const inlineCompletion = augmentedCompletion.completion.inlineCompletion; - inlineCompletion.source.provider.handlePartialAccept?.( - inlineCompletion.source.inlineCompletions, - inlineCompletion.sourceInlineCompletion, + const source = augmentedCompletion.completion.source; + const sourceInlineCompletion = augmentedCompletion.completion.sourceInlineCompletion; + source.provider.handlePartialAccept?.( + source.inlineCompletions, + sourceInlineCompletion, itemEdit.text.length, { kind: PartialAcceptTriggerKind.Suggest, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index f83a8dfaf005..660c4403e37e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -323,7 +323,7 @@ export class InlineCompletionWithUpdatedRange { ]); public get forwardStable() { - return this.inlineCompletion.source.inlineCompletions.enableForwardStability ?? false; + return this.source.inlineCompletions.enableForwardStability ?? false; } private readonly _updatedRange = derivedOpts({ owner: this, equalsFn: Range.equalsRange }, reader => { @@ -342,6 +342,9 @@ export class InlineCompletionWithUpdatedRange { public _inlineEdit: ISettableObservable; public get inlineEdit() { return this._inlineEdit.get(); } + public get source() { return this.inlineCompletion.source; } + public get sourceInlineCompletion() { return this.inlineCompletion.sourceInlineCompletion; } + private readonly _creationTime: number = Date.now(); constructor( diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index 5d6dd535722f..9fa6bb711e44 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -190,7 +190,7 @@ export class InlineEditsGutterIndicator extends Disposable { const displayName = derived(this, reader => { const state = this._model.read(reader)?.inlineEditState; const item = state?.read(reader); - const completionSource = item?.inlineCompletion?.inlineCompletion.source; + const completionSource = item?.inlineCompletion?.source; // TODO: expose the provider (typed) and expose the provider the edit belongs totyping and get correct edit const displayName = (completionSource?.inlineCompletions as any).edits[0]?.provider?.displayName ?? localize('inlineEdit', "Inline Edit"); return displayName; @@ -207,7 +207,7 @@ export class InlineEditsGutterIndicator extends Disposable { } h?.dispose(); }, - this._model.map((m, r) => m?.state.read(r)?.inlineCompletion?.inlineCompletion.source.inlineCompletions.commands), + this._model.map((m, r) => m?.state.read(r)?.inlineCompletion?.source.inlineCompletions.commands), ).toDisposableLiveElement()); const focusTracker = disposableStore.add(trackFocus(content.element)); From bb655894c2d4067d7f62c5d98a793e4b3c2f5286 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 17 Jan 2025 18:19:04 +0100 Subject: [PATCH 0647/3587] GitHub - add "Open on GitHub" action to timeline context menu (#238144) --- extensions/github/package.json | 19 +++++++++++- extensions/github/src/commands.ts | 49 +++++++++++++++++++++---------- extensions/github/tsconfig.json | 3 +- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/extensions/github/package.json b/extensions/github/package.json index a1d9b4eb9615..55e04b308044 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -32,7 +32,8 @@ "contribShareMenu", "contribSourceControlHistoryItemMenu", "scmHistoryProvider", - "shareProvider" + "shareProvider", + "timeline" ], "contributes": { "commands": [ @@ -61,6 +62,11 @@ "command": "github.graph.openOnGitHub", "title": "%command.openOnGitHub%", "icon": "$(github)" + }, + { + "command": "github.timeline.openOnGitHub", + "title": "%command.openOnGitHub%", + "icon": "$(github)" } ], "continueEditSession": [ @@ -97,6 +103,10 @@ { "command": "github.openOnVscodeDev", "when": "false" + }, + { + "command": "github.timeline.openOnGitHub", + "when": "false" } ], "file/share": [ @@ -152,6 +162,13 @@ "when": "github.hasGitHubRepo", "group": "1_open@1" } + ], + "timeline/item/context": [ + { + "command": "github.timeline.openOnGitHub", + "group": "1_actions@3", + "when": "github.hasGitHubRepo && timelineItem =~ /git:file:commit\\b/" + } ] }, "configuration": [ diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 805d91c7296b..4e5587c09b53 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { API as GitAPI, RefType } from './typings/git'; +import { API as GitAPI, RefType, Repository } from './typings/git'; import { publishRepository } from './publish'; import { DisposableStore, getRepositoryFromUrl } from './util'; import { LinkContext, getCommitLink, getLink, getVscodeDevHost } from './links'; @@ -34,6 +34,29 @@ async function openVscodeDevLink(gitAPI: GitAPI): Promise { + // Get the unique remotes that contain the commit + const branches = await repository.getBranches({ contains: commit, remote: true }); + const remoteNames = new Set(branches.filter(b => b.type === RefType.RemoteHead && b.remote).map(b => b.remote!)); + + // GitHub remotes that contain the commit + const remotes = repository.state.remotes + .filter(r => remoteNames.has(r.name) && r.fetchUrl && getRepositoryFromUrl(r.fetchUrl)); + + if (remotes.length === 0) { + vscode.window.showInformationMessage(vscode.l10n.t('No GitHub remotes found that contain this commit.')); + return; + } + + // upstream -> origin -> first + const remote = remotes.find(r => r.name === 'upstream') + ?? remotes.find(r => r.name === 'origin') + ?? remotes[0]; + + const link = getCommitLink(remote.fetchUrl!, commit); + vscode.env.openExternal(vscode.Uri.parse(link)); +} + export function registerCommands(gitAPI: GitAPI): vscode.Disposable { const disposables = new DisposableStore(); @@ -72,26 +95,20 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { return; } - // Get the unique remotes that contain the commit - const branches = await apiRepository.getBranches({ contains: historyItem.id, remote: true }); - const remoteNames = new Set(branches.filter(b => b.type === RefType.RemoteHead && b.remote).map(b => b.remote!)); - - // GitHub remotes that contain the commit - const remotes = apiRepository.state.remotes - .filter(r => remoteNames.has(r.name) && r.fetchUrl && getRepositoryFromUrl(r.fetchUrl)); + await openOnGitHub(apiRepository, historyItem.id); + })); - if (remotes.length === 0) { - vscode.window.showInformationMessage(vscode.l10n.t('No GitHub remotes found that contain this commit.')); + disposables.add(vscode.commands.registerCommand('github.timeline.openOnGitHub', async (item: vscode.TimelineItem, uri: vscode.Uri) => { + if (!item.id || !uri) { return; } - // upstream -> origin -> first - const remote = remotes.find(r => r.name === 'upstream') - ?? remotes.find(r => r.name === 'origin') - ?? remotes[0]; + const apiRepository = gitAPI.getRepository(uri); + if (!apiRepository) { + return; + } - const link = getCommitLink(remote.fetchUrl!, historyItem.id); - vscode.env.openExternal(vscode.Uri.parse(link)); + await openOnGitHub(apiRepository, item.id); })); disposables.add(vscode.commands.registerCommand('github.openOnVscodeDev', async () => { diff --git a/extensions/github/tsconfig.json b/extensions/github/tsconfig.json index 6b9a6e7db0a3..8435c0d09e88 100644 --- a/extensions/github/tsconfig.json +++ b/extensions/github/tsconfig.json @@ -12,6 +12,7 @@ "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts", "../../src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts", - "../../src/vscode-dts/vscode.proposed.shareProvider.d.ts" + "../../src/vscode-dts/vscode.proposed.shareProvider.d.ts", + "../../src/vscode-dts/vscode.proposed.timeline.d.ts" ] } From f1b4bb8d76d72e8114c6b5e97eb01347e0a77b23 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 17 Jan 2025 11:20:10 -0600 Subject: [PATCH 0648/3587] check if windows file is executable (#238142) fix #237596 --- .../src/helpers/executable.ts | 39 +++++++++++++++++++ extensions/terminal-suggest/src/helpers/os.ts | 10 +++++ .../src/terminalSuggestMain.ts | 33 ++++------------ .../suggest/browser/simpleCompletionModel.ts | 2 + 4 files changed, 58 insertions(+), 26 deletions(-) create mode 100644 extensions/terminal-suggest/src/helpers/executable.ts create mode 100644 extensions/terminal-suggest/src/helpers/os.ts diff --git a/extensions/terminal-suggest/src/helpers/executable.ts b/extensions/terminal-suggest/src/helpers/executable.ts new file mode 100644 index 000000000000..32c6a3d05a14 --- /dev/null +++ b/extensions/terminal-suggest/src/helpers/executable.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { osIsWindows } from './os'; +import * as fs from 'fs/promises'; + +export async function isExecutable(filePath: string): Promise { + if (osIsWindows()) { + return windowsExecutableExtensions.find(ext => filePath.endsWith(ext)) !== undefined; + } + try { + const stats = await fs.stat(filePath); + // On macOS/Linux, check if the executable bit is set + return (stats.mode & 0o100) !== 0; + } catch (error) { + // If the file does not exist or cannot be accessed, it's not executable + return false; + } +} +const windowsExecutableExtensions: string[] = [ + '.exe', // Executable file + '.bat', // Batch file + '.cmd', // Command script + '.com', // Command file + + '.msi', // Windows Installer package + + '.ps1', // PowerShell script + + '.vbs', // VBScript file + '.js', // JScript file + '.jar', // Java Archive (requires Java runtime) + '.py', // Python script (requires Python interpreter) + '.rb', // Ruby script (requires Ruby interpreter) + '.pl', // Perl script (requires Perl interpreter) + '.sh', // Shell script (via WSL or third-party tools) +]; diff --git a/extensions/terminal-suggest/src/helpers/os.ts b/extensions/terminal-suggest/src/helpers/os.ts new file mode 100644 index 000000000000..3c2a34987197 --- /dev/null +++ b/extensions/terminal-suggest/src/helpers/os.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; + +export function osIsWindows(): boolean { + return os.platform() === 'win32'; +} diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 4ed1a26fe6d3..ea5a750cec1f 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as os from 'os'; import * as fs from 'fs/promises'; import * as path from 'path'; import { ExecOptionsWithStringEncoding, execSync } from 'child_process'; @@ -11,7 +10,10 @@ import { upstreamSpecs } from './constants'; import codeCompletionSpec from './completions/code'; import cdSpec from './completions/cd'; import codeInsidersCompletionSpec from './completions/code-insiders'; +import { osIsWindows } from './helpers/os'; +import { isExecutable } from './helpers/executable'; +const isWindows = osIsWindows(); let cachedAvailableCommandsPath: string | undefined; let cachedAvailableCommands: Set | undefined; const cachedBuiltinCommands: Map = new Map(); @@ -101,7 +103,7 @@ export async function activate(context: vscode.ExtensionContext) { const result = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, terminal.shellIntegration?.cwd, token); if (result.cwd && (result.filesRequested || result.foldersRequested)) { // const cwd = resolveCwdFromPrefix(prefix, terminal.shellIntegration?.cwd) ?? terminal.shellIntegration?.cwd; - return new vscode.TerminalCompletionList(result.items, { filesRequested: result.filesRequested, foldersRequested: result.foldersRequested, cwd: result.cwd, pathSeparator: osIsWindows() ? '\\' : '/' }); + return new vscode.TerminalCompletionList(result.items, { filesRequested: result.filesRequested, foldersRequested: result.foldersRequested, cwd: result.cwd, pathSeparator: isWindows ? '\\' : '/' }); } return result.items; } @@ -123,7 +125,7 @@ export async function resolveCwdFromPrefix(prefix: string, currentCwd?: vscode.U // Get the nearest folder path from the prefix. This ignores everything after the `/` as // they are what triggers changes in the directory. let lastSlashIndex: number; - if (osIsWindows()) { + if (isWindows) { // TODO: This support is very basic, ideally the slashes supported would depend upon the // shell type. For example git bash under Windows does not allow using \ as a path // separator. @@ -179,28 +181,10 @@ function createCompletionItem(cursorPosition: number, prefix: string, label: str }; } -async function isExecutable(filePath: string): Promise { - // Windows doesn't have the concept of an executable bit and running any - // file is possible. We considered using $PATHEXT here but since it's mostly - // there for legacy reasons and it would be easier and more intuitive to add - // a setting if needed instead. - if (osIsWindows()) { - return true; - } - try { - const stats = await fs.stat(filePath); - // On macOS/Linux, check if the executable bit is set - return (stats.mode & 0o100) !== 0; - } catch (error) { - // If the file does not exist or cannot be accessed, it's not executable - return false; - } -} - async function getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise | undefined> { // Get PATH value let pathValue: string | undefined; - if (osIsWindows()) { + if (isWindows) { const caseSensitivePathKey = Object.keys(env).find(key => key.toLowerCase() === 'path'); if (caseSensitivePathKey) { pathValue = env[caseSensitivePathKey]; @@ -218,7 +202,6 @@ async function getCommandsInPath(env: { [key: string]: string | undefined } = pr } // Extract executables from PATH - const isWindows = osIsWindows(); const paths = pathValue.split(isWindows ? ';' : ':'); const pathSeparator = isWindows ? '\\' : '/'; const executables = new Set(); @@ -484,9 +467,7 @@ function getCompletionItemsFromArgs(args: Fig.SingleOrArray | undefined return { items, filesRequested, foldersRequested }; } -function osIsWindows(): boolean { - return os.platform() === 'win32'; -} + function getFirstCommand(commandLine: string): string | undefined { const wordsOnLine = commandLine.split(' '); diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts index e7b52f603436..df8b119efba2 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts @@ -234,6 +234,8 @@ const fileExtScores = new Map(isWindows ? [ ['exe', 0.08], ['bat', 0.07], ['cmd', 0.07], + ['msi', 0.06], + ['com', 0.06], // Non-Windows ['sh', -0.05], ['bash', -0.05], From 60230a46df1b1f86737f0af9d8f508b7ff8c3881 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 17 Jan 2025 10:46:29 -0800 Subject: [PATCH 0649/3587] Fix the booleans on the MSAL flows (#238148) * Loopback does _not_ work in REH or WebWorker * UrlHandler _does_ work in REH --- extensions/microsoft-authentication/src/node/flows.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/microsoft-authentication/src/node/flows.ts b/extensions/microsoft-authentication/src/node/flows.ts index 3e1d8c513f08..c6f40a943f61 100644 --- a/extensions/microsoft-authentication/src/node/flows.ts +++ b/extensions/microsoft-authentication/src/node/flows.ts @@ -41,8 +41,8 @@ interface IMsalFlow { class DefaultLoopbackFlow implements IMsalFlow { label = 'default'; options: IMsalFlowOptions = { - supportsRemoteExtensionHost: true, - supportsWebWorkerExtensionHost: true + supportsRemoteExtensionHost: false, + supportsWebWorkerExtensionHost: false }; async trigger({ cachedPca, scopes, loginHint, windowHandle, logger }: IMsalFlowTriggerOptions): Promise { @@ -62,7 +62,7 @@ class DefaultLoopbackFlow implements IMsalFlow { class UrlHandlerFlow implements IMsalFlow { label = 'protocol handler'; options: IMsalFlowOptions = { - supportsRemoteExtensionHost: false, + supportsRemoteExtensionHost: true, supportsWebWorkerExtensionHost: false }; From a821bbf97f46a6b3e14c38f420689319e6980059 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 17 Jan 2025 10:47:01 -0800 Subject: [PATCH 0650/3587] Remove grid lines and "Presets" in wording (#238150) Cleaner. --- .../browser/quickInputController.ts | 34 ------------------- .../browser/actions/layoutActions.ts | 2 +- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index f3ef3033729a..b9ff1f3ad472 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -897,9 +897,6 @@ class QuickInputDragAndDropController extends Disposable { private readonly _snapThreshold = 20; private readonly _snapLineHorizontalRatio = 0.25; - private readonly _snapLineHorizontal: HTMLElement; - private readonly _snapLineVertical1: HTMLElement; - private readonly _snapLineVertical2: HTMLElement; private _quickInputAlignmentContext = QuickInputAlignmentContextKey.bindTo(this._contextKeyService); @@ -911,22 +908,11 @@ class QuickInputDragAndDropController extends Disposable { @IContextKeyService private readonly _contextKeyService: IContextKeyService ) { super(); - - this._snapLineHorizontal = dom.append(this._container, $('.quick-input-widget-snapline.horizontal.hidden')); - this._snapLineVertical1 = dom.append(this._container, $('.quick-input-widget-snapline.vertical.hidden')); - this._snapLineVertical2 = dom.append(this._container, $('.quick-input-widget-snapline.vertical.hidden')); - this.registerMouseListeners(); } reparentUI(container: HTMLElement): void { this._container = container; - this._snapLineHorizontal.remove(); - this._snapLineVertical1.remove(); - this._snapLineVertical2.remove(); - dom.append(this._container, this._snapLineHorizontal); - dom.append(this._container, this._snapLineVertical1); - dom.append(this._container, this._snapLineVertical2); } setAlignment(alignment: 'top' | 'center' | { top: number; left: number }, done = true): void { @@ -1000,7 +986,6 @@ class QuickInputDragAndDropController extends Disposable { mouseMoveEvent.preventDefault(); if (!isMovingQuickInput) { - this._showSnapLines(snapCoordinateY, snapCoordinateX); isMovingQuickInput = true; } @@ -1036,9 +1021,6 @@ class QuickInputDragAndDropController extends Disposable { // Mouse up const mouseUpListener = dom.addDisposableGenericMouseUpListener(activeWindow, (e: MouseEvent) => { if (isMovingQuickInput) { - // Hide snaplines - this._hideSnapLines(); - // Save position const state = this.dndViewState.get(); this.dndViewState.set({ top: state?.top, left: state?.left, done: true }, undefined); @@ -1062,20 +1044,4 @@ class QuickInputDragAndDropController extends Disposable { private _getCenterXSnapValue() { return Math.round(this._container.clientWidth / 2) - Math.round(this._quickInputContainer.clientWidth / 2); } - - private _showSnapLines(horizontal: number, vertical: number) { - this._snapLineHorizontal.style.top = `${horizontal}px`; - this._snapLineVertical1.style.left = `${vertical}px`; - this._snapLineVertical2.style.left = `${vertical + this._quickInputContainer.clientWidth}px`; - - this._snapLineHorizontal.classList.remove('hidden'); - this._snapLineVertical1.classList.remove('hidden'); - this._snapLineVertical2.classList.remove('hidden'); - } - - private _hideSnapLines() { - this._snapLineHorizontal.classList.add('hidden'); - this._snapLineVertical1.classList.add('hidden'); - this._snapLineVertical2.classList.add('hidden'); - } } diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index b38c5c9a6316..d8217f384aba 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -1491,7 +1491,7 @@ registerAction2(class CustomizeLayoutAction extends Action2 { ...AlignPanelActions.map(toQuickPickItem), { type: 'separator', - label: localize('quickOpen', "Quick Input Position Presets") + label: localize('quickOpen', "Quick Input Position") }, ...QuickInputActions.map(toQuickPickItem), { From 87ed97df8bb37ee2222f5494aca5c28e3c12ed8f Mon Sep 17 00:00:00 2001 From: Devraj Mehta Date: Fri, 17 Jan 2025 14:05:10 -0500 Subject: [PATCH 0651/3587] Use Electron fetch or Node fetch for github-authentication to support proxies (#238149) * Attempt to use Electron fetch for github-authentication Also changes fallback from node-fetch to the built-in Node fetch * Remove Content-Length header Electron compatibility It looks like it was set incorrectly to the body contents anyways. --- extensions/github-authentication/src/flows.ts | 2 -- extensions/github-authentication/src/node/fetch.ts | 9 +++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/extensions/github-authentication/src/flows.ts b/extensions/github-authentication/src/flows.ts index a2497b2b0b2e..8920ea5d3578 100644 --- a/extensions/github-authentication/src/flows.ts +++ b/extensions/github-authentication/src/flows.ts @@ -105,8 +105,6 @@ async function exchangeCodeForToken( headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': body.toString() - }, body: body.toString() }); diff --git a/extensions/github-authentication/src/node/fetch.ts b/extensions/github-authentication/src/node/fetch.ts index 58718078e699..ca344d436b11 100644 --- a/extensions/github-authentication/src/node/fetch.ts +++ b/extensions/github-authentication/src/node/fetch.ts @@ -2,6 +2,11 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import fetch from 'node-fetch'; -export const fetching = fetch; +let _fetch: typeof fetch; +try { + _fetch = require('electron').net.fetch; +} catch { + _fetch = fetch; +} +export const fetching = _fetch; From 81215329f3f83409801c44ec2c509b8d3039ffb0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 17 Jan 2025 11:18:07 -0800 Subject: [PATCH 0652/3587] Share GPU device across editors, access sync when possible Fixes #238157 --- src/vs/editor/browser/gpu/viewGpuContext.ts | 35 +++++++++++-------- .../viewParts/viewLinesGpu/viewLinesGpu.ts | 3 +- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index c8e4fc8a21e9..f9759a4b0dd8 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -46,7 +46,8 @@ export class ViewGpuContext extends Disposable { readonly canvas: FastDomNode; readonly ctx: GPUCanvasContext; - readonly device: Promise; + static device: Promise; + static deviceSync: GPUDevice | undefined; readonly rectangleRenderer: RectangleRenderer; @@ -109,18 +110,23 @@ export class ViewGpuContext extends Disposable { this.ctx = ensureNonNullable(this.canvas.domNode.getContext('webgpu')); - this.device = GPULifecycle.requestDevice((message) => { - const choices: IPromptChoice[] = [{ - label: nls.localize('editor.dom.render', "Use DOM-based rendering"), - run: () => this.configurationService.updateValue('editor.experimentalGpuAcceleration', 'off'), - }]; - this._notificationService.prompt(Severity.Warning, message, choices); - }).then(ref => this._register(ref).object); - this.device.then(device => { - if (!ViewGpuContext._atlas) { - ViewGpuContext._atlas = this._instantiationService.createInstance(TextureAtlas, device.limits.maxTextureDimension2D, undefined); - } - }); + // Request the GPU device, we only want to do this a single time per window as it's async + // and can delay the initial render. + if (!ViewGpuContext.device) { + ViewGpuContext.device = GPULifecycle.requestDevice((message) => { + const choices: IPromptChoice[] = [{ + label: nls.localize('editor.dom.render', "Use DOM-based rendering"), + run: () => this.configurationService.updateValue('editor.experimentalGpuAcceleration', 'off'), + }]; + this._notificationService.prompt(Severity.Warning, message, choices); + }).then(ref => { + ViewGpuContext.deviceSync = ref.object; + if (!ViewGpuContext._atlas) { + ViewGpuContext._atlas = this._instantiationService.createInstance(TextureAtlas, ref.object.limits.maxTextureDimension2D, undefined); + } + return ref.object; + }); + } const dprObs = observableValue(this, getActiveWindow().devicePixelRatio); this._register(addDisposableListener(getActiveWindow(), 'resize', () => { @@ -147,8 +153,7 @@ export class ViewGpuContext extends Disposable { })); this.contentLeft = contentLeft; - - this.rectangleRenderer = this._instantiationService.createInstance(RectangleRenderer, context, this.contentLeft, this.devicePixelRatio, this.canvas.domNode, this.ctx, this.device); + this.rectangleRenderer = this._instantiationService.createInstance(RectangleRenderer, context, this.contentLeft, this.devicePixelRatio, this.canvas.domNode, this.ctx, ViewGpuContext.device); } /** diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index ec749ef2fdb6..196c1f45dd3c 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -92,7 +92,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { async initWebgpu() { // #region General - this._device = await this._viewGpuContext.device; + this._device = ViewGpuContext.deviceSync || await ViewGpuContext.device; if (this._store.isDisposed) { return; @@ -650,7 +650,6 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { column++; } - console.log(lineNumber, column); return new Position(lineNumber, column + 1); } } From cff1a6a7035a744c01def033db08f3f0de866768 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 17 Jan 2025 13:26:37 -0600 Subject: [PATCH 0653/3587] add `path` to details for executable terminal suggestions, prevent duplication (#238080) --- .../src/terminalSuggestMain.ts | 71 ++++++++++++------- .../src/test/terminalSuggestMain.test.ts | 2 +- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index ea5a750cec1f..4e4913c78ba7 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -15,8 +15,8 @@ import { isExecutable } from './helpers/executable'; const isWindows = osIsWindows(); let cachedAvailableCommandsPath: string | undefined; -let cachedAvailableCommands: Set | undefined; -const cachedBuiltinCommands: Map = new Map(); +let cachedAvailableCommands: Set | undefined; +const cachedBuiltinCommands: Map = new Map(); export const availableSpecs: Fig.Spec[] = [ cdSpec, @@ -27,14 +27,14 @@ for (const spec of upstreamSpecs) { availableSpecs.push(require(`./completions/upstream/${spec}`).default); } -function getBuiltinCommands(shell: string): string[] | undefined { +function getBuiltinCommands(shell: string, existingCommands?: Set): ICompletionResource[] | undefined { try { const shellType = path.basename(shell, path.extname(shell)); const cachedCommands = cachedBuiltinCommands.get(shellType); if (cachedCommands) { return cachedCommands; } - const filter = (cmd: string) => cmd; + const filter = (cmd: string) => cmd && !existingCommands?.has(cmd); const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell }; let commands: string[] | undefined; switch (shellType) { @@ -68,8 +68,10 @@ function getBuiltinCommands(shell: string): string[] | undefined { break; } } - cachedBuiltinCommands.set(shellType, commands); - return commands; + + const commandResources = commands?.map(command => ({ label: command })); + cachedBuiltinCommands.set(shellType, commandResources); + return commandResources; } catch (error) { console.error('Error fetching builtin commands:', error); @@ -92,11 +94,11 @@ export async function activate(context: vscode.ExtensionContext) { } const commandsInPath = await getCommandsInPath(terminal.shellIntegration?.env); - const builtinCommands = getBuiltinCommands(shellPath); - if (!commandsInPath || !builtinCommands) { + const builtinCommands = getBuiltinCommands(shellPath, commandsInPath?.labels) ?? []; + if (!commandsInPath?.completionResources) { return; } - const commands = [...commandsInPath, ...builtinCommands]; + const commands = [...commandsInPath.completionResources, ...builtinCommands]; const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition); @@ -169,20 +171,24 @@ function getLabel(spec: Fig.Spec | Fig.Arg | Fig.Suggestion | string): string[] return spec.name; } -function createCompletionItem(cursorPosition: number, prefix: string, label: string, description?: string, kind?: vscode.TerminalCompletionItemKind): vscode.TerminalCompletionItem { +function createCompletionItem(cursorPosition: number, prefix: string, commandResource: ICompletionResource, description?: string, kind?: vscode.TerminalCompletionItemKind): vscode.TerminalCompletionItem { const endsWithSpace = prefix.endsWith(' '); const lastWord = endsWithSpace ? '' : prefix.split(' ').at(-1) ?? ''; return { - label, - detail: description ?? '', + label: commandResource.label, + detail: description ?? commandResource.path ?? '', replacementIndex: cursorPosition - lastWord.length, replacementLength: lastWord.length, kind: kind ?? vscode.TerminalCompletionItemKind.Method }; } -async function getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise | undefined> { - // Get PATH value +interface ICompletionResource { + label: string; + path?: string; +} +async function getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { + const labels: Set = new Set(); let pathValue: string | undefined; if (isWindows) { const caseSensitivePathKey = Object.keys(env).find(key => key.toLowerCase() === 'path'); @@ -198,24 +204,26 @@ async function getCommandsInPath(env: { [key: string]: string | undefined } = pr // Check cache if (cachedAvailableCommands && cachedAvailableCommandsPath === pathValue) { - return cachedAvailableCommands; + return { completionResources: cachedAvailableCommands, labels }; } // Extract executables from PATH const paths = pathValue.split(isWindows ? ';' : ':'); const pathSeparator = isWindows ? '\\' : '/'; - const executables = new Set(); + const executables = new Set(); for (const path of paths) { try { const dirExists = await fs.stat(path).then(stat => stat.isDirectory()).catch(() => false); if (!dirExists) { continue; } - const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(path)); - + const fileResource = vscode.Uri.file(path); + const files = await vscode.workspace.fs.readDirectory(fileResource); for (const [file, fileType] of files) { - if (fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(path + pathSeparator + file)) { - executables.add(file); + const formattedPath = getFriendlyFilePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); + if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath)) { + executables.add({ label: file, path: formattedPath }); + labels.add(file); } } } catch (e) { @@ -224,7 +232,7 @@ async function getCommandsInPath(env: { [key: string]: string | undefined } = pr } } cachedAvailableCommands = executables; - return executables; + return { completionResources: executables, labels }; } function getPrefix(commandLine: string, cursorPosition: number): string { @@ -257,7 +265,7 @@ export function asArray(x: T | T[]): T[] { export async function getCompletionItemsFromSpecs( specs: Fig.Spec[], terminalContext: { commandLine: string; cursorPosition: number }, - availableCommands: string[], + availableCommands: ICompletionResource[], prefix: string, shellIntegrationCwd?: vscode.Uri, token?: vscode.CancellationToken @@ -277,7 +285,7 @@ export async function getCompletionItemsFromSpecs( } for (const specLabel of specLabels) { - if (!availableCommands.includes(specLabel) || (token && token.isCancellationRequested)) { + if (!availableCommands.find(command => command.label === specLabel) || (token && token.isCancellationRequested)) { continue; } @@ -288,7 +296,7 @@ export async function getCompletionItemsFromSpecs( || !!firstCommand && specLabel.startsWith(firstCommand) ) { // push it to the completion items - items.push(createCompletionItem(terminalContext.cursorPosition, prefix, specLabel)); + items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: specLabel })); } if (!terminalContext.commandLine.startsWith(specLabel)) { @@ -323,7 +331,7 @@ export async function getCompletionItemsFromSpecs( // Include builitin/available commands in the results const labels = new Set(items.map((i) => i.label)); for (const command of availableCommands) { - if (!labels.has(command)) { + if (!labels.has(command.label)) { items.push(createCompletionItem(terminalContext.cursorPosition, prefix, command)); } } @@ -393,7 +401,7 @@ function handleOptions(specLabel: string, spec: Fig.Spec, terminalContext: { com createCompletionItem( terminalContext.cursorPosition, prefix, - optionLabel, + { label: optionLabel }, option.description, vscode.TerminalCompletionItemKind.Flag ) @@ -455,7 +463,7 @@ function getCompletionItemsFromArgs(args: Fig.SingleOrArray | undefined } if (suggestionLabel && suggestionLabel.startsWith(currentPrefix.trim())) { const description = typeof suggestion !== 'string' ? suggestion.description : ''; - items.push(createCompletionItem(terminalContext.cursorPosition, wordBefore ?? '', suggestionLabel, description, vscode.TerminalCompletionItemKind.Argument)); + items.push(createCompletionItem(terminalContext.cursorPosition, wordBefore ?? '', { label: suggestionLabel }, description, vscode.TerminalCompletionItemKind.Argument)); } } } @@ -479,3 +487,12 @@ function getFirstCommand(commandLine: string): string | undefined { } return firstCommand; } + +function getFriendlyFilePath(uri: vscode.Uri, pathSeparator: string): string { + let path = uri.fsPath; + // Ensure drive is capitalized on Windows + if (pathSeparator === '\\' && path.match(/^[a-zA-Z]:\\/)) { + path = `${path[0].toUpperCase()}:${path.slice(2)}`; + } + return path; +} diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index 6e4520e2df14..d72996b8f702 100644 --- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -150,7 +150,7 @@ suite('Terminal Suggest', () => { const prefix = commandLine.slice(0, cursorPosition).split(' ').at(-1) || ''; const filesRequested = testSpec.expectedResourceRequests?.type === 'files' || testSpec.expectedResourceRequests?.type === 'both'; const foldersRequested = testSpec.expectedResourceRequests?.type === 'folders' || testSpec.expectedResourceRequests?.type === 'both'; - const result = await getCompletionItemsFromSpecs(completionSpecs, { commandLine, cursorPosition }, availableCommands, prefix, testCwd); + const result = await getCompletionItemsFromSpecs(completionSpecs, { commandLine, cursorPosition }, availableCommands.map(c => { return { label: c }; }), prefix, testCwd); deepStrictEqual(result.items.map(i => i.label).sort(), (testSpec.expectedCompletions ?? []).sort()); strictEqual(result.filesRequested, filesRequested); strictEqual(result.foldersRequested, foldersRequested); From a4fb8d6c588e92c9d4ccc0040b879b1094704033 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:07:50 -0800 Subject: [PATCH 0654/3587] Get SPAA working against token metadata bg This does not yet support colors applied via decorations like find. Part of #227101 --- .../browser/gpu/raster/glyphRasterizer.ts | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 1a8fe6ac4d95..7c9c8d9a2863 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -5,6 +5,7 @@ import { memoize } from '../../../../base/common/decorators.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; +import { isMacintosh } from '../../../../base/common/platform.js'; import { StringBuilder } from '../../../common/core/stringBuilder.js'; import { FontStyle, TokenMetadata } from '../../../common/encodedTokenAttributes.js'; import { ensureNonNullable } from '../gpuUtils.js'; @@ -43,6 +44,9 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { }; private _workGlyphConfig: { chars: string | undefined; tokenMetadata: number; decorationStyleSetId: number } = { chars: undefined, tokenMetadata: 0, decorationStyleSetId: 0 }; + // TODO: Support workbench.fontAliasing correctly + private _antiAliasing: 'subpixel' | 'greyscale' = isMacintosh ? 'greyscale' : 'subpixel'; + constructor( readonly fontSize: number, readonly fontFamily: string, @@ -53,7 +57,8 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { const devicePixelFontSize = Math.ceil(this.fontSize * devicePixelRatio); this._canvas = new OffscreenCanvas(devicePixelFontSize * 3, devicePixelFontSize * 3); this._ctx = ensureNonNullable(this._canvas.getContext('2d', { - willReadFrequently: true + willReadFrequently: true, + alpha: this._antiAliasing === 'greyscale', })); this._ctx.textBaseline = 'top'; this._ctx.fillStyle = '#FFFFFF'; @@ -61,7 +66,6 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._textMetrics = this._ctx.measureText('A'); } - // TODO: Support drawing multiple fonts and sizes /** * Rasterizes a glyph. Note that the returned object is reused across different glyphs and * therefore is only safe for synchronous access. @@ -108,10 +112,18 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._ctx.save(); + const bgId = TokenMetadata.getBackground(tokenMetadata); + const bg = colorMap[bgId]; + const decorationStyleSet = ViewGpuContext.decorationStyleCache.getStyleSet(decorationStyleSetId); - // TODO: Support workbench.fontAliasing - this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + // When SPAA is used, the background color must be present to get the right glyph + if (this._antiAliasing === 'subpixel') { + this._ctx.fillStyle = bg; + this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); + } else { + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + } const fontSb = new StringBuilder(200); const fontStyle = TokenMetadata.getFontStyle(tokenMetadata); @@ -149,6 +161,13 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._ctx.restore(); const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); + if (this._antiAliasing === 'subpixel') { + const bgR = parseInt(bg.substring(1, 3), 16); + const bgG = parseInt(bg.substring(3, 5), 16); + const bgB = parseInt(bg.substring(5, 7), 16); + this._clearColor(imageData, bgR, bgG, bgB); + this._ctx.putImageData(imageData, 0, 0); + } this._findGlyphBoundingBox(imageData, this._workGlyph.boundingBox); // const offset = { // x: textMetrics.actualBoundingBoxLeft, @@ -204,6 +223,17 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { return this._workGlyph; } + private _clearColor(imageData: ImageData, r: number, g: number, b: number) { + for (let offset = 0; offset < imageData.data.length; offset += 4) { + // Check exact match + if (imageData.data[offset] === r && + imageData.data[offset + 1] === g && + imageData.data[offset + 2] === b) { + imageData.data[offset + 3] = 0; + } + } + } + // TODO: Does this even need to happen when measure text is used? private _findGlyphBoundingBox(imageData: ImageData, outBoundingBox: IBoundingBox) { const height = this._canvas.height; From 8a3a687ca4bf754a1e54e9d9a1792f0e89b4802f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:28:18 -0800 Subject: [PATCH 0655/3587] Expose gpu acceleration setting to users Fixes #238175 --- src/vs/editor/common/config/editorOptions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 3b85ec2f45af..d13bae1abbca 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -5827,12 +5827,12 @@ export const EditorOptions = { 'off' as 'off' | 'on', ['off', 'on'] as const, { - included: false, // Hide the setting from users while it's unstable + tags: ['experimental'], enumDescriptions: [ nls.localize('experimentalGpuAcceleration.off', "Use regular DOM-based rendering."), nls.localize('experimentalGpuAcceleration.on', "Use GPU acceleration."), ], - description: nls.localize('experimentalGpuAcceleration', "Controls whether to use the (very) experimental GPU acceleration to render the editor.") + description: nls.localize('experimentalGpuAcceleration', "Controls whether to use the experimental GPU acceleration to render the editor.") } )), experimentalWhitespaceRendering: register(new EditorStringEnumOption( From c2c7ab5a8848d5fe31a7cb6edf8c488d9a8e55fb Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 17 Jan 2025 15:05:33 -0600 Subject: [PATCH 0656/3587] add suggest setting `WindowsExecutableExtensions`, default values (#238155) --- .../src/helpers/executable.ts | 23 ++++++++++++-- .../src/terminalSuggestMain.ts | 16 ++++++++-- .../common/terminalSuggestConfiguration.ts | 31 +++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/extensions/terminal-suggest/src/helpers/executable.ts b/extensions/terminal-suggest/src/helpers/executable.ts index 32c6a3d05a14..20f936fb5b12 100644 --- a/extensions/terminal-suggest/src/helpers/executable.ts +++ b/extensions/terminal-suggest/src/helpers/executable.ts @@ -6,9 +6,10 @@ import { osIsWindows } from './os'; import * as fs from 'fs/promises'; -export async function isExecutable(filePath: string): Promise { +export async function isExecutable(filePath: string, configuredWindowsExecutableExtensions?: Object): Promise { if (osIsWindows()) { - return windowsExecutableExtensions.find(ext => filePath.endsWith(ext)) !== undefined; + const resolvedWindowsExecutableExtensions = resolveWindowsExecutableExtensions(configuredWindowsExecutableExtensions); + return resolvedWindowsExecutableExtensions.find(ext => filePath.endsWith(ext)) !== undefined; } try { const stats = await fs.stat(filePath); @@ -19,7 +20,23 @@ export async function isExecutable(filePath: string): Promise { return false; } } -const windowsExecutableExtensions: string[] = [ + +function resolveWindowsExecutableExtensions(configuredWindowsExecutableExtensions?: Object): string[] { + const resolvedWindowsExecutableExtensions: string[] = windowsDefaultExecutableExtensions; + const excluded = new Set(); + if (configuredWindowsExecutableExtensions) { + for (const [key, value] of Object.entries(configuredWindowsExecutableExtensions)) { + if (value === true) { + resolvedWindowsExecutableExtensions.push(key); + } else { + excluded.add(key); + } + } + } + return Array.from(new Set(resolvedWindowsExecutableExtensions)).filter(ext => !excluded.has(ext)); +} + +export const windowsDefaultExecutableExtensions: string[] = [ '.exe', // Executable file '.bat', // Batch file '.cmd', // Command script diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 4e4913c78ba7..662df64816d6 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -15,6 +15,8 @@ import { isExecutable } from './helpers/executable'; const isWindows = osIsWindows(); let cachedAvailableCommandsPath: string | undefined; +let cachedWindowsExecutableExtensions: Object | undefined; +const cachedWindowsExecutableExtensionsSettingId = 'terminal.integrated.suggest.windowsExecutableExtensions'; let cachedAvailableCommands: Set | undefined; const cachedBuiltinCommands: Map = new Map(); @@ -110,8 +112,18 @@ export async function activate(context: vscode.ExtensionContext) { return result.items; } }, '/', '\\')); -} + if (isWindows) { + cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration(cachedWindowsExecutableExtensionsSettingId); + context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(cachedWindowsExecutableExtensionsSettingId)) { + cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration(cachedWindowsExecutableExtensionsSettingId); + cachedAvailableCommands = undefined; + cachedAvailableCommandsPath = undefined; + } + })); + } +} /** * Adjusts the current working directory based on a given prefix if it is a folder. @@ -221,7 +233,7 @@ async function getCommandsInPath(env: { [key: string]: string | undefined } = pr const files = await vscode.workspace.fs.readDirectory(fileResource); for (const [file, fileType] of files) { const formattedPath = getFriendlyFilePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); - if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath)) { + if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath), cachedWindowsExecutableExtensions) { executables.add({ label: file, path: formattedPath }); labels.add(file); } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts index ac7e94ead6a7..6b2dd0618103 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts @@ -15,9 +15,29 @@ export const enum TerminalSuggestSettingId { RunOnEnter = 'terminal.integrated.suggest.runOnEnter', BuiltinCompletions = 'terminal.integrated.suggest.builtinCompletions', EnableExtensionCompletions = 'terminal.integrated.suggest.enableExtensionCompletions', + WindowsExecutableExtensions = 'terminal.integrated.suggest.windowsExecutableExtensions', Providers = 'terminal.integrated.suggest.providers', } +export const windowsDefaultExecutableExtensions: string[] = [ + 'exe', // Executable file + 'bat', // Batch file + 'cmd', // Command script + 'com', // Command file + + 'msi', // Windows Installer package + + 'ps1', // PowerShell script + + 'vbs', // VBScript file + 'js', // JScript file + 'jar', // Java Archive (requires Java runtime) + 'py', // Python script (requires Python interpreter) + 'rb', // Ruby script (requires Ruby interpreter) + 'pl', // Perl script (requires Perl interpreter) + 'sh', // Shell script (via WSL or third-party tools) +]; + export const terminalSuggestConfigSection = 'terminal.integrated.suggest'; export interface ITerminalSuggestConfiguration { @@ -106,4 +126,15 @@ export const terminalSuggestConfiguration: IStringDictionary `- ${extension}`).join('\n'), + ), + type: 'object', + default: {}, + tags: ['experimental'], + } }; + + From bc0427374f542d9c886f45d4391b180500829884 Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Fri, 17 Jan 2025 13:17:59 -0800 Subject: [PATCH 0657/3587] Support nb cell Inline Values after cell execution (#237995) * support inline vars after cell execution * dispose listeners and cleanup * clear all outputs -> clear inline values + setting mention * feedback --- .../notebookInlineVariables.ts | 342 ++++++++++++++++++ .../browser/controller/editActions.ts | 4 + .../notebook/browser/media/notebook.css | 7 + .../notebook/browser/notebook.contribution.ts | 6 + .../contrib/notebook/common/notebookCommon.ts | 1 + 5 files changed, 360 insertions(+) create mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts new file mode 100644 index 000000000000..702e3a94fc00 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts @@ -0,0 +1,342 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, CancellationTokenSource } from '../../../../../../base/common/cancellation.js'; +import { onUnexpectedExternalError } from '../../../../../../base/common/errors.js'; +import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../../../base/common/map.js'; +import { isEqual } from '../../../../../../base/common/resources.js'; +import { format, noBreakWhitespace } from '../../../../../../base/common/strings.js'; +import { Constants } from '../../../../../../base/common/uint.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { InlineValueContext, InlineValueText, InlineValueVariableLookup } from '../../../../../../editor/common/languages.js'; +import { IModelDeltaDecoration, InjectedTextCursorStops } from '../../../../../../editor/common/model.js'; +import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; +import { localize } from '../../../../../../nls.js'; +import { registerAction2 } from '../../../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IDebugService, State } from '../../../../debug/common/debug.js'; +import { NotebookSetting } from '../../../common/notebookCommon.js'; +import { ICellExecutionStateChangedEvent, INotebookExecutionStateService, NotebookExecutionType } from '../../../common/notebookExecutionStateService.js'; +import { INotebookKernelMatchResult, INotebookKernelService, VariablesResult } from '../../../common/notebookKernelService.js'; +import { INotebookActionContext, NotebookAction } from '../../controller/coreActions.js'; +import { ICellViewModel, INotebookEditor, INotebookEditorContribution } from '../../notebookBrowser.js'; +import { registerNotebookContribution } from '../../notebookEditorExtensions.js'; + +// value from debug, may need to keep an eye on and shorter to account for cells having a narrower viewport width +const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator. If exceeded ... is added + +const variableRegex = new RegExp( + '(?:[a-zA-Z_][a-zA-Z0-9_]*\\.)*' + //match any number of variable names separated by '.' + '[a-zA-Z_][a-zA-Z0-9_]*', //math variable name + 'g', +); + +class InlineSegment { + constructor(public column: number, public text: string) { + } +} + +export class NotebookInlineVariablesController extends Disposable implements INotebookEditorContribution { + + static readonly id: string = 'notebook.inlineVariablesController'; + + private cellDecorationIds = new Map(); + private cellContentListeners = new ResourceMap(); + + constructor( + private readonly notebookEditor: INotebookEditor, + @INotebookKernelService private readonly notebookKernelService: INotebookKernelService, + @INotebookExecutionStateService private readonly notebookExecutionStateService: INotebookExecutionStateService, + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IDebugService private readonly debugService: IDebugService, + ) { + super(); + + this._register(this.notebookExecutionStateService.onDidChangeExecution(async e => { + if (!this.configurationService.getValue(NotebookSetting.notebookInlineValues)) { + return; + } + + if (e.type === NotebookExecutionType.cell) { + await this.updateInlineVariables(e); + } + })); + } + + private async updateInlineVariables(event: ICellExecutionStateChangedEvent): Promise { + if (this.debugService.state !== State.Inactive) { + this._clearNotebookInlineDecorations(); + return; + } + + if (this.notebookEditor.textModel?.uri && isEqual(this.notebookEditor.textModel.uri, event.notebook)) { + return; + } + + if (event.changed) { // undefined -> execution was completed, so return on all else + return; + } + + const cell = this.notebookEditor.getCellByHandle(event.cellHandle); + if (!cell) { + return; + } + const model = await cell.resolveTextModel(); + if (!model) { + return; + } + + this.clearCellInlineDecorations(cell); + + const cts = new CancellationTokenSource(); + const inlineDecorations: IModelDeltaDecoration[] = []; + + if (this.languageFeaturesService.inlineValuesProvider.has(model)) { + // use extension based provider, borrowed from https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L679 + const lastLine = model.getLineCount(); + const lastColumn = model.getLineMaxColumn(lastLine); + const ctx: InlineValueContext = { + frameId: 0, // ignored, we won't have a stack from since not in a debug session + stoppedLocation: new Range(lastLine + 1, 0, lastLine + 1, 0) // executing cell by cell, so "stopped" location would just be the end of document + }; + + const providers = this.languageFeaturesService.inlineValuesProvider.ordered(model).reverse(); + const lineDecorations = new Map(); + + const fullCellRange = new Range(1, 1, lastLine, lastColumn); + + const promises = providers.flatMap(provider => Promise.resolve(provider.provideInlineValues(model, fullCellRange, ctx, cts.token)).then(async (result) => { + if (result) { + + let kernel: INotebookKernelMatchResult; + const kernelVars: VariablesResult[] = []; + if (result.some(iv => iv.type === 'variable')) { // if anyone will need a lookup, get vars now to avoid needing to do it multiple times + if (!this.notebookEditor.hasModel()) { + return; // should not happen, a cell will be executed + } + kernel = this.notebookKernelService.getMatchingKernel(this.notebookEditor.textModel); + const variables = kernel.selected?.provideVariables(event.notebook, undefined, 'named', 0, cts.token); + if (!variables) { + return; + } + for await (const v of variables) { + kernelVars.push(v); + } + } + + for (const iv of result) { + let text: string | undefined = undefined; + switch (iv.type) { + case 'text': + text = (iv as InlineValueText).text; + break; + case 'variable': { + const name = (iv as InlineValueVariableLookup).variableName; + if (!name) { + continue; // skip to next var, no valid name to lookup with + } + const value = kernelVars.find(v => v.name === name)?.value; + if (!value) { + continue; + } + text = format('{0} = {1}', name, value); + } + case 'expression': { + continue; // no active debug session, so evaluate would break + } + } + + if (text) { + const line = iv.range.startLineNumber; + let lineSegments = lineDecorations.get(line); + if (!lineSegments) { + lineSegments = []; + lineDecorations.set(line, lineSegments); + } + if (!lineSegments.some(iv => iv.text === text)) { // de-dupe + lineSegments.push(new InlineSegment(iv.range.startColumn, text)); + } + } + } + } + }, err => { + onUnexpectedExternalError(err); + })); + + await Promise.all(promises); + + // sort line segments and concatenate them into a decoration + lineDecorations.forEach((segments, line) => { + if (segments.length > 0) { + segments = segments.sort((a, b) => a.column - b.column); + const text = segments.map(s => s.text).join(', '); + inlineDecorations.push(...this.createNotebookInlineValueDecoration(line, text)); + + } + }); + + } else { // generic regex matching approach + if (!this.notebookEditor.hasModel()) { + return; // should not happen, a cell will be executed + } + const kernel = this.notebookKernelService.getMatchingKernel(this.notebookEditor.textModel); + const variables = kernel?.selected?.provideVariables(event.notebook, undefined, 'named', 0, CancellationToken.None); + if (!variables) { + return; + } + + const vars: VariablesResult[] = []; + for await (const v of variables) { + vars.push(v); + } + const varNames: string[] = vars.map(v => v.name); + + const document = cell.textModel; + if (!document) { + return; + } + + for (let i = 1; i <= document.getLineCount(); i++) { + const line = document.getLineContent(i); + + if (line.trimStart().startsWith('#')) { + continue; + } + for (let match = variableRegex.exec(line); match; match = variableRegex.exec(line)) { + const name = match[0]; + if (varNames.includes(name)) { + const inlineVal = name + ' = ' + vars.find(v => v.name === name)?.value; + inlineDecorations.push(...this.createNotebookInlineValueDecoration(i, inlineVal)); + } + } + } + } + + if (inlineDecorations.length > 0) { + this.updateCellInlineDecorations(cell, inlineDecorations); + this.initCellContentListener(cell); + } + } + + private updateCellInlineDecorations(cell: ICellViewModel, decorations: IModelDeltaDecoration[]) { + const oldDecorations = this.cellDecorationIds.get(cell) ?? []; + this.cellDecorationIds.set(cell, cell.deltaModelDecorations( + oldDecorations, + decorations + )); + } + + private initCellContentListener(cell: ICellViewModel) { + const cellModel = cell.textModel; + if (!cellModel) { + return; // should not happen + } + + this.cellContentListeners.set(cell.uri, cellModel.onDidChangeContent(() => { + this.clearCellInlineDecorations(cell); + })); + } + + private clearCellInlineDecorations(cell: ICellViewModel) { + const cellDecorations = this.cellDecorationIds.get(cell) ?? []; + if (cellDecorations) { + cell.deltaModelDecorations(cellDecorations, []); + this.cellDecorationIds.delete(cell); + } + + const listener = this.cellContentListeners.get(cell.uri); + if (listener) { + listener.dispose(); + this.cellContentListeners.delete(cell.uri); + } + } + + private _clearNotebookInlineDecorations() { + this.cellDecorationIds.forEach((_, cell) => { + this.clearCellInlineDecorations(cell); + }); + } + + public clearNotebookInlineDecorations() { + this._clearNotebookInlineDecorations(); + } + + // taken from /src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts + private createNotebookInlineValueDecoration(lineNumber: number, contentText: string, column = Constants.MAX_SAFE_SMALL_INTEGER): IModelDeltaDecoration[] { + // If decoratorText is too long, trim and add ellipses. This could happen for minified files with everything on a single line + if (contentText.length > MAX_INLINE_DECORATOR_LENGTH) { + contentText = contentText.substring(0, MAX_INLINE_DECORATOR_LENGTH) + '...'; + } + + return [ + { + range: { + startLineNumber: lineNumber, + endLineNumber: lineNumber, + startColumn: column, + endColumn: column + }, + options: { + description: 'nb-inline-value-decoration-spacer', + after: { + content: noBreakWhitespace, + cursorStops: InjectedTextCursorStops.None + }, + showIfCollapsed: true, + } + }, + { + range: { + startLineNumber: lineNumber, + endLineNumber: lineNumber, + startColumn: column, + endColumn: column + }, + options: { + description: 'nb-inline-value-decoration', + after: { + content: this.replaceWsWithNoBreakWs(contentText), + inlineClassName: 'nb-inline-value', + inlineClassNameAffectsLetterSpacing: true, + cursorStops: InjectedTextCursorStops.None + }, + showIfCollapsed: true, + } + }, + ]; + } + + private replaceWsWithNoBreakWs(str: string): string { + return str.replace(/[ \t]/g, noBreakWhitespace); + } + + override dispose(): void { + super.dispose(); + this._clearNotebookInlineDecorations(); + } +} + + +registerNotebookContribution(NotebookInlineVariablesController.id, NotebookInlineVariablesController); + +registerAction2(class ClearNotebookInlineValues extends NotebookAction { + constructor() { + super({ + id: 'notebook.clearAllInlineValues', + title: localize('clearAllInlineValues', 'Clear All Inline Values'), + }); + } + + override runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { + const editor = context.notebookEditor; + const controller = editor.getContribution(NotebookInlineVariablesController.id); + controller.clearNotebookInlineDecorations(); + return Promise.resolve(); + } + +}); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts index 1dc3bb5b7d7c..f613e034b6aa 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts @@ -40,6 +40,7 @@ import { INotebookKernelService } from '../../common/notebookKernelService.js'; import { ICellRange } from '../../common/notebookRange.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ILanguageDetectionService } from '../../../../services/languageDetection/common/languageDetectionWorkerService.js'; +import { NotebookInlineVariablesController } from '../contrib/notebookVariables/notebookInlineVariables.js'; const CLEAR_ALL_CELLS_OUTPUTS_COMMAND_ID = 'notebook.clearAllCellsOutputs'; const EDIT_CELL_COMMAND_ID = 'notebook.cell.edit'; @@ -338,6 +339,9 @@ registerAction2(class ClearAllCellOutputsAction extends NotebookAction { if (clearExecutionMetadataEdits.length) { context.notebookEditor.textModel.applyEdits(clearExecutionMetadataEdits, true, undefined, () => undefined, undefined, computeUndoRedo); } + + const controller = editor.getContribution(NotebookInlineVariablesController.id); + controller.clearNotebookInlineDecorations(); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 179fd0d62406..26e4ba6612f5 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -449,6 +449,13 @@ background-color: var(--vscode-notebook-symbolHighlightBackground) !important; } +/** Cell execution inline vars */ +.nb-inline-value { + background-color: var(--vscode-editorInlayHint-background); + color: var(--vscode-editorInlayHint-foreground) !important; + font-size: 90%; +} + /** Notebook Textual Selection Highlight */ .nb-selection-highlight { background-color: var(--vscode-editor-selectionBackground); diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 17568757a132..0c7358aeeb32 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -100,6 +100,7 @@ import './contrib/kernelDetection/notebookKernelDetection.js'; import './contrib/cellDiagnostics/cellDiagnostics.js'; import './contrib/multicursor/notebookMulticursor.js'; import './contrib/multicursor/notebookSelectionHighlight.js'; +import './contrib/notebookVariables/notebookInlineVariables.js'; // Diff Editor Contribution import './diff/notebookDiffActions.js'; @@ -1235,6 +1236,11 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: false }, + [NotebookSetting.notebookInlineValues]: { + markdownDescription: nls.localize('notebook.inlineValues.description', "Enable the showing of inline values within notebook code cells after cell execution. Values will remain until the cell is edited, re-executed, or explicitly cleared via the Clear All Outputs toolbar button or the `Notebook: Clear Inline Values` command. "), + type: 'boolean', + default: false + }, [NotebookSetting.cellFailureDiagnostics]: { markdownDescription: nls.localize('notebook.cellFailureDiagnostics', "Show available diagnostics for cell failures."), type: 'boolean', diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index d656f74241f9..e52053da2d71 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -1029,6 +1029,7 @@ export const NotebookSetting = { cellChat: 'notebook.experimental.cellChat', cellGenerate: 'notebook.experimental.generate', notebookVariablesView: 'notebook.variablesView', + notebookInlineValues: 'notebook.inlineValues', InteractiveWindowPromptToSave: 'interactiveWindow.promptToSaveOnClose', cellFailureDiagnostics: 'notebook.cellFailureDiagnostics', outputBackupSizeLimit: 'notebook.backup.sizeLimit', From 57e4810cae057869729a2d75991de8c692d9a1c7 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Fri, 17 Jan 2025 16:19:02 -0500 Subject: [PATCH 0658/3587] Expose shell type to extensions (#238071) * copy everything from #237624 * try to better word notes in proposed.d.ts * why is test being so flaky * try sending one more text * strictEqual only on isInteractedWith always fails * update the name as recommended * embed to make sure we are selecting event we are interested in as recommended * add node as part of TerminalShellType * getting type ..extHostTypes.TerminalShellType.Bash is not comparable to type ..vscode.TerminalShellType.Bash * just use one enum? * figured out how to get from extHostTypes * clean up --- .../src/singlefolder-tests/terminal.test.ts | 31 ++++++++++-- .../common/extensionsApiProposals.ts | 3 ++ .../api/browser/mainThreadTerminalService.ts | 8 ++++ .../workbench/api/common/extHost.api.impl.ts | 1 + .../workbench/api/common/extHost.protocol.ts | 3 +- .../api/common/extHostTerminalService.ts | 48 +++++++++++++++++-- src/vs/workbench/api/common/extHostTypes.ts | 16 +++++++ .../contrib/terminal/browser/terminal.ts | 1 + .../terminal/browser/terminalService.ts | 2 +- .../vscode.proposed.terminalShellType.d.ts | 40 ++++++++++++++++ 10 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.terminalShellType.d.ts diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index 7411d9c094a3..f43ba17975c6 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -231,19 +231,40 @@ import { assertNoRpc, poll } from '../utils'; }); }); - test('onDidChangeTerminalState should fire after writing to a terminal', async () => { + test('onDidChangeTerminalState should fire with isInteractedWith after writing to a terminal', async () => { const terminal = window.createTerminal(); - deepStrictEqual(terminal.state, { isInteractedWith: false }); + strictEqual(terminal.state.isInteractedWith, false); const eventState = await new Promise(r => { disposables.push(window.onDidChangeTerminalState(e => { - if (e === terminal) { + if (e === terminal && e.state.isInteractedWith) { r(e.state); } })); terminal.sendText('test'); }); - deepStrictEqual(eventState, { isInteractedWith: true }); - deepStrictEqual(terminal.state, { isInteractedWith: true }); + strictEqual(eventState.isInteractedWith, true); + await new Promise(r => { + disposables.push(window.onDidCloseTerminal(t => { + if (t === terminal) { + r(); + } + })); + terminal.dispose(); + }); + }); + + test('onDidChangeTerminalState should fire with shellType when created', async () => { + const terminal = window.createTerminal(); + if (terminal.state.shellType) { + return; + } + await new Promise(r => { + disposables.push(window.onDidChangeTerminalState(e => { + if (e === terminal && e.state.shellType) { + r(); + } + })); + }); await new Promise(r => { disposables.push(window.onDidCloseTerminal(t => { if (t === terminal) { diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 0ca98611fb8b..102841305a43 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -355,6 +355,9 @@ const _allApiProposals = { terminalShellEnv: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts', }, + terminalShellType: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalShellType.d.ts', + }, testObserver: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', }, diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index bf7e6953bf0b..9482ade46611 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -87,6 +87,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._store.add(_terminalService.onAnyInstanceTitleChange(instance => instance && this._onTitleChanged(instance.instanceId, instance.title))); this._store.add(_terminalService.onAnyInstanceDataInput(instance => this._proxy.$acceptTerminalInteraction(instance.instanceId))); this._store.add(_terminalService.onAnyInstanceSelectionChange(instance => this._proxy.$acceptTerminalSelection(instance.instanceId, instance.selection))); + this._store.add(_terminalService.onAnyInstanceShellTypeChanged(instance => this._onShellTypeChanged(instance.instanceId))); // Set initial ext host state for (const instance of this._terminalService.instances) { @@ -358,6 +359,13 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._proxy.$acceptTerminalTitleChange(terminalId, name); } + private _onShellTypeChanged(terminalId: number): void { + const terminalInstance = this._terminalService.getInstanceFromId(terminalId); + if (terminalInstance) { + this._proxy.$acceptTerminalShellType(terminalId, terminalInstance.shellType); + } + } + private _onTerminalDisposed(terminalInstance: ITerminalInstance): void { this._proxy.$acceptTerminalClosed(terminalInstance.instanceId, terminalInstance.exitCode, terminalInstance.exitReason ?? TerminalExitReason.Unknown); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 8e8e6d71054d..d058e61b0da9 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1667,6 +1667,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TerminalCompletionItem: extHostTypes.TerminalCompletionItem, TerminalCompletionItemKind: extHostTypes.TerminalCompletionItemKind, TerminalCompletionList: extHostTypes.TerminalCompletionList, + TerminalShellType: extHostTypes.TerminalShellType, TextDocumentSaveReason: extHostTypes.TextDocumentSaveReason, TextEdit: extHostTypes.TextEdit, SnippetTextEdit: extHostTypes.SnippetTextEdit, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f2b2d33f06bb..4728613e2ba1 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -43,7 +43,7 @@ import { AuthInfo, Credentials } from '../../../platform/request/common/request. import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from '../../../platform/telemetry/common/gdprTypings.js'; import { TelemetryLevel } from '../../../platform/telemetry/common/telemetry.js'; import { ISerializableEnvironmentDescriptionMap, ISerializableEnvironmentVariableCollection } from '../../../platform/terminal/common/environmentVariable.js'; -import { ICreateContributedTerminalProfileOptions, IProcessProperty, IProcessReadyWindowsPty, IShellLaunchConfigDto, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile, TerminalExitReason, TerminalLocation } from '../../../platform/terminal/common/terminal.js'; +import { ICreateContributedTerminalProfileOptions, IProcessProperty, IProcessReadyWindowsPty, IShellLaunchConfigDto, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile, TerminalExitReason, TerminalLocation, TerminalShellType } from '../../../platform/terminal/common/terminal.js'; import { ProvidedPortAttributes, TunnelCreationOptions, TunnelOptions, TunnelPrivacyId, TunnelProviderFeatures } from '../../../platform/tunnel/common/tunnel.js'; import { EditSessionIdentityMatch } from '../../../platform/workspace/common/editSessions.js'; import { WorkspaceTrustRequestOptions } from '../../../platform/workspace/common/workspaceTrust.js'; @@ -2416,6 +2416,7 @@ export interface ExtHostTerminalServiceShape { $acceptTerminalMaximumDimensions(id: number, cols: number, rows: number): void; $acceptTerminalInteraction(id: number): void; $acceptTerminalSelection(id: number, selection: string | undefined): void; + $acceptTerminalShellType(id: number, shellType: TerminalShellType | undefined): void; $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise; $acceptProcessAckDataEvent(id: number, charCount: number): void; $acceptProcessInput(id: number, data: string): void; diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 907fb7125e53..1860666282db 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -10,7 +10,7 @@ import { createDecorator } from '../../../platform/instantiation/common/instanti import { URI } from '../../../base/common/uri.js'; import { IExtHostRpcService } from './extHostRpcService.js'; import { IDisposable, DisposableStore, Disposable, MutableDisposable } from '../../../base/common/lifecycle.js'; -import { Disposable as VSCodeDisposable, EnvironmentVariableMutatorType, TerminalExitReason, TerminalCompletionItem } from './extHostTypes.js'; +import { Disposable as VSCodeDisposable, EnvironmentVariableMutatorType, TerminalExitReason, TerminalCompletionItem, TerminalShellType as VSCodeTerminalShellType } from './extHostTypes.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { localize } from '../../../nls.js'; import { NotSupportedError } from '../../../base/common/errors.js'; @@ -18,7 +18,7 @@ import { serializeEnvironmentDescriptionMap, serializeEnvironmentVariableCollect import { CancellationTokenSource } from '../../../base/common/cancellation.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { IEnvironmentVariableCollectionDescription, IEnvironmentVariableMutator, ISerializableEnvironmentVariableCollection } from '../../../platform/terminal/common/environmentVariable.js'; -import { ICreateContributedTerminalProfileOptions, IProcessReadyEvent, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalLaunchError, ITerminalProfile, TerminalIcon, TerminalLocation, IProcessProperty, ProcessPropertyType, IProcessPropertyMap } from '../../../platform/terminal/common/terminal.js'; +import { ICreateContributedTerminalProfileOptions, IProcessReadyEvent, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalLaunchError, ITerminalProfile, TerminalIcon, TerminalLocation, IProcessProperty, ProcessPropertyType, IProcessPropertyMap, TerminalShellType, PosixShellType, WindowsShellType, GeneralShellType } from '../../../platform/terminal/common/terminal.js'; import { TerminalDataBufferer } from '../../../platform/terminal/common/terminalDataBuffering.js'; import { ThemeColor } from '../../../base/common/themables.js'; import { Promises } from '../../../base/common/async.js'; @@ -85,7 +85,7 @@ export class ExtHostTerminal extends Disposable { private _pidPromiseComplete: ((value: number | undefined) => any) | undefined; private _rows: number | undefined; private _exitStatus: vscode.TerminalExitStatus | undefined; - private _state: vscode.TerminalState = { isInteractedWith: false }; + private _state: vscode.TerminalState = { isInteractedWith: false, shellType: undefined }; private _selection: string | undefined; shellIntegration: vscode.TerminalShellIntegration | undefined; @@ -258,7 +258,40 @@ export class ExtHostTerminal extends Disposable { public setInteractedWith(): boolean { if (!this._state.isInteractedWith) { - this._state = { isInteractedWith: true }; + this._state = { + ...this._state, + isInteractedWith: true + }; + return true; + } + return false; + } + + public setShellType(shellType: TerminalShellType | undefined): boolean { + + let extHostType: VSCodeTerminalShellType | undefined; + + switch (shellType) { + case PosixShellType.Sh: extHostType = VSCodeTerminalShellType.Sh; break; + case PosixShellType.Bash: extHostType = VSCodeTerminalShellType.Bash; break; + case PosixShellType.Fish: extHostType = VSCodeTerminalShellType.Fish; break; + case PosixShellType.Csh: extHostType = VSCodeTerminalShellType.Csh; break; + case PosixShellType.Ksh: extHostType = VSCodeTerminalShellType.Ksh; break; + case PosixShellType.Zsh: extHostType = VSCodeTerminalShellType.Zsh; break; + case WindowsShellType.CommandPrompt: extHostType = VSCodeTerminalShellType.CommandPrompt; break; + case WindowsShellType.GitBash: extHostType = VSCodeTerminalShellType.GitBash; break; + case GeneralShellType.PowerShell: extHostType = VSCodeTerminalShellType.PowerShell; break; + case GeneralShellType.Python: extHostType = VSCodeTerminalShellType.Python; break; + case GeneralShellType.Julia: extHostType = VSCodeTerminalShellType.Julia; break; + case GeneralShellType.NuShell: extHostType = VSCodeTerminalShellType.NuShell; break; + default: extHostType = undefined; break; + } + + if (this._state.shellType !== shellType) { + this._state = { + ...this._state, + shellType: extHostType + }; return true; } return false; @@ -765,6 +798,13 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I return completions; } + public $acceptTerminalShellType(id: number, shellType: TerminalShellType | undefined): void { + const terminal = this.getTerminalById(id); + if (terminal?.setShellType(shellType)) { + this._onDidChangeTerminalState.fire(terminal.value); + } + } + public registerTerminalQuickFixProvider(id: string, extensionId: string, provider: vscode.TerminalQuickFixProvider): vscode.Disposable { if (this._quickFixProviders.has(id)) { throw new Error(`Terminal quick fix provider "${id}" is already registered`); diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index abb6a3e737b2..748ecb99ad62 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2072,6 +2072,22 @@ export enum TerminalShellExecutionCommandLineConfidence { High = 2 } +export enum TerminalShellType { + Sh = 1, + Bash = 2, + Fish = 3, + Csh = 4, + Ksh = 5, + Zsh = 6, + CommandPrompt = 7, + GitBash = 8, + PowerShell = 9, + Python = 10, + Julia = 11, + NuShell = 12, + Node = 13 +} + export class TerminalLink implements vscode.TerminalLink { constructor( public startIndex: number, diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index a860d7ae9746..7e65338102bc 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -276,6 +276,7 @@ export interface ITerminalService extends ITerminalInstanceHost { readonly onAnyInstanceProcessIdReady: Event; readonly onAnyInstanceSelectionChange: Event; readonly onAnyInstanceTitleChange: Event; + readonly onAnyInstanceShellTypeChanged: Event; /** * Creates a terminal. diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 7fea79a51a70..0aa49202b037 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -165,7 +165,7 @@ export class TerminalService extends Disposable implements ITerminalService { @memoize get onAnyInstanceProcessIdReady() { return this._register(this.createOnInstanceEvent(e => e.onProcessIdReady)).event; } @memoize get onAnyInstanceSelectionChange() { return this._register(this.createOnInstanceEvent(e => e.onDidChangeSelection)).event; } @memoize get onAnyInstanceTitleChange() { return this._register(this.createOnInstanceEvent(e => e.onTitleChanged)).event; } - + @memoize get onAnyInstanceShellTypeChanged() { return this._register(this.createOnInstanceEvent(e => Event.map(e.onDidChangeShellType, () => e))).event; } constructor( @IContextKeyService private _contextKeyService: IContextKeyService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, diff --git a/src/vscode-dts/vscode.proposed.terminalShellType.d.ts b/src/vscode-dts/vscode.proposed.terminalShellType.d.ts new file mode 100644 index 000000000000..e76defc65683 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.terminalShellType.d.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/230165 + + /** + * Known terminal shell types. + */ + export enum TerminalShellType { + Sh = 1, + Bash = 2, + Fish = 3, + Csh = 4, + Ksh = 5, + Zsh = 6, + CommandPrompt = 7, + GitBash = 8, + PowerShell = 9, + Python = 10, + Julia = 11, + NuShell = 12, + Node = 13 + } + + // Part of TerminalState since the shellType can change multiple times and this comes with an event. + export interface TerminalState { + /** + * The current detected shell type of the terminal. New shell types may be added in the + * future in which case they will be returned as a number that is not part of + * {@link TerminalShellType}. + * Includes number type to prevent the breaking change when new enum members are added? + */ + readonly shellType?: TerminalShellType | number | undefined; + } + +} From 2a5b54b71baab8dffc16468535c73a4b44168a9e Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Fri, 17 Jan 2025 13:22:53 -0800 Subject: [PATCH 0659/3587] Include commands from default keybindings in `_getAllCommands` (#238163) --- .../preferences/browser/preferencesActions.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts b/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts index 4af21a7e964e..46fa025a87ee 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts @@ -19,6 +19,7 @@ import { MenuId, MenuRegistry, isIMenuItem } from '../../../../platform/actions/ import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { isLocalizedString } from '../../../../platform/action/common/action.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { KeybindingsRegistry } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; export class ConfigureLanguageBasedSettingsAction extends Action { @@ -119,6 +120,27 @@ CommandsRegistry.registerCommand('_getAllCommands', function (accessor, filterBy }); } } + for (const command of KeybindingsRegistry.getDefaultKeybindings()) { + if (filterByPrecondition && !contextKeyService.contextMatchesRules(command.when ?? undefined)) { + continue; + } + + const keybinding = keybindingService.lookupKeybinding(command.command ?? ''); + if (!keybinding) { + continue; + } + + if (actions.some(a => a.command === command.command)) { + continue; + } + actions.push({ + command: command.command ?? '', + label: command.command ?? '', + keybinding: keybinding?.getLabel() ?? 'Not set', + precondition: command.when?.serialize() + }); + } + return actions; }); //#endregion From c919ff1f2cefaefe148acfaf4b657dd3602ac39c Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 17 Jan 2025 16:34:48 -0500 Subject: [PATCH 0660/3587] aim to better handle - in vsc_escape_value in hex, similar to other escapes --- .../terminal/common/scripts/shellIntegration-rc.zsh | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index 737eca2804a7..75ab0ceb44d5 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -75,13 +75,15 @@ __vsc_escape_value() { for (( i = 0; i < ${#str}; ++i )); do byte="${str:$i:1}" - # Escape backslashes, semi-colons and newlines + # Escape backslashes, semi-colons, newlines, and hyphens. if [ "$byte" = "\\" ]; then token="\\\\" elif [ "$byte" = ";" ]; then token="\\x3b" elif [ "$byte" = $'\n' ]; then token="\x0a" + elif [ "$byte" = "-" ]; then + token="\\x2d" else token="$byte" fi @@ -120,11 +122,6 @@ __vsc_update_env() { for var in ${(k)parameters}; do if printenv "$var" >/dev/null 2>&1; then value=$(builtin printf '%s' "${(P)var}") - # It is not valid to have hypen in variable name - # It is not valid to have hypen to start variable value - if [[ "$value" == -* || "$var" == *-* ]]; then - continue - fi builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$var" "$(__vsc_escape_value "$value")" $__vsc_nonce fi done From d427cae0d59309c8bc367d8f93fb8c6e91b8e7de Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 17 Jan 2025 15:43:06 -0600 Subject: [PATCH 0661/3587] revert pressing delete should remove a file from working set (#238177) revert #237513 --- .../contrib/chat/browser/chatEditing/chatEditingActions.ts | 5 ----- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 6 ------ src/vs/workbench/contrib/chat/common/chatContextKeys.ts | 1 - 3 files changed, 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 57df67d6ed88..3abcbda8766b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -127,11 +127,6 @@ registerAction2(class RemoveFileFromWorkingSet extends WorkingSetAction { order: 5, group: 'navigation' }], - keybinding: { - primary: KeyCode.Delete, - when: ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), ChatContextKeys.inChatEditWorkingSet), - weight: KeybindingWeight.WorkbenchContrib, - } }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 7cac98241cf9..7986317212ae 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -155,8 +155,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge readonly onDidAcceptFollowup = this._onDidAcceptFollowup.event; private readonly _attachmentModel: ChatAttachmentModel; - private _inChatEditWorkingSetCtx: IContextKey | undefined; - public get attachmentModel(): ChatAttachmentModel { return this._attachmentModel; } @@ -689,8 +687,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ChatContextKeys.inChatInput.bindTo(inputScopedContextKeyService).set(true); const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, inputScopedContextKeyService]))); - this._inChatEditWorkingSetCtx = ChatContextKeys.inChatEditWorkingSet.bindTo(this.contextKeyService); - const { historyNavigationBackwardsEnablement, historyNavigationForwardsEnablement } = this._register(registerAndCreateHistoryNavigationContext(inputScopedContextKeyService, this)); this.historyNavigationBackwardsEnablement = historyNavigationBackwardsEnablement; this.historyNavigationForewardsEnablement = historyNavigationForwardsEnablement; @@ -1320,8 +1316,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Working set const workingSetContainer = innerContainer.querySelector('.chat-editing-session-list') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-list')); - this._register(addDisposableListener(workingSetContainer, 'focusin', () => this._inChatEditWorkingSetCtx?.set(true))); - this._register(addDisposableListener(workingSetContainer, 'focusout', () => this._inChatEditWorkingSetCtx?.set(false))); if (!this._chatEditList) { this._chatEditList = this._chatEditsListPool.get(); const list = this._chatEditList.object; diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 1b52263721a1..3108cd8be7ba 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -28,7 +28,6 @@ export namespace ChatContextKeys { export const inputHasFocus = new RawContextKey('chatInputHasFocus', false, { type: 'boolean', description: localize('interactiveInputHasFocus', "True when the chat input has focus.") }); export const inChatInput = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); export const inChatSession = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); - export const inChatEditWorkingSet = new RawContextKey('inChatEditWorkingSet', false, { type: 'boolean', description: localize('inChatEditWorkingSet', "True when focus is in the chat edit working set, false otherwise.") }); export const instructionsAttached = new RawContextKey('chatInstructionsAttached', false, { type: 'boolean', description: localize('chatInstructionsAttachedContextDescription', "True when the chat has a prompt instructions attached.") }); export const supported = ContextKeyExpr.or(IsWebContext.toNegated(), RemoteNameContext.notEqualsTo('')); // supported on desktop and in web only with a remote connection From 0d57cf5304672f3cd0c2776aaec1e5dedfead053 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 17 Jan 2025 15:55:59 -0600 Subject: [PATCH 0662/3587] add aria label for rm suggestion button (#238185) fix #238182 --- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 7986317212ae..ef074cc4d93a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -1400,7 +1400,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const rmBtn = this._chatEditsActionsDisposables.add(new Button(addFilesElement, { supportIcons: false, secondary: true, - hoverDelegate + hoverDelegate, + ariaLabel: localize('chatEditingSession.removeSuggestion', 'Remove suggestion {0}', this.labelService.getUriLabel(uri, { relative: true })), })); rmBtn.icon = Codicon.close; rmBtn.setTitle(localize('chatEditingSession.removeSuggested', 'Remove suggestion')); From f672ef982e585803eb25aeb64dcf314a225c0f34 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 17 Jan 2025 16:33:30 -0600 Subject: [PATCH 0663/3587] fix aria label override bug (#238186) fix #238183 --- .../chat/browser/attachments/implicitContextAttachment.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts index dfff8cee32bb..73ab2960823f 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts @@ -79,7 +79,6 @@ export class ImplicitContextAttachmentWidget extends Disposable { this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), hintElement, title)); const buttonMsg = this.attachment.enabled ? localize('disable', "Disable current file context") : localize('enable', "Enable current file context"); - this.domNode.ariaLabel = buttonMsg; const toggleButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: buttonMsg })); toggleButton.icon = this.attachment.enabled ? Codicon.eye : Codicon.eyeClosed; this.renderDisposables.add(toggleButton.onDidClick((e) => { From 4c84c14479e618c62838648be5fa625328fff42b Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Fri, 17 Jan 2025 15:30:44 -0800 Subject: [PATCH 0664/3587] add capabilities API and image warning (#238103) * bump distro * supports vision work * better promise handling, clean up if statements * more ui update * make capabilities optional * some cleanup * fix function --- .../api/common/extHostLanguageModels.ts | 1 + .../chatAttachmentsContentPart.ts | 50 ++++++++++------ .../contrib/chat/browser/chatInputPart.ts | 58 +++++++++++-------- .../contrib/chat/common/languageModels.ts | 3 + .../vscode.proposed.chatProvider.d.ts | 3 + 5 files changed, 74 insertions(+), 41 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index 9264a42d2899..7ae740ff9fa5 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -180,6 +180,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { targetExtensions: metadata.extensions, isDefault: metadata.isDefault, isUserSelectable: metadata.isUserSelectable, + capabilities: metadata.capabilities, }); const responseReceivedListener = provider.onDidReceiveLanguageModelResponse2?.(({ extensionId, participant, tokenCount }) => { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index 591413c9501a..5fcd5d455c9d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -7,6 +7,7 @@ import * as dom from '../../../../../base/browser/dom.js'; import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; import { IManagedHoverTooltipMarkdownString } from '../../../../../base/browser/ui/hover/hover.js'; import { createInstantHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { Promises } from '../../../../../base/common/async.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; @@ -78,6 +79,7 @@ export class ChatAttachmentsContentPart extends Disposable { this.attachedContextDisposables.clear(); const hoverDelegate = this.attachedContextDisposables.add(createInstantHoverDelegate()); + const attachmentInitPromises: Promise[] = []; 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; @@ -129,34 +131,41 @@ export class ChatAttachmentsContentPart extends Disposable { }); } else if (attachment.isImage) { ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name); - const hoverElement = dom.$('div.chat-attached-context-hover'); hoverElement.setAttribute('aria-label', ariaLabel); // Custom label - const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-file-media')); + const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$(isAttachmentOmitted ? 'span.codicon.codicon-warning' : 'span.codicon.codicon-file-media')); const textLabel = dom.$('span.chat-attached-context-custom-text', {}, attachment.name); widget.appendChild(pillIcon); widget.appendChild(textLabel); - let buffer: Uint8Array; - try { - if (attachment.value instanceof URI) { - const readFile = await this.fileService.readFile(attachment.value); - buffer = readFile.value.buffer; - - } else { - buffer = attachment.value as Uint8Array; - } - await this.createImageElements(buffer, widget, hoverElement); - } catch (error) { - console.error('Error processing attachment:', error); + if (isAttachmentPartialOrOmitted) { + hoverElement.textContent = localize('chat.imageAttachmentHover', "Image was not sent to the model."); + textLabel.style.textDecoration = 'line-through'; + this.attachedContextDisposables.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: true })); + } else { + attachmentInitPromises.push(Promises.withAsyncBody(async (resolve) => { + let buffer: Uint8Array; + try { + if (attachment.value instanceof URI) { + const readFile = await this.fileService.readFile(attachment.value); + if (this.attachedContextDisposables.isDisposed) { + return; + } + buffer = readFile.value.buffer; + } else { + buffer = attachment.value as Uint8Array; + } + this.createImageElements(buffer, widget, hoverElement); + } catch (error) { + console.error('Error processing attachment:', error); + } + this.attachedContextDisposables.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: false })); + resolve(); + })); } - widget.style.position = 'relative'; - if (!this.attachedContextDisposables.isDisposed) { - this.attachedContextDisposables.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement)); - } } else if (isPasteVariableEntry(attachment)) { ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); @@ -219,6 +228,11 @@ export class ChatAttachmentsContentPart extends Disposable { } } + await Promise.all(attachmentInitPromises); + if (this.attachedContextDisposables.isDisposed) { + return; + } + if (resource) { widget.style.cursor = 'pointer'; if (!this.attachedContextDisposables.isDisposed) { diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index ef074cc4d93a..932c62a3c4e5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -442,6 +442,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } + private supportsVision(): boolean { + const model = this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel) : undefined; + return model?.capabilities?.vision ?? false; + } + private setCurrentLanguageModelToDefault() { const defaultLanguageModel = this.languageModelsService.getLanguageModelIds().find(id => this.languageModelsService.lookupLanguageModel(id)?.isDefault); const hasUserSelectableLanguageModels = this.languageModelsService.getLanguageModelIds().find(id => { @@ -792,6 +797,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge onDidChangeModel: this._onDidChangeCurrentLanguageModel.event, setModel: (modelId: string) => { this.setCurrentLanguageModelByUser(modelId); + this.renderAttachedContext(); } }; return this.instantiationService.createInstance(ModelPickerActionViewItem, action, this._currentLanguageModel, itemDelegate, { hoverDelegate: options.hoverDelegate, keybinding: options.keybinding ?? undefined }); @@ -930,38 +936,44 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } else if (attachment.isImage) { ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name); - const hoverElement = dom.$('div.chat-attached-context-hover'); hoverElement.setAttribute('aria-label', ariaLabel); - // Custom label - const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-file-media')); + const supportsVision = this.supportsVision(); + const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$(supportsVision ? 'span.codicon.codicon-file-media' : 'span.codicon.codicon-warning')); const textLabel = dom.$('span.chat-attached-context-custom-text', {}, attachment.name); widget.appendChild(pillIcon); widget.appendChild(textLabel); - attachmentInitPromises.push(Promises.withAsyncBody(async (resolve) => { - let buffer: Uint8Array; - try { - this.attachButtonAndDisposables(widget, index, attachment, hoverDelegate); - if (attachment.value instanceof URI) { - const readFile = await this.fileService.readFile(attachment.value); - if (store.isDisposed) { - return; + if (!supportsVision) { + widget.classList.add('warning'); + hoverElement.textContent = localize('chat.imageAttachmentHover', "{0} does not support images.", this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel)?.name : this.currentLanguageModel); + textLabel.style.textDecoration = 'line-through'; + store.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: true })); + this.attachButtonAndDisposables(widget, index, attachment, hoverDelegate); + } else { + attachmentInitPromises.push(Promises.withAsyncBody(async (resolve) => { + let buffer: Uint8Array; + try { + this.attachButtonAndDisposables(widget, index, attachment, hoverDelegate); + if (attachment.value instanceof URI) { + const readFile = await this.fileService.readFile(attachment.value); + if (store.isDisposed) { + return; + } + buffer = readFile.value.buffer; + } else { + buffer = attachment.value as Uint8Array; } - buffer = readFile.value.buffer; - } else { - buffer = attachment.value as Uint8Array; + this.createImageElements(buffer, widget, hoverElement); + } catch (error) { + console.error('Error processing attachment:', error); } - this.createImageElements(buffer, widget, hoverElement); - } catch (error) { - console.error('Error processing attachment:', error); - } - - widget.style.position = 'relative'; - store.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: false })); - resolve(); - })); + store.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: false })); + resolve(); + })); + } + widget.style.position = 'relative'; } else if (isPasteVariableEntry(attachment)) { ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 66fee6ef6c27..3a64fc746da0 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -86,6 +86,9 @@ export interface ILanguageModelChatMetadata { readonly providerLabel: string; readonly accountLabel?: string; }; + readonly capabilities?: { + readonly vision?: boolean; + }; } export interface ILanguageModelChatResponse { diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index 851a0f2ad036..d00a42a22afb 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -68,6 +68,9 @@ declare module 'vscode' { // TODO@API maybe an enum, LanguageModelChatProviderPickerAvailability? readonly isDefault?: boolean; readonly isUserSelectable?: boolean; + readonly capabilities?: { + readonly vision?: boolean; + }; } export interface ChatResponseProviderMetadata { From 909c1538c2d6c7e08e0842d65f7c38e7e44fe287 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 17 Jan 2025 19:51:28 -0800 Subject: [PATCH 0665/3587] Respond to layout changes (#238189) * Respond to layout changes This seems to work fairly well. I think a follow up change I'll make is maybe redoing this to leverage CSS variables instead... like how it's done in the debugToolBar which looks cleaner, but that can come later. Fixes #237627 * use `onDidLayoutContainer` --- .../browser/quickInputController.ts | 96 +++++++++++-------- 1 file changed, 55 insertions(+), 41 deletions(-) diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index b9ff1f3ad472..68c9a9cd8089 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -908,6 +908,7 @@ class QuickInputDragAndDropController extends Disposable { @IContextKeyService private readonly _contextKeyService: IContextKeyService ) { super(); + this._registerLayoutListener(); this.registerMouseListeners(); } @@ -936,9 +937,24 @@ class QuickInputDragAndDropController extends Disposable { } } + private _registerLayoutListener() { + this._layoutService.onDidLayoutContainer((e) => { + if (e.container !== this._container) { + return; + } + const state = this.dndViewState.get(); + const dragAreaRect = this._quickInputContainer.getBoundingClientRect(); + if (state?.top && state?.left) { + const a = Math.round(state.left * 1e2) / 1e2; + const b = e.dimension.width; + const c = dragAreaRect.width; + const d = a * b - c / 2; + this._layout(state.top * e.dimension.height, d); + } + }); + } + private registerMouseListeners(): void { - let top: number | undefined; - let left: number | undefined; const dragArea = this._quickInputContainer; // Double click @@ -953,10 +969,7 @@ class QuickInputDragAndDropController extends Disposable { return; } - top = undefined; - left = undefined; - - this.dndViewState.set({ top, left, done: true }, undefined); + this.dndViewState.set({ top: undefined, left: undefined, done: true }, undefined); })); // Mouse down @@ -974,13 +987,7 @@ class QuickInputDragAndDropController extends Disposable { const dragOffsetX = originEvent.browserEvent.clientX - dragAreaRect.left; const dragOffsetY = originEvent.browserEvent.clientY - dragAreaRect.top; - // Snap lines let isMovingQuickInput = false; - const snapCoordinateYTop = this._getTopSnapValue(); - const snapCoordinateY = this._getCenterYSnapValue(); - const snapCoordinateX = this._getCenterXSnapValue(); - - // Mouse move const mouseMoveListener = dom.addDisposableGenericMouseMoveListener(activeWindow, (e: MouseEvent) => { const mouseMoveEvent = new StandardMouseEvent(activeWindow, e); mouseMoveEvent.preventDefault(); @@ -989,36 +996,8 @@ class QuickInputDragAndDropController extends Disposable { isMovingQuickInput = true; } - let topCoordinate = e.clientY - dragOffsetY; - // Make sure the quick input is not moved outside the container - topCoordinate = Math.max(0, Math.min(topCoordinate, this._container.clientHeight - this._quickInputContainer.clientHeight)); - const snappingToTop = Math.abs(topCoordinate - snapCoordinateYTop) < this._snapThreshold; - topCoordinate = snappingToTop ? snapCoordinateYTop : topCoordinate; - const snappingToCenter = Math.abs(topCoordinate - snapCoordinateY) < this._snapThreshold; - topCoordinate = snappingToCenter ? snapCoordinateY : topCoordinate; - top = topCoordinate / this._container.clientHeight; - - let leftCoordinate = e.clientX - dragOffsetX; - // Make sure the quick input is not moved outside the container - leftCoordinate = Math.max(0, Math.min(leftCoordinate, this._container.clientWidth - this._quickInputContainer.clientWidth)); - const snappingToCenterX = Math.abs(leftCoordinate - snapCoordinateX) < this._snapThreshold; - leftCoordinate = snappingToCenterX ? snapCoordinateX : leftCoordinate; - left = (leftCoordinate + (this._quickInputContainer.clientWidth / 2)) / this._container.clientWidth; - - this.dndViewState.set({ top, left, done: false }, undefined); - if (snappingToCenterX) { - if (snappingToTop) { - this._quickInputAlignmentContext.set('top'); - return; - } else if (snappingToCenter) { - this._quickInputAlignmentContext.set('center'); - return; - } - } - this._quickInputAlignmentContext.set(undefined); + this._layout(e.clientY - dragOffsetY, e.clientX - dragOffsetX); }); - - // Mouse up const mouseUpListener = dom.addDisposableGenericMouseUpListener(activeWindow, (e: MouseEvent) => { if (isMovingQuickInput) { // Save position @@ -1033,6 +1012,41 @@ class QuickInputDragAndDropController extends Disposable { })); } + private _layout(topCoordinate: number, leftCoordinate: number) { + const snapCoordinateYTop = this._getTopSnapValue(); + const snapCoordinateY = this._getCenterYSnapValue(); + const snapCoordinateX = this._getCenterXSnapValue(); + // Make sure the quick input is not moved outside the container + topCoordinate = Math.max(0, Math.min(topCoordinate, this._container.clientHeight - this._quickInputContainer.clientHeight)); + const snappingToTop = Math.abs(topCoordinate - snapCoordinateYTop) < this._snapThreshold; + topCoordinate = snappingToTop ? snapCoordinateYTop : topCoordinate; + const snappingToCenter = Math.abs(topCoordinate - snapCoordinateY) < this._snapThreshold; + topCoordinate = snappingToCenter ? snapCoordinateY : topCoordinate; + const top = topCoordinate / this._container.clientHeight; + + // Make sure the quick input is not moved outside the container + leftCoordinate = Math.max(0, Math.min(leftCoordinate, this._container.clientWidth - this._quickInputContainer.clientWidth)); + const snappingToCenterX = Math.abs(leftCoordinate - snapCoordinateX) < this._snapThreshold; + leftCoordinate = snappingToCenterX ? snapCoordinateX : leftCoordinate; + + const b = this._container.clientWidth; + const c = this._quickInputContainer.clientWidth; + const d = leftCoordinate; + const left = (d + c / 2) / b; + + this.dndViewState.set({ top, left, done: false }, undefined); + if (snappingToCenterX) { + if (snappingToTop) { + this._quickInputAlignmentContext.set('top'); + return; + } else if (snappingToCenter) { + this._quickInputAlignmentContext.set('center'); + return; + } + } + this._quickInputAlignmentContext.set(undefined); + } + private _getTopSnapValue() { return this._layoutService.activeContainerOffset.quickPickTop; } From 1600fb4c7604d571f34e58dcc2242eaafdd995de Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Sun, 19 Jan 2025 16:44:40 +0100 Subject: [PATCH 0666/3587] GitHub - avatar resolution improvements (#238239) * Add support for size * Add support for extracting user id from GitHub no-reply email addresses * Fix git blame email parsing * Extrat link into function --- extensions/git/src/api/git.d.ts | 11 +++- extensions/git/src/blame.ts | 19 ++++--- extensions/git/src/git.ts | 2 +- .../git/src/historyItemDetailsProvider.ts | 2 +- .../github/src/historyItemDetailsProvider.ts | 56 ++++++++++--------- extensions/github/src/links.ts | 4 ++ extensions/github/src/typings/git.d.ts | 11 +++- 7 files changed, 63 insertions(+), 42 deletions(-) diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 81e778dfa3a6..51cfd496fda7 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -326,14 +326,19 @@ export interface BranchProtectionProvider { provideBranchProtection(): BranchProtection[]; } -export interface AvatarQuery { - readonly commit: string; +export interface AvatarQueryCommit { + readonly hash: string; readonly authorName?: string; readonly authorEmail?: string; } +export interface AvatarQuery { + readonly commits: AvatarQueryCommit[]; + readonly size: number; +} + export interface SourceControlHistoryItemDetailsProvider { - provideAvatar(repository: Repository, query: AvatarQuery[]): ProviderResult>; + provideAvatar(repository: Repository, query: AvatarQuery): ProviderResult>; provideHoverCommands(repository: Repository): ProviderResult; provideMessageLinks(repository: Repository, message: string): ProviderResult; } diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index b407dbfc4e70..da145d8323f6 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -13,7 +13,9 @@ import { fromGitUri, isGitUri } from './uri'; import { emojify, ensureEmojis } from './emoji'; import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation } from './staging'; import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; -import { AvatarQuery } from './api/git'; +import { AvatarQuery, AvatarQueryCommit } from './api/git'; + +const AVATAR_SIZE = 20; function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean { return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive); @@ -219,13 +221,16 @@ export class GitBlameController { // Avatar const avatarQuery = { - commit: blameInformation.hash, - authorName: blameInformation.authorName, - authorEmail: blameInformation.authorEmail + commits: [{ + hash: blameInformation.hash, + authorName: blameInformation.authorName, + authorEmail: blameInformation.authorEmail + } satisfies AvatarQueryCommit], + size: AVATAR_SIZE } satisfies AvatarQuery; - const avatarResult = await provideSourceControlHistoryItemAvatar(this._model, repository, [avatarQuery]); - commitAvatar = avatarResult?.get(avatarQuery.commit); + const avatarResult = await provideSourceControlHistoryItemAvatar(this._model, repository, avatarQuery); + commitAvatar = avatarResult?.get(blameInformation.hash); } catch { } } @@ -250,7 +255,7 @@ export class GitBlameController { const authorName = commitInformation?.authorName ?? blameInformation.authorName; const authorEmail = commitInformation?.authorEmail ?? blameInformation.authorEmail; const authorDate = commitInformation?.authorDate ?? blameInformation.authorDate; - const avatar = commitAvatar ? `![${authorName}](${commitAvatar})` : '$(account)'; + const avatar = commitAvatar ? `![${authorName}](${commitAvatar}|width=${AVATAR_SIZE},height=${AVATAR_SIZE})` : '$(account)'; if (authorName) { if (authorEmail) { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 7e2fd062f888..4072e4ae7068 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1098,7 +1098,7 @@ function parseGitBlame(data: string): BlameInformation[] { authorName = line.substring('author '.length); } if (commitHash && line.startsWith('author-mail ')) { - authorEmail = line.substring('author-mail '.length); + authorEmail = line.substring('author-mail <'.length, line.length - 1); } if (commitHash && line.startsWith('author-time ')) { authorTime = Number(line.substring('author-time '.length)) * 1000; diff --git a/extensions/git/src/historyItemDetailsProvider.ts b/extensions/git/src/historyItemDetailsProvider.ts index c9928b824b9b..be0e2b337f8f 100644 --- a/extensions/git/src/historyItemDetailsProvider.ts +++ b/extensions/git/src/historyItemDetailsProvider.ts @@ -16,7 +16,7 @@ export interface ISourceControlHistoryItemDetailsProviderRegistry { export async function provideSourceControlHistoryItemAvatar( registry: ISourceControlHistoryItemDetailsProviderRegistry, repository: Repository, - query: AvatarQuery[] + query: AvatarQuery ): Promise | undefined> { for (const provider of registry.getSourceControlHistoryItemDetailsProviders()) { const result = await provider.provideAvatar(new ApiRepository(repository), query); diff --git a/extensions/github/src/historyItemDetailsProvider.ts b/extensions/github/src/historyItemDetailsProvider.ts index 6c4f5aa8adfb..fbf6ec93baec 100644 --- a/extensions/github/src/historyItemDetailsProvider.ts +++ b/extensions/github/src/historyItemDetailsProvider.ts @@ -5,11 +5,10 @@ import { authentication, Command, l10n, LogOutputChannel } from 'vscode'; import { Commit, Repository as GitHubRepository, Maybe } from '@octokit/graphql-schema'; -import { API, AvatarQuery, Repository, SourceControlHistoryItemDetailsProvider } from './typings/git'; +import { API, AvatarQuery, AvatarQueryCommit, Repository, SourceControlHistoryItemDetailsProvider } from './typings/git'; import { DisposableStore, getRepositoryDefaultRemote, getRepositoryDefaultRemoteUrl, getRepositoryFromUrl, groupBy, sequentialize } from './util'; import { AuthenticationError, getOctokitGraphql } from './auth'; - -const AVATAR_SIZE = 20; +import { getAvatarLink } from './links'; const ISSUE_EXPRESSION = /(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/g; @@ -22,7 +21,7 @@ const ASSIGNABLE_USERS_QUERY = ` login name email - avatarUrl(size: ${AVATAR_SIZE}) + avatarUrl } } } @@ -37,7 +36,7 @@ const COMMIT_AUTHOR_QUERY = ` author { name email - avatarUrl(size: ${AVATAR_SIZE}) + avatarUrl user { id login @@ -62,7 +61,12 @@ interface GitHubUser { readonly avatarUrl: string; } -function compareAvatarQuery(a: AvatarQuery, b: AvatarQuery): number { +function getUserIdFromNoReplyEmail(email: string | undefined): string | undefined { + const match = email?.match(/^([0-9]+)\+[^@]+@users\.noreply\.github\.com$/); + return match?.[1]; +} + +function compareAvatarQuery(a: AvatarQueryCommit, b: AvatarQueryCommit): number { // Email const emailComparison = (a.authorEmail ?? '').localeCompare(b.authorEmail ?? ''); if (emailComparison !== 0) { @@ -88,8 +92,8 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont })); } - async provideAvatar(repository: Repository, query: AvatarQuery[]): Promise | undefined> { - this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution for ${query.length} commit(s) in ${repository.rootUri.fsPath}.`); + async provideAvatar(repository: Repository, query: AvatarQuery): Promise | undefined> { + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution for ${query.commits.length} commit(s) in ${repository.rootUri.fsPath}.`); if (!this._enabled) { this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution is disabled.`); @@ -113,7 +117,7 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont } // Group the query by author - const authorQuery = groupBy(query, compareAvatarQuery); + const authorQuery = groupBy(query.commits, compareAvatarQuery); const results = new Map(); await Promise.all(authorQuery.map(async q => { @@ -128,29 +132,33 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont // Cache hit if (avatarUrl) { // Add avatar for each commit - for (const { commit } of q) { - results.set(commit, this._getAvatarUrl(avatarUrl, AVATAR_SIZE)); - } + q.forEach(({ hash }) => results.set(hash, avatarUrl)); return; } // Check if any of the commit are being tracked in the list // of known commits that have incomplte author information - if (q.some(({ commit }) => repositoryStore.commits.has(commit))) { - for (const { commit } of q) { - results.set(commit, undefined); - } + if (q.some(({ hash }) => repositoryStore.commits.has(hash))) { + q.forEach(({ hash }) => results.set(hash, undefined)); + return; + } + + // Try to extract the user identifier from GitHub no-reply emails + const userIdFromEmail = getUserIdFromNoReplyEmail(q[0].authorEmail); + if (userIdFromEmail) { + const avatarUrl = getAvatarLink(userIdFromEmail, query.size); + q.forEach(({ hash }) => results.set(hash, avatarUrl)); return; } // Get the commit details - const commitAuthor = await this._getCommitAuthor(descriptor, q[0].commit); + const commitAuthor = await this._getCommitAuthor(descriptor, q[0].hash); if (!commitAuthor) { // The commit has incomplete author information, so // we should not try to query the authors details again - for (const { commit } of q) { - repositoryStore.commits.add(commit); - results.set(commit, undefined); + for (const { hash } of q) { + repositoryStore.commits.add(hash); + results.set(hash, undefined); } return; } @@ -159,9 +167,7 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont repositoryStore.users.push(commitAuthor); // Add avatar for each commit - for (const { commit } of q) { - results.set(commit, this._getAvatarUrl(commitAuthor.avatarUrl, AVATAR_SIZE)); - } + q.forEach(({ hash }) => results.set(hash, `${commitAuthor.avatarUrl}&s=${query.size}`)); })); return results; @@ -296,10 +302,6 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont } } - private _getAvatarUrl(url: string, size: number): string { - return `${url}|height=${size},width=${size}`; - } - private _getRepositoryKey(descriptor: { owner: string; repo: string }): string { return `${descriptor.owner}/${descriptor.repo}`; } diff --git a/extensions/github/src/links.ts b/extensions/github/src/links.ts index fe97d1722496..fdcac0c5cfdc 100644 --- a/extensions/github/src/links.ts +++ b/extensions/github/src/links.ts @@ -176,6 +176,10 @@ export async function getLink(gitAPI: GitAPI, useSelection: boolean, shouldEnsur return `${uriWithoutFileSegments}${fileSegments}`; } +export function getAvatarLink(userId: string, size: number): string { + return `https://avatars.githubusercontent.com/u/${userId}?s=${size}`; +} + export function getBranchLink(url: string, branch: string, hostPrefix: string = 'https://github.com') { const repo = getRepositoryFromUrl(url); if (!repo) { diff --git a/extensions/github/src/typings/git.d.ts b/extensions/github/src/typings/git.d.ts index 7ad60e954001..e600b767c7cd 100644 --- a/extensions/github/src/typings/git.d.ts +++ b/extensions/github/src/typings/git.d.ts @@ -289,14 +289,19 @@ export interface BranchProtectionProvider { provideBranchProtection(): BranchProtection[]; } -export interface AvatarQuery { - readonly commit: string; +export interface AvatarQueryCommit { + readonly hash: string; readonly authorName?: string; readonly authorEmail?: string; } +export interface AvatarQuery { + readonly commits: AvatarQueryCommit[]; + readonly size: number; +} + export interface SourceControlHistoryItemDetailsProvider { - provideAvatar(repository: Repository, query: AvatarQuery[]): Promise | undefined>; + provideAvatar(repository: Repository, query: AvatarQuery): Promise | undefined>; provideHoverCommands(repository: Repository): Promise; provideMessageLinks(repository: Repository, message: string): Promise; } From 58145651f791272d683a293ca515d30dc0011bbc Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Sun, 19 Jan 2025 10:02:50 -0600 Subject: [PATCH 0667/3587] use type vs object (#238187) use object type instead --- extensions/terminal-suggest/src/helpers/executable.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/terminal-suggest/src/helpers/executable.ts b/extensions/terminal-suggest/src/helpers/executable.ts index 20f936fb5b12..e93c77819019 100644 --- a/extensions/terminal-suggest/src/helpers/executable.ts +++ b/extensions/terminal-suggest/src/helpers/executable.ts @@ -6,7 +6,7 @@ import { osIsWindows } from './os'; import * as fs from 'fs/promises'; -export async function isExecutable(filePath: string, configuredWindowsExecutableExtensions?: Object): Promise { +export async function isExecutable(filePath: string, configuredWindowsExecutableExtensions?: { [key: string]: boolean | undefined }): Promise { if (osIsWindows()) { const resolvedWindowsExecutableExtensions = resolveWindowsExecutableExtensions(configuredWindowsExecutableExtensions); return resolvedWindowsExecutableExtensions.find(ext => filePath.endsWith(ext)) !== undefined; @@ -21,7 +21,8 @@ export async function isExecutable(filePath: string, configuredWindowsExecutable } } -function resolveWindowsExecutableExtensions(configuredWindowsExecutableExtensions?: Object): string[] { + +function resolveWindowsExecutableExtensions(configuredWindowsExecutableExtensions?: { [key: string]: boolean | undefined }): string[] { const resolvedWindowsExecutableExtensions: string[] = windowsDefaultExecutableExtensions; const excluded = new Set(); if (configuredWindowsExecutableExtensions) { From d64829f0521905312eb65a59d54c1be374dd829e Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Sun, 19 Jan 2025 21:30:42 +0100 Subject: [PATCH 0668/3587] Git - add avatar to graph hover (#238246) --- extensions/git/src/historyProvider.ts | 19 +++++++++++++++++-- .../github/src/historyItemDetailsProvider.ts | 2 +- src/vs/workbench/api/browser/mainThreadSCM.ts | 10 ++++++---- .../workbench/api/common/extHost.protocol.ts | 1 + src/vs/workbench/api/common/extHostSCM.ts | 3 ++- .../contrib/scm/browser/scmHistoryViewPane.ts | 10 ++++++++-- .../workbench/contrib/scm/common/history.ts | 1 + .../vscode.proposed.scmHistoryProvider.d.ts | 3 ++- 8 files changed, 38 insertions(+), 11 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 6322a025a3f7..1a01ae4c05e1 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -8,11 +8,11 @@ import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider import { Repository, Resource } from './repository'; import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent, getCommitShortHash } from './util'; import { toGitUri } from './uri'; -import { Branch, LogOptions, Ref, RefType } from './api/git'; +import { AvatarQuery, AvatarQueryCommit, Branch, LogOptions, Ref, RefType } from './api/git'; import { emojify, ensureEmojis } from './emoji'; import { Commit } from './git'; import { OperationKind, OperationResult } from './operation'; -import { ISourceControlHistoryItemDetailsProviderRegistry, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; +import { ISourceControlHistoryItemDetailsProviderRegistry, provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; function toSourceControlHistoryItemRef(repository: Repository, ref: Ref): SourceControlHistoryItemRef { const rootUri = Uri.file(repository.root); @@ -267,6 +267,19 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec const commits = await this.repository.log({ ...logOptions, silent: true }); + // Avatars + const avatarQuery = { + commits: commits.map(c => ({ + hash: c.hash, + authorName: c.authorName, + authorEmail: c.authorEmail + } satisfies AvatarQueryCommit)), + size: 20 + } satisfies AvatarQuery; + + const commitAvatars = await provideSourceControlHistoryItemAvatar( + this.historyItemDetailProviderRegistry, this.repository, avatarQuery); + await ensureEmojis(); const historyItems: SourceControlHistoryItem[] = []; @@ -280,6 +293,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec ? `${message.substring(0, newLineIndex)}\u2026` : message; + const avatarUrl = commitAvatars?.get(commit.hash); const references = this._resolveHistoryItemRefs(commit); historyItems.push({ @@ -289,6 +303,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec message: messageWithLinks, author: commit.authorName, authorEmail: commit.authorEmail, + authorIcon: avatarUrl ? Uri.parse(avatarUrl) : new ThemeIcon('account'), displayId: getCommitShortHash(Uri.file(this.repository.root), commit.hash), timestamp: commit.authorDate?.getTime(), statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, diff --git a/extensions/github/src/historyItemDetailsProvider.ts b/extensions/github/src/historyItemDetailsProvider.ts index fbf6ec93baec..7c09ba0c618d 100644 --- a/extensions/github/src/historyItemDetailsProvider.ts +++ b/extensions/github/src/historyItemDetailsProvider.ts @@ -132,7 +132,7 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont // Cache hit if (avatarUrl) { // Add avatar for each commit - q.forEach(({ hash }) => results.set(hash, avatarUrl)); + q.forEach(({ hash }) => results.set(hash, `${avatarUrl}&s=${query.size}`)); return; } diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 2157d1ef5775..934aa9256752 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Barrier } from '../../../base/common/async.js'; -import { URI, UriComponents } from '../../../base/common/uri.js'; +import { isUriComponents, URI, UriComponents } from '../../../base/common/uri.js'; import { Event, Emitter } from '../../../base/common/event.js'; import { IObservable, observableValue, observableValueOpts, transaction } from '../../../base/common/observable.js'; import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from '../../../base/common/lifecycle.js'; @@ -34,10 +34,10 @@ import { ColorIdentifier } from '../../../platform/theme/common/colorUtils.js'; function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon): URI | { light: URI; dark: URI } | ThemeIcon | undefined { if (iconDto === undefined) { return undefined; - } else if (URI.isUri(iconDto)) { - return URI.revive(iconDto); } else if (ThemeIcon.isThemeIcon(iconDto)) { return iconDto; + } else if (isUriComponents(iconDto)) { + return URI.revive(iconDto); } else { const icon = iconDto as { light: UriComponents; dark: UriComponents }; return { light: URI.revive(icon.light), dark: URI.revive(icon.dark) }; @@ -45,11 +45,13 @@ function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; da } function toISCMHistoryItem(historyItemDto: SCMHistoryItemDto): ISCMHistoryItem { + const authorIcon = getIconFromIconDto(historyItemDto.authorIcon); + const references = historyItemDto.references?.map(r => ({ ...r, icon: getIconFromIconDto(r.icon) })); - return { ...historyItemDto, references }; + return { ...historyItemDto, authorIcon, references }; } function toISCMHistoryItemRef(historyItemRefDto?: SCMHistoryItemRefDto, color?: ColorIdentifier): ISCMHistoryItemRef | undefined { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 4728613e2ba1..8eee9d953f98 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1606,6 +1606,7 @@ export interface SCMHistoryItemDto { readonly message: string; readonly displayId?: string; readonly author?: string; + readonly authorIcon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; readonly authorEmail?: string; readonly timestamp?: number; readonly statistics?: { diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index ae2930c763f2..41684493051f 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -73,11 +73,12 @@ function getHistoryItemIconDto(icon: vscode.Uri | { light: vscode.Uri; dark: vsc } function toSCMHistoryItemDto(historyItem: vscode.SourceControlHistoryItem): SCMHistoryItemDto { + const authorIcon = getHistoryItemIconDto(historyItem.authorIcon); const references = historyItem.references?.map(r => ({ ...r, icon: getHistoryItemIconDto(r.icon) })); - return { ...historyItem, references }; + return { ...historyItem, authorIcon, references }; } function toSCMHistoryItemRefDto(historyItemRef?: vscode.SourceControlHistoryItemRef): SCMHistoryItemRefDto | undefined { diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 5544f8649389..0d20c1d6de1d 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -475,11 +475,17 @@ class HistoryItemRenderer implements ITreeRenderer Date: Sun, 19 Jan 2025 21:43:15 +0100 Subject: [PATCH 0669/3587] Timeline - enable paging by default (#238247) --- .../contrib/timeline/browser/timeline.contribution.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts b/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts index 5fffc20c8a00..535b637dd54d 100644 --- a/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts +++ b/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts @@ -51,13 +51,13 @@ configurationRegistry.registerConfiguration({ properties: { 'timeline.pageSize': { type: ['number', 'null'], - default: null, - markdownDescription: localize('timeline.pageSize', "The number of items to show in the Timeline view by default and when loading more items. Setting to `null` (the default) will automatically choose a page size based on the visible area of the Timeline view."), + default: 50, + markdownDescription: localize('timeline.pageSize', "The number of items to show in the Timeline view by default and when loading more items. Setting to `null` will automatically choose a page size based on the visible area of the Timeline view."), }, 'timeline.pageOnScroll': { type: 'boolean', - default: false, - description: localize('timeline.pageOnScroll', "Experimental. Controls whether the Timeline view will load the next page of items when you scroll to the end of the list."), + default: true, + description: localize('timeline.pageOnScroll', "Controls whether the Timeline view will load the next page of items when you scroll to the end of the list."), }, } }); From 0bccf5608baceb8fc7db0159f907e1d6f3296ad2 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Sun, 19 Jan 2025 21:53:03 +0100 Subject: [PATCH 0670/3587] Git - add avatar to timeline hover (#238248) --- extensions/git/src/timelineProvider.ts | 31 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 20daaebe2634..f29264e4078b 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -12,7 +12,10 @@ import { CommandCenter } from './commands'; import { OperationKind, OperationResult } from './operation'; import { getCommitShortHash } from './util'; import { CommitShortStat } from './git'; -import { provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; +import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; +import { AvatarQuery, AvatarQueryCommit } from './api/git'; + +const AVATAR_SIZE = 20; export class GitTimelineItem extends TimelineItem { static is(item: TimelineItem): item is GitTimelineItem { @@ -51,16 +54,20 @@ export class GitTimelineItem extends TimelineItem { return this.shortenRef(this.previousRef); } - setItemDetails(uri: Uri, hash: string | undefined, author: string, email: string | undefined, date: string, message: string, shortStat?: CommitShortStat, remoteSourceCommands: Command[] = []): void { + setItemDetails(uri: Uri, hash: string | undefined, avatar: string | undefined, author: string, email: string | undefined, date: string, message: string, shortStat?: CommitShortStat, remoteSourceCommands: Command[] = []): void { this.tooltip = new MarkdownString('', true); this.tooltip.isTrusted = true; this.tooltip.supportHtml = true; + const avatarMarkdown = avatar + ? `![${author}](${avatar}|width=${AVATAR_SIZE},height=${AVATAR_SIZE})` + : '$(account)'; + if (email) { const emailTitle = l10n.t('Email'); - this.tooltip.appendMarkdown(`$(account) [**${author}**](mailto:${email} "${emailTitle} ${author}")`); + this.tooltip.appendMarkdown(`${avatarMarkdown} [**${author}**](mailto:${email} "${emailTitle} ${author}")`); } else { - this.tooltip.appendMarkdown(`$(account) **${author}**`); + this.tooltip.appendMarkdown(`${avatarMarkdown} **${author}**`); } this.tooltip.appendMarkdown(`, $(history) ${date}\n\n`); @@ -218,6 +225,16 @@ export class GitTimelineProvider implements TimelineProvider { const unpublishedCommits = await repo.getUnpublishedCommits(); const remoteHoverCommands = await provideSourceControlHistoryItemHoverCommands(this.model, repo); + const avatarQuery = { + commits: commits.map(c => ({ + hash: c.hash, + authorName: c.authorName, + authorEmail: c.authorEmail + }) satisfies AvatarQueryCommit), + size: 20 + } satisfies AvatarQuery; + const avatars = await provideSourceControlHistoryItemAvatar(this.model, repo, avatarQuery); + const items: GitTimelineItem[] = []; for (let index = 0; index < commits.length; index++) { const c = commits[index]; @@ -235,7 +252,7 @@ export class GitTimelineProvider implements TimelineProvider { const commitRemoteSourceCommands = !unpublishedCommits.has(c.hash) ? remoteHoverCommands : []; const messageWithLinks = await provideSourceControlHistoryItemMessageLinks(this.model, repo, message) ?? message; - item.setItemDetails(uri, c.hash, c.authorName!, c.authorEmail, dateFormatter.format(date), messageWithLinks, c.shortStat, commitRemoteSourceCommands); + item.setItemDetails(uri, c.hash, avatars?.get(c.hash), c.authorName!, c.authorEmail, dateFormatter.format(date), messageWithLinks, c.shortStat, commitRemoteSourceCommands); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { @@ -260,7 +277,7 @@ export class GitTimelineProvider implements TimelineProvider { // TODO@eamodio: Replace with a better icon -- reflecting its status maybe? item.iconPath = new ThemeIcon('git-commit'); item.description = ''; - item.setItemDetails(uri, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(index.type)); + item.setItemDetails(uri, undefined, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(index.type)); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { @@ -282,7 +299,7 @@ export class GitTimelineProvider implements TimelineProvider { const item = new GitTimelineItem('', index ? '~' : 'HEAD', l10n.t('Uncommitted Changes'), date.getTime(), 'working', 'git:file:working'); item.iconPath = new ThemeIcon('circle-outline'); item.description = ''; - item.setItemDetails(uri, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(working.type)); + item.setItemDetails(uri, undefined, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(working.type)); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { From 3d0aeb47a2ecfde9ff5141470b30c36d41c321d9 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sun, 19 Jan 2025 17:06:03 -0800 Subject: [PATCH 0671/3587] Throw from tool instead of returning an error result so the extension can manage retrying (#238258) --- src/vs/workbench/contrib/chat/browser/tools/tools.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/tools.ts b/src/vs/workbench/contrib/chat/browser/tools/tools.ts index 5d8bc3d0f89f..bcf466e85171 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/tools.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/tools.ts @@ -104,17 +104,13 @@ class EditTool implements IToolImpl { } const parameters = invocation.parameters as EditToolParams; - if (!parameters.filePath || !parameters.explanation || !parameters.code) { - throw new Error(`Invalid tool input: ${JSON.stringify(parameters)}`); - } - const uri = URI.file(parameters.filePath); if (!this.workspaceContextService.isInsideWorkspace(uri)) { - return { content: [{ kind: 'text', value: `Error: file ${parameters.filePath} can't be edited because it's not inside the current workspace` }] }; + throw new Error(`File ${parameters.filePath} can't be edited because it's not inside the current workspace`); } if (await this.ignoredFilesService.fileIsIgnored(uri, token)) { - return { content: [{ kind: 'text', value: `Error: file ${parameters.filePath} can't be edited because it is configured to be ignored by Copilot` }] }; + throw new Error(`File ${parameters.filePath} can't be edited because it is configured to be ignored by Copilot`); } const model = this.chatService.getSession(invocation.context?.sessionId) as ChatModel; From 4b4cd6b702fcabef6b9ed7a474086779b5736c8d Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 20 Jan 2025 09:46:10 +0100 Subject: [PATCH 0672/3587] GitHub - add more logs to avatar resolution (#238266) --- extensions/github/src/branchProtection.ts | 16 +++++----- extensions/github/src/extension.ts | 4 +-- .../github/src/historyItemDetailsProvider.ts | 31 ++++++++++++------- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/extensions/github/src/branchProtection.ts b/extensions/github/src/branchProtection.ts index a7d18c41b9f8..52b4fbfae222 100644 --- a/extensions/github/src/branchProtection.ts +++ b/extensions/github/src/branchProtection.ts @@ -48,7 +48,7 @@ const REPOSITORY_RULESETS_QUERY = ` } `; -export class GithubBranchProtectionProviderManager { +export class GitHubBranchProtectionProviderManager { private readonly disposables = new DisposableStore(); private readonly providerDisposables = new DisposableStore(); @@ -61,7 +61,7 @@ export class GithubBranchProtectionProviderManager { if (enabled) { for (const repository of this.gitAPI.repositories) { - this.providerDisposables.add(this.gitAPI.registerBranchProtectionProvider(repository.rootUri, new GithubBranchProtectionProvider(repository, this.globalState, this.logger, this.telemetryReporter))); + this.providerDisposables.add(this.gitAPI.registerBranchProtectionProvider(repository.rootUri, new GitHubBranchProtectionProvider(repository, this.globalState, this.logger, this.telemetryReporter))); } } else { this.providerDisposables.dispose(); @@ -77,7 +77,7 @@ export class GithubBranchProtectionProviderManager { private readonly telemetryReporter: TelemetryReporter) { this.disposables.add(this.gitAPI.onDidOpenRepository(repository => { if (this._enabled) { - this.providerDisposables.add(gitAPI.registerBranchProtectionProvider(repository.rootUri, new GithubBranchProtectionProvider(repository, this.globalState, this.logger, this.telemetryReporter))); + this.providerDisposables.add(gitAPI.registerBranchProtectionProvider(repository.rootUri, new GitHubBranchProtectionProvider(repository, this.globalState, this.logger, this.telemetryReporter))); } })); @@ -102,7 +102,7 @@ export class GithubBranchProtectionProviderManager { } -export class GithubBranchProtectionProvider implements BranchProtectionProvider { +export class GitHubBranchProtectionProvider implements BranchProtectionProvider { private readonly _onDidChangeBranchProtection = new EventEmitter(); onDidChangeBranchProtection = this._onDidChangeBranchProtection.event; @@ -173,12 +173,12 @@ export class GithubBranchProtectionProvider implements BranchProtectionProvider } // Repository details - this.logger.trace(`Fetching repository details for "${repository.owner}/${repository.repo}".`); + this.logger.trace(`[GitHubBranchProtectionProvider][updateRepositoryBranchProtection] Fetching repository details for "${repository.owner}/${repository.repo}".`); const repositoryDetails = await this.getRepositoryDetails(repository.owner, repository.repo); // Check repository write permission if (repositoryDetails.viewerPermission !== 'ADMIN' && repositoryDetails.viewerPermission !== 'MAINTAIN' && repositoryDetails.viewerPermission !== 'WRITE') { - this.logger.trace(`Skipping branch protection for "${repository.owner}/${repository.repo}" due to missing repository write permission.`); + this.logger.trace(`[GitHubBranchProtectionProvider][updateRepositoryBranchProtection] Skipping branch protection for "${repository.owner}/${repository.repo}" due to missing repository write permission.`); continue; } @@ -201,7 +201,7 @@ export class GithubBranchProtectionProvider implements BranchProtectionProvider // Save branch protection to global state await this.globalState.update(this.globalStateKey, branchProtection); - this.logger.trace(`Branch protection for "${this.repository.rootUri.toString()}": ${JSON.stringify(branchProtection)}.`); + this.logger.trace(`[GitHubBranchProtectionProvider][updateRepositoryBranchProtection] Branch protection for "${this.repository.rootUri.toString()}": ${JSON.stringify(branchProtection)}.`); /* __GDPR__ "branchProtection" : { @@ -211,7 +211,7 @@ export class GithubBranchProtectionProvider implements BranchProtectionProvider */ this.telemetryReporter.sendTelemetryEvent('branchProtection', undefined, { rulesetCount: this.branchProtection.length }); } catch (err) { - this.logger.warn(`Failed to update repository branch protection: ${err.message}`); + this.logger.warn(`[GitHubBranchProtectionProvider][updateRepositoryBranchProtection] Failed to update repository branch protection: ${err.message}`); if (err instanceof AuthenticationError) { // A GitHub authentication session could be missing if the user has not yet diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts index 72a9111326e7..de0349ba9d3a 100644 --- a/extensions/github/src/extension.ts +++ b/extensions/github/src/extension.ts @@ -13,7 +13,7 @@ import { DisposableStore, repositoryHasGitHubRemote } from './util'; import { GithubPushErrorHandler } from './pushErrorHandler'; import { GitBaseExtension } from './typings/git-base'; import { GithubRemoteSourcePublisher } from './remoteSourcePublisher'; -import { GithubBranchProtectionProviderManager } from './branchProtection'; +import { GitHubBranchProtectionProviderManager } from './branchProtection'; import { GitHubCanonicalUriProvider } from './canonicalUriProvider'; import { VscodeDevShareProvider } from './shareProviders'; import { GitHubSourceControlHistoryItemDetailsProvider } from './historyItemDetailsProvider'; @@ -98,7 +98,7 @@ function initializeGitExtension(context: ExtensionContext, telemetryReporter: Te disposables.add(registerCommands(gitAPI)); disposables.add(new GithubCredentialProviderManager(gitAPI)); - disposables.add(new GithubBranchProtectionProviderManager(gitAPI, context.globalState, logger, telemetryReporter)); + disposables.add(new GitHubBranchProtectionProviderManager(gitAPI, context.globalState, logger, telemetryReporter)); disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler(telemetryReporter))); disposables.add(gitAPI.registerRemoteSourcePublisher(new GithubRemoteSourcePublisher(gitAPI))); disposables.add(gitAPI.registerSourceControlHistoryItemDetailsProvider(new GitHubSourceControlHistoryItemDetailsProvider(gitAPI, logger))); diff --git a/extensions/github/src/historyItemDetailsProvider.ts b/extensions/github/src/historyItemDetailsProvider.ts index 7c09ba0c618d..33383141f48b 100644 --- a/extensions/github/src/historyItemDetailsProvider.ts +++ b/extensions/github/src/historyItemDetailsProvider.ts @@ -106,7 +106,10 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont return undefined; } + try { + const logs = { cached: 0, email: 0, github: 0, incomplete: 0 }; + // Warm up the in-memory cache with the first page // (100 users) from this list of assignable users await this._loadAssignableUsers(descriptor); @@ -120,43 +123,46 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont const authorQuery = groupBy(query.commits, compareAvatarQuery); const results = new Map(); - await Promise.all(authorQuery.map(async q => { - if (q.length === 0) { + await Promise.all(authorQuery.map(async commits => { + if (commits.length === 0) { return; } // Query the in-memory cache for the user const avatarUrl = repositoryStore.users.find( - user => user.email === q[0].authorEmail || user.name === q[0].authorName)?.avatarUrl; + user => user.email === commits[0].authorEmail || user.name === commits[0].authorName)?.avatarUrl; // Cache hit if (avatarUrl) { // Add avatar for each commit - q.forEach(({ hash }) => results.set(hash, `${avatarUrl}&s=${query.size}`)); + logs.cached += commits.length; + commits.forEach(({ hash }) => results.set(hash, `${avatarUrl}&s=${query.size}`)); return; } // Check if any of the commit are being tracked in the list // of known commits that have incomplte author information - if (q.some(({ hash }) => repositoryStore.commits.has(hash))) { - q.forEach(({ hash }) => results.set(hash, undefined)); + if (commits.some(({ hash }) => repositoryStore.commits.has(hash))) { + commits.forEach(({ hash }) => results.set(hash, undefined)); return; } // Try to extract the user identifier from GitHub no-reply emails - const userIdFromEmail = getUserIdFromNoReplyEmail(q[0].authorEmail); + const userIdFromEmail = getUserIdFromNoReplyEmail(commits[0].authorEmail); if (userIdFromEmail) { + logs.email += commits.length; const avatarUrl = getAvatarLink(userIdFromEmail, query.size); - q.forEach(({ hash }) => results.set(hash, avatarUrl)); + commits.forEach(({ hash }) => results.set(hash, avatarUrl)); return; } // Get the commit details - const commitAuthor = await this._getCommitAuthor(descriptor, q[0].hash); + const commitAuthor = await this._getCommitAuthor(descriptor, commits[0].hash); if (!commitAuthor) { // The commit has incomplete author information, so // we should not try to query the authors details again - for (const { hash } of q) { + logs.incomplete += commits.length; + for (const { hash } of commits) { repositoryStore.commits.add(hash); results.set(hash, undefined); } @@ -167,9 +173,12 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont repositoryStore.users.push(commitAuthor); // Add avatar for each commit - q.forEach(({ hash }) => results.set(hash, `${commitAuthor.avatarUrl}&s=${query.size}`)); + logs.github += commits.length; + commits.forEach(({ hash }) => results.set(hash, `${commitAuthor.avatarUrl}&s=${query.size}`)); })); + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution for ${query.commits.length} commit(s) in ${repository.rootUri.fsPath} complete: ${JSON.stringify(logs)}.`); + return results; } catch (err) { // A GitHub authentication session could be missing if the user has not yet From 7667cdd1ed84e8d3250db28e264bcc31d85dd432 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 20 Jan 2025 10:32:51 +0100 Subject: [PATCH 0673/3587] GitHub - add setting to disable avatar resolution (#238270) --- extensions/github/package.json | 6 +++++ extensions/github/package.nls.json | 1 + .../github/src/historyItemDetailsProvider.ts | 23 ++++++++++++++----- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/extensions/github/package.json b/extensions/github/package.json index 55e04b308044..524cee5bbea6 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -195,6 +195,12 @@ ], "default": "https", "description": "%config.gitProtocol%" + }, + "github.showAvatar": { + "type": "boolean", + "scope": "resource", + "default": true, + "description": "%config.showAvatar%" } } } diff --git a/extensions/github/package.nls.json b/extensions/github/package.nls.json index bceb83403eff..40271bea980e 100644 --- a/extensions/github/package.nls.json +++ b/extensions/github/package.nls.json @@ -8,6 +8,7 @@ "config.branchProtection": "Controls whether to query repository rules for GitHub repositories", "config.gitAuthentication": "Controls whether to enable automatic GitHub authentication for git commands within VS Code.", "config.gitProtocol": "Controls which protocol is used to clone a GitHub repository", + "config.showAvatar": "Controls whether to show the GitHub avatar of the commit author in various hovers (ex: Git blame, Timeline, Source Control Graph, etc.)", "welcome.publishFolder": { "message": "You can directly publish this folder to a GitHub repository. Once published, you'll have access to source control features powered by Git and GitHub.\n[$(github) Publish to GitHub](command:github.publish)", "comment": [ diff --git a/extensions/github/src/historyItemDetailsProvider.ts b/extensions/github/src/historyItemDetailsProvider.ts index 33383141f48b..00a67bcebcd7 100644 --- a/extensions/github/src/historyItemDetailsProvider.ts +++ b/extensions/github/src/historyItemDetailsProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { authentication, Command, l10n, LogOutputChannel } from 'vscode'; +import { authentication, Command, l10n, LogOutputChannel, workspace } from 'vscode'; import { Commit, Repository as GitHubRepository, Maybe } from '@octokit/graphql-schema'; import { API, AvatarQuery, AvatarQueryCommit, Repository, SourceControlHistoryItemDetailsProvider } from './typings/git'; import { DisposableStore, getRepositoryDefaultRemote, getRepositoryDefaultRemoteUrl, getRepositoryFromUrl, groupBy, sequentialize } from './util'; @@ -78,7 +78,7 @@ function compareAvatarQuery(a: AvatarQueryCommit, b: AvatarQueryCommit): number } export class GitHubSourceControlHistoryItemDetailsProvider implements SourceControlHistoryItemDetailsProvider { - private _enabled = true; + private _isUserAuthenticated = true; private readonly _store = new Map(); private readonly _disposables = new DisposableStore(); @@ -87,16 +87,27 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont this._disposables.add(authentication.onDidChangeSessions(e => { if (e.provider.id === 'github') { - this._enabled = true; + this._isUserAuthenticated = true; } })); + + this._disposables.add(workspace.onDidChangeConfiguration(e => { + if (!e.affectsConfiguration('github.showAvatar')) { + return; + } + + this._store.clear(); + })); } async provideAvatar(repository: Repository, query: AvatarQuery): Promise | undefined> { this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution for ${query.commits.length} commit(s) in ${repository.rootUri.fsPath}.`); - if (!this._enabled) { - this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution is disabled.`); + const config = workspace.getConfiguration('github', repository.rootUri); + const showAvatar = config.get('showAvatar', true) === true; + + if (!this._isUserAuthenticated || !showAvatar) { + this._logger.trace(`[GitHubSourceControlHistoryItemDetailsProvider][provideAvatar] Avatar resolution is disabled. (${showAvatar === false ? 'setting' : 'auth'})`); return undefined; } @@ -185,7 +196,7 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont // signed in with their GitHub account or they have signed out. Disable the // avatar resolution until the user signes in with their GitHub account. if (err instanceof AuthenticationError) { - this._enabled = false; + this._isUserAuthenticated = false; } return undefined; From f6ec9aa2e67ab2c98e7bdb5bd1470bb23f7a1f98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Mon, 20 Jan 2025 10:39:21 +0100 Subject: [PATCH 0674/3587] bump katex (#238271) --- .../package-lock.json | 17 ++++++++++------- .../markdown-language-features/package.json | 2 +- extensions/markdown-math/package-lock.json | 8 +++++--- extensions/markdown-math/package.json | 10 ++++++---- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/extensions/markdown-language-features/package-lock.json b/extensions/markdown-language-features/package-lock.json index c13d2a007ad7..d4c7fec69bf7 100644 --- a/extensions/markdown-language-features/package-lock.json +++ b/extensions/markdown-language-features/package-lock.json @@ -29,7 +29,7 @@ "@types/picomatch": "^2.3.0", "@types/vscode-notebook-renderer": "^1.60.0", "@types/vscode-webview": "^1.57.0", - "@vscode/markdown-it-katex": "^1.0.2", + "@vscode/markdown-it-katex": "^1.1.1", "lodash.throttle": "^4.1.1", "vscode-languageserver-types": "^3.17.2", "vscode-markdown-languageservice": "^0.3.0-alpha.3" @@ -252,10 +252,11 @@ "integrity": "sha512-ukOMWnCg1tCvT7WnDfsUKQOFDQGsyR5tNgRpwmqi+5/vzU3ghdDXzvIM4IOPdSb3OeSsBNvmSL8nxIVOqi2WXA==" }, "node_modules/@vscode/markdown-it-katex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@vscode/markdown-it-katex/-/markdown-it-katex-1.0.2.tgz", - "integrity": "sha512-QY/OnOHPTqc8tQoCoAjVblILX4yE6xGZHKODtiTKqA328OXra+lSpeJO5Ouo9AAvrs9AwcCLz6xvW3zwcsPBQg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@vscode/markdown-it-katex/-/markdown-it-katex-1.1.1.tgz", + "integrity": "sha512-3KTlbsRBPJQLE2YmLL7K6nunTlU+W9T5+FjfNdWuIUKgxSS6HWLQHaO3L4MkJi7z7MpIPpY+g4N+cWNBPE/MSA==", "dev": true, + "license": "MIT", "dependencies": { "katex": "^0.16.4" } @@ -289,6 +290,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true, + "license": "MIT", "engines": { "node": ">= 12" } @@ -408,14 +410,15 @@ } }, "node_modules/katex": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz", - "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==", + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", "dev": true, "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" ], + "license": "MIT", "dependencies": { "commander": "^8.3.0" }, diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 6d097be5470a..c411df235707 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -793,7 +793,7 @@ "@types/picomatch": "^2.3.0", "@types/vscode-notebook-renderer": "^1.60.0", "@types/vscode-webview": "^1.57.0", - "@vscode/markdown-it-katex": "^1.0.2", + "@vscode/markdown-it-katex": "^1.1.1", "lodash.throttle": "^4.1.1", "vscode-languageserver-types": "^3.17.2", "vscode-markdown-languageservice": "^0.3.0-alpha.3" diff --git a/extensions/markdown-math/package-lock.json b/extensions/markdown-math/package-lock.json index 56d5bd40faa4..73ae907e680a 100644 --- a/extensions/markdown-math/package-lock.json +++ b/extensions/markdown-math/package-lock.json @@ -44,18 +44,20 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", "engines": { "node": ">= 12" } }, "node_modules/katex": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz", - "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==", + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" ], + "license": "MIT", "dependencies": { "commander": "^8.3.0" }, diff --git a/extensions/markdown-math/package.json b/extensions/markdown-math/package.json index 5628816d6c33..6e599ae2a0eb 100644 --- a/extensions/markdown-math/package.json +++ b/extensions/markdown-math/package.json @@ -94,7 +94,9 @@ }, "markdown.math.macros": { "type": "object", - "additionalProperties": { "type": "string" }, + "additionalProperties": { + "type": "string" + }, "default": {}, "description": "%config.markdown.math.macros%", "scope": "resource" @@ -108,9 +110,6 @@ "watch": "npm run build-notebook", "build-notebook": "node ./esbuild" }, - "dependencies": { - "@vscode/markdown-it-katex": "^1.1.1" - }, "devDependencies": { "@types/markdown-it": "^0.0.0", "@types/vscode-notebook-renderer": "^1.60.0" @@ -118,5 +117,8 @@ "repository": { "type": "git", "url": "https://github.com/microsoft/vscode.git" + }, + "dependencies": { + "@vscode/markdown-it-katex": "^1.1.1" } } From f034b029a6759fdf7a0281e54b400bf60392fde8 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 20 Jan 2025 10:53:03 +0100 Subject: [PATCH 0675/3587] fix filtering (#238274) --- src/vs/workbench/contrib/output/browser/outputView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index d9bd19f09007..250bf7afa2dd 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -493,6 +493,6 @@ export class FilterController extends Disposable implements IEditorContribution if (!entry.source) { return true; } - return !filters.hasSource(`${activeChannelId}-${entry.source}`); + return !filters.hasSource(`${activeChannelId}:${entry.source}`); } } From 203a51a67fba4436f9d171636fba2fdb49d8d2b5 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 20 Jan 2025 11:43:23 +0100 Subject: [PATCH 0676/3587] Fix replacement view rendering over gutter (#238276) fix replacement view renders over gutter --- src/vs/editor/browser/rect.ts | 8 + .../view/inlineEdits/wordReplacementView.ts | 176 ++++++++++-------- 2 files changed, 109 insertions(+), 75 deletions(-) diff --git a/src/vs/editor/browser/rect.ts b/src/vs/editor/browser/rect.ts index 6de464d201af..bcbe21146c3e 100644 --- a/src/vs/editor/browser/rect.ts +++ b/src/vs/editor/browser/rect.ts @@ -133,4 +133,12 @@ export class Rect { withTop(top: number): Rect { return new Rect(this.left, top, this.right, this.bottom); } + + moveLeft(delta: number): Rect { + return new Rect(this.left - delta, this.top, this.right - delta, this.bottom); + } + + moveUp(delta: number): Rect { + return new Rect(this.left, this.top - delta, this.right, this.bottom - delta); + } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index dc24e0bbb972..0fbc41442e76 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -74,10 +74,11 @@ export class WordReplacementView extends Disposable { const w = this._editor.getOption(EditorOption.fontInfo).read(reader).typicalHalfwidthCharacterWidth; const modifiedLeftOffset = 20; const modifiedTopOffset = 5; + const PADDING = 4; const originalLine = Rect.fromLeftTopWidthHeight(widgetStart.x + contentLeft - scrollLeft, widgetStart.y, widgetEnd.x - widgetStart.x, lineHeight); const modifiedLine = Rect.fromLeftTopWidthHeight(originalLine.left + modifiedLeftOffset, originalLine.top + lineHeight + modifiedTopOffset, this._edit.text.length * w + 5, originalLine.height); - const background = Rect.hull([originalLine, modifiedLine]).withMargin(4); + const background = Rect.hull([originalLine, modifiedLine]).withMargin(PADDING); let textLengthDelta = 0; const editLocations = this._editLocations.read(reader); @@ -109,6 +110,7 @@ export class WordReplacementView extends Disposable { innerEdits, lowerBackground, lowerText, + padding: PADDING }; }); @@ -121,89 +123,113 @@ export class WordReplacementView extends Disposable { return []; } - const edits = layout.read(reader).innerEdits; + const layoutProps = layout.read(reader); + const scrollLeft = this._editor.scrollLeft.read(reader); + let contentLeft = this._editor.layoutInfoContentLeft.read(reader); + let contentWidth = this._editor.contentWidth.read(reader); + const contentHeight = this._editor.editor.getContentHeight(); - return [ - n.div({ - style: { - position: 'absolute', - ...rectToProps(reader => layout.read(reader).lowerBackground), - borderRadius: '4px', - background: 'var(--vscode-editor-background)', - }, - }, []), - n.div({ - style: { - position: 'absolute', - padding: '0px', - boxSizing: 'border-box', - ...rectToProps(reader => layout.read(reader).lowerText), - fontFamily: this._editor.getOption(EditorOption.fontFamily), - fontSize: this._editor.getOption(EditorOption.fontSize), - fontWeight: this._editor.getOption(EditorOption.fontWeight), - pointerEvents: 'none', - } - }, [this._line]), - ...edits.map(edit => n.div({ - style: { - position: 'absolute', - top: edit.modified.top, - left: edit.modified.left, - width: edit.modified.width, - height: edit.modified.height, - borderRadius: '4px', + if (scrollLeft === 0) { + contentLeft -= layoutProps.padding; + contentWidth += layoutProps.padding; + } - background: 'var(--vscode-inlineEdit-modifiedChangedTextBackground)', - pointerEvents: 'none', - } - }), []), - ...edits.map(edit => n.div({ - style: { - position: 'absolute', - top: edit.original.top, - left: edit.original.left, - width: edit.original.width, - height: edit.original.height, - borderRadius: '4px', - boxSizing: 'border-box', - background: 'var(--vscode-inlineEdit-originalChangedTextBackground)', - pointerEvents: 'none', - } - }, [])), + const edits = layoutProps.innerEdits.map(edit => ({ modified: edit.modified.moveLeft(contentLeft), original: edit.original.moveLeft(contentLeft) })); + + return [ n.div({ style: { position: 'absolute', - ...rectToProps(reader => layout.read(reader).background), - borderRadius: '4px', - - border: '1px solid var(--vscode-editorHoverWidget-border)', - //background: 'rgba(122, 122, 122, 0.12)', looks better - background: 'var(--vscode-inlineEdit-wordReplacementView-background)', + top: 0, + left: contentLeft, + width: contentWidth, + height: contentHeight, + overflow: 'hidden', pointerEvents: 'none', } - }, []), - - n.svg({ - width: 11, - height: 13, - viewBox: '0 0 11 13', - fill: 'none', - style: { - position: 'absolute', - left: derived(reader => layout.read(reader).modifiedLine.left - 15), - top: derived(reader => layout.read(reader).modifiedLine.top), - } }, [ - n.svgElem('path', { - d: 'M1 0C1 2.98966 1 4.92087 1 7.49952C1 8.60409 1.89543 9.5 3 9.5H10.5', - stroke: 'var(--vscode-editorHoverWidget-foreground)', - }), - n.svgElem('path', { - d: 'M6 6.5L9.99999 9.49998L6 12.5', - stroke: 'var(--vscode-editorHoverWidget-foreground)', - }) - ]), + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).lowerBackground.moveLeft(contentLeft)), + borderRadius: '4px', + background: 'var(--vscode-editor-background)', + boxShadow: 'var(--vscode-scrollbar-shadow) 0 6px 6px -6px' + }, + }, []), + n.div({ + style: { + position: 'absolute', + padding: '0px', + boxSizing: 'border-box', + ...rectToProps(reader => layout.read(reader).lowerText.moveLeft(contentLeft)), + fontFamily: this._editor.getOption(EditorOption.fontFamily), + fontSize: this._editor.getOption(EditorOption.fontSize), + fontWeight: this._editor.getOption(EditorOption.fontWeight), + pointerEvents: 'none', + } + }, [this._line]), + ...edits.map(edit => n.div({ + style: { + position: 'absolute', + top: edit.modified.top, + left: edit.modified.left, + width: edit.modified.width, + height: edit.modified.height, + borderRadius: '4px', + + background: 'var(--vscode-inlineEdit-modifiedChangedTextBackground)', + pointerEvents: 'none', + } + }), []), + ...edits.map(edit => n.div({ + style: { + position: 'absolute', + top: edit.original.top, + left: edit.original.left, + width: edit.original.width, + height: edit.original.height, + borderRadius: '4px', + boxSizing: 'border-box', + background: 'var(--vscode-inlineEdit-originalChangedTextBackground)', + pointerEvents: 'none', + } + }, [])), + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).background.moveLeft(contentLeft)), + borderRadius: '4px', + + border: '1px solid var(--vscode-editorHoverWidget-border)', + //background: 'rgba(122, 122, 122, 0.12)', looks better + background: 'var(--vscode-inlineEdit-wordReplacementView-background)', + pointerEvents: 'none', + } + }, []), + + n.svg({ + width: 11, + height: 13, + viewBox: '0 0 11 13', + fill: 'none', + style: { + position: 'absolute', + left: derived(reader => layout.read(reader).modifiedLine.moveLeft(contentLeft).left - 15), + top: derived(reader => layout.read(reader).modifiedLine.top), + } + }, [ + n.svgElem('path', { + d: 'M1 0C1 2.98966 1 4.92087 1 7.49952C1 8.60409 1.89543 9.5 3 9.5H10.5', + stroke: 'var(--vscode-editorHoverWidget-foreground)', + }), + n.svgElem('path', { + d: 'M6 6.5L9.99999 9.49998L6 12.5', + stroke: 'var(--vscode-editorHoverWidget-foreground)', + }) + ]), + ]) ]; }) ]).keepUpdated(this._store); From 4ef041e4bf14d3e19583f7fa9b7fa0e31549cc5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Mon, 20 Jan 2025 11:51:57 +0100 Subject: [PATCH 0677/3587] break up recursive configuration loop (#238277) Co-authored-by: Sandeep Co-authored-by: Christof --- src/vs/platform/request/common/request.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index 9d2afcf3a5f5..fddb00f65979 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -151,7 +151,16 @@ export const USER_LOCAL_AND_REMOTE_SETTINGS = [ ]; let proxyConfiguration: IConfigurationNode[] = []; +let previousUseHostProxy: boolean | undefined = undefined; +let previousUseHostProxyDefault: boolean | undefined = undefined; function registerProxyConfigurations(useHostProxy = true, useHostProxyDefault = true): void { + if (previousUseHostProxy === useHostProxy && previousUseHostProxyDefault === useHostProxyDefault) { + return; + } + + previousUseHostProxy = useHostProxy; + previousUseHostProxyDefault = useHostProxyDefault; + const configurationRegistry = Registry.as(Extensions.Configuration); const oldProxyConfiguration = proxyConfiguration; proxyConfiguration = [ From 61760c4641fedd45a66153f0e914f32caf544a25 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 20 Jan 2025 11:57:08 +0100 Subject: [PATCH 0678/3587] delete unwanted telemetry events (#238130) * delete unwanted telemetry events * fix compilation --- .../sharedProcess/sharedProcessMain.ts | 19 ------------ .../common/extensionsProfileScannerService.ts | 17 +++-------- .../extensionsProfileScannerService.ts | 4 +-- .../node/extensionsProfileScannerService.ts | 4 +-- .../node/extensionsScannerService.test.ts | 3 +- .../browser/indexedDBFileSystemProvider.ts | 28 ++---------------- .../common/abstractSynchronizer.ts | 8 ----- .../common/userDataAutoSyncService.ts | 22 -------------- .../common/userDataSyncEnablementService.ts | 9 ------ src/vs/workbench/browser/web.main.ts | 10 +------ .../extensionRecommendationsService.ts | 15 ---------- .../extensions/browser/extensionsWidgets.ts | 11 ------- .../browser/extensionsWorkbenchService.ts | 29 +------------------ .../preferences/browser/settingsEditor2.ts | 4 --- .../browser/telemetry.contribution.ts | 26 ----------------- .../browser/userDataProfilesEditorModel.ts | 2 -- .../userDataSync/browser/userDataSync.ts | 2 -- .../extensionsProfileScannerService.ts | 4 +-- .../extensions/browser/extensionUrlHandler.ts | 15 ---------- .../nativeExtensionService.ts | 13 --------- .../browser/userDataProfileManagement.ts | 17 ----------- .../browser/userDataSyncWorkbenchService.ts | 3 -- 22 files changed, 12 insertions(+), 253 deletions(-) diff --git a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts index 98824d792188..fbbae411cdb0 100644 --- a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts @@ -162,7 +162,6 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { instantiationService.invokeFunction(accessor => { const logService = accessor.get(ILogService); const telemetryService = accessor.get(ITelemetryService); - const userDataProfilesService = accessor.get(IUserDataProfilesService); // Log info logService.trace('sharedProcess configuration', JSON.stringify(this.configuration)); @@ -173,10 +172,6 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { // Error handler this.registerErrorHandler(logService); - // Report Profiles Info - this.reportProfilesInfo(telemetryService, userDataProfilesService); - this._register(userDataProfilesService.onDidChangeProfiles(() => this.reportProfilesInfo(telemetryService, userDataProfilesService))); - // Report Client OS/DE Info this.reportClientOSInfo(telemetryService, logService); }); @@ -455,20 +450,6 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { }); } - private reportProfilesInfo(telemetryService: ITelemetryService, userDataProfilesService: IUserDataProfilesService): void { - type ProfilesInfoClassification = { - owner: 'sandy081'; - comment: 'Report profiles information'; - count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of profiles' }; - }; - type ProfilesInfoEvent = { - count: number; - }; - telemetryService.publicLog2('profilesInfo', { - count: userDataProfilesService.profiles.length - }); - } - private async reportClientOSInfo(telemetryService: ITelemetryService, logService: ILogService): Promise { if (isLinux) { const [releaseInfo, displayProtocol] = await Promise.all([ diff --git a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index 162f11b5c968..c0eefb920636 100644 --- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -19,7 +19,6 @@ import { IUserDataProfilesService } from '../../userDataProfile/common/userDataP import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; import { Mutable, isObject, isString, isUndefined } from '../../../base/common/types.js'; import { getErrorMessage } from '../../../base/common/errors.js'; -import { ITelemetryService } from '../../telemetry/common/telemetry.js'; interface IStoredProfileExtension { identifier: IExtensionIdentifier; @@ -110,7 +109,6 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable @IFileService private readonly fileService: IFileService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, ) { super(); @@ -244,13 +242,13 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable } if (storedProfileExtensions) { if (!Array.isArray(storedProfileExtensions)) { - this.reportAndThrowInvalidConentError(file); + this.throwInvalidConentError(file); } // TODO @sandy081: Remove this migration after couple of releases let migrate = false; for (const e of storedProfileExtensions) { if (!isStoredProfileExtension(e)) { - this.reportAndThrowInvalidConentError(file); + this.throwInvalidConentError(file); } let location: URI; if (isString(e.relativeLocation) && e.relativeLocation) { @@ -302,15 +300,8 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable }); } - private reportAndThrowInvalidConentError(file: URI): void { - type ErrorClassification = { - owner: 'sandy081'; - comment: 'Information about the error that occurred while scanning'; - code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code' }; - }; - const error = new ExtensionsProfileScanningError(`Invalid extensions content in ${file.toString()}`, ExtensionsProfileScanningErrorCode.ERROR_INVALID_CONTENT); - this.telemetryService.publicLogError2<{ code: string }, ErrorClassification>('extensionsProfileScanningError', { code: error.code }); - throw error; + private throwInvalidConentError(file: URI): void { + throw new ExtensionsProfileScanningError(`Invalid extensions content in ${file.toString()}`, ExtensionsProfileScanningErrorCode.ERROR_INVALID_CONTENT); } private toRelativePath(extensionLocation: URI): string | undefined { diff --git a/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts index 2299ce880033..c9ae2f8a5e06 100644 --- a/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts @@ -6,7 +6,6 @@ import { ILogService } from '../../log/common/log.js'; import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; -import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { AbstractExtensionsProfileScannerService, IExtensionsProfileScannerService } from '../common/extensionsProfileScannerService.js'; import { IFileService } from '../../files/common/files.js'; import { INativeEnvironmentService } from '../../environment/common/environment.js'; @@ -19,10 +18,9 @@ export class ExtensionsProfileScannerService extends AbstractExtensionsProfileSc @IFileService fileService: IFileService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, @IUriIdentityService uriIdentityService: IUriIdentityService, - @ITelemetryService telemetryService: ITelemetryService, @ILogService logService: ILogService, ) { - super(URI.file(environmentService.extensionsPath), fileService, userDataProfilesService, uriIdentityService, telemetryService, logService); + super(URI.file(environmentService.extensionsPath), fileService, userDataProfilesService, uriIdentityService, logService); } } diff --git a/src/vs/platform/extensionManagement/node/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/node/extensionsProfileScannerService.ts index 3f1e919161f0..131d91855624 100644 --- a/src/vs/platform/extensionManagement/node/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/node/extensionsProfileScannerService.ts @@ -6,7 +6,6 @@ import { ILogService } from '../../log/common/log.js'; import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; -import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { AbstractExtensionsProfileScannerService } from '../common/extensionsProfileScannerService.js'; import { IFileService } from '../../files/common/files.js'; import { INativeEnvironmentService } from '../../environment/common/environment.js'; @@ -18,9 +17,8 @@ export class ExtensionsProfileScannerService extends AbstractExtensionsProfileSc @IFileService fileService: IFileService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, @IUriIdentityService uriIdentityService: IUriIdentityService, - @ITelemetryService telemetryService: ITelemetryService, @ILogService logService: ILogService, ) { - super(URI.file(environmentService.extensionsPath), fileService, userDataProfilesService, uriIdentityService, telemetryService, logService); + super(URI.file(environmentService.extensionsPath), fileService, userDataProfilesService, uriIdentityService, logService); } } diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts index ea4108b2c17e..1d349f5822ab 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -19,7 +19,6 @@ import { IInstantiationService } from '../../../instantiation/common/instantiati import { TestInstantiationService } from '../../../instantiation/test/common/instantiationServiceMock.js'; import { ILogService, NullLogService } from '../../../log/common/log.js'; import { IProductService } from '../../../product/common/productService.js'; -import { NullTelemetryService } from '../../../telemetry/common/telemetryUtils.js'; import { IUriIdentityService } from '../../../uriIdentity/common/uriIdentity.js'; import { UriIdentityService } from '../../../uriIdentity/common/uriIdentityService.js'; import { IUserDataProfilesService, UserDataProfilesService } from '../../../userDataProfile/common/userDataProfile.js'; @@ -81,7 +80,7 @@ suite('NativeExtensionsScanerService Test', () => { instantiationService.stub(IUriIdentityService, uriIdentityService); const userDataProfilesService = disposables.add(new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService)); instantiationService.stub(IUserDataProfilesService, userDataProfilesService); - instantiationService.stub(IExtensionsProfileScannerService, disposables.add(new ExtensionsProfileScannerService(environmentService, fileService, userDataProfilesService, uriIdentityService, NullTelemetryService, logService))); + instantiationService.stub(IExtensionsProfileScannerService, disposables.add(new ExtensionsProfileScannerService(environmentService, fileService, userDataProfilesService, uriIdentityService, logService))); await fileService.createFolder(systemExtensionsLocation); await fileService.createFolder(userExtensionsLocation); }); diff --git a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts index d2b0da2e857a..5da9d0728806 100644 --- a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts +++ b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts @@ -11,24 +11,10 @@ import { ExtUri } from '../../../base/common/resources.js'; import { isString } from '../../../base/common/types.js'; import { URI, UriDto } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; -import { createFileSystemProviderError, FileChangeType, IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions } from '../common/files.js'; -import { DBClosedError, IndexedDB } from '../../../base/browser/indexedDB.js'; +import { createFileSystemProviderError, FileChangeType, IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions } from '../common/files.js'; +import { IndexedDB } from '../../../base/browser/indexedDB.js'; import { BroadcastDataChannel } from '../../../base/browser/broadcast.js'; -export type IndexedDBFileSystemProviderErrorDataClassification = { - owner: 'sandy081'; - comment: 'Information about errors that occur in the IndexedDB file system provider'; - readonly scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'IndexedDB file system provider scheme for which this error occurred' }; - readonly operation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'operation during which this error occurred' }; - readonly code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'error code' }; -}; - -export type IndexedDBFileSystemProviderErrorData = { - readonly scheme: string; - readonly operation: string; - readonly code: string; -}; - // Standard FS Errors (expected to be thrown in production when invalid FS operations are requested) const ERR_FILE_NOT_FOUND = createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound); const ERR_FILE_IS_DIR = createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory); @@ -179,9 +165,6 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst private readonly _onDidChangeFile = this._register(new Emitter()); readonly onDidChangeFile: Event = this._onDidChangeFile.event; - private readonly _onReportError = this._register(new Emitter()); - readonly onReportError = this._onReportError.event; - private readonly mtimes = new Map(); private cachedFiletree: Promise | undefined; @@ -255,7 +238,6 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst return [...entry.children.entries()].map(([name, node]) => [name, node.type]); } } catch (error) { - this.reportError('readDir', error); throw error; } } @@ -277,7 +259,6 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst return buffer; } catch (error) { - this.reportError('readFile', error); throw error; } } @@ -290,7 +271,6 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst } await this.bulkWrite([[resource, content]]); } catch (error) { - this.reportError('writeFile', error); throw error; } } @@ -452,8 +432,4 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst await this.indexedDB.runInTransaction(this.store, 'readwrite', objectStore => objectStore.clear()); } - private reportError(operation: string, error: Error): void { - this._onReportError.fire({ scheme: this.scheme, operation, code: error instanceof FileSystemProviderError || error instanceof DBClosedError ? error.code : 'unknown' }); - } - } diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 11c0315468aa..54408a57abae 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -36,12 +36,6 @@ import { } from './userDataSync.js'; import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; -type IncompatibleSyncSourceClassification = { - owner: 'sandy081'; - comment: 'Information about the sync resource that is incompatible'; - source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'settings sync resource. eg., settings, keybindings...' }; -}; - export function isRemoteUserData(thing: any): thing is IRemoteUserData { if (thing && (thing.ref !== undefined && typeof thing.ref === 'string' && thing.ref !== '') @@ -325,8 +319,6 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, strategy: SyncStrategy, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise { if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) { - // current version is not compatible with cloud version - this.telemetryService.publicLog2<{ source: string }, IncompatibleSyncSourceClassification>('sync/incompatible', { source: this.resource }); throw new UserDataSyncError(localize({ key: 'incompatible', comment: ['This is an error while syncing a resource that its local version is not compatible with its remote version.'] }, "Cannot sync {0} as its local version {1} is not compatible with its remote version {2}", this.resource, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.IncompatibleLocalContent, this.resource); } diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index f868cccd7e3b..5ab5b3dd21d5 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -21,20 +21,6 @@ import { IUserDataSyncTask, IUserDataAutoSyncService, IUserDataManifest, IUserDa import { IUserDataSyncAccountService } from './userDataSyncAccount.js'; import { IUserDataSyncMachinesService } from './userDataSyncMachines.js'; -type AutoSyncClassification = { - owner: 'sandy081'; - comment: 'Information about the sources triggering auto sync'; - sources: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Source that triggered auto sync' }; - providerId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Auth provider id used for sync' }; -}; - -type AutoSyncErrorClassification = { - owner: 'sandy081'; - comment: 'Information about the error that causes auto sync to fail'; - code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code' }; - service: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings sync service for which this error has occurred' }; -}; - const disableMachineEventuallyKey = 'sync.disableMachineEventually'; const sessionIdKey = 'sync.sessionId'; const storeUrlKey = 'sync.storeUrl'; @@ -199,7 +185,6 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto // Reset if (everywhere) { - this.telemetryService.publicLog2<{}, { owner: 'sandy081'; comment: 'Reporting when settings sync is turned off in all devices' }>('sync/turnOffEveryWhere'); await this.userDataSyncService.reset(); } else { await this.userDataSyncService.resetLocal(); @@ -235,11 +220,6 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto // Error while syncing const userDataSyncError = UserDataSyncError.toUserDataSyncError(error); - // Log to telemetry - if (userDataSyncError instanceof UserDataAutoSyncError) { - this.telemetryService.publicLog2<{ code: string; service: string }, AutoSyncErrorClassification>(`autosync/error`, { code: userDataSyncError.code, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() }); - } - // Session got expired if (userDataSyncError.code === UserDataSyncErrorCode.SessionExpired) { await this.turnOff(false, true /* force soft turnoff on error */); @@ -361,8 +341,6 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto this.sources.push(...sources); return this.syncTriggerDelayer.trigger(async () => { this.logService.trace('activity sources', ...this.sources); - const providerId = this.userDataSyncAccountService.account?.authenticationProviderId || ''; - this.telemetryService.publicLog2<{ sources: string[]; providerId: string }, AutoSyncClassification>('sync/triggered', { sources: this.sources, providerId }); this.sources = []; if (this.autoSync.value) { await this.autoSync.value.sync('Activity', disableCache); diff --git a/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts b/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts index 858dd8d3b47f..163f805c7309 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts @@ -8,15 +8,8 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { isWeb } from '../../../base/common/platform.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; import { IApplicationStorageValueChangeEvent, IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; -import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { ALL_SYNC_RESOURCES, getEnablementKey, IUserDataSyncEnablementService, IUserDataSyncStoreManagementService, SyncResource } from './userDataSync.js'; -type SyncEnablementClassification = { - owner: 'sandy081'; - comment: 'Reporting when Settings Sync is turned on or off'; - enabled?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if settings sync is enabled or not' }; -}; - const enablementKey = 'sync.enable'; export class UserDataSyncEnablementService extends Disposable implements IUserDataSyncEnablementService { @@ -31,7 +24,6 @@ export class UserDataSyncEnablementService extends Disposable implements IUserDa constructor( @IStorageService private readonly storageService: IStorageService, - @ITelemetryService private readonly telemetryService: ITelemetryService, @IEnvironmentService protected readonly environmentService: IEnvironmentService, @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, ) { @@ -57,7 +49,6 @@ export class UserDataSyncEnablementService extends Disposable implements IUserDa if (enabled && !this.canToggleEnablement()) { return; } - this.telemetryService.publicLog2<{ enabled: boolean }, SyncEnablementClassification>(enablementKey, { enabled }); this.storageService.store(enablementKey, enabled, StorageScope.APPLICATION, StorageTarget.MACHINE); } diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 09bf7f6afac6..5dcdad8aaacc 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -40,7 +40,7 @@ import { isWorkspaceToOpen, isFolderToOpen } from '../../platform/window/common/ import { getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier } from '../services/workspaces/browser/workspaces.js'; import { InMemoryFileSystemProvider } from '../../platform/files/common/inMemoryFilesystemProvider.js'; import { ICommandService } from '../../platform/commands/common/commands.js'; -import { IndexedDBFileSystemProviderErrorDataClassification, IndexedDBFileSystemProvider, IndexedDBFileSystemProviderErrorData } from '../../platform/files/browser/indexedDBFileSystemProvider.js'; +import { IndexedDBFileSystemProvider } from '../../platform/files/browser/indexedDBFileSystemProvider.js'; import { BrowserRequestService } from '../services/request/browser/requestService.js'; import { IRequestService } from '../../platform/request/common/request.js'; import { IUserDataInitializationService, IUserDataInitializer, UserDataInitializationService } from '../services/userData/browser/userDataInit.js'; @@ -64,7 +64,6 @@ import { IOpenerService } from '../../platform/opener/common/opener.js'; import { mixin, safeStringify } from '../../base/common/objects.js'; import { IndexedDB } from '../../base/browser/indexedDB.js'; import { WebFileSystemAccess } from '../../platform/files/browser/webFileSystemAccess.js'; -import { ITelemetryService } from '../../platform/telemetry/common/telemetry.js'; import { IProgressService } from '../../platform/progress/common/progress.js'; import { DelayedLogChannel } from '../services/output/common/delayedLogChannel.js'; import { dirname, joinPath } from '../../base/common/resources.js'; @@ -137,13 +136,6 @@ export class BrowserMain extends Disposable { // Logging services.logService.trace('workbench#open with configuration', safeStringify(this.configuration)); - instantiationService.invokeFunction(accessor => { - const telemetryService = accessor.get(ITelemetryService); - for (const indexedDbFileSystemProvider of this.indexedDBFileSystemProviders) { - this._register(indexedDbFileSystemProvider.onReportError(e => telemetryService.publicLog2('indexedDBFileSystemProviderError', e))); - } - }); - // Return API Facade return instantiationService.invokeFunction(accessor => { const commandService = accessor.get(ICommandService); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index f9284ebbd8a5..12cf0a6c61fc 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -30,13 +30,6 @@ import { IRemoteExtensionsScannerService } from '../../../../platform/remote/com import { IUserDataInitializationService } from '../../../services/userData/browser/userDataInit.js'; import { isString } from '../../../../base/common/types.js'; -type IgnoreRecommendationClassification = { - owner: 'sandy081'; - comment: 'Report when a recommendation is ignored'; - recommendationReason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Reason why extension is recommended' }; - extensionId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Id of the extension recommendation that is being ignored' }; -}; - export class ExtensionRecommendationsService extends Disposable implements IExtensionRecommendationsService { declare readonly _serviceBrand: undefined; @@ -115,14 +108,6 @@ export class ExtensionRecommendationsService extends Disposable implements IExte ]); this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations, this.extensionRecommendationsManagementService.onDidChangeIgnoredRecommendations)(() => this._onDidChangeRecommendations.fire())); - this._register(this.extensionRecommendationsManagementService.onDidChangeGlobalIgnoredRecommendation(({ extensionId, isRecommended }) => { - if (!isRecommended) { - const reason = this.getAllRecommendationsWithReason()[extensionId]; - if (reason && reason.reasonId) { - this.telemetryService.publicLog2<{ extensionId: string; recommendationReason: ExtensionRecommendationReason }, IgnoreRecommendationClassification>('extensionsRecommendations:ignoreRecommendation', { extensionId, recommendationReason: reason.reasonId }); - } - } - })); this.promptWorkspaceRecommendations(); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 729d5d96019b..3f508b8bf899 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -38,7 +38,6 @@ import { onUnexpectedError } from '../../../../base/common/errors.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { defaultCountBadgeStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; @@ -247,7 +246,6 @@ export class SponsorWidget extends ExtensionWidget { private container: HTMLElement, @IHoverService private readonly hoverService: IHoverService, @IOpenerService private readonly openerService: IOpenerService, - @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); this.render(); @@ -267,15 +265,6 @@ export class SponsorWidget extends ExtensionWidget { const label = $('span', undefined, localize('sponsor', "Sponsor")); append(sponsor, sponsorIconElement, label); this.disposables.add(onClick(sponsor, () => { - type SponsorExtensionClassification = { - owner: 'sandy081'; - comment: 'Reporting when sponosor extension action is executed'; - 'extensionId': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the extension to be sponsored' }; - }; - type SponsorExtensionEvent = { - 'extensionId': string; - }; - this.telemetryService.publicLog2('extensionsAction.sponsorExtension', { extensionId: this.extension!.identifier.id }); this.openerService.open(this.extension!.publisherSponsorLink!); })); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 56e0f1638ca7..e68c51bcfa4e 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -847,11 +847,6 @@ class Extensions extends Disposable { if (!this.galleryService.isEnabled()) { return; } - type GalleryServiceMatchInstalledExtensionClassification = { - owner: 'sandy081'; - comment: 'Report when a request is made to match installed extension with gallery'; - }; - this.telemetryService.publicLog2<{}, GalleryServiceMatchInstalledExtensionClassification>('galleryService:matchInstalledExtension'); const galleryExtensions = await this.galleryService.getExtensions(toMatch.map(e => ({ ...e.identifier, preRelease: e.local?.preRelease })), { compatible: true, targetPlatform: await this.server.extensionManagementService.getTargetPlatform() }, CancellationToken.None); for (const extension of extensions) { const compatible = galleryExtensions.find(e => areSameExtensions(e.identifier, extension.identifier)); @@ -2925,29 +2920,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private async doSetEnablement(extensions: IExtension[], enablementState: EnablementState): Promise { - const changed = await this.extensionEnablementService.setEnablement(extensions.map(e => e.local!), enablementState); - for (let i = 0; i < changed.length; i++) { - if (changed[i]) { - /* __GDPR__ - "extension:enable" : { - "owner": "sandy081", - "${include}": [ - "${GalleryExtensionTelemetryData}" - ] - } - */ - /* __GDPR__ - "extension:disable" : { - "owner": "sandy081", - "${include}": [ - "${GalleryExtensionTelemetryData}" - ] - } - */ - this.telemetryService.publicLog(enablementState === EnablementState.EnabledGlobally || enablementState === EnablementState.EnabledWorkspace ? 'extension:enable' : 'extension:disable', extensions[i].telemetryData); - } - } - return changed; + return await this.extensionEnablementService.setEnablement(extensions.map(e => e.local!), enablementState); } // Current service reports progress when installing/uninstalling extensions diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index eb6d7d3bb43b..5de358b38d51 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -1923,10 +1923,6 @@ class SyncControls extends Disposable { DOM.hide(this.turnOnSyncButton.element); this._register(this.turnOnSyncButton.onDidClick(async () => { - telemetryService.publicLog2<{}, { - owner: 'sandy081'; - comment: 'This event tracks whenever settings sync is turned on from settings editor.'; - }>('sync/turnOnSyncFromSettings'); await this.commandService.executeCommand('workbench.userDataSync.actions.turnOn'); })); diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index 73de190f4cc4..e77b3838e7d5 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -20,7 +20,6 @@ import { ConfigurationTarget, ConfigurationTargetToString, IConfigurationService import { ITextFileService, ITextFileSaveEvent, ITextFileResolveEvent } from '../../../services/textfile/common/textfiles.js'; import { extname, basename, isEqual, isEqualOrParent } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; -import { Event } from '../../../../base/common/event.js'; import { Schemas } from '../../../../base/common/network.js'; import { getMimeTypes } from '../../../../editor/common/services/languagesAssociations.js'; import { hash } from '../../../../base/common/hash.js'; @@ -246,31 +245,6 @@ class ConfigurationTelemetryContribution extends Disposable implements IWorkbenc ) { super(); - // Debounce the event by 1000 ms and merge all affected keys into one event - const debouncedConfigService = Event.debounce(configurationService.onDidChangeConfiguration, (last, cur) => { - const newAffectedKeys: ReadonlySet = last ? new Set([...last.affectedKeys, ...cur.affectedKeys]) : cur.affectedKeys; - return { ...cur, affectedKeys: newAffectedKeys }; - }, 1000, true); - - this._register(debouncedConfigService(event => { - if (event.source !== ConfigurationTarget.DEFAULT) { - type UpdateConfigurationClassification = { - owner: 'sandy081'; - comment: 'Event which fires when user updates settings'; - configurationSource: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'What configuration file was updated i.e user or workspace' }; - configurationKeys: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'What configuration keys were updated' }; - }; - type UpdateConfigurationEvent = { - configurationSource: string; - configurationKeys: string[]; - }; - telemetryService.publicLog2('updateConfiguration', { - configurationSource: ConfigurationTargetToString(event.source), - configurationKeys: Array.from(event.affectedKeys) - }); - } - })); - const { user, workspace } = configurationService.keys(); for (const setting of user) { this.reportTelemetry(setting, ConfigurationTarget.USER_LOCAL); diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts index bef5074bad83..6cd4321ee424 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts @@ -1209,7 +1209,6 @@ export class UserDataProfilesEditorModel extends EditorModel { ); } } else if (isUserDataProfile(copyFrom)) { - this.telemetryService.publicLog2('userDataProfile.createFromProfile', createProfileTelemetryData); profile = await this.userDataProfileImportExportService.createFromProfile( copyFrom, { @@ -1222,7 +1221,6 @@ export class UserDataProfilesEditorModel extends EditorModel { token ?? CancellationToken.None ); } else { - this.telemetryService.publicLog2('userDataProfile.createEmptyProfile', createProfileTelemetryData); profile = await this.userDataProfileManagementService.createProfile(name, { useDefaultFlags, icon, transient }); } } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 6a2c8ceb17a6..261987d460e9 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -189,14 +189,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo { label: localize('replace remote', "Replace Remote"), run: () => { - this.telemetryService.publicLog2<{ source: string; action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflict.syncResource, action: 'acceptLocal' }); this.acceptLocal(conflict, conflict.conflicts[0]); } }, { label: localize('replace local', "Replace Local"), run: () => { - this.telemetryService.publicLog2<{ source: string; action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflict.syncResource, action: 'acceptRemote' }); this.acceptRemote(conflict, conflict.conflicts[0]); } }, diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionsProfileScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionsProfileScannerService.ts index edcd2c4775c2..cd083d884f9f 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionsProfileScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionsProfileScannerService.ts @@ -6,7 +6,6 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { AbstractExtensionsProfileScannerService, IExtensionsProfileScannerService } from '../../../../platform/extensionManagement/common/extensionsProfileScannerService.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; @@ -18,10 +17,9 @@ export class ExtensionsProfileScannerService extends AbstractExtensionsProfileSc @IFileService fileService: IFileService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, @IUriIdentityService uriIdentityService: IUriIdentityService, - @ITelemetryService telemetryService: ITelemetryService, @ILogService logService: ILogService, ) { - super(environmentService.userRoamingDataHome, fileService, userDataProfilesService, uriIdentityService, telemetryService, logService); + super(environmentService.userRoamingDataHome, fileService, userDataProfilesService, uriIdentityService, logService); } } diff --git a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index 04aa6fe51f84..b5f67878d99a 100644 --- a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -26,7 +26,6 @@ import { mainWindow } from '../../../../base/browser/window.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; import { ResourceMap } from '../../../../base/common/map.js'; const FIVE_MINUTES = 5 * 60 * 1000; @@ -88,18 +87,6 @@ type ExtensionUrlHandlerClassification = { comment: 'This is used to understand the drop funnel of extension URI handling by the OS & VS Code.'; }; -interface ExtensionUrlReloadHandlerEvent { - readonly extensionId: string; - readonly isRemote: boolean; -} - -type ExtensionUrlReloadHandlerClassification = { - owner: 'sandy081'; - readonly extensionId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The ID of the extension that should handle the URI' }; - readonly isRemote: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Whether the current window is a remote window' }; - comment: 'This is used to understand the drop funnel of extension URI handling by the OS & VS Code.'; -}; - export interface IExtensionUrlHandlerOverride { handleURL(uri: URI): Promise; } @@ -148,7 +135,6 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { @ITelemetryService private readonly telemetryService: ITelemetryService, @INotificationService private readonly notificationService: INotificationService, @IProductService private readonly productService: IProductService, - @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService ) { this.userTrustedExtensionsStorage = new UserTrustedExtensionIdStorage(storageService); @@ -311,7 +297,6 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { /* Extension cannot be added and require window reload */ else { - this.telemetryService.publicLog2('uri_invoked/install_extension/reload', { extensionId, isRemote: !!this.workbenchEnvironmentService.remoteAuthority }); const result = await this.dialogService.confirm({ message: localize('reloadAndHandle', "Extension '{0}' is not loaded. Would you like to reload the window to load the extension and open the URL?", extensionId), primaryButton: localize({ key: 'reloadAndOpen', comment: ['&& denotes a mnemonic'] }, "&&Reload Window and Open") diff --git a/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts b/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts index a552fa153333..d38ab6b10ae3 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts @@ -467,16 +467,6 @@ export class NativeExtensionService extends AbstractExtensionService implements if (!recommendation) { return false; } - const sendTelemetry = (userReaction: 'install' | 'enable' | 'cancel') => { - /* __GDPR__ - "remoteExtensionRecommendations:popup" : { - "owner": "sandy081", - "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } - } - */ - this._telemetryService.publicLog('remoteExtensionRecommendations:popup', { userReaction, extensionId: resolverExtensionId }); - }; const resolverExtensionId = recommendation.extensionId; const allExtensions = await this._scanAllLocalExtensions(); @@ -488,7 +478,6 @@ export class NativeExtensionService extends AbstractExtensionService implements [{ label: nls.localize('enable', 'Enable and Reload'), run: async () => { - sendTelemetry('enable'); await this._extensionEnablementService.setEnablement([toExtension(extension)], EnablementState.EnabledGlobally); await this._hostService.reload(); } @@ -506,7 +495,6 @@ export class NativeExtensionService extends AbstractExtensionService implements [{ label: nls.localize('install', 'Install and Reload'), run: async () => { - sendTelemetry('install'); const [galleryExtension] = await this._extensionGalleryService.getExtensions([{ id: resolverExtensionId }], CancellationToken.None); if (galleryExtension) { await this._extensionManagementService.installFromGallery(galleryExtension); @@ -520,7 +508,6 @@ export class NativeExtensionService extends AbstractExtensionService implements { sticky: true, priority: NotificationPriority.URGENT, - onCancel: () => sendTelemetry('cancel') } ); diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts index e4b29d57e539..cc56d9a6ac53 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts @@ -15,7 +15,6 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta import { ILogService } from '../../../../platform/log/common/log.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IRequestService, asJson } from '../../../../platform/request/common/request.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IUserDataProfile, IUserDataProfileOptions, IUserDataProfilesService, IUserDataProfileUpdateOptions } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { isEmptyWorkspaceIdentifier, IWorkspaceContextService, toWorkspaceIdentifier } from '../../../../platform/workspace/common/workspace.js'; @@ -25,16 +24,6 @@ import { IExtensionService } from '../../extensions/common/extensions.js'; import { IHostService } from '../../host/browser/host.js'; import { DidChangeUserDataProfileEvent, IProfileTemplateInfo, IUserDataProfileManagementService, IUserDataProfileService } from '../common/userDataProfile.js'; -export type ProfileManagementActionExecutedClassification = { - owner: 'sandy081'; - comment: 'Logged when profile management action is excuted'; - id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the action that was run.' }; -}; - -export type ProfileManagementActionExecutedEvent = { - id: string; -}; - export class UserDataProfileManagementService extends Disposable implements IUserDataProfileManagementService { readonly _serviceBrand: undefined; @@ -46,7 +35,6 @@ export class UserDataProfileManagementService extends Disposable implements IUse @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IExtensionService private readonly extensionService: IExtensionService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @ITelemetryService private readonly telemetryService: ITelemetryService, @IProductService private readonly productService: IProductService, @IRequestService private readonly requestService: IRequestService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -122,14 +110,12 @@ export class UserDataProfileManagementService extends Disposable implements IUse async createAndEnterProfile(name: string, options?: IUserDataProfileOptions): Promise { const profile = await this.userDataProfilesService.createNamedProfile(name, options, toWorkspaceIdentifier(this.workspaceContextService.getWorkspace())); await this.changeCurrentProfile(profile); - this.telemetryService.publicLog2('profileManagementActionExecuted', { id: 'createAndEnterProfile' }); return profile; } async createAndEnterTransientProfile(): Promise { const profile = await this.userDataProfilesService.createTransientProfile(toWorkspaceIdentifier(this.workspaceContextService.getWorkspace())); await this.changeCurrentProfile(profile); - this.telemetryService.publicLog2('profileManagementActionExecuted', { id: 'createAndEnterTransientProfile' }); return profile; } @@ -141,7 +127,6 @@ export class UserDataProfileManagementService extends Disposable implements IUse throw new Error(localize('cannotRenameDefaultProfile', "Cannot rename the default profile")); } const updatedProfile = await this.userDataProfilesService.updateProfile(profile, updateOptions); - this.telemetryService.publicLog2('profileManagementActionExecuted', { id: 'updateProfile' }); return updatedProfile; } @@ -153,7 +138,6 @@ export class UserDataProfileManagementService extends Disposable implements IUse throw new Error(localize('cannotDeleteDefaultProfile', "Cannot delete the default profile")); } await this.userDataProfilesService.removeProfile(profile); - this.telemetryService.publicLog2('profileManagementActionExecuted', { id: 'removeProfile' }); } async switchProfile(profile: IUserDataProfile): Promise { @@ -169,7 +153,6 @@ export class UserDataProfileManagementService extends Disposable implements IUse } const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceContextService.getWorkspace()); await this.userDataProfilesService.setProfileForWorkspace(workspaceIdentifier, profile); - this.telemetryService.publicLog2('profileManagementActionExecuted', { id: 'switchProfile' }); if (isEmptyWorkspaceIdentifier(workspaceIdentifier)) { await this.changeCurrentProfile(profile); } diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 6a1eb065c6c1..ea27f81b3bdf 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { IUserDataSyncService, IAuthenticationProvider, isAuthenticationProvider, IUserDataAutoSyncService, IUserDataSyncStoreManagementService, SyncStatus, IUserDataSyncEnablementService, IUserDataSyncResource, IResourcePreview, USER_DATA_SYNC_SCHEME, USER_DATA_SYNC_LOG_ID, } from '../../../../platform/userDataSync/common/userDataSync.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE, SHOW_SYNC_LOG_COMMAND_ID, CONTEXT_ENABLE_ACTIVITY_VIEWS, SYNC_VIEW_CONTAINER_ID, SYNC_TITLE, SYNC_CONFLICTS_VIEW_ID, CONTEXT_ENABLE_SYNC_CONFLICTS_VIEW, CONTEXT_HAS_CONFLICTS, IUserDataSyncConflictsView, getSyncAreaLabel } from '../common/userDataSync.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; @@ -104,7 +103,6 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat @IStorageService private readonly storageService: IStorageService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, - @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, @IProductService private readonly productService: IProductService, @IExtensionService private readonly extensionService: IExtensionService, @@ -716,7 +714,6 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } private async onDidAuthFailure(): Promise { - this.telemetryService.publicLog2<{}, { owner: 'sandy081'; comment: 'Report when there are successive auth failures during settings sync' }>('sync/successiveAuthFailures'); this.currentSessionId = undefined; await this.update('auth failure'); } From 19365345e2e9e3142ebb505bf6056379354a89a6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 20 Jan 2025 03:19:32 -0800 Subject: [PATCH 0679/3587] More aggressive quick suggest Fixes #238281 --- .../suggest/browser/terminalSuggestAddon.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index c0cb3ab00f16..50f04542a564 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -266,13 +266,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest // If input has been added let sent = false; - // Quick suggestions + // Quick suggestions - Trigger whenever a new non-whitespace character is used if (!this._terminalSuggestWidgetVisibleContextKey.get()) { if (config.quickSuggestions) { - // TODO: Make the regex code generic - // TODO: Don't use `\[` in bash/zsh - // If first character or first character after a space (or `[` in pwsh), request completions - if (promptInputState.cursorIndex === 1 || promptInputState.prefix.match(/([\s\[])[^\s]$/)) { + if (promptInputState.prefix.match(/[^\s]$/)) { // Never request completions if the last key sequence was up or down as the user was likely // navigating history if (!this._lastUserData?.match(/^\x1b[\[O]?[A-D]$/)) { From 45a258fdb2c4dc51b6d924a2887249957aeef3cc Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:36:32 +0100 Subject: [PATCH 0680/3587] Fix border issue in WordReplacementView (#238283) fix border problem --- .../browser/view/inlineEdits/wordReplacementView.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index 0fbc41442e76..51086055147b 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -205,6 +205,7 @@ export class WordReplacementView extends Disposable { //background: 'rgba(122, 122, 122, 0.12)', looks better background: 'var(--vscode-inlineEdit-wordReplacementView-background)', pointerEvents: 'none', + boxSizing: 'border-box', } }, []), From 09e49a5252e81ef9860c35a710edbf25e50e4e0b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 20 Jan 2025 03:45:21 -0800 Subject: [PATCH 0681/3587] Fix duplicate matching and windowsExecutableExtensions setting Fixes #238285 --- extensions/terminal-suggest/src/helpers/executable.ts | 2 +- extensions/terminal-suggest/src/terminalSuggestMain.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/terminal-suggest/src/helpers/executable.ts b/extensions/terminal-suggest/src/helpers/executable.ts index e93c77819019..cd8fce4f8dc8 100644 --- a/extensions/terminal-suggest/src/helpers/executable.ts +++ b/extensions/terminal-suggest/src/helpers/executable.ts @@ -6,7 +6,7 @@ import { osIsWindows } from './os'; import * as fs from 'fs/promises'; -export async function isExecutable(filePath: string, configuredWindowsExecutableExtensions?: { [key: string]: boolean | undefined }): Promise { +export async function isExecutable(filePath: string, configuredWindowsExecutableExtensions?: { [key: string]: boolean | undefined } | undefined): Promise { if (osIsWindows()) { const resolvedWindowsExecutableExtensions = resolveWindowsExecutableExtensions(configuredWindowsExecutableExtensions); return resolvedWindowsExecutableExtensions.find(ext => filePath.endsWith(ext)) !== undefined; diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 662df64816d6..1ffedf212360 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -15,7 +15,7 @@ import { isExecutable } from './helpers/executable'; const isWindows = osIsWindows(); let cachedAvailableCommandsPath: string | undefined; -let cachedWindowsExecutableExtensions: Object | undefined; +let cachedWindowsExecutableExtensions: { [key: string]: boolean | undefined } | undefined; const cachedWindowsExecutableExtensionsSettingId = 'terminal.integrated.suggest.windowsExecutableExtensions'; let cachedAvailableCommands: Set | undefined; const cachedBuiltinCommands: Map = new Map(); @@ -114,10 +114,10 @@ export async function activate(context: vscode.ExtensionContext) { }, '/', '\\')); if (isWindows) { - cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration(cachedWindowsExecutableExtensionsSettingId); + cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration('terminal.integrated.suggest').get('windowsExecutableExtensions'); context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration(cachedWindowsExecutableExtensionsSettingId)) { - cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration(cachedWindowsExecutableExtensionsSettingId); + cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration('terminal.integrated.suggest').get('windowsExecutableExtensions'); cachedAvailableCommands = undefined; cachedAvailableCommandsPath = undefined; } @@ -233,7 +233,7 @@ async function getCommandsInPath(env: { [key: string]: string | undefined } = pr const files = await vscode.workspace.fs.readDirectory(fileResource); for (const [file, fileType] of files) { const formattedPath = getFriendlyFilePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); - if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath), cachedWindowsExecutableExtensions) { + if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath, cachedWindowsExecutableExtensions)) { executables.add({ label: file, path: formattedPath }); labels.add(file); } From ec8061447561b9145478b55a0e6901a06dade664 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 18 Jan 2025 14:03:06 +0100 Subject: [PATCH 0682/3587] setup - adjust chat width based on window size --- src/vs/workbench/contrib/chat/browser/chat.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 47540018a2de..970754af7c40 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -60,8 +60,16 @@ export function ensureSideBarChatViewSize(viewDescriptorService: IViewDescriptor const viewPart = location === ViewContainerLocation.Sidebar ? Parts.SIDEBAR_PART : Parts.AUXILIARYBAR_PART; const partSize = layoutService.getSize(viewPart); - if (partSize.width < 400) { - layoutService.setSize(viewPart, { width: 400, height: partSize.height }); + + let adjustedChatWidth: number | undefined; + if (partSize.width < 400 && layoutService.mainContainerDimension.width > 1200) { + adjustedChatWidth = 400; // up to 400px if window bounds permit + } else if (partSize.width < 300) { + adjustedChatWidth = 300; // at minimum 300px + } + + if (typeof adjustedChatWidth === 'number') { + layoutService.setSize(viewPart, { width: adjustedChatWidth, height: partSize.height }); } } From 79c86506470694d6bbe9a32b82336181e836ee58 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 18 Jan 2025 15:08:58 +0100 Subject: [PATCH 0683/3587] setup - offer retry for setup failures --- .../contrib/chat/browser/chatSetup.ts | 74 ++++++++++++++----- 1 file changed, 55 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 9a2e5f5e4a84..0f6c3ba9e686 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -62,6 +62,7 @@ import { IWorkbenchEnvironmentService } from '../../../services/environment/comm import { isWeb } from '../../../../base/common/platform.js'; import { ExtensionUrlHandlerOverrideRegistry } from '../../../services/extensions/browser/extensionUrlHandler.js'; import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; +import { toErrorMessage } from '../../../../base/common/errorMessage.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -611,8 +612,8 @@ class ChatSetupRequests extends Disposable { const response = await this.request(defaultChat.entitlementSignupLimitedUrl, 'POST', body, session, CancellationToken.None); if (!response) { - this.onUnknownSignUpError('[chat setup] sign-up: no response'); - return { errorCode: 1 }; + const retry = await this.onUnknownSignUpError(localize('signUpNoResponseError', "No response received."), '[chat setup] sign-up: no response'); + return retry ? this.signUpLimited(session) : { errorCode: 1 }; } if (response.res.statusCode && response.res.statusCode !== 200) { @@ -630,8 +631,8 @@ class ChatSetupRequests extends Disposable { // ignore - handled below } } - this.onUnknownSignUpError(`[chat setup] sign-up: unexpected status code ${response.res.statusCode}`); - return { errorCode: response.res.statusCode }; + const retry = await this.onUnknownSignUpError(localize('signUpUnexpectedStatusError', "Unexpected status code {0}.", response.res.statusCode), `[chat setup] sign-up: unexpected status code ${response.res.statusCode}`); + return retry ? this.signUpLimited(session) : { errorCode: response.res.statusCode }; } let responseText: string | null = null; @@ -642,8 +643,8 @@ class ChatSetupRequests extends Disposable { } if (!responseText) { - this.onUnknownSignUpError('[chat setup] sign-up: response has no content'); - return { errorCode: 2 }; + const retry = await this.onUnknownSignUpError(localize('signUpNoResponseContentsError', "Response has no contents."), '[chat setup] sign-up: response has no content'); + return retry ? this.signUpLimited(session) : { errorCode: 2 }; } let parsedResult: { subscribed: boolean } | undefined = undefined; @@ -651,8 +652,8 @@ class ChatSetupRequests extends Disposable { parsedResult = JSON.parse(responseText); this.logService.trace(`[chat setup] sign-up: response is ${responseText}`); } catch (err) { - this.onUnknownSignUpError(`[chat setup] sign-up: error parsing response (${err})`); - return { errorCode: 3 }; + const retry = await this.onUnknownSignUpError(localize('signUpInvalidResponseError', "Invalid response contents."), `[chat setup] sign-up: error parsing response (${err})`); + return retry ? this.signUpLimited(session) : { errorCode: 3 }; } // We have made it this far, so the user either did sign-up or was signed-up already. @@ -662,12 +663,22 @@ class ChatSetupRequests extends Disposable { return Boolean(parsedResult?.subscribed); } - private onUnknownSignUpError(logMessage: string): void { - this.dialogService.error(localize('unknownSignUpError', "An error occurred while signing up for Copilot Free."), localize('unknownSignUpErrorDetail', "Please try again.")); + private async onUnknownSignUpError(detail: string, logMessage: string): Promise { this.logService.error(logMessage); + + const { confirmed } = await this.dialogService.confirm({ + type: Severity.Error, + message: localize('unknownSignUpError', "An error occurred while signing up for Copilot Free. Would you like to try again?"), + detail, + primaryButton: localize('retry', "Retry") + }); + + return confirmed; } private onUnprocessableSignUpError(logMessage: string, logDetails: string): void { + this.logService.error(logMessage); + this.dialogService.prompt({ type: Severity.Error, message: localize('unprocessableSignUpError', "An error occurred while signing up for Copilot Free."), @@ -683,7 +694,6 @@ class ChatSetupRequests extends Disposable { } ] }); - this.logService.error(logMessage); } override dispose(): void { @@ -741,7 +751,8 @@ class ChatSetupController extends Disposable { @IActivityService private readonly activityService: IActivityService, @ICommandService private readonly commandService: ICommandService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService + @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, + @IDialogService private readonly dialogService: IDialogService ) { super(); @@ -868,13 +879,7 @@ class ChatSetupController extends Disposable { } } - await this.extensionsWorkbenchService.install(defaultChat.extensionId, { - enable: true, - isApplicationScoped: true, // install into all profiles - isMachineScoped: false, // do not ask to sync - installEverywhere: true, // install in local and remote - installPreReleaseVersion: this.productService.quality !== 'stable' - }, isCopilotEditsViewActive(this.viewsService) ? EditsViewId : ChatViewId); + await this.doInstall(); installResult = 'installed'; } catch (error) { @@ -896,6 +901,37 @@ class ChatSetupController extends Disposable { this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult, signedIn, signUpErrorCode: undefined }); } + + private async doInstall(): Promise { + let error: Error | undefined; + try { + await this.extensionsWorkbenchService.install(defaultChat.extensionId, { + enable: true, + isApplicationScoped: true, // install into all profiles + isMachineScoped: false, // do not ask to sync + installEverywhere: true, // install in local and remote + installPreReleaseVersion: this.productService.quality !== 'stable' + }, isCopilotEditsViewActive(this.viewsService) ? EditsViewId : ChatViewId); + } catch (e) { + this.logService.error(`[chat setup] install: error ${error}`); + error = e; + } + + if (error) { + const { confirmed } = await this.dialogService.confirm({ + type: Severity.Error, + message: localize('unknownSetupError', "An error occurred while setting up Copilot Free. Would you like to try again?"), + detail: toErrorMessage(error), + primaryButton: localize('retry', "Retry") + }); + + if (confirmed) { + return this.doInstall(); + } + + throw error; + } + } } class ChatSetupWelcomeContent extends Disposable { From 7344ef1b5b84ff90be6fb2b894b59e88d6baa230 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 18 Jan 2025 17:14:58 +0100 Subject: [PATCH 0684/3587] setup - account for thrown errors when finding session --- .../contrib/chat/browser/chatSetup.ts | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 0f6c3ba9e686..349ef2390c2d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -442,26 +442,31 @@ class ChatSetupRequests extends Disposable { } private async findMatchingProviderSession(token: CancellationToken): Promise { - let sessions: ReadonlyArray = []; - const authProviderConfigValue = this.configurationService.getValue('github.copilot.advanced.authProvider'); - if (authProviderConfigValue) { - sessions = await this.authenticationService.getSessions(authProviderConfigValue); + const authProviders: string[] = []; + const configuredAuthProvider = this.configurationService.getValue('github.copilot.advanced.authProvider'); + if (configuredAuthProvider) { + authProviders.push(configuredAuthProvider); } else { - for (const providerId of defaultChat.providerIds) { - if (token.isCancellationRequested) { - return undefined; - } - sessions = await this.authenticationService.getSessions(providerId); - if (sessions.length) { - break; - } - } + authProviders.push(...defaultChat.providerIds); } - for (const session of sessions) { - for (const scopes of defaultChat.providerScopes) { - if (this.scopesMatch(session.scopes, scopes)) { - return session; + let sessions: ReadonlyArray = []; + for (const authProvider of authProviders) { + if (token.isCancellationRequested) { + return undefined; + } + + sessions = await this.doGetSessions(authProvider); + + if (token.isCancellationRequested) { + return undefined; + } + + for (const session of sessions) { + for (const scopes of defaultChat.providerScopes) { + if (this.scopesMatch(session.scopes, scopes)) { + return session; + } } } } @@ -469,6 +474,16 @@ class ChatSetupRequests extends Disposable { return undefined; } + private async doGetSessions(providerId: string): Promise { + try { + return await this.authenticationService.getSessions(providerId); + } catch (error) { + // ignore - errors can throw if a provider is not registered + } + + return []; + } + private scopesMatch(scopes: ReadonlyArray, expectedScopes: string[]): boolean { return scopes.length === expectedScopes.length && expectedScopes.every(scope => scopes.includes(scope)); } From 7def6d872b11f96a57a3850a991c8ba55e9a23f0 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 19 Jan 2025 06:43:19 +0100 Subject: [PATCH 0685/3587] setup - also show edits view for setup ootb --- src/vs/workbench/contrib/chat/browser/actions/chatActions.ts | 4 ---- .../contrib/chat/browser/actions/chatClearActions.ts | 1 - .../contrib/chat/browser/chatParticipant.contribution.ts | 1 + 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 4b1b916b7511..bbe671bd992f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -92,10 +92,6 @@ class OpenChatGlobalAction extends Action2 { title: OpenChatGlobalAction.TITLE, icon: Codicon.copilot, f1: true, - precondition: ContextKeyExpr.or( - ChatContextKeys.Setup.installed, - ChatContextKeys.panelParticipantRegistered - ), category: CHAT_CATEGORY, keybinding: { weight: KeybindingWeight.WorkbenchContrib, diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index 07f9ce1cc935..8ab3798c9fb5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -315,7 +315,6 @@ export function registerNewChatActions() { title: localize2('chat.openEdits.label', "Open {0}", 'Copilot Edits'), category: CHAT_CATEGORY, icon: Codicon.goToEditingSession, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.editingParticipantRegistered), f1: true, menu: [{ id: MenuId.ViewTitle, diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts index 04c182bff6b2..552834e8276b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts @@ -362,6 +362,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { }, ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.EditingSession }]), when: ContextKeyExpr.or( + ChatContextKeys.Setup.hidden.negate(), ChatContextKeys.Setup.installed, ChatContextKeys.editingParticipantRegistered ) From e8cfe77bc78318ff5aa31a4f37be470ca01c6b48 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 19 Jan 2025 06:52:07 +0100 Subject: [PATCH 0686/3587] setup - set Chat as default for aux bar --- .../contrib/chat/browser/chatParticipant.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts index 552834e8276b..4c13a9ced3a8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts @@ -284,7 +284,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { storageId: viewContainerId, hideIfEmpty: true, order: 100, - }, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true }); + }, ViewContainerLocation.AuxiliaryBar, { isDefault: true, doNotRegisterOpenCommand: true }); return viewContainer; } From e0b0b41632203af2755cbcefc2c0cb330179651f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 20 Jan 2025 08:39:49 +0100 Subject: [PATCH 0687/3587] layout - adjust wording for auxbar --- src/vs/workbench/browser/layout.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index bf1f54199d13..b0684e64e293 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -1074,7 +1074,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Restore Auxiliary Bar layoutReadyPromises.push((async () => { - // Restoring views could mean that panel already + // Restoring views could mean that auxbar already // restored, as such we need to test again await restoreDefaultViewsPromise; if (!this.state.initialization.views.containerToRestore.auxiliaryBar) { @@ -1083,9 +1083,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi mark('code/willRestoreAuxiliaryBar'); - const panel = await this.paneCompositeService.openPaneComposite(this.state.initialization.views.containerToRestore.auxiliaryBar, ViewContainerLocation.AuxiliaryBar); - if (!panel) { - await this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id, ViewContainerLocation.AuxiliaryBar); // fallback to default panel as needed + const viewlet = await this.paneCompositeService.openPaneComposite(this.state.initialization.views.containerToRestore.auxiliaryBar, ViewContainerLocation.AuxiliaryBar); + if (!viewlet) { + await this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id, ViewContainerLocation.AuxiliaryBar); // fallback to default viewlet as needed } mark('code/didRestoreAuxiliaryBar'); From 9b46b0bb978504d723cd8aa050547e99f97ff827 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 20 Jan 2025 12:21:44 +0100 Subject: [PATCH 0688/3587] layout - adopt `getDefaultViewContainer` in more places for panel and auxbar --- src/vs/workbench/browser/layout.ts | 24 ++++++++++++------- .../parts/auxiliarybar/auxiliaryBarPart.ts | 8 +++---- .../contrib/chat/browser/chatSetup.ts | 2 +- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index b0684e64e293..9ec9e4a45599 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -724,9 +724,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } } - // Auxiliary Panel to restore + // Auxiliary View to restore if (this.isVisible(Parts.AUXILIARYBAR_PART)) { - const viewContainerToRestore = this.storageService.get(AuxiliaryBarPart.activePanelSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id); + const viewContainerToRestore = this.storageService.get(AuxiliaryBarPart.activeViewSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id); if (viewContainerToRestore) { this.state.initialization.views.containerToRestore.auxiliaryBar = viewContainerToRestore; } else { @@ -1932,7 +1932,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (panelToOpen) { const focus = !skipLayout; - this.paneCompositeService.openPaneComposite(panelToOpen, ViewContainerLocation.Panel, focus); + const panel = this.paneCompositeService.openPaneComposite(panelToOpen, ViewContainerLocation.Panel, focus); + if (!panel) { + this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Panel)?.id, ViewContainerLocation.Panel, focus); + } } } @@ -2020,19 +2023,22 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // If auxiliary bar becomes visible, show last active pane composite or default pane composite else if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { - let panelToOpen: string | undefined = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.AuxiliaryBar); + let viewletToOpen: string | undefined = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.AuxiliaryBar); - // verify that the panel we try to open has views before we default to it + // verify that the viewlet we try to open has views before we default to it // otherwise fall back to any view that has views still refs #111463 - if (!panelToOpen || !this.hasViews(panelToOpen)) { - panelToOpen = this.viewDescriptorService + if (!viewletToOpen || !this.hasViews(viewletToOpen)) { + viewletToOpen = this.viewDescriptorService .getViewContainersByLocation(ViewContainerLocation.AuxiliaryBar) .find(viewContainer => this.hasViews(viewContainer.id))?.id; } - if (panelToOpen) { + if (viewletToOpen) { const focus = !skipLayout; - this.paneCompositeService.openPaneComposite(panelToOpen, ViewContainerLocation.AuxiliaryBar, focus); + const viewlet = this.paneCompositeService.openPaneComposite(viewletToOpen, ViewContainerLocation.AuxiliaryBar, focus); + if (!viewlet) { + this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id, ViewContainerLocation.AuxiliaryBar, focus); + } } } diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index 71337cabefeb..9250304af195 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -39,8 +39,8 @@ import { IHoverService } from '../../../../platform/hover/browser/hover.js'; export class AuxiliaryBarPart extends AbstractPaneCompositePart { - static readonly activePanelSettingsKey = 'workbench.auxiliarybar.activepanelid'; - static readonly pinnedPanelsKey = 'workbench.auxiliarybar.pinnedPanels'; + static readonly activeViewSettingsKey = 'workbench.auxiliarybar.activepanelid'; + static readonly pinnedViewsKey = 'workbench.auxiliarybar.pinnedPanels'; static readonly placeholdeViewContainersKey = 'workbench.auxiliarybar.placeholderPanels'; static readonly viewContainersWorkspaceStateKey = 'workbench.auxiliarybar.viewContainersWorkspaceState'; @@ -95,7 +95,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { hasTitle: true, borderWidth: () => (this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder)) ? 1 : 0, }, - AuxiliaryBarPart.activePanelSettingsKey, + AuxiliaryBarPart.activeViewSettingsKey, ActiveAuxiliaryContext.bindTo(contextKeyService), AuxiliaryBarFocusContext.bindTo(contextKeyService), 'auxiliarybar', @@ -156,7 +156,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { const $this = this; return { partContainerClass: 'auxiliarybar', - pinnedViewContainersKey: AuxiliaryBarPart.pinnedPanelsKey, + pinnedViewContainersKey: AuxiliaryBarPart.pinnedViewsKey, placeholderViewContainersKey: AuxiliaryBarPart.placeholdeViewContainersKey, viewContainersWorkspaceStateKey: AuxiliaryBarPart.viewContainersWorkspaceStateKey, icon: true, diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 349ef2390c2d..1c76f52d2209 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -935,7 +935,7 @@ class ChatSetupController extends Disposable { if (error) { const { confirmed } = await this.dialogService.confirm({ type: Severity.Error, - message: localize('unknownSetupError', "An error occurred while setting up Copilot Free. Would you like to try again?"), + message: localize('unknownSetupError', "An error occurred while setting up Copilot. Would you like to try again?"), detail: toErrorMessage(error), primaryButton: localize('retry', "Retry") }); From 0e5c32619bc6f8551399eacfb78d00f3e6788b61 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 20 Jan 2025 12:37:18 +0100 Subject: [PATCH 0689/3587] chat - register views statically --- .../browser/chatParticipant.contribution.ts | 193 ++++++++---------- 1 file changed, 84 insertions(+), 109 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts index 4c13a9ced3a8..3256369a754a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts @@ -9,7 +9,7 @@ import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { Event } from '../../../../base/common/event.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js'; import * as strings from '../../../../base/common/strings.js'; import { localize, localize2 } from '../../../../nls.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -32,6 +32,89 @@ import { IRawChatParticipantContribution } from '../common/chatParticipantContri import { ChatViewId } from './chat.js'; import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from './chatViewPane.js'; +// --- Chat Container & View Registration + +const chatViewContainer: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ + id: CHAT_SIDEBAR_PANEL_ID, + title: localize2('chat.viewContainer.label', "Chat"), + icon: Codicon.commentDiscussion, + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [CHAT_SIDEBAR_PANEL_ID, { mergeViewWithContainerWhenSingleView: true }]), + storageId: CHAT_SIDEBAR_PANEL_ID, + hideIfEmpty: true, + order: 100, +}, ViewContainerLocation.AuxiliaryBar, { isDefault: true, doNotRegisterOpenCommand: true }); + +const chatViewDescriptor: IViewDescriptor[] = [{ + id: ChatViewId, + containerIcon: chatViewContainer.icon, + containerTitle: chatViewContainer.title.value, + singleViewPaneContainerTitle: chatViewContainer.title.value, + name: localize2('chat.viewContainer.label', "Chat"), + canToggleVisibility: false, + canMoveView: true, + openCommandActionDescriptor: { + id: CHAT_SIDEBAR_PANEL_ID, + title: chatViewContainer.title, + mnemonicTitle: localize({ key: 'miToggleChat', comment: ['&& denotes a mnemonic'] }, "&&Chat"), + keybindings: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI, + mac: { + primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI + } + }, + order: 1 + }, + ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Panel }]), + when: ContextKeyExpr.or( + ChatContextKeys.Setup.hidden.negate(), + ChatContextKeys.Setup.installed, + ChatContextKeys.panelParticipantRegistered, + ChatContextKeys.extensionInvalid + ) +}]; +Registry.as(ViewExtensions.ViewsRegistry).registerViews(chatViewDescriptor, chatViewContainer); + +// --- Edits Container & View Registration + +const editsViewContainer: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ + id: CHAT_EDITING_SIDEBAR_PANEL_ID, + title: localize2('chatEditing.viewContainer.label', "Copilot Edits"), + icon: Codicon.editSession, + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [CHAT_EDITING_SIDEBAR_PANEL_ID, { mergeViewWithContainerWhenSingleView: true }]), + storageId: CHAT_EDITING_SIDEBAR_PANEL_ID, + hideIfEmpty: true, + order: 101, +}, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true }); + +const editsViewDescriptor: IViewDescriptor[] = [{ + id: 'workbench.panel.chat.view.edits', + containerIcon: editsViewContainer.icon, + containerTitle: editsViewContainer.title.value, + singleViewPaneContainerTitle: editsViewContainer.title.value, + name: editsViewContainer.title, + canToggleVisibility: false, + canMoveView: true, + openCommandActionDescriptor: { + id: CHAT_EDITING_SIDEBAR_PANEL_ID, + title: editsViewContainer.title, + mnemonicTitle: localize({ key: 'miToggleEdits', comment: ['&& denotes a mnemonic'] }, "Copilot Ed&&its"), + keybindings: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI, + linux: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI + } + }, + order: 2 + }, + ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.EditingSession }]), + when: ContextKeyExpr.or( + ChatContextKeys.Setup.hidden.negate(), + ChatContextKeys.Setup.installed, + ChatContextKeys.editingParticipantRegistered + ) +}]; +Registry.as(ViewExtensions.ViewsRegistry).registerViews(editsViewDescriptor, editsViewContainer); + const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'chatParticipants', jsonSchema: { @@ -166,16 +249,12 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatExtensionPointHandler'; - private _viewContainer: ViewContainer; private _participantRegistrationDisposables = new DisposableMap(); constructor( @IChatAgentService private readonly _chatAgentService: IChatAgentService, @ILogService private readonly logService: ILogService ) { - this._viewContainer = this.registerViewContainer(); - this.registerDefaultParticipantView(); - this.registerChatEditingView(); this.handleAndRegisterChatExtensions(); } @@ -270,110 +349,6 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { } }); } - - private registerViewContainer(): ViewContainer { - // Register View Container - const title = localize2('chat.viewContainer.label', "Chat"); - const icon = Codicon.commentDiscussion; - const viewContainerId = CHAT_SIDEBAR_PANEL_ID; - const viewContainer: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ - id: viewContainerId, - title, - icon, - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [viewContainerId, { mergeViewWithContainerWhenSingleView: true }]), - storageId: viewContainerId, - hideIfEmpty: true, - order: 100, - }, ViewContainerLocation.AuxiliaryBar, { isDefault: true, doNotRegisterOpenCommand: true }); - - return viewContainer; - } - - private registerDefaultParticipantView(): IDisposable { - const viewDescriptor: IViewDescriptor[] = [{ - id: ChatViewId, - containerIcon: this._viewContainer.icon, - containerTitle: this._viewContainer.title.value, - singleViewPaneContainerTitle: this._viewContainer.title.value, - name: localize2('chat.viewContainer.label', "Chat"), - canToggleVisibility: false, - canMoveView: true, - openCommandActionDescriptor: { - id: CHAT_SIDEBAR_PANEL_ID, - title: this._viewContainer.title, - mnemonicTitle: localize({ key: 'miToggleChat', comment: ['&& denotes a mnemonic'] }, "&&Chat"), - keybindings: { - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI, - mac: { - primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI - } - }, - order: 1 - }, - ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Panel }]), - when: ContextKeyExpr.or( - ChatContextKeys.Setup.hidden.negate(), - ChatContextKeys.Setup.installed, - ChatContextKeys.panelParticipantRegistered, - ChatContextKeys.extensionInvalid - ) - }]; - Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, this._viewContainer); - - return toDisposable(() => { - Registry.as(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, this._viewContainer); - }); - } - - private registerChatEditingView(): IDisposable { - const title = localize2('chatEditing.viewContainer.label', "Copilot Edits"); - const icon = Codicon.editSession; - const viewContainerId = CHAT_EDITING_SIDEBAR_PANEL_ID; - const viewContainer: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ - id: viewContainerId, - title, - icon, - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [viewContainerId, { mergeViewWithContainerWhenSingleView: true }]), - storageId: viewContainerId, - hideIfEmpty: true, - order: 101, - }, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true }); - - const id = 'workbench.panel.chat.view.edits'; - const viewDescriptor: IViewDescriptor[] = [{ - id, - containerIcon: viewContainer.icon, - containerTitle: title.value, - singleViewPaneContainerTitle: title.value, - name: { value: title.value, original: title.value }, - canToggleVisibility: false, - canMoveView: true, - openCommandActionDescriptor: { - id: viewContainerId, - title, - mnemonicTitle: localize({ key: 'miToggleEdits', comment: ['&& denotes a mnemonic'] }, "Copilot Ed&&its"), - keybindings: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI, - linux: { - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI - } - }, - order: 2 - }, - ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.EditingSession }]), - when: ContextKeyExpr.or( - ChatContextKeys.Setup.hidden.negate(), - ChatContextKeys.Setup.installed, - ChatContextKeys.editingParticipantRegistered - ) - }]; - Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, viewContainer); - - return toDisposable(() => { - Registry.as(ViewExtensions.ViewContainersRegistry).deregisterViewContainer(viewContainer); - Registry.as(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, viewContainer); - }); - } } function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string { From 0000f9dffea8710dbef04422d840a8ba847cb540 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 20 Jan 2025 04:30:51 -0800 Subject: [PATCH 0690/3587] Add details for pwsh globals via get-command Fixes #238181 --- .../src/terminalSuggestMain.ts | 50 +++++++++++++++---- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 662df64816d6..333de42c55e9 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs/promises'; import * as path from 'path'; -import { ExecOptionsWithStringEncoding, execSync } from 'child_process'; +import { exec, ExecOptionsWithStringEncoding, execSync } from 'child_process'; import { upstreamSpecs } from './constants'; import codeCompletionSpec from './completions/code'; import cdSpec from './completions/cd'; @@ -13,6 +13,10 @@ import codeInsidersCompletionSpec from './completions/code-insiders'; import { osIsWindows } from './helpers/os'; import { isExecutable } from './helpers/executable'; +const enum PwshCommandType { + Alias = 1 +} + const isWindows = osIsWindows(); let cachedAvailableCommandsPath: string | undefined; let cachedWindowsExecutableExtensions: Object | undefined; @@ -29,7 +33,7 @@ for (const spec of upstreamSpecs) { availableSpecs.push(require(`./completions/upstream/${spec}`).default); } -function getBuiltinCommands(shell: string, existingCommands?: Set): ICompletionResource[] | undefined { +async function getBuiltinCommands(shell: string, existingCommands?: Set): Promise { try { const shellType = path.basename(shell, path.extname(shell)); const cachedCommands = cachedBuiltinCommands.get(shellType); @@ -57,8 +61,18 @@ function getBuiltinCommands(shell: string, existingCommands?: Set): ICom break; } case 'pwsh': { - // TODO: Select `CommandType, DisplayName` and map to a rich type with kind and detail - const output = execSync('Get-Command -All | Select-Object Name | ConvertTo-Json', options); + const output = await new Promise((resolve, reject) => { + exec('Get-Command -All | Select-Object Name, CommandType, DisplayName, Definition | ConvertTo-Json', { + ...options, + maxBuffer: 1024 * 1024 * 100 // This is a lot of content, increase buffer size + }, (error, stdout) => { + if (error) { + reject(error); + return; + } + resolve(stdout); + }); + }); let json: any; try { json = JSON.parse(output); @@ -66,8 +80,24 @@ function getBuiltinCommands(shell: string, existingCommands?: Set): ICom console.error('Error parsing pwsh output:', e); return []; } - commands = (json as any[]).map(e => e.Name); - break; + const commandResources = (json as any[]).map(e => { + switch (e.CommandType) { + case PwshCommandType.Alias: { + return { + label: e.Name, + detail: e.DisplayName, + }; + } + default: { + return { + label: e.Name, + detail: e.Definition, + }; + } + } + }); + cachedBuiltinCommands.set(shellType, commandResources); + return commandResources; } } @@ -96,7 +126,7 @@ export async function activate(context: vscode.ExtensionContext) { } const commandsInPath = await getCommandsInPath(terminal.shellIntegration?.env); - const builtinCommands = getBuiltinCommands(shellPath, commandsInPath?.labels) ?? []; + const builtinCommands = await getBuiltinCommands(shellPath, commandsInPath?.labels) ?? []; if (!commandsInPath?.completionResources) { return; } @@ -188,7 +218,7 @@ function createCompletionItem(cursorPosition: number, prefix: string, commandRes const lastWord = endsWithSpace ? '' : prefix.split(' ').at(-1) ?? ''; return { label: commandResource.label, - detail: description ?? commandResource.path ?? '', + detail: description ?? commandResource.detail ?? '', replacementIndex: cursorPosition - lastWord.length, replacementLength: lastWord.length, kind: kind ?? vscode.TerminalCompletionItemKind.Method @@ -197,7 +227,7 @@ function createCompletionItem(cursorPosition: number, prefix: string, commandRes interface ICompletionResource { label: string; - path?: string; + detail?: string; } async function getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { const labels: Set = new Set(); @@ -234,7 +264,7 @@ async function getCommandsInPath(env: { [key: string]: string | undefined } = pr for (const [file, fileType] of files) { const formattedPath = getFriendlyFilePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath), cachedWindowsExecutableExtensions) { - executables.add({ label: file, path: formattedPath }); + executables.add({ label: file, detail: formattedPath }); labels.add(file); } } From 4a0183a278a4a076fac91702632c576cc2267a18 Mon Sep 17 00:00:00 2001 From: Robo Date: Mon, 20 Jan 2025 21:34:41 +0900 Subject: [PATCH 0691/3587] fix: incorrect ripgrep binaries bundled for windows arm64 (#238287) --- package-lock.json | 9 +++++---- package.json | 2 +- remote/package-lock.json | 9 +++++---- remote/package.json | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 99a6c490a60d..3669fef0d86f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.8", "@vscode/proxy-agent": "^0.30.0", - "@vscode/ripgrep": "^1.15.9", + "@vscode/ripgrep": "^1.15.10", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", "@vscode/sudo-prompt": "9.3.1", @@ -2867,10 +2867,11 @@ } }, "node_modules/@vscode/ripgrep": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.9.tgz", - "integrity": "sha512-4q2PXRvUvr3bF+LsfrifmUZgSPmCNcUZo6SbEAZgArIChchkezaxLoIeQMJe/z3CCKStvaVKpBXLxN3Z8lQjFQ==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.10.tgz", + "integrity": "sha512-83Q6qFrELpFgf88bPOcwSWDegfY2r/cb6bIfdLTSZvN73Dg1wviSfO+1v6lTFMd0mAvUYYcTUu+Mn5xMroZMxA==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "https-proxy-agent": "^7.0.2", "proxy-from-env": "^1.1.0", diff --git a/package.json b/package.json index 2a792905f9b0..8aa946734a94 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.8", "@vscode/proxy-agent": "^0.30.0", - "@vscode/ripgrep": "^1.15.9", + "@vscode/ripgrep": "^1.15.10", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", "@vscode/sudo-prompt": "9.3.1", diff --git a/remote/package-lock.json b/remote/package-lock.json index 975417a80b2e..b4383b1c9598 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -14,7 +14,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/proxy-agent": "^0.30.0", - "@vscode/ripgrep": "^1.15.9", + "@vscode/ripgrep": "^1.15.10", "@vscode/spdlog": "^0.15.0", "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", @@ -437,10 +437,11 @@ } }, "node_modules/@vscode/ripgrep": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.9.tgz", - "integrity": "sha512-4q2PXRvUvr3bF+LsfrifmUZgSPmCNcUZo6SbEAZgArIChchkezaxLoIeQMJe/z3CCKStvaVKpBXLxN3Z8lQjFQ==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.10.tgz", + "integrity": "sha512-83Q6qFrELpFgf88bPOcwSWDegfY2r/cb6bIfdLTSZvN73Dg1wviSfO+1v6lTFMd0mAvUYYcTUu+Mn5xMroZMxA==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "https-proxy-agent": "^7.0.2", "proxy-from-env": "^1.1.0", diff --git a/remote/package.json b/remote/package.json index cffdccb4ff3a..458479b67e14 100644 --- a/remote/package.json +++ b/remote/package.json @@ -9,7 +9,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/proxy-agent": "^0.30.0", - "@vscode/ripgrep": "^1.15.9", + "@vscode/ripgrep": "^1.15.10", "@vscode/spdlog": "^0.15.0", "@vscode/tree-sitter-wasm": "^0.0.5", "@vscode/vscode-languagedetection": "1.0.21", From 60fe8381330473e12601b21bbfbb590d58b09be2 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 20 Jan 2025 13:52:46 +0100 Subject: [PATCH 0692/3587] files - ignore errors from disposed models (#238288) --- .../services/textfile/common/textFileEditorModelManager.ts | 4 +++- .../workingCopy/common/storedFileWorkingCopyManager.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index e48add78adb7..0da81d1e2f6b 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -394,7 +394,9 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE try { await model.resolve(options); } catch (error) { - onUnexpectedError(error); + if (!model.isDisposed()) { + onUnexpectedError(error); // only log if the model is still around + } } })(); } diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts index 5a8f9d88bd3f..b45e11e5c184 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts @@ -505,7 +505,9 @@ export class StoredFileWorkingCopyManager try { await workingCopy.resolve(resolveOptions); } catch (error) { - onUnexpectedError(error); + if (!workingCopy.isDisposed()) { + onUnexpectedError(error); // only log if the working copy is still around + } } })(); } From e55ac22a120aab8d290194c1501c14e805e698a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Mon, 20 Jan 2025 15:58:40 +0100 Subject: [PATCH 0693/3587] add debug statement (#238299) --- .../src/singlefolder-tests/terminal.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index f43ba17975c6..15aa81288e0d 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -771,9 +771,14 @@ import { assertNoRpc, poll } from '../utils'; terminal.sendText('echo $C'); // Poll for the echo results to show up - await poll(() => Promise.resolve(), () => data.includes('~a2~'), '~a2~ should be printed'); - await poll(() => Promise.resolve(), () => data.includes('b1~b2~'), 'b1~b2~ should be printed'); - await poll(() => Promise.resolve(), () => data.includes('~c2~c1'), '~c2~c1 should be printed'); + try { + await poll(() => Promise.resolve(), () => data.includes('~a2~'), '~a2~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('b1~b2~'), 'b1~b2~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('~c2~c1'), '~c2~c1 should be printed'); + } catch (err) { + console.error('DATA UP UNTIL NOW:', data); + throw err; + } // Wait for terminal to be disposed await new Promise(r => { From 11b2f94022b6063c335f63b0b3f293db4d974647 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:17:04 +0100 Subject: [PATCH 0694/3587] Git - add missing event to the Repository interface (#238298) --- extensions/git/src/api/git.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 51cfd496fda7..5bbb4b03a9c6 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -200,6 +200,7 @@ export interface Repository { readonly ui: RepositoryUIState; readonly onDidCommit: Event; + readonly onDidCheckout: Event; getConfigs(): Promise<{ key: string; value: string; }[]>; getConfig(key: string): Promise; From e5165663d9fda26a18612b85ec5f56fda3defcfc Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:58:45 +0100 Subject: [PATCH 0695/3587] Git - add commands to toggle git blame editor decoration/status bar item (#238302) --- extensions/git/package.json | 10 ++++++++++ extensions/git/package.nls.json | 2 ++ extensions/git/src/commands.ts | 17 +++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/extensions/git/package.json b/extensions/git/package.json index a05c557a2b13..cc06a2f63d11 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -933,6 +933,16 @@ "command": "git.copyCommitMessage", "title": "%command.timelineCopyCommitMessage%", "category": "Git" + }, + { + "command": "git.blame.toggleEditorDecoration", + "title": "%command.blameToggleEditorDecoration%", + "category": "Git" + }, + { + "command": "git.blame.toggleStatusBarItem", + "title": "%command.blameToggleStatusBarItem%", + "category": "Git" } ], "continueEditSession": [ diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 61168a79c344..5cfb2b1a7b24 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -128,6 +128,8 @@ "command.graphCherryPick": "Cherry Pick", "command.graphDeleteBranch": "Delete Branch", "command.graphDeleteTag": "Delete Tag", + "command.blameToggleEditorDecoration": "Toggle Git Blame Editor Decoration", + "command.blameToggleStatusBarItem": "Toggle Git Blame Status Bar Item", "command.api.getRepositories": "Get Repositories", "command.api.getRepositoryState": "Get Repository State", "command.api.getRemoteSources": "Get Remote Sources", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 26d0a1a1c3da..01b9ef29fea6 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -4356,6 +4356,23 @@ export class CommandCenter { env.clipboard.writeText(content); } + @command('git.blame.toggleEditorDecoration') + toggleBlameEditorDecoration(): void { + this._toggleBlameSetting('blame.editorDecoration.enabled'); + } + + @command('git.blame.toggleStatusBarItem') + toggleBlameStatusBarItem(): void { + this._toggleBlameSetting('blame.statusBarItem.enabled'); + } + + private _toggleBlameSetting(setting: string): void { + const config = workspace.getConfiguration('git'); + const enabled = config.get(setting) === true; + + config.update(setting, !enabled, true); + } + private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any { const result = (...args: any[]) => { let result: Promise; From 2a00f253b32c54a021199c4caf7da4089307ce02 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 20 Jan 2025 17:27:48 +0100 Subject: [PATCH 0696/3587] chat - distinguish better failed sign in attempts --- .../contrib/chat/browser/chatSetup.ts | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 1c76f52d2209..884f2970ee73 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -726,12 +726,10 @@ type InstallChatClassification = { owner: 'bpasero'; comment: 'Provides insight into chat installation.'; installResult: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the extension was installed successfully, cancelled or failed to install.' }; - signedIn: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user did sign in prior to installing the extension.' }; signUpErrorCode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The error code in case of an error signing up.' }; }; type InstallChatEvent = { - installResult: 'installed' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn' | 'failedSignUp' | 'failedNotTrusted'; - signedIn: boolean; + installResult: 'installed' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn' | 'failedSignUp' | 'failedNotTrusted' | 'failedNoSession'; signUpErrorCode: number | undefined; }; @@ -817,7 +815,7 @@ class ChatSetupController extends Disposable { this.setStep(ChatSetupStep.SigningIn); const result = await this.signIn(); if (!result.session) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signedIn: false, signUpErrorCode: undefined }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signUpErrorCode: undefined }); return; // user cancelled } @@ -825,19 +823,11 @@ class ChatSetupController extends Disposable { entitlement = result.entitlement; } - if (!session) { - session = (await this.authenticationService.getSessions(defaultChat.providerIds[0])).at(0); - if (!session) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signedIn: false, signUpErrorCode: undefined }); - return; // unexpected - } - } - const trusted = await this.workspaceTrustRequestService.requestWorkspaceTrust({ message: localize('copilotWorkspaceTrust', "Copilot is currently only supported in trusted workspaces.") }); if (!trusted) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotTrusted', signedIn: true, signUpErrorCode: undefined }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotTrusted', signUpErrorCode: undefined }); return; } @@ -877,9 +867,7 @@ class ChatSetupController extends Disposable { return { session, entitlement }; } - private async install(session: AuthenticationSession, entitlement: ChatEntitlement,): Promise { - const signedIn = !!session; - + private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement,): Promise { let installResult: 'installed' | 'cancelled' | 'failedInstall' | undefined = undefined; const wasInstalled = this.context.state.installed; let didSignUp: boolean | { errorCode: number } | undefined = undefined; @@ -887,10 +875,23 @@ class ChatSetupController extends Disposable { showCopilotView(this.viewsService, this.layoutService); if (entitlement !== ChatEntitlement.Limited && entitlement !== ChatEntitlement.Pro && entitlement !== ChatEntitlement.Unavailable) { + if (!session) { + try { + session = (await this.authenticationService.getSessions(defaultChat.providerIds[0])).at(0); + } catch (error) { + // ignore - errors can throw if a provider is not registered + } + + if (!session) { + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNoSession', signUpErrorCode: undefined }); + return; // unexpected + } + } + didSignUp = await this.requests.signUpLimited(session); if (typeof didSignUp !== 'boolean' /* error */) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', signedIn, signUpErrorCode: didSignUp.errorCode }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', signUpErrorCode: didSignUp.errorCode }); } } @@ -914,7 +915,7 @@ class ChatSetupController extends Disposable { } } - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult, signedIn, signUpErrorCode: undefined }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult, signUpErrorCode: undefined }); } private async doInstall(): Promise { From fa68c7c788357c6bc3cc48bb4954262c794d783d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 20 Jan 2025 17:44:36 +0100 Subject: [PATCH 0697/3587] chat - allow to retry failed sign in attempts in setup --- .../contrib/chat/browser/chatSetup.ts | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 884f2970ee73..e0e1a15b70ee 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -856,12 +856,25 @@ class ChatSetupController extends Disposable { showCopilotView(this.viewsService, this.layoutService); session = await this.authenticationService.createSession(defaultChat.providerIds[0], defaultChat.providerScopes[0]); - entitlement = await this.requests.forceResolveEntitlement(session); this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, defaultChat.providerIds[0], session.account); this.authenticationExtensionsService.updateAccountPreference(defaultChat.chatExtensionId, defaultChat.providerIds[0], session.account); - } catch (error) { - this.logService.error(`[chat setup] signIn: error ${error}`); + + entitlement = await this.requests.forceResolveEntitlement(session); + } catch (e) { + this.logService.error(`[chat setup] signIn: error ${e}`); + } + + if (!session) { + const { confirmed } = await this.dialogService.confirm({ + type: Severity.Error, + message: localize('unknownSignInError', "Signing in to Copilot was unsuccessful. Would you like to try again?"), + primaryButton: localize('retry', "Retry") + }); + + if (confirmed) { + return this.signIn(); + } } return { session, entitlement }; @@ -930,14 +943,16 @@ class ChatSetupController extends Disposable { }, isCopilotEditsViewActive(this.viewsService) ? EditsViewId : ChatViewId); } catch (e) { this.logService.error(`[chat setup] install: error ${error}`); - error = e; + if (!isCancellationError(e)) { + error = e; + } } if (error) { const { confirmed } = await this.dialogService.confirm({ type: Severity.Error, message: localize('unknownSetupError', "An error occurred while setting up Copilot. Would you like to try again?"), - detail: toErrorMessage(error), + detail: error ? toErrorMessage(error) : undefined, primaryButton: localize('retry', "Retry") }); From 31122d79c7bfce5dfc32eb6b49ecc27706c19e76 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 20 Jan 2025 19:04:23 +0100 Subject: [PATCH 0698/3587] Persist view across same edit (#238308) persist view across same edit --- .../browser/model/provideInlineCompletions.ts | 6 + .../browser/view/inlineEdits/view.ts | 130 ++++++++++++------ 2 files changed, 93 insertions(+), 43 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index d9d1de0342d8..d964f81ac36a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -337,6 +337,8 @@ export class InlineCompletionItem { ); } + static ID = 1; + private _didCallShow = false; constructor( @@ -362,6 +364,8 @@ export class InlineCompletionItem { * Used for event data to ensure referential equality. */ readonly source: InlineCompletionList, + + readonly id = `InlineCompletion:${InlineCompletionItem.ID++}`, ) { // TODO: these statements are no-ops filterText = filterText.replace(/\r\n|\r/g, '\n'); @@ -387,6 +391,7 @@ export class InlineCompletionItem { this.additionalTextEdits, this.sourceInlineCompletion, this.source, + this.id, ); } @@ -402,6 +407,7 @@ export class InlineCompletionItem { this.additionalTextEdits, this.sourceInlineCompletion, this.source, + this.id, ); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 37aea03afe9c..5e7b1163bbc4 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -35,6 +35,8 @@ export class InlineEditsView extends Disposable { private readonly _useWordReplacementView = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useWordReplacementView); private readonly _useWordInsertionView = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useWordInsertionView); + private _previousView: { id: string; view: ReturnType; userJumpedToIt: boolean } | undefined; + constructor( private readonly _editor: ICodeEditor, private readonly _edit: IObservable, @@ -65,6 +67,10 @@ export class InlineEditsView extends Disposable { let diff = lineRangeMappingFromRangeMappings(mappings, edit.originalText, new StringText(newText)); const state = this.determineRenderState(edit, reader, diff, new StringText(newText)); + if (!state) { + this._model.get()?.stop(); + return undefined; + } if (state.kind === 'sideBySide') { const indentationAdjustmentEdit = createReindentEdit(newText, edit.modifiedLineRange); @@ -110,7 +116,7 @@ export class InlineEditsView extends Disposable { this._editor, this._edit, this._previewTextModel, - this._uiState.map(s => s && s.state.kind === 'sideBySide' ? ({ + this._uiState.map(s => s && s.state?.kind === 'sideBySide' ? ({ edit: s.edit, newTextLineCount: s.newTextLineCount, originalDisplayRange: s.originalDisplayRange, @@ -120,7 +126,7 @@ export class InlineEditsView extends Disposable { protected readonly _deletion = this._register(this._instantiationService.createInstance(InlineEditsDeletionView, this._editor, this._edit, - this._uiState.map(s => s && s.state.kind === 'deletion' ? ({ + this._uiState.map(s => s && s.state?.kind === 'deletion' ? ({ edit: s.edit, originalDisplayRange: s.originalDisplayRange, widgetStartColumn: s.state.widgetStartColumn, @@ -129,7 +135,7 @@ export class InlineEditsView extends Disposable { private readonly _inlineDiffViewState = derived(this, reader => { const e = this._uiState.read(reader); - if (!e) { return undefined; } + if (!e || !e.state) { return undefined; } if (e.state.kind === 'wordReplacements' || e.state.kind === 'lineReplacement') { return undefined; } @@ -143,7 +149,7 @@ export class InlineEditsView extends Disposable { protected readonly _inlineDiffView = this._register(new OriginalEditorInlineDiffView(this._editor, this._inlineDiffViewState, this._previewTextModel)); - protected readonly _wordReplacementViews = mapObservableArrayCached(this, this._uiState.map(s => s?.state.kind === 'wordReplacements' ? s.state.replacements : []), (e, store) => { + protected readonly _wordReplacementViews = mapObservableArrayCached(this, this._uiState.map(s => s?.state?.kind === 'wordReplacements' ? s.state.replacements : []), (e, store) => { if (e.range.isEmpty()) { return store.add(this._instantiationService.createInstance(WordInsertView, this._editorObs, e)); } else { @@ -151,7 +157,7 @@ export class InlineEditsView extends Disposable { } }).recomputeInitiallyAndOnChange(this._store); - protected readonly _lineReplacementView = mapObservableArrayCached(this, this._uiState.map(s => s?.state.kind === 'lineReplacement' ? [s.state] : []), (e, store) => { // TODO: no need for map here, how can this be done with observables + protected readonly _lineReplacementView = mapObservableArrayCached(this, this._uiState.map(s => s?.state?.kind === 'lineReplacement' ? [s.state] : []), (e, store) => { // TODO: no need for map here, how can this be done with observables return store.add(this._instantiationService.createInstance(WordReplacementView, this._editorObs, e.edit, e.replacements)); }).recomputeInitiallyAndOnChange(this._store); @@ -172,7 +178,7 @@ export class InlineEditsView extends Disposable { this._editorObs, derived(reader => { const state = this._uiState.read(reader); - if (!state) { return undefined; } + if (!state || !state.state) { return undefined; } const range = state.originalDisplayRange; const top = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); return { editTop: top, showAlways: state.state.kind !== 'sideBySide' }; @@ -182,14 +188,24 @@ export class InlineEditsView extends Disposable { } })); - private determineRenderState(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText) { + private determineView(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText): string { + // Check if we can use the previous view if it is the same InlineCompletion as previously shown + if (this._previousView?.id === edit.inlineCompletion.id) { + const canUseCache = edit.userJumpedToIt === this._previousView.userJumpedToIt || + !(this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible' && this._previousView.view !== 'mixedLines') && + !(this._useInterleavedLinesDiff.read(reader) === 'afterJump' && this._previousView.view !== 'interleavedLines'); + + if (canUseCache) { + return this._previousView.view; + } + } + if (edit.isCollapsed) { - return { kind: 'collapsed' as const }; + return 'collapsed'; } const inner = diff.flatMap(d => d.innerChanges ?? []); const isSingleInnerEdit = inner.length === 1; - if ( isSingleInnerEdit && ( this._useMixedLinesDiff.read(reader) === 'forStableInsertions' @@ -197,13 +213,11 @@ export class InlineEditsView extends Disposable { || isSingleLineDeletion(diff) ) ) { - return { kind: 'ghostText' as const }; + return 'ghostText'; } if (inner.every(m => newText.getValueOfRange(m.modifiedRange).trim() === '')) { - const trimLength = getPrefixTrimLength(edit.originalText.getLineAt(edit.originalLineRange.startLineNumber), newText.getLineAt(edit.modifiedLineRange.startLineNumber)); - const widgetStartColumn = Math.min(trimLength, ...inner.map(m => m.originalRange.startLineNumber !== m.originalRange.endLineNumber ? 0 : m.originalRange.startColumn - 1)); - return { kind: 'deletion' as const, widgetStartColumn }; + return 'deletion'; } if (diff.length === 1 && diff[0].original.length === 1 && diff[0].modified.length === 1) { @@ -213,36 +227,12 @@ export class InlineEditsView extends Disposable { )); if (canUseWordReplacementView) { - const allInnerModifiedTexts = inner.map(m => newText.getValueOfRange(m.modifiedRange)); - const allInnerOriginalTexts = inner.map(m => edit.originalText.getValueOfRange(m.originalRange)); - const allInnerEditsAreTheSame = allInnerModifiedTexts.every(text => text === allInnerModifiedTexts[0]) && allInnerOriginalTexts.every(text => text === allInnerOriginalTexts[0]); const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); - if (allInnerChangesNotTooLong && isSingleInnerEdit && allInnerEditsAreTheSame) { - return { - kind: 'wordReplacements' as const, - replacements: inner.map(i => - new SingleTextEdit(i.originalRange, allInnerModifiedTexts[0]) - ) - }; + if (allInnerChangesNotTooLong && isSingleInnerEdit) { + return 'wordReplacements'; } else { - const replacements = inner.map((m, i) => new SingleTextEdit(m.originalRange, allInnerModifiedTexts[i])); - - const originalLine = edit.originalText.getLineAt(edit.originalLineRange.startLineNumber); - const editedLine = newText.getLineAt(edit.modifiedLineRange.startLineNumber); - const trimLength = Math.min(getPrefixTrimLength(originalLine, editedLine), replacements[0].range.startColumn - 1); - - const textEdit = edit.lineEdit.toSingleTextEdit(edit.originalText); - const lineEdit = new SingleTextEdit( - new Range(textEdit.range.startLineNumber, textEdit.range.startColumn + trimLength, textEdit.range.endLineNumber, textEdit.range.endColumn), - textEdit.text.slice(trimLength) - ); - - return { - kind: 'lineReplacement' as const, - edit: lineEdit, - replacements, - }; + return 'lineReplacement'; } } } @@ -251,14 +241,68 @@ export class InlineEditsView extends Disposable { (this._useMixedLinesDiff.read(reader) === 'whenPossible' || (edit.userJumpedToIt && this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible')) && diff.every(m => OriginalEditorInlineDiffView.supportsInlineDiffRendering(m)) ) { - return { kind: 'mixedLines' as const }; + return 'mixedLines'; } if (this._useInterleavedLinesDiff.read(reader) === 'always' || (edit.userJumpedToIt && this._useInterleavedLinesDiff.read(reader) === 'afterJump')) { - return { kind: 'interleavedLines' as const }; + return 'interleavedLines'; + } + + return 'sideBySide'; + } + + private determineRenderState(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText) { + + const view = this.determineView(edit, reader, diff, newText); + this._previousView = { id: edit.inlineCompletion.id, view, userJumpedToIt: edit.userJumpedToIt }; + + switch (view) { + case 'collapsed': return { kind: 'collapsed' as const }; + case 'ghostText': return { kind: 'ghostText' as const }; + case 'mixedLines': return { kind: 'mixedLines' as const }; + case 'interleavedLines': return { kind: 'interleavedLines' as const }; + case 'sideBySide': return { kind: 'sideBySide' as const }; + } + + const inner = diff.flatMap(d => d.innerChanges ?? []); + + if (view === 'deletion') { + const trimLength = getPrefixTrimLength(edit.originalText.getLineAt(edit.originalLineRange.startLineNumber), newText.getLineAt(edit.modifiedLineRange.startLineNumber)); + const widgetStartColumn = Math.min(trimLength, ...inner.map(m => m.originalRange.startLineNumber !== m.originalRange.endLineNumber ? 0 : m.originalRange.startColumn - 1)); + return { kind: 'deletion' as const, widgetStartColumn }; + } + + const replacements = inner.map(m => new SingleTextEdit(m.originalRange, newText.getValueOfRange(m.modifiedRange))); + if (replacements.length === 0) { + return undefined; + } + + if (view === 'wordReplacements') { + return { + kind: 'wordReplacements' as const, + replacements + }; + } + + if (view === 'lineReplacement') { + const originalLine = edit.originalText.getLineAt(edit.originalLineRange.startLineNumber); + const editedLine = newText.getLineAt(edit.modifiedLineRange.startLineNumber); + const trimLength = Math.min(getPrefixTrimLength(originalLine, editedLine), replacements[0].range.startColumn - 1); + + const textEdit = edit.lineEdit.toSingleTextEdit(edit.originalText); + const lineEdit = new SingleTextEdit( + new Range(textEdit.range.startLineNumber, textEdit.range.startColumn + trimLength, textEdit.range.endLineNumber, textEdit.range.endColumn), + textEdit.text.slice(trimLength) + ); + + return { + kind: 'lineReplacement' as const, + edit: lineEdit, + replacements, + }; } - return { kind: 'sideBySide' as const }; + return undefined; } } From 3f5e7810ca96a7bc870abb153e571bf85303254f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 20 Jan 2025 20:32:26 +0100 Subject: [PATCH 0699/3587] chat - cleanup setup (#238313) --- .../contrib/chat/browser/chatSetup.ts | 46 ++++++++----------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index e0e1a15b70ee..a8b5ce20a5b8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -745,9 +745,7 @@ class ChatSetupController extends Disposable { readonly onDidChange = this._onDidChange.event; private _step = ChatSetupStep.Initial; - get step(): ChatSetupStep { - return this._step; - } + get step(): ChatSetupStep { return this._step; } constructor( private readonly context: ChatSetupContext, @@ -816,7 +814,7 @@ class ChatSetupController extends Disposable { const result = await this.signIn(); if (!result.session) { this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signUpErrorCode: undefined }); - return; // user cancelled + return; } session = result.session; @@ -881,9 +879,9 @@ class ChatSetupController extends Disposable { } private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement,): Promise { - let installResult: 'installed' | 'cancelled' | 'failedInstall' | undefined = undefined; const wasInstalled = this.context.state.installed; - let didSignUp: boolean | { errorCode: number } | undefined = undefined; + let signUpResult: boolean | { errorCode: number } | undefined = undefined; + try { showCopilotView(this.viewsService, this.layoutService); @@ -901,34 +899,30 @@ class ChatSetupController extends Disposable { } } - didSignUp = await this.requests.signUpLimited(session); + signUpResult = await this.requests.signUpLimited(session); - if (typeof didSignUp !== 'boolean' /* error */) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', signUpErrorCode: didSignUp.errorCode }); + if (typeof signUpResult !== 'boolean' /* error */) { + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', signUpErrorCode: signUpResult.errorCode }); } } await this.doInstall(); - - installResult = 'installed'; } catch (error) { this.logService.error(`[chat setup] install: error ${error}`); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: isCancellationError(error) ? 'cancelled' : 'failedInstall', signUpErrorCode: undefined }); + return; + } - installResult = isCancellationError(error) ? 'cancelled' : 'failedInstall'; - } finally { - if (wasInstalled && didSignUp) { - refreshTokens(this.commandService); - } + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'installed', signUpErrorCode: undefined }); - if (installResult === 'installed') { - await Promise.race([ - timeout(5000), // helps prevent flicker with sign-in welcome view - Event.toPromise(this.chatAgentService.onDidChangeAgents) // https://github.com/microsoft/vscode-copilot/issues/9274 - ]); - } + if (wasInstalled && signUpResult === true) { + refreshTokens(this.commandService); } - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult, signUpErrorCode: undefined }); + await Promise.race([ + timeout(5000), // helps prevent flicker with sign-in welcome view + Event.toPromise(this.chatAgentService.onDidChangeAgents) // https://github.com/microsoft/vscode-copilot/issues/9274 + ]); } private async doInstall(): Promise { @@ -943,16 +937,14 @@ class ChatSetupController extends Disposable { }, isCopilotEditsViewActive(this.viewsService) ? EditsViewId : ChatViewId); } catch (e) { this.logService.error(`[chat setup] install: error ${error}`); - if (!isCancellationError(e)) { - error = e; - } + error = e; } if (error) { const { confirmed } = await this.dialogService.confirm({ type: Severity.Error, message: localize('unknownSetupError', "An error occurred while setting up Copilot. Would you like to try again?"), - detail: error ? toErrorMessage(error) : undefined, + detail: error && !isCancellationError(error) ? toErrorMessage(error) : undefined, primaryButton: localize('retry', "Retry") }); From dac52c9521cb06b89d5f7d1dcdd5277d486011dd Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 20 Jan 2025 21:32:06 -0800 Subject: [PATCH 0700/3587] Small fix to saving chat history (#238327) Trim it before saving, not after, and make it a bit smaller just because --- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 4 ++-- .../workbench/contrib/chat/common/chatWidgetHistoryService.ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 932c62a3c4e5..1cefed6c759c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -87,7 +87,7 @@ import { ChatRequestDynamicVariablePart } from '../common/chatParserTypes.js'; import { IChatFollowup } from '../common/chatService.js'; import { IChatVariablesService } from '../common/chatVariables.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; -import { IChatHistoryEntry, IChatInputState, IChatWidgetHistoryService } from '../common/chatWidgetHistoryService.js'; +import { ChatInputHistoryMaxEntries, IChatHistoryEntry, IChatInputState, IChatWidgetHistoryService } from '../common/chatWidgetHistoryService.js'; import { ILanguageModelChatMetadata, ILanguageModelsService } from '../common/languageModels.js'; import { CancelAction, ChatModelPickerActionId, ChatSubmitAction, ChatSubmitSecondaryAgentAction, IChatExecuteActionContext } from './actions/chatExecuteActions.js'; import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js'; @@ -392,7 +392,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.promptInstructionsAttached = ChatContextKeys.instructionsAttached.bindTo(contextKeyService); this.history = this.loadHistory(); - this._register(this.historyService.onDidClearHistory(() => this.history = new HistoryNavigator2([{ text: '' }], 50, historyKeyFn))); + this._register(this.historyService.onDidClearHistory(() => this.history = new HistoryNavigator2([{ text: '' }], ChatInputHistoryMaxEntries, historyKeyFn))); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.Chat)) { diff --git a/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts b/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts index 2ea99b4bcfac..d2ab31d58ce3 100644 --- a/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts +++ b/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts @@ -40,6 +40,8 @@ interface IChatHistory { history: { [providerId: string]: IChatHistoryEntry[] }; } +export const ChatInputHistoryMaxEntries = 40; + export class ChatWidgetHistoryService implements IChatWidgetHistoryService { _serviceBrand: undefined; @@ -78,7 +80,7 @@ export class ChatWidgetHistoryService implements IChatWidgetHistoryService { } const key = this.getKey(location); - this.viewState.history[key] = history; + this.viewState.history[key] = history.slice(-ChatInputHistoryMaxEntries); this.memento.saveMemento(); } From ae56ecf9e1b211bbcaec567b2a88c3826f9c7ec8 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 21 Jan 2025 07:23:40 +0100 Subject: [PATCH 0701/3587] chat - setup tweaks (#238333) --- .../browser/actions/chatGettingStarted.ts | 11 ++++---- src/vs/workbench/contrib/chat/browser/chat.ts | 23 ++++++++++++++++ .../browser/chatEditing/chatEditingService.ts | 4 +-- .../contrib/chat/browser/chatSetup.ts | 27 +++---------------- 4 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts b/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts index 9b7e8b707d56..208a6498d147 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts @@ -6,17 +6,16 @@ import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; -import { CHAT_OPEN_ACTION_ID } from './chatActions.js'; import { IExtensionManagementService, InstallOperation } from '../../../../../platform/extensionManagement/common/extensionManagement.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IDefaultChatAgent } from '../../../../../base/common/product.js'; import { IViewDescriptorService } from '../../../../common/views.js'; import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; -import { ensureSideBarChatViewSize } from '../chat.js'; +import { ensureSideBarChatViewSize, showCopilotView } from '../chat.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IViewsService } from '../../../../services/views/common/viewsService.js'; export class ChatGettingStartedContribution extends Disposable implements IWorkbenchContribution { @@ -28,7 +27,7 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb constructor( @IProductService private readonly productService: IProductService, @IExtensionService private readonly extensionService: IExtensionService, - @ICommandService private readonly commandService: ICommandService, + @IViewsService private readonly viewsService: IViewsService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IStorageService private readonly storageService: IStorageService, @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @@ -75,8 +74,8 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb // Enable chat command center if previously disabled this.configurationService.updateValue('chat.commandCenter.enabled', true); - // Open and configure chat view - await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID); + // Open Copilot view + showCopilotView(this.viewsService, this.layoutService); ensureSideBarChatViewSize(this.viewDescriptorService, this.layoutService); // Only do this once diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 970754af7c40..ef6da957b264 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -52,6 +52,29 @@ export async function showEditsView(viewsService: IViewsService): Promise(EditsViewId))?.widget; } +export function preferCopilotEditsView(viewsService: IViewsService): boolean { + if (viewsService.getFocusedView()?.id === ChatViewId || !!viewsService.getActiveViewWithId(ChatViewId)) { + return false; + } + + return !!viewsService.getActiveViewWithId(EditsViewId); +} + +export function showCopilotView(viewsService: IViewsService, layoutService: IWorkbenchLayoutService): Promise { + + // Ensure main window is in front + if (layoutService.activeContainer !== layoutService.mainContainer) { + layoutService.mainContainer.focus(); + } + + // Bring up the correct view + if (preferCopilotEditsView(viewsService)) { + return showEditsView(viewsService); + } else { + return showChatView(viewsService); + } +} + export function ensureSideBarChatViewSize(viewDescriptorService: IViewDescriptorService, layoutService: IWorkbenchLayoutService): void { const location = viewDescriptorService.getViewLocationById(ChatViewId); if (location === ViewContainerLocation.Panel) { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts index cadf30ab1602..bddeb051851f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts @@ -7,7 +7,7 @@ import { coalesce, compareBy, delta } from '../../../../../base/common/arrays.js import { AsyncIterableSource } from '../../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; -import { BugIndicatingError } from '../../../../../base/common/errors.js'; +import { BugIndicatingError, ErrorNoTelemetry } from '../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { Iterable } from '../../../../../base/common/iterator.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; @@ -285,7 +285,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic const chatModel = this._chatService.getOrRestoreSession(session.chatSessionId); if (!chatModel) { - throw new Error(`Edit session was created for a non-existing chat session: ${session.chatSessionId}`); + throw new ErrorNoTelemetry(`Edit session was created for a non-existing chat session: ${session.chatSessionId}`); } const observerDisposables = new DisposableStore(); diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index a8b5ce20a5b8..285ce801eb4a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -49,7 +49,7 @@ import { IExtensionsWorkbenchService } from '../../extensions/common/extensions. import { IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { CHAT_CATEGORY } from './actions/chatActions.js'; -import { ChatViewId, EditsViewId, ensureSideBarChatViewSize, IChatWidget, showChatView, showEditsView } from './chat.js'; +import { ChatViewId, EditsViewId, ensureSideBarChatViewSize, preferCopilotEditsView, showCopilotView } from './chat.js'; import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID } from './chatViewPane.js'; import { ChatViewsWelcomeExtensions, IChatViewsWelcomeContributionRegistry } from './viewsWelcome/chatViewsWelcome.js'; import { IChatQuotasService } from './chatQuotasService.js'; @@ -785,7 +785,7 @@ class ChatSetupController extends Disposable { async setup(): Promise { const title = localize('setupChatProgress', "Getting Copilot ready..."); - const badge = this.activityService.showViewContainerActivity(isCopilotEditsViewActive(this.viewsService) ? CHAT_EDITING_SIDEBAR_PANEL_ID : CHAT_SIDEBAR_PANEL_ID, { + const badge = this.activityService.showViewContainerActivity(preferCopilotEditsView(this.viewsService) ? CHAT_EDITING_SIDEBAR_PANEL_ID : CHAT_SIDEBAR_PANEL_ID, { badge: new ProgressBadge(() => title), }); @@ -866,7 +866,7 @@ class ChatSetupController extends Disposable { if (!session) { const { confirmed } = await this.dialogService.confirm({ type: Severity.Error, - message: localize('unknownSignInError', "Signing in to Copilot was unsuccessful. Would you like to try again?"), + message: localize('unknownSignInError', "Unable to sign in to {0}. Would you like to try again?", defaultChat.providerName), primaryButton: localize('retry', "Retry") }); @@ -934,7 +934,7 @@ class ChatSetupController extends Disposable { isMachineScoped: false, // do not ask to sync installEverywhere: true, // install in local and remote installPreReleaseVersion: this.productService.quality !== 'stable' - }, isCopilotEditsViewActive(this.viewsService) ? EditsViewId : ChatViewId); + }, preferCopilotEditsView(this.viewsService) ? EditsViewId : ChatViewId); } catch (e) { this.logService.error(`[chat setup] install: error ${error}`); error = e; @@ -1223,25 +1223,6 @@ class ChatSetupContext extends Disposable { //#endregion -function isCopilotEditsViewActive(viewsService: IViewsService): boolean { - return viewsService.getFocusedView()?.id === EditsViewId; -} - -function showCopilotView(viewsService: IViewsService, layoutService: IWorkbenchLayoutService): Promise { - - // Ensure main window is in front - if (layoutService.activeContainer !== layoutService.mainContainer) { - layoutService.mainContainer.focus(); - } - - // Bring up the correct view - if (isCopilotEditsViewActive(viewsService)) { - return showEditsView(viewsService); - } else { - return showChatView(viewsService); - } -} - function refreshTokens(commandService: ICommandService): void { // ugly, but we need to signal to the extension that entitlements changed commandService.executeCommand('github.copilot.signIn'); From 57e5f4bba7073e29d466865e5fbbef72e5f79b70 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 21 Jan 2025 09:05:21 +0100 Subject: [PATCH 0702/3587] StatusBarItem - proposed API for async hover (#238297) Co-authored-by: Benjamin Pasero --- .../common/extensionsApiProposals.ts | 3 + .../api/browser/mainThreadStatusBar.ts | 27 ++++++--- .../api/browser/statusBarExtensionPoint.ts | 10 ++-- .../workbench/api/common/extHost.protocol.ts | 5 +- .../workbench/api/common/extHostStatusBar.ts | 58 +++++++++++++++++-- .../vscode.proposed.statusBarItemTooltip.d.ts | 19 ++++++ 6 files changed, 104 insertions(+), 18 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.statusBarItemTooltip.d.ts diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 102841305a43..79fc43225b96 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -322,6 +322,9 @@ const _allApiProposals = { speech: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.speech.d.ts', }, + statusBarItemTooltip: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.statusBarItemTooltip.d.ts', + }, tabInputMultiDiff: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts', }, diff --git a/src/vs/workbench/api/browser/mainThreadStatusBar.ts b/src/vs/workbench/api/browser/mainThreadStatusBar.ts index 4988e519f39d..92d66d7a2f08 100644 --- a/src/vs/workbench/api/browser/mainThreadStatusBar.ts +++ b/src/vs/workbench/api/browser/mainThreadStatusBar.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MainThreadStatusBarShape, MainContext, ExtHostContext, StatusBarItemDto } from '../common/extHost.protocol.js'; +import { MainThreadStatusBarShape, MainContext, ExtHostContext, StatusBarItemDto, ExtHostStatusBarShape } from '../common/extHost.protocol.js'; import { ThemeColor } from '../../../base/common/themables.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js'; @@ -12,17 +12,20 @@ import { IAccessibilityInformation } from '../../../platform/accessibility/commo import { IMarkdownString } from '../../../base/common/htmlContent.js'; import { IExtensionStatusBarItemService, StatusBarUpdateKind } from './statusBarExtensionPoint.js'; import { IStatusbarEntry, StatusbarAlignment } from '../../services/statusbar/browser/statusbar.js'; +import { IManagedHoverTooltipMarkdownString } from '../../../base/browser/ui/hover/hover.js'; +import { CancellationToken } from '../../../base/common/cancellation.js'; @extHostNamedCustomer(MainContext.MainThreadStatusBar) export class MainThreadStatusBar implements MainThreadStatusBarShape { + private readonly _proxy: ExtHostStatusBarShape; private readonly _store = new DisposableStore(); constructor( extHostContext: IExtHostContext, @IExtensionStatusBarItemService private readonly statusbarService: IExtensionStatusBarItemService ) { - const proxy = extHostContext.getProxy(ExtHostContext.ExtHostStatusBar); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostStatusBar); // once, at startup read existing items and send them over const entries: StatusBarItemDto[] = []; @@ -30,12 +33,11 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { entries.push(asDto(entryId, item)); } - proxy.$acceptStaticEntries(entries); + this._proxy.$acceptStaticEntries(entries); this._store.add(statusbarService.onDidChange(e => { - if (e.added) { - proxy.$acceptStaticEntries([asDto(e.added[0], e.added[1])]); - } + const added = e.added ? [asDto(e.added[0], e.added[1])] : []; + this._proxy.$acceptStaticEntries(added, e.removed); })); function asDto(entryId: string, item: { entry: IStatusbarEntry; alignment: StatusbarAlignment; priority: number }): StatusBarItemDto { @@ -56,8 +58,17 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { this._store.dispose(); } - $setEntry(entryId: string, id: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): void { - const kind = this.statusbarService.setOrUpdateEntry(entryId, id, extensionId, name, text, tooltip, command, color, backgroundColor, alignLeft, priority, accessibilityInformation); + $setEntry(entryId: string, id: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined, hasTooltipProvider: boolean, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): void { + const tooltipOrTooltipProvider = hasTooltipProvider + ? { + markdown: (cancellation: CancellationToken) => { + return this._proxy.$provideTooltip(entryId, cancellation); + }, + markdownNotSupportedFallback: undefined + } satisfies IManagedHoverTooltipMarkdownString + : tooltip; + + const kind = this.statusbarService.setOrUpdateEntry(entryId, id, extensionId, name, text, tooltipOrTooltipProvider, command, color, backgroundColor, alignLeft, priority, accessibilityInformation); if (kind === StatusBarUpdateKind.DidDefine) { this._store.add(toDisposable(() => this.statusbarService.unsetEntry(entryId))); } diff --git a/src/vs/workbench/api/browser/statusBarExtensionPoint.ts b/src/vs/workbench/api/browser/statusBarExtensionPoint.ts index 8b2c415ab0b4..fa0d7fd2307c 100644 --- a/src/vs/workbench/api/browser/statusBarExtensionPoint.ts +++ b/src/vs/workbench/api/browser/statusBarExtensionPoint.ts @@ -13,7 +13,7 @@ import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment, import { ThemeColor } from '../../../base/common/themables.js'; import { Command } from '../../../editor/common/languages.js'; import { IAccessibilityInformation, isAccessibilityInformation } from '../../../platform/accessibility/common/accessibility.js'; -import { IMarkdownString } from '../../../base/common/htmlContent.js'; +import { IMarkdownString, isMarkdownString } from '../../../base/common/htmlContent.js'; import { getCodiconAriaLabel } from '../../../base/common/iconLabels.js'; import { hash } from '../../../base/common/hash.js'; import { Event, Emitter } from '../../../base/common/event.js'; @@ -22,6 +22,7 @@ import { Iterable } from '../../../base/common/iterator.js'; import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; import { asStatusBarItemIdentifier } from '../common/extHostTypes.js'; import { STATUS_BAR_ERROR_ITEM_BACKGROUND, STATUS_BAR_WARNING_ITEM_BACKGROUND } from '../../common/theme.js'; +import { IManagedHoverTooltipMarkdownString } from '../../../base/browser/ui/hover/hover.js'; // --- service @@ -49,7 +50,7 @@ export interface IExtensionStatusBarItemService { onDidChange: Event; - setOrUpdateEntry(id: string, statusId: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): StatusBarUpdateKind; + setOrUpdateEntry(id: string, statusId: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined | IManagedHoverTooltipMarkdownString, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): StatusBarUpdateKind; unsetEntry(id: string): void; @@ -75,7 +76,8 @@ class ExtensionStatusBarItemService implements IExtensionStatusBarItemService { } setOrUpdateEntry(entryId: string, - id: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined, + id: string, extensionId: string | undefined, name: string, text: string, + tooltip: IMarkdownString | string | undefined | IManagedHoverTooltipMarkdownString, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined ): StatusBarUpdateKind { @@ -87,7 +89,7 @@ class ExtensionStatusBarItemService implements IExtensionStatusBarItemService { role = accessibilityInformation.role; } else { ariaLabel = getCodiconAriaLabel(text); - if (tooltip) { + if (typeof tooltip === 'string' || isMarkdownString(tooltip)) { const tooltipString = typeof tooltip === 'string' ? tooltip : tooltip.value; ariaLabel += `, ${tooltipString}`; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 8eee9d953f98..3f31329b105a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -676,7 +676,7 @@ export interface MainThreadQuickOpenShape extends IDisposable { } export interface MainThreadStatusBarShape extends IDisposable { - $setEntry(id: string, statusId: string, extensionId: string | undefined, statusName: string, text: string, tooltip: IMarkdownString | string | undefined, command: ICommandDto | undefined, color: string | ThemeColor | undefined, backgroundColor: string | ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): void; + $setEntry(id: string, statusId: string, extensionId: string | undefined, statusName: string, text: string, tooltip: IMarkdownString | string | undefined, hasTooltipProvider: boolean, command: ICommandDto | undefined, color: string | ThemeColor | undefined, backgroundColor: string | ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): void; $disposeEntry(id: string): void; } @@ -692,7 +692,8 @@ export type StatusBarItemDto = { }; export interface ExtHostStatusBarShape { - $acceptStaticEntries(added?: StatusBarItemDto[]): void; + $acceptStaticEntries(added?: StatusBarItemDto[], removed?: string): void; + $provideTooltip(entryId: string, cancellation: CancellationToken): Promise; } export interface MainThreadStorageShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostStatusBar.ts b/src/vs/workbench/api/common/extHostStatusBar.ts index 327d53c5c925..b05a1879f104 100644 --- a/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/src/vs/workbench/api/common/extHostStatusBar.ts @@ -14,6 +14,8 @@ import { DisposableStore } from '../../../base/common/lifecycle.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { MarkdownString } from './extHostTypeConverters.js'; import { isNumber } from '../../../base/common/types.js'; +import * as htmlContent from '../../../base/common/htmlContent.js'; +import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js'; export class ExtHostStatusBarEntry implements vscode.StatusBarItem { @@ -43,6 +45,7 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private _text: string = ''; private _tooltip?: string | vscode.MarkdownString; + private _tooltip2?: string | vscode.MarkdownString | undefined | ((token: vscode.CancellationToken) => Promise); private _name?: string; private _color?: string | ThemeColor; private _backgroundColor?: ThemeColor; @@ -113,6 +116,10 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { return this._id ?? this._extension!.identifier.value; } + public get entryId(): string { + return this._entryId; + } + public get alignment(): vscode.StatusBarAlignment { return this._alignment; } @@ -133,6 +140,14 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { return this._tooltip; } + public get tooltip2(): vscode.MarkdownString | string | undefined | ((token: vscode.CancellationToken) => Promise) { + if (this._extension) { + checkProposedApiEnabled(this._extension, 'statusBarItemTooltip'); + } + + return this._tooltip2; + } + public get color(): string | ThemeColor | undefined { return this._color; } @@ -164,6 +179,15 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { this.update(); } + public set tooltip2(tooltip: vscode.MarkdownString | string | undefined | ((token: vscode.CancellationToken) => Promise)) { + if (this._extension) { + checkProposedApiEnabled(this._extension, 'statusBarItemTooltip'); + } + + this._tooltip2 = tooltip; + this.update(); + } + public set color(color: string | ThemeColor | undefined) { this._color = color; this.update(); @@ -258,10 +282,18 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { color = ExtHostStatusBarEntry.ALLOWED_BACKGROUND_COLORS.get(this._backgroundColor.id); } - const tooltip = MarkdownString.fromStrict(this._tooltip); + let tooltip: undefined | string | htmlContent.IMarkdownString; + let hasTooltipProvider: boolean; + if (typeof this._tooltip2 === 'function') { + tooltip = MarkdownString.fromStrict(this._tooltip); + hasTooltipProvider = true; + } else { + tooltip = MarkdownString.fromStrict(this._tooltip2 ?? this._tooltip); + hasTooltipProvider = false; + } // Set to status bar - this.#proxy.$setEntry(this._entryId, id, this._extension?.identifier.value, name, this._text, tooltip, this._command?.internal, color, + this.#proxy.$setEntry(this._entryId, id, this._extension?.identifier.value, name, this._text, tooltip, hasTooltipProvider, this._command?.internal, color, this._backgroundColor, this._alignment === ExtHostStatusBarAlignment.Left, this._priority, this._accessibilityInformation); @@ -320,6 +352,7 @@ export class ExtHostStatusBar implements ExtHostStatusBarShape { private readonly _proxy: MainThreadStatusBarShape; private readonly _commands: CommandsConverter; private readonly _statusMessage: StatusBarMessage; + private readonly _entries = new Map(); private readonly _existingItems = new Map(); constructor(mainContext: IMainContext, commands: CommandsConverter) { @@ -328,16 +361,33 @@ export class ExtHostStatusBar implements ExtHostStatusBarShape { this._statusMessage = new StatusBarMessage(this); } - $acceptStaticEntries(added: StatusBarItemDto[]): void { + $acceptStaticEntries(added: StatusBarItemDto[], removed?: string): void { for (const item of added) { this._existingItems.set(item.entryId, item); } + + if (removed) { + this._entries.delete(removed); + } + } + + async $provideTooltip(entryId: string, cancellation: vscode.CancellationToken): Promise { + const entry = this._entries.get(entryId); + if (!entry) { + return undefined; + } + + const tooltip = typeof entry.tooltip2 === 'function' ? await entry.tooltip2(cancellation) : entry.tooltip2; + return !cancellation.isCancellationRequested ? MarkdownString.fromStrict(tooltip) : undefined; } createStatusBarEntry(extension: IExtensionDescription | undefined, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem; createStatusBarEntry(extension: IExtensionDescription, id?: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem; createStatusBarEntry(extension: IExtensionDescription, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem { - return new ExtHostStatusBarEntry(this._proxy, this._commands, this._existingItems, extension, id, alignment, priority); + const entry = new ExtHostStatusBarEntry(this._proxy, this._commands, this._existingItems, extension, id, alignment, priority); + this._entries.set(entry.entryId, entry); + + return entry; } setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): Disposable { diff --git a/src/vscode-dts/vscode.proposed.statusBarItemTooltip.d.ts b/src/vscode-dts/vscode.proposed.statusBarItemTooltip.d.ts new file mode 100644 index 000000000000..5db0495b5a73 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.statusBarItemTooltip.d.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/234339 + + export interface StatusBarItem { + + /** + * The tooltip text when you hover over this entry. + * + * Can optionally return the tooltip in a thenable if the computation is expensive. + */ + tooltip2: string | MarkdownString | undefined | ((token: CancellationToken) => ProviderResult); + } +} From 735640be5a19abdf3d772673961fb9ce50a0f84f Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Tue, 21 Jan 2025 09:36:36 +0100 Subject: [PATCH 0703/3587] Avoid setting (#237890) --- .../src/singlefolder-tests/proxy.test.ts | 95 ++++++++++++++----- src/vs/platform/native/electron-main/auth.ts | 19 ++-- 2 files changed, 78 insertions(+), 36 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts index 99c55cc2e4f1..7845e3113cca 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts @@ -5,12 +5,12 @@ import * as https from 'https'; import 'mocha'; -import { assertNoRpc, delay } from '../utils'; +import { assertNoRpc } from '../utils'; import { pki } from 'node-forge'; import { AddressInfo } from 'net'; import { resetCaches } from '@vscode/proxy-agent'; import * as vscode from 'vscode'; -import { middleware, Straightforward } from 'straightforward'; +import { Straightforward, Middleware, RequestContext, ConnectContext, isRequest, isConnect } from 'straightforward'; import assert from 'assert'; (vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('vscode API - network proxy support', () => { @@ -85,7 +85,8 @@ import assert from 'assert'; const sf = new Straightforward(); let authEnabled = false; - const auth = middleware.auth({ user, pass }); + const authOpts: AuthOpts = { user, pass }; + const auth = middlewareAuth(authOpts); sf.onConnect.use(async (context, next) => { if (authEnabled) { return auth(context, next); @@ -132,27 +133,17 @@ import assert from 'assert'; .on('error', reject); }); - for (let i = 0; i < 3; i++) { - await vscode.workspace.getConfiguration().update('integration-test.http.proxyAuth', `${user}:${pass}`, vscode.ConfigurationTarget.Global); - await delay(1000); // Wait for the configuration change to propagate. - try { - await new Promise((resolve, reject) => { - https.get(url, res => { - if (res.statusCode === 204) { - resolve(); - } else { - reject(new Error(`Unexpected status code (expected 204): ${res.statusCode}`)); - } - }) - .on('error', reject); - }); - break; // Exit the loop if the request is successful - } catch (err) { - if (i === 2) { - throw err; // Rethrow the error if it's the last attempt + authOpts.realm = Buffer.from(JSON.stringify({ username: user, password: pass })).toString('base64'); + await new Promise((resolve, reject) => { + https.get(url, res => { + if (res.statusCode === 204) { + resolve(); + } else { + reject(new Error(`Unexpected status code (expected 204): ${res.statusCode}`)); } - } - } + }) + .on('error', reject); + }); } finally { sf.close(); const change = waitForConfigChange('http.proxy'); @@ -219,3 +210,61 @@ import assert from 'assert'; }); } }); + +// Added 'realm'. From MIT licensed https://github.com/berstend/straightforward/blob/84a4cb88024cffce37a05870da7d9d0aba7dcca8/src/middleware/auth.ts + +export interface AuthOpts { + realm?: string; + user?: string; + pass?: string; + dynamic?: boolean; +} + +export interface RequestAdditionsAuth { + locals: { proxyUser: string; proxyPass: string }; +} + +/** + * Authenticate an incoming proxy request + * Supports static `user` and `pass` or `dynamic`, + * in which case `ctx.req.locals` will be populated with `proxyUser` and `proxyPass` + * This middleware supports both onRequest and onConnect + */ +export const middlewareAuth = + (opts: AuthOpts): Middleware< + RequestContext | ConnectContext + > => + async (ctx, next) => { + const { realm, user, pass, dynamic } = opts; + const sendAuthRequired = () => { + const realmStr = realm ? ` realm="${realm}"` : ''; + if (isRequest(ctx)) { + ctx.res.writeHead(407, { 'Proxy-Authenticate': `Basic${realmStr}` }); + ctx.res.end(); + } else if (isConnect(ctx)) { + ctx.clientSocket.end( + 'HTTP/1.1 407\r\n' + `Proxy-Authenticate: basic${realmStr}\r\n` + '\r\n' + ); + } + }; + const proxyAuth = ctx.req.headers['proxy-authorization']; + if (!proxyAuth) { + return sendAuthRequired(); + } + const [proxyUser, proxyPass] = Buffer.from( + proxyAuth.replace('Basic ', ''), + 'base64' + ) + .toString() + .split(':'); + + if (!dynamic && !!(!!user && !!pass)) { + if (user !== proxyUser || pass !== proxyPass) { + return sendAuthRequired(); + } + } + ctx.req.locals.proxyUser = proxyUser; + ctx.req.locals.proxyPass = proxyPass; + + return next(); + }; diff --git a/src/vs/platform/native/electron-main/auth.ts b/src/vs/platform/native/electron-main/auth.ts index 34adcc69f698..dfac2bf96a89 100644 --- a/src/vs/platform/native/electron-main/auth.ts +++ b/src/vs/platform/native/electron-main/auth.ts @@ -139,20 +139,13 @@ export class ProxyAuthService extends Disposable implements IProxyAuthService { // For testing. if (this.environmentMainService.extensionTestsLocationURI) { - const credentials = this.configurationService.getValue('integration-test.http.proxyAuth'); - if (credentials) { - const j = credentials.indexOf(':'); - if (j !== -1) { - return { - username: credentials.substring(0, j), - password: credentials.substring(j + 1) - }; - } else { - return { - username: credentials, - password: '' - }; + try { + const decodedRealm = Buffer.from(authInfo.realm, 'base64').toString('utf-8'); + if (decodedRealm.startsWith('{')) { + return JSON.parse(decodedRealm); } + } catch { + // ignore } return undefined; } From 05e660dbeb13f851c5a71ee6c769ac85de30066d Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 21 Jan 2025 10:56:33 +0100 Subject: [PATCH 0704/3587] Support forward stability for inline edits (#238344) Support forwardStable for inline edits --- .../browser/model/inlineCompletionsModel.ts | 23 ++++--- .../browser/model/inlineCompletionsSource.ts | 68 ++++++++++++++----- .../browser/model/inlineEditsAdapter.ts | 1 + 3 files changed, 66 insertions(+), 26 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index 5ae0b0670124..0a545b965c41 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -227,7 +227,7 @@ export class InlineCompletionsModel extends Disposable { includeInlineCompletions: !changeSummary.onlyRequestInlineEdits, includeInlineEdits: this._inlineEditsEnabled.read(reader), }; - const itemToPreserveCandidate = this.selectedInlineCompletion.get(); + const itemToPreserveCandidate = this.selectedInlineCompletion.get() ?? this._inlineCompletionItems.get()?.inlineEdit; const itemToPreserve = changeSummary.preserveCurrentCompletion || itemToPreserveCandidate?.forwardStable ? itemToPreserveCandidate : undefined; return this._source.fetch(cursorPosition, context, itemToPreserve); @@ -376,13 +376,18 @@ export class InlineCompletionsModel extends Disposable { const model = this.textModel; const item = this._inlineCompletionItems.read(reader); - if (item?.inlineEdit) { - let edit = item.inlineEdit.toSingleTextEdit(reader); + const inlineEditResult = item?.inlineEdit; + if (inlineEditResult) { + if (inlineEditResult.inlineEdit.read(reader) === null) { + return undefined; + } + + let edit = inlineEditResult.toSingleTextEdit(reader); edit = singleTextRemoveCommonPrefix(edit, model); const cursorPos = this.primaryPosition.read(reader); const cursorAtInlineEdit = LineRange.fromRangeInclusive(edit.range).addMargin(1, 1).contains(cursorPos.lineNumber); - const cursorInsideShowRange = cursorAtInlineEdit || (item.inlineEdit.inlineCompletion.cursorShowRange?.containsPosition(cursorPos) ?? true); + const cursorInsideShowRange = cursorAtInlineEdit || (inlineEditResult.inlineCompletion.cursorShowRange?.containsPosition(cursorPos) ?? true); if (!cursorInsideShowRange && !this._inAcceptFlow.read(reader)) { return undefined; @@ -390,13 +395,13 @@ export class InlineCompletionsModel extends Disposable { const cursorDist = LineRange.fromRange(edit.range).distanceToLine(this.primaryPosition.read(reader).lineNumber); const disableCollapsing = true; - const currentItemIsCollapsed = !disableCollapsing && (cursorDist > 1 && this._collapsedInlineEditId.read(reader) === item.inlineEdit.semanticId); + const currentItemIsCollapsed = !disableCollapsing && (cursorDist > 1 && this._collapsedInlineEditId.read(reader) === inlineEditResult.semanticId); - const commands = item.inlineEdit.inlineCompletion.source.inlineCompletions.commands; + const commands = inlineEditResult.inlineCompletion.source.inlineCompletions.commands; const renderExplicitly = this._jumpedTo.read(reader); - const inlineEdit = new InlineEdit(edit, currentItemIsCollapsed, renderExplicitly, commands ?? [], item.inlineEdit.inlineCompletion); + const inlineEdit = new InlineEdit(edit, currentItemIsCollapsed, renderExplicitly, commands ?? [], inlineEditResult.inlineCompletion); - return { kind: 'inlineEdit', inlineEdit, inlineCompletion: item.inlineEdit, edits: [edit], cursorAtInlineEdit }; + return { kind: 'inlineEdit', inlineEdit, inlineCompletion: inlineEditResult, edits: [edit], cursorAtInlineEdit }; } this._jumpedTo.set(false, undefined); @@ -590,7 +595,7 @@ export class InlineCompletionsModel extends Disposable { completion = state.inlineCompletion.toInlineCompletion(undefined); } else if (state?.kind === 'inlineEdit') { completion = state.inlineCompletion.toInlineCompletion(undefined); - acceptDecorationOffsetRanges.push(...(state.inlineCompletion.inlineEdit?.getNewTextRanges() ?? [])); + acceptDecorationOffsetRanges.push(...(state.inlineCompletion.inlineEdit?.get()?.getNewTextRanges() ?? [])); } else { return; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index 660c4403e37e..cea455d54158 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -38,6 +38,8 @@ export class InlineCompletionsSource extends Disposable { public readonly suggestWidgetInlineCompletions = disposableObservableValue('suggestWidgetInlineCompletions', undefined); private readonly _loggingEnabled = observableConfigValue('editor.inlineSuggest.logFetch', false, this._configurationService).recomputeInitiallyAndOnChange(this._store); + private readonly _invalidationDelay = observableConfigValue('editor.inlineSuggest.edits.experimental.invalidationDelay', 4000, this._configurationService).recomputeInitiallyAndOnChange(this._store); + private readonly _structuredFetchLogger = this._register(this._instantiationService.createInstance(StructuredLogger.cast< { kind: 'start'; requestId: number; context: unknown } & IRecordableEditorLogEntry | { kind: 'end'; error: any; durationMs: number; result: unknown; requestId: number } & IRecordableLogEntry @@ -153,7 +155,7 @@ export class InlineCompletionsSource extends Disposable { const endTime = new Date(); this._debounceValue.update(this._textModel, endTime.getTime() - startTime.getTime()); - const completions = new UpToDateInlineCompletions(updatedCompletions, request, this._textModel, this._versionId); + const completions = new UpToDateInlineCompletions(updatedCompletions, request, this._textModel, this._versionId, this._invalidationDelay); if (activeInlineCompletion) { const asInlineCompletion = activeInlineCompletion.toInlineCompletion(undefined); if (activeInlineCompletion.canBeReused(this._textModel, position) && !updatedCompletions.has(asInlineCompletion)) { @@ -258,6 +260,7 @@ export class UpToDateInlineCompletions implements IDisposable { public readonly request: UpdateRequest, private readonly _textModel: ITextModel, private readonly _versionId: IObservable, + private readonly _invalidationDelay: IObservable, ) { const ids = _textModel.deltaDecorations([], inlineCompletionProviderResult.completions.map(i => ({ range: i.range, @@ -267,7 +270,7 @@ export class UpToDateInlineCompletions implements IDisposable { }))); this._inlineCompletions = inlineCompletionProviderResult.completions.map( - (i, index) => new InlineCompletionWithUpdatedRange(i, ids[index], this._textModel, this._versionId, this.request) + (i, index) => new InlineCompletionWithUpdatedRange(i, ids[index], this._textModel, this._versionId, this._invalidationDelay, this.request) ); } @@ -310,7 +313,7 @@ export class UpToDateInlineCompletions implements IDisposable { description: 'inline-completion-tracking-range' }, }])[0]; - this._inlineCompletions.unshift(new InlineCompletionWithUpdatedRange(inlineCompletion, id, this._textModel, this._versionId, this.request)); + this._inlineCompletions.unshift(new InlineCompletionWithUpdatedRange(inlineCompletion, id, this._textModel, this._versionId, this._invalidationDelay, this.request)); this._prependedInlineCompletionItems.push(inlineCompletion); } } @@ -339,19 +342,22 @@ export class InlineCompletionWithUpdatedRange { /** * This will be null for ghost text completions */ - public _inlineEdit: ISettableObservable; - public get inlineEdit() { return this._inlineEdit.get(); } + private _inlineEdit: ISettableObservable; + public get inlineEdit(): IObservable { return this._inlineEdit; } public get source() { return this.inlineCompletion.source; } public get sourceInlineCompletion() { return this.inlineCompletion.sourceInlineCompletion; } - private readonly _creationTime: number = Date.now(); + private _invalidationTime: number | undefined = Date.now() + this._invalidationDelay.get(); + + private _lastChangePartOfInlineEdit = false; constructor( public readonly inlineCompletion: InlineCompletionItem, public readonly decorationId: string, private readonly _textModel: ITextModel, private readonly _modelVersion: IObservable, + private readonly _invalidationDelay: IObservable, public readonly request: UpdateRequest, ) { const inlineCompletions = this.inlineCompletion.source.inlineCompletions.items; @@ -382,33 +388,54 @@ export class InlineCompletionWithUpdatedRange { } public acceptTextModelChangeEvent(e: IModelContentChangedEvent, tx: ITransaction): void { + this._lastChangePartOfInlineEdit = false; + const offsetEdit = this._inlineEdit.get(); if (!offsetEdit) { return; } - if (this._creationTime + 4000 < Date.now()) { + const editUpdates = offsetEdit.edits.map(edit => acceptTextModelChange(edit, e.changes)); + + const emptyEdit = editUpdates.find(editUpdate => editUpdate.edit.isEmpty); + if (emptyEdit) { + // A change collided with one of our edits, so we will have to drop the completion + this._inlineEdit.set(new OffsetEdit([emptyEdit.edit]), tx); + return; + } + + const partOfInlineEdit = editUpdates.some(({ changePartOfEdit }) => changePartOfEdit); + if (partOfInlineEdit) { + this._invalidationTime = undefined; + } + + if (this._invalidationTime && this._invalidationTime < Date.now()) { // The completion has been shown for a while and the user // has been working on a different part of the document, so invalidate it this._inlineEdit.set(new OffsetEdit([new SingleOffsetEdit(new OffsetRange(0, 0), '')]), tx); return; } - const newEdits = offsetEdit.edits.map(edit => acceptTextModelChange(edit, e.changes)); - const emptyEdit = newEdits.find(edit => edit.isEmpty); - if (emptyEdit) { - // A change collided with one of our edits, so we will have to drop the completion - this._inlineEdit.set(new OffsetEdit([emptyEdit]), tx); - return; - } - this._inlineEdit.set(new OffsetEdit(newEdits), tx); + this._lastChangePartOfInlineEdit = partOfInlineEdit; + this._inlineEdit.set(new OffsetEdit(editUpdates.map(({ edit }) => edit)), tx); - function acceptTextModelChange(edit: SingleOffsetEdit, changes: readonly IModelContentChange[]): SingleOffsetEdit { + function acceptTextModelChange(edit: SingleOffsetEdit, changes: readonly IModelContentChange[]): { edit: SingleOffsetEdit; changePartOfEdit: boolean } { let start = edit.replaceRange.start; let end = edit.replaceRange.endExclusive; let newText = edit.newText; + let changePartOfEdit = false; for (let i = changes.length - 1; i >= 0; i--) { const change = changes[i]; + + // user inserted text at the start of the completion + if (edit.replaceRange.isEmpty && change.rangeLength === 0 && change.rangeOffset === start && newText.startsWith(change.text)) { + start += change.text.length; + end = Math.max(start, end); + newText = newText.substring(change.text.length); + changePartOfEdit = true; + continue; + } + if (change.rangeOffset >= end) { // the change happens after the completion range continue; @@ -425,7 +452,7 @@ export class InlineCompletionWithUpdatedRange { end = change.rangeOffset; newText = ''; } - return new SingleOffsetEdit(new OffsetRange(start, end), newText); + return { edit: new SingleOffsetEdit(new OffsetRange(start, end), newText), changePartOfEdit }; } } @@ -500,6 +527,13 @@ export class InlineCompletionWithUpdatedRange { } public canBeReused(model: ITextModel, position: Position): boolean { + const inlineEdit = this._inlineEdit.get(); + if (inlineEdit !== null) { + return model === this._textModel + && !inlineEdit.isEmpty + && this._lastChangePartOfInlineEdit; + } + const updatedRange = this._updatedRange.read(undefined); const result = !!updatedRange && updatedRange.containsPosition(position) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts index cf0346586475..2511825052e5 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts @@ -79,6 +79,7 @@ export class InlineEditsAdapter extends Disposable { }; }), commands: definedEdits.flatMap(e => e.result.commands ?? []), + enableForwardStability: true, }; }, handleRejection: (completions: InlineCompletions, item: InlineCompletionsAndEdits['items'][number]): void => { From d9069970f94c133d4bb5d300e9f8d7f174843cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Tue, 21 Jan 2025 11:11:30 +0100 Subject: [PATCH 0705/3587] fix build (#238346) --- build/package-lock.json | 28 +++++++++++++++++++++++++--- build/package.json | 1 + 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/build/package-lock.json b/build/package-lock.json index e8f5ce67f248..801e8eb1ed62 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -42,6 +42,7 @@ "@types/workerpool": "^6.4.0", "@types/xml2js": "0.0.33", "@vscode/iconv-lite-umd": "0.7.0", + "@vscode/ripgrep": "^1.15.10", "@vscode/vsce": "2.20.1", "byline": "^5.0.0", "debug": "^4.3.2", @@ -1274,6 +1275,19 @@ "integrity": "sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg==", "dev": true }, + "node_modules/@vscode/ripgrep": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.10.tgz", + "integrity": "sha512-83Q6qFrELpFgf88bPOcwSWDegfY2r/cb6bIfdLTSZvN73Dg1wviSfO+1v6lTFMd0mAvUYYcTUu+Mn5xMroZMxA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "https-proxy-agent": "^7.0.2", + "proxy-from-env": "^1.1.0", + "yauzl": "^2.9.2" + } + }, "node_modules/@vscode/vsce": { "version": "2.20.1", "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.20.1.tgz", @@ -1873,10 +1887,11 @@ "devOptional": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3631,6 +3646,13 @@ "node": ">=0.4.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", diff --git a/build/package.json b/build/package.json index c85b600983f4..76caffbce891 100644 --- a/build/package.json +++ b/build/package.json @@ -36,6 +36,7 @@ "@types/workerpool": "^6.4.0", "@types/xml2js": "0.0.33", "@vscode/iconv-lite-umd": "0.7.0", + "@vscode/ripgrep": "^1.15.10", "@vscode/vsce": "2.20.1", "byline": "^5.0.0", "debug": "^4.3.2", From 98f16e88effd83ab3b710c806e8966ac4632a18c Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 21 Jan 2025 11:42:32 +0100 Subject: [PATCH 0706/3587] fix https://github.com/microsoft/vscode-copilot/issues/11965 (#238348) --- .../browser/view/inlineEdits/inlineDiffView.ts | 4 ++-- .../inlineCompletions/browser/view/inlineEdits/view.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts index 38f9be9092ae..796e21e260f0 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts @@ -23,7 +23,7 @@ import { classNames } from './utils.js'; export interface IOriginalEditorInlineDiffViewState { diff: DetailedLineRangeMapping[]; modifiedText: AbstractText; - mode: 'mixedLines' | 'ghostText' | 'interleavedLines' | 'sideBySide'; + mode: 'mixedLines' | 'ghostText' | 'interleavedLines' | 'sideBySide' | 'deletion'; modifiedCodeEditor: ICodeEditor; } @@ -193,7 +193,7 @@ export class OriginalEditorInlineDiffView extends Disposable { 'inlineCompletions-char-delete', i.originalRange.isSingleLine() && diff.mode === 'ghostText' && 'single-line-inline', i.originalRange.isEmpty() && 'empty', - (i.originalRange.isEmpty() && showEmptyDecorations && !useInlineDiff) && 'diff-range-empty' + ((i.originalRange.isEmpty() || diff.mode === 'deletion') && showEmptyDecorations && !useInlineDiff) && 'diff-range-empty' ), inlineClassName: useInlineDiff ? classNames('strike-through', 'inlineCompletions') : null, zIndex: 1 diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 5e7b1163bbc4..8b9981bc54b2 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -142,7 +142,7 @@ export class InlineEditsView extends Disposable { return { modifiedText: new StringText(e.newText), diff: e.diff, - mode: e.state.kind === 'collapsed' || e.state.kind === 'deletion' ? 'sideBySide' : e.state.kind, + mode: e.state.kind === 'collapsed' ? 'sideBySide' : e.state.kind, modifiedCodeEditor: this._sideBySide.previewEditor, }; }); From 959d01a2a5ccad380c1c2737361d57472579eab3 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 21 Jan 2025 11:44:21 +0100 Subject: [PATCH 0707/3587] Git - fix regression with commits that contain addition/deletion/rename (#238349) * Git - fix regression with commits that contain addition/deletion/rename * Fix compilation error --- extensions/git/src/fileSystemProvider.ts | 7 +++++-- extensions/git/src/historyProvider.ts | 8 +++----- extensions/git/src/main.ts | 2 +- extensions/git/src/uri.ts | 20 +++++++++++++++---- src/vs/workbench/api/browser/mainThreadSCM.ts | 3 +-- .../workbench/api/common/extHost.protocol.ts | 1 - .../workbench/contrib/scm/common/history.ts | 1 - .../vscode.proposed.scmHistoryProvider.d.ts | 1 - 8 files changed, 26 insertions(+), 17 deletions(-) diff --git a/extensions/git/src/fileSystemProvider.ts b/extensions/git/src/fileSystemProvider.ts index 0847fe8d745a..896d933cddfb 100644 --- a/extensions/git/src/fileSystemProvider.ts +++ b/extensions/git/src/fileSystemProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { workspace, Uri, Disposable, Event, EventEmitter, window, FileSystemProvider, FileChangeEvent, FileStat, FileType, FileChangeType, FileSystemError } from 'vscode'; +import { workspace, Uri, Disposable, Event, EventEmitter, window, FileSystemProvider, FileChangeEvent, FileStat, FileType, FileChangeType, FileSystemError, LogOutputChannel } from 'vscode'; import { debounce, throttle } from './decorators'; import { fromGitUri, toGitUri } from './uri'; import { Model, ModelChangeEvent, OriginalResourceChangeEvent } from './model'; @@ -43,7 +43,7 @@ export class GitFileSystemProvider implements FileSystemProvider { private mtime = new Date().getTime(); private disposables: Disposable[] = []; - constructor(private model: Model) { + constructor(private readonly model: Model, private readonly logger: LogOutputChannel) { this.disposables.push( model.onDidChangeRepository(this.onDidChangeRepository, this), model.onDidChangeOriginalResource(this.onDidChangeOriginalResource, this), @@ -136,6 +136,7 @@ export class GitFileSystemProvider implements FileSystemProvider { const { submoduleOf, path, ref } = fromGitUri(uri); const repository = submoduleOf ? this.model.getRepository(submoduleOf) : this.model.getRepository(uri); if (!repository) { + this.logger.warn(`[GitFileSystemProvider][stat] Repository not found - ${uri.toString()}`); throw FileSystemError.FileNotFound(); } @@ -181,6 +182,7 @@ export class GitFileSystemProvider implements FileSystemProvider { const repository = this.model.getRepository(uri); if (!repository) { + this.logger.warn(`[GitFileSystemProvider][readFile] Repository not found - ${uri.toString()}`); throw FileSystemError.FileNotFound(); } @@ -192,6 +194,7 @@ export class GitFileSystemProvider implements FileSystemProvider { try { return await repository.buffer(sanitizeRef(ref, path, repository), path); } catch (err) { + this.logger.warn(`[GitFileSystemProvider][readFile] File not found - ${uri.toString()}`); // File does not exist in git. This could be // because the file is untracked or ignored throw FileSystemError.FileNotFound(); diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 1a01ae4c05e1..f5605c354ab8 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -7,7 +7,7 @@ import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent } from 'vscode'; import { Repository, Resource } from './repository'; import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent, getCommitShortHash } from './util'; -import { toGitUri } from './uri'; +import { toMultiFileDiffEditorUris } from './uri'; import { AvatarQuery, AvatarQueryCommit, Branch, LogOptions, Ref, RefType } from './api/git'; import { emojify, ensureEmojis } from './emoji'; import { Commit } from './git'; @@ -333,10 +333,8 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec // History item change historyItemChanges.push({ uri: historyItemUri, - originalUri: toGitUri(change.originalUri, historyItemParentId), - modifiedUri: toGitUri(change.uri, historyItemId), - renameUri: change.renameUri, - }); + ...toMultiFileDiffEditorUris(change, historyItemParentId, historyItemId) + } satisfies SourceControlHistoryItemChange); // History item change decoration const letter = Resource.getStatusLetter(change.status); diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 515f57c12cf1..8205d8de69f9 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -112,7 +112,7 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, const cc = new CommandCenter(git, model, context.globalState, logger, telemetryReporter); disposables.push( cc, - new GitFileSystemProvider(model), + new GitFileSystemProvider(model, logger), new GitDecorations(model), new GitBlameController(model), new GitTimelineProvider(model, cc), diff --git a/extensions/git/src/uri.ts b/extensions/git/src/uri.ts index 169abd1bc35d..8b04fabe583e 100644 --- a/extensions/git/src/uri.ts +++ b/extensions/git/src/uri.ts @@ -64,12 +64,24 @@ export function toMergeUris(uri: Uri): { base: Uri; ours: Uri; theirs: Uri } { export function toMultiFileDiffEditorUris(change: Change, originalRef: string, modifiedRef: string): { originalUri: Uri | undefined; modifiedUri: Uri | undefined } { switch (change.status) { case Status.INDEX_ADDED: - return { originalUri: undefined, modifiedUri: toGitUri(change.uri, modifiedRef) }; + return { + originalUri: undefined, + modifiedUri: toGitUri(change.uri, modifiedRef) + }; case Status.DELETED: - return { originalUri: toGitUri(change.uri, originalRef), modifiedUri: undefined }; + return { + originalUri: toGitUri(change.uri, originalRef), + modifiedUri: undefined + }; case Status.INDEX_RENAMED: - return { originalUri: toGitUri(change.originalUri, originalRef), modifiedUri: toGitUri(change.uri, modifiedRef) }; + return { + originalUri: toGitUri(change.originalUri, originalRef), + modifiedUri: toGitUri(change.uri, modifiedRef) + }; default: - return { originalUri: toGitUri(change.uri, originalRef), modifiedUri: toGitUri(change.uri, modifiedRef) }; + return { + originalUri: toGitUri(change.uri, originalRef), + modifiedUri: toGitUri(change.uri, modifiedRef) + }; } } diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 934aa9256752..b643356323c3 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -212,8 +212,7 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { return changes?.map(change => ({ uri: URI.revive(change.uri), originalUri: change.originalUri && URI.revive(change.originalUri), - modifiedUri: change.modifiedUri && URI.revive(change.modifiedUri), - renameUri: change.renameUri && URI.revive(change.renameUri) + modifiedUri: change.modifiedUri && URI.revive(change.modifiedUri) })); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 3f31329b105a..dfe7ac5466f4 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1622,7 +1622,6 @@ export interface SCMHistoryItemChangeDto { readonly uri: UriComponents; readonly originalUri: UriComponents | undefined; readonly modifiedUri: UriComponents | undefined; - readonly renameUri: UriComponents | undefined; } export interface MainThreadSCMShape extends IDisposable { diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index 217c6f584c31..3e9846603f32 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -98,5 +98,4 @@ export interface ISCMHistoryItemChange { readonly uri: URI; readonly originalUri?: URI; readonly modifiedUri?: URI; - readonly renameUri?: URI; } diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index 690a931260e9..9eedd16d7675 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -72,7 +72,6 @@ declare module 'vscode' { readonly uri: Uri; readonly originalUri: Uri | undefined; readonly modifiedUri: Uri | undefined; - readonly renameUri: Uri | undefined; } export interface SourceControlHistoryItemRefsChangeEvent { From ac97234e4b52d31b2720e5513870fe0372af79e8 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:51:00 +0100 Subject: [PATCH 0708/3587] Git - adopt async status bar item hover (#238355) --- extensions/git/package.json | 1 + extensions/git/src/blame.ts | 59 ++++++++++++++++++++++-------------- extensions/git/tsconfig.json | 3 +- 3 files changed, 39 insertions(+), 24 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index cc06a2f63d11..bfff94c02cb4 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -32,6 +32,7 @@ "scmSelectedProvider", "scmTextDocument", "scmValidation", + "statusBarItemTooltip", "tabInputMultiDiff", "tabInputTextMerge", "textEditorDiffInformation", diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index da145d8323f6..aafbbd1cd99f 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -206,7 +206,7 @@ export class GitBlameController { }); } - async getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation, includeCommitDetails = false): Promise { + async getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation): Promise { const remoteHoverCommands: Command[] = []; let commitAvatar: string | undefined; let commitInformation: Commit | undefined; @@ -214,25 +214,23 @@ export class GitBlameController { const repository = this._model.getRepository(documentUri); if (repository) { - // Commit details - if (includeCommitDetails) { - try { - commitInformation = await repository.getCommit(blameInformation.hash); - - // Avatar - const avatarQuery = { - commits: [{ - hash: blameInformation.hash, - authorName: blameInformation.authorName, - authorEmail: blameInformation.authorEmail - } satisfies AvatarQueryCommit], - size: AVATAR_SIZE - } satisfies AvatarQuery; - - const avatarResult = await provideSourceControlHistoryItemAvatar(this._model, repository, avatarQuery); - commitAvatar = avatarResult?.get(blameInformation.hash); - } catch { } - } + try { + // Commit details + commitInformation = await repository.getCommit(blameInformation.hash); + + // Avatar + const avatarQuery = { + commits: [{ + hash: blameInformation.hash, + authorName: blameInformation.authorName, + authorEmail: blameInformation.authorEmail + } satisfies AvatarQueryCommit], + size: AVATAR_SIZE + } satisfies AvatarQuery; + + const avatarResult = await provideSourceControlHistoryItemAvatar(this._model, repository, avatarQuery); + commitAvatar = avatarResult?.get(blameInformation.hash); + } catch { } // Remote hover commands const unpublishedCommits = await repository.getUnpublishedCommits(); @@ -612,7 +610,7 @@ class GitBlameEditorDecoration implements HoverProvider { return undefined; } - const contents = await this._controller.getBlameInformationHover(textEditor.document.uri, lineBlameInformation.blameInformation, true); + const contents = await this._controller.getBlameInformationHover(textEditor.document.uri, lineBlameInformation.blameInformation); if (!contents || token.isCancellationRequested) { return undefined; @@ -744,8 +742,14 @@ class GitBlameStatusBarItem { const config = workspace.getConfiguration('git'); const template = config.get('blame.statusBarItem.template', '${authorName} (${authorDateAgo})'); - this._statusBarItem.text = `$(git-commit) ${this._controller.formatBlameInformationMessage(window.activeTextEditor.document.uri, template, blameInformation[0].blameInformation)}`; - this._statusBarItem.tooltip = await this._controller.getBlameInformationHover(window.activeTextEditor.document.uri, blameInformation[0].blameInformation); + this._statusBarItem.text = `$(git-commit) ${this._controller.formatBlameInformationMessage( + window.activeTextEditor.document.uri, template, blameInformation[0].blameInformation)}`; + + this._statusBarItem.tooltip2 = (cancellationToken: CancellationToken) => { + return this._provideTooltip(window.activeTextEditor!.document.uri, + blameInformation[0].blameInformation as BlameInformation, cancellationToken); + }; + this._statusBarItem.command = { title: l10n.t('Open Commit'), command: 'git.viewCommit', @@ -756,6 +760,15 @@ class GitBlameStatusBarItem { this._statusBarItem.show(); } + private async _provideTooltip(uri: Uri, blameInformation: BlameInformation, cancellationToken: CancellationToken): Promise { + if (cancellationToken.isCancellationRequested) { + return undefined; + } + + const tooltip = await this._controller.getBlameInformationHover(uri, blameInformation); + return cancellationToken.isCancellationRequested ? undefined : tooltip; + } + dispose() { this._disposables = dispose(this._disposables); } diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index 5a65f5c82ae2..42218d8decb0 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -14,6 +14,7 @@ "../../src/vscode-dts/vscode.proposed.diffCommand.d.ts", "../../src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts", "../../src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts", + "../../src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts", "../../src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts", "../../src/vscode-dts/vscode.proposed.scmActionButton.d.ts", "../../src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts", @@ -21,11 +22,11 @@ "../../src/vscode-dts/vscode.proposed.scmValidation.d.ts", "../../src/vscode-dts/vscode.proposed.scmMultiDiffEditor.d.ts", "../../src/vscode-dts/vscode.proposed.scmTextDocument.d.ts", + "../../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.textEditorDiffInformation.d.ts", "../../src/vscode-dts/vscode.proposed.timeline.d.ts", - "../../src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts", "../types/lib.textEncoder.d.ts" ] } From 830e5c32c0c2274e6834dcc5effe34b350208aaa Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 21 Jan 2025 13:24:55 +0100 Subject: [PATCH 0709/3587] chat - log setup duration (#238357) --- .../contrib/chat/browser/chatSetup.ts | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 285ce801eb4a..0591784a2265 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -63,6 +63,7 @@ import { isWeb } from '../../../../base/common/platform.js'; import { ExtensionUrlHandlerOverrideRegistry } from '../../../services/extensions/browser/extensionUrlHandler.js'; import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; +import { StopWatch } from '../../../../base/common/stopwatch.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -726,10 +727,12 @@ type InstallChatClassification = { owner: 'bpasero'; comment: 'Provides insight into chat installation.'; installResult: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the extension was installed successfully, cancelled or failed to install.' }; + installDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The duration it took to install the extension.' }; signUpErrorCode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The error code in case of an error signing up.' }; }; type InstallChatEvent = { installResult: 'installed' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn' | 'failedSignUp' | 'failedNotTrusted' | 'failedNoSession'; + installDuration: number; signUpErrorCode: number | undefined; }; @@ -784,6 +787,7 @@ class ChatSetupController extends Disposable { } async setup(): Promise { + const watch = new StopWatch(false); const title = localize('setupChatProgress', "Getting Copilot ready..."); const badge = this.activityService.showViewContainerActivity(preferCopilotEditsView(this.viewsService) ? CHAT_EDITING_SIDEBAR_PANEL_ID : CHAT_SIDEBAR_PANEL_ID, { badge: new ProgressBadge(() => title), @@ -794,13 +798,13 @@ class ChatSetupController extends Disposable { location: ProgressLocation.Window, command: TRIGGER_SETUP_COMMAND_ID, title, - }, () => this.doSetup()); + }, () => this.doSetup(watch)); } finally { badge.dispose(); } } - private async doSetup(): Promise { + private async doSetup(watch: StopWatch): Promise { this.context.suspend(); // reduces flicker let focusChatInput = false; @@ -813,7 +817,7 @@ class ChatSetupController extends Disposable { this.setStep(ChatSetupStep.SigningIn); const result = await this.signIn(); if (!result.session) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signUpErrorCode: undefined }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', installDuration: watch.elapsed(), signUpErrorCode: undefined }); return; } @@ -825,7 +829,7 @@ class ChatSetupController extends Disposable { message: localize('copilotWorkspaceTrust', "Copilot is currently only supported in trusted workspaces.") }); if (!trusted) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotTrusted', signUpErrorCode: undefined }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotTrusted', installDuration: watch.elapsed(), signUpErrorCode: undefined }); return; } @@ -833,7 +837,7 @@ class ChatSetupController extends Disposable { // Install this.setStep(ChatSetupStep.Installing); - await this.install(session, entitlement ?? this.context.state.entitlement); + await this.install(session, entitlement ?? this.context.state.entitlement, watch); const currentActiveElement = getActiveElement(); focusChatInput = activeElement === currentActiveElement || currentActiveElement === mainWindow.document.body; @@ -878,7 +882,7 @@ class ChatSetupController extends Disposable { return { session, entitlement }; } - private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement,): Promise { + private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, watch: StopWatch,): Promise { const wasInstalled = this.context.state.installed; let signUpResult: boolean | { errorCode: number } | undefined = undefined; @@ -894,7 +898,7 @@ class ChatSetupController extends Disposable { } if (!session) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNoSession', signUpErrorCode: undefined }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNoSession', installDuration: watch.elapsed(), signUpErrorCode: undefined }); return; // unexpected } } @@ -902,18 +906,18 @@ class ChatSetupController extends Disposable { signUpResult = await this.requests.signUpLimited(session); if (typeof signUpResult !== 'boolean' /* error */) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', signUpErrorCode: signUpResult.errorCode }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', installDuration: watch.elapsed(), signUpErrorCode: signUpResult.errorCode }); } } await this.doInstall(); } catch (error) { this.logService.error(`[chat setup] install: error ${error}`); - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: isCancellationError(error) ? 'cancelled' : 'failedInstall', signUpErrorCode: undefined }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: isCancellationError(error) ? 'cancelled' : 'failedInstall', installDuration: watch.elapsed(), signUpErrorCode: undefined }); return; } - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'installed', signUpErrorCode: undefined }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'installed', installDuration: watch.elapsed(), signUpErrorCode: undefined }); if (wasInstalled && signUpResult === true) { refreshTokens(this.commandService); From 8cfb26560f27d8932c40af5ddd5cfe3777884a65 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 21 Jan 2025 05:41:40 -0800 Subject: [PATCH 0710/3587] Try make terminal api tests less flaky in remote --- .../src/singlefolder-tests/terminal.test.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index 15aa81288e0d..1719ee4839fd 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -26,6 +26,8 @@ import { assertNoRpc, poll } from '../utils'; await config.update('gpuAcceleration', 'off', ConfigurationTarget.Global); // Disable env var relaunch for tests to prevent terminals relaunching themselves await config.update('environmentChangesRelaunch', false, ConfigurationTarget.Global); + // Disable local echo in case it causes any problems in remote tests + await config.update('localEchoEnabled', "off", ConfigurationTarget.Global); await config.update('shellIntegration.enabled', false); }); @@ -761,14 +763,9 @@ import { assertNoRpc, poll } from '../utils'; data += sanitizeData(e.data); })); - // Run both PowerShell and sh commands, errors don't matter we're just looking for - // the correct output - terminal.sendText('$env:A'); - terminal.sendText('echo $A'); - terminal.sendText('$env:B'); - terminal.sendText('echo $B'); - terminal.sendText('$env:C'); - terminal.sendText('echo $C'); + // Run sh commands, if this is ever enabled on Windows we would also want to run + // the pwsh equivalent + terminal.sendText('echo "$A $B $C'); // Poll for the echo results to show up try { From 28f5c4f0797afdb7624e9260eca37937a2893c58 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 21 Jan 2025 05:43:51 -0800 Subject: [PATCH 0711/3587] Print failing poll results on all similar tests --- .../src/singlefolder-tests/terminal.test.ts | 60 ++++++++++--------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index 1719ee4839fd..9c1c097aab21 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -765,7 +765,7 @@ import { assertNoRpc, poll } from '../utils'; // Run sh commands, if this is ever enabled on Windows we would also want to run // the pwsh equivalent - terminal.sendText('echo "$A $B $C'); + terminal.sendText('echo "$A $B $C"'); // Poll for the echo results to show up try { @@ -808,19 +808,19 @@ import { assertNoRpc, poll } from '../utils'; data += sanitizeData(e.data); })); - // Run both PowerShell and sh commands, errors don't matter we're just looking for - // the correct output - terminal.sendText('$env:A'); - terminal.sendText('echo $A'); - terminal.sendText('$env:B'); - terminal.sendText('echo $B'); - terminal.sendText('$env:C'); - terminal.sendText('echo $C'); + // Run sh commands, if this is ever enabled on Windows we would also want to run + // the pwsh equivalent + terminal.sendText('echo "$A $B $C"'); // Poll for the echo results to show up - await poll(() => Promise.resolve(), () => data.includes('~a2~'), '~a2~ should be printed'); - await poll(() => Promise.resolve(), () => data.includes('~b2~'), '~b2~ should be printed'); - await poll(() => Promise.resolve(), () => data.includes('~c2~'), '~c2~ should be printed'); + try { + await poll(() => Promise.resolve(), () => data.includes('~a2~'), '~a2~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('~b2~'), '~b2~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('~c2~'), '~c2~ should be printed'); + } catch (err) { + console.error('DATA UP UNTIL NOW:', data); + throw err; + } // Wait for terminal to be disposed await new Promise(r => { @@ -852,16 +852,18 @@ import { assertNoRpc, poll } from '../utils'; data += sanitizeData(e.data); })); - // Run both PowerShell and sh commands, errors don't matter we're just looking for - // the correct output - terminal.sendText('$env:A'); - terminal.sendText('echo $A'); - terminal.sendText('$env:B'); - terminal.sendText('echo $B'); + // Run sh commands, if this is ever enabled on Windows we would also want to run + // the pwsh equivalent + terminal.sendText('echo "$A $B"'); // Poll for the echo results to show up - await poll(() => Promise.resolve(), () => data.includes('~a1~'), '~a1~ should be printed'); - await poll(() => Promise.resolve(), () => data.includes('~b1~'), '~b1~ should be printed'); + try { + await poll(() => Promise.resolve(), () => data.includes('~a1~'), '~a1~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('~b1~'), '~b1~ should be printed'); + } catch (err) { + console.error('DATA UP UNTIL NOW:', data); + throw err; + } // Wait for terminal to be disposed await new Promise(r => { @@ -893,16 +895,18 @@ import { assertNoRpc, poll } from '../utils'; data += sanitizeData(e.data); })); - // Run both PowerShell and sh commands, errors don't matter we're just looking for - // the correct output - terminal.sendText('$env:A'); - terminal.sendText('echo $A'); - terminal.sendText('$env:B'); - terminal.sendText('echo $B'); + // Run sh commands, if this is ever enabled on Windows we would also want to run + // the pwsh equivalent + terminal.sendText('echo "$A $B"'); // Poll for the echo results to show up - await poll(() => Promise.resolve(), () => data.includes('~a1~'), '~a1~ should be printed'); - await poll(() => Promise.resolve(), () => data.includes('~b2~'), '~b2~ should be printed'); + try { + await poll(() => Promise.resolve(), () => data.includes('~a1~'), '~a1~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('~b2~'), '~b2~ should be printed'); + } catch (err) { + console.error('DATA UP UNTIL NOW:', data); + throw err; + } // Wait for terminal to be disposed await new Promise(r => { From 3a459b0c144745eb5f5282ddd68e9eeeee1d2242 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 21 Jan 2025 15:35:05 +0100 Subject: [PATCH 0712/3587] Chat looses state on reload (fix microsoft/vscode-copilot#11826) (#238366) --- .../contrib/chat/browser/chatSetup.ts | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 0591784a2265..ffeea094bf47 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -106,10 +106,6 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr static readonly ID = 'workbench.chat.setup'; - private readonly context = this._register(this.instantiationService.createInstance(ChatSetupContext)); - private readonly requests = this._register(this.instantiationService.createInstance(ChatSetupRequests, this.context)); - private readonly controller = new Lazy(() => this._register(this.instantiationService.createInstance(ChatSetupController, this.context, this.requests))); - constructor( @IProductService private readonly productService: IProductService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -126,22 +122,25 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr return; } - this.registerChatWelcome(); - this.registerActions(); + const context = this._register(this.instantiationService.createInstance(ChatSetupContext)); + const requests = this._register(this.instantiationService.createInstance(ChatSetupRequests, context)); + const controller = new Lazy(() => this._register(this.instantiationService.createInstance(ChatSetupController, context, requests))); + + this.registerChatWelcome(controller, context); + this.registerActions(controller, context, requests); this.registerUrlLinkHandler(); } - private registerChatWelcome(): void { + private registerChatWelcome(controller: Lazy, context: ChatSetupContext): void { Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ title: localize('welcomeChat', "Welcome to Copilot"), when: ChatContextKeys.SetupViewCondition, icon: Codicon.copilotLarge, - content: disposables => disposables.add(this.instantiationService.createInstance(ChatSetupWelcomeContent, this.controller.value, this.context)).element, + content: disposables => disposables.add(this.instantiationService.createInstance(ChatSetupWelcomeContent, controller.value, context)).element, }); } - private registerActions(): void { - const that = this; + private registerActions(controller: Lazy, context: ChatSetupContext, requests: ChatSetupRequests): void { const chatSetupTriggerContext = ContextKeyExpr.or( ChatContextKeys.Setup.installed.negate(), @@ -171,13 +170,13 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr const configurationService = accessor.get(IConfigurationService); const layoutService = accessor.get(IWorkbenchLayoutService); - await that.context.update({ hidden: false }); + await context.update({ hidden: false }); showCopilotView(viewsService, layoutService); ensureSideBarChatViewSize(viewDescriptorService, layoutService); if (startSetup === true) { - that.controller.value.setup(); + controller.value.setup(); } configurationService.updateValue('chat.commandCenter.enabled', true); @@ -261,7 +260,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr openerService.open(URI.parse(defaultChat.upgradePlanUrl)); - const entitlement = that.context.state.entitlement; + const entitlement = context.state.entitlement; if (entitlement !== ChatEntitlement.Pro) { // If the user is not yet Pro, we listen to window focus to refresh the token // when the user has come back to the window assuming the user signed up. @@ -273,7 +272,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr if (focus) { windowFocusListener.clear(); - const entitlement = await that.requests.forceResolveEntitlement(undefined); + const entitlement = await requests.forceResolveEntitlement(undefined); if (entitlement === ChatEntitlement.Pro) { refreshTokens(commandService); } @@ -284,7 +283,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr async function hideSetupView(viewsDescriptorService: IViewDescriptorService, layoutService: IWorkbenchLayoutService): Promise { const location = viewsDescriptorService.getViewLocationById(ChatViewId); - await that.context.update({ hidden: true }); + await context.update({ hidden: true }); if (location === ViewContainerLocation.AuxiliaryBar) { const activeContainers = viewsDescriptorService.getViewContainersByLocation(location).filter(container => viewsDescriptorService.getViewContainerModel(container).activeViewDescriptors.length > 0); From a0ecf511fdde8a33e46688374ab0fbef201403f5 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:38:47 +0100 Subject: [PATCH 0713/3587] Git - pass the similarityThreshold to git diff-tree (#238367) --- extensions/git/src/git.ts | 10 ++++++++-- extensions/git/src/repository.ts | 5 ++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 4072e4ae7068..d235b926b16e 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1592,8 +1592,14 @@ export class Repository { return parseGitChanges(this.repositoryRoot, gitResult.stdout); } - async diffTrees(treeish1: string, treeish2?: string): Promise { - const args = ['diff-tree', '-r', '--name-status', '-z', '--diff-filter=ADMR', treeish1]; + async diffTrees(treeish1: string, treeish2?: string, options?: { similarityThreshold?: number }): Promise { + const args = ['diff-tree', '-r', '--name-status', '-z', '--diff-filter=ADMR']; + + if (options?.similarityThreshold) { + args.push(`--find-renames=${options.similarityThreshold}%`); + } + + args.push(treeish1); if (treeish2) { args.push(treeish2); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 79b8379426e8..20fb28e8e587 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1166,7 +1166,10 @@ export class Repository implements Disposable { } diffTrees(treeish1: string, treeish2?: string): Promise { - return this.run(Operation.Diff, () => this.repository.diffTrees(treeish1, treeish2)); + const scopedConfig = workspace.getConfiguration('git', Uri.file(this.root)); + const similarityThreshold = scopedConfig.get('similarityThreshold', 50); + + return this.run(Operation.Diff, () => this.repository.diffTrees(treeish1, treeish2, { similarityThreshold })); } getMergeBase(ref1: string, ref2: string, ...refs: string[]): Promise { From d24b8fb4905a30b76a1205986ddd7a57be2547a6 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 21 Jan 2025 16:02:16 +0100 Subject: [PATCH 0714/3587] Add a token store for tree sitter (#237885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add a token store for tree sitter * Handle root node change * Split tokens, changes between syntax nodes, proper new end position when multiple edits * Holes between nodes * Add guess token store * Fix deleting tokens * Switch over to range based and capture offsets early * 🧹 * 🗑️ * Only block for 5 ms, comments in monaco.d.ts * Faster and more scoped changed range finding * Revert "Faster and more scoped changed range finding" This reverts commit 84b3f4a6127f5355fcb13397ac772483e14b63cd. * Faster and more scoped changed range finding * Use already computed end position * Only expose fully parsed trees from tree sitter and do capture immediately. * Improve race by only firing tree change event when the tree parse is on the same version as the model * Fix race * Fix disposable leak in test * Fix parsing loop bug * Fix race condition in the token store layer * Don't block when collecting tokens * Fix tests * Fix some tests * Add Performance to CORE_TYPES * Add missing js file --- build/lib/layersChecker.js | 1 + build/lib/layersChecker.ts | 1 + src/vs/base/common/event.ts | 4 +- .../pieceTreeTextBuffer.ts | 18 +- src/vs/editor/common/model/textModel.ts | 7 +- src/vs/editor/common/model/tokenStore.ts | 462 ++++++++++++ .../model/treeSitterTokenStoreService.ts | 129 ++++ .../editor/common/model/treeSitterTokens.ts | 20 +- .../treeSitter/treeSitterParserService.ts | 336 +++++++-- .../services/treeSitterParserService.ts | 23 +- src/vs/editor/common/textModelEvents.ts | 7 +- .../browser/standaloneTreeSitterService.ts | 7 +- .../services/treeSitterParserService.test.ts | 2 +- .../widget/observableCodeEditor.test.ts | 20 +- .../test/common/model/tokenStore.test.ts | 701 ++++++++++++++++++ .../common/services/testTreeSitterService.ts | 7 +- src/vs/monaco.d.ts | 6 +- .../test/browser/extHostDocumentData.test.ts | 6 + .../extHostDocumentSaveParticipant.test.ts | 2 + ...eSitterTokenizationFeature.contribution.ts | 6 +- .../treeSitterTokenizationFeature.ts | 175 ++++- .../treeSitterTokenizationFeature.test.ts | 371 +++++++++ 22 files changed, 2161 insertions(+), 150 deletions(-) create mode 100644 src/vs/editor/common/model/tokenStore.ts create mode 100644 src/vs/editor/common/model/treeSitterTokenStoreService.ts rename src/vs/editor/{browser => common}/services/treeSitter/treeSitterParserService.ts (59%) create mode 100644 src/vs/editor/test/common/model/tokenStore.test.ts rename src/vs/workbench/services/treeSitter/{browser => common}/treeSitterTokenizationFeature.ts (61%) create mode 100644 src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index 978ce2625b52..e52525bf61d1 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -72,6 +72,7 @@ const CORE_TYPES = [ 'Body', '__type', '__global', + 'Performance', 'PerformanceMark', 'PerformanceObserver', 'ImportMeta', diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 454f8874e3d0..e16a775b23e9 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -73,6 +73,7 @@ const CORE_TYPES = [ 'Body', '__type', '__global', + 'Performance', 'PerformanceMark', 'PerformanceObserver', 'ImportMeta', diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index fe10aaf1c892..6603fa22c390 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -596,8 +596,8 @@ export namespace Event { /** * Creates a promise out of an event, using the {@link Event.once} helper. */ - export function toPromise(event: Event): Promise { - return new Promise(resolve => once(event)(resolve)); + export function toPromise(event: Event, disposables?: IDisposable[] | DisposableStore): Promise { + return new Promise(resolve => once(event)(resolve, null, disposables)); } /** diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts index 42d1995e3679..1b812d383bc1 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts @@ -27,6 +27,10 @@ export interface IValidatedEditOperation { isAutoWhitespaceEdit: boolean; } +export interface IValidatedEditOperationWithReverseRange extends IValidatedEditOperation { + reverseRange: Range; +} + interface IReverseSingleEditOperation extends IValidEditOperation { sortIndex: number; } @@ -323,7 +327,7 @@ export class PieceTreeTextBuffer extends Disposable implements ITextBuffer { } // Delta encode operations - const reverseRanges = (computeUndoEdits || recordTrimAutoWhitespace ? PieceTreeTextBuffer._getInverseEditRanges(operations) : []); + const reverseRanges = PieceTreeTextBuffer._getInverseEditRanges(operations); const newTrimAutoWhitespaceCandidates: { lineNumber: number; oldContent: string }[] = []; if (recordTrimAutoWhitespace) { for (let i = 0; i < operations.length; i++) { @@ -373,12 +377,19 @@ export class PieceTreeTextBuffer extends Disposable implements ITextBuffer { } } + const operationsWithReverseRanges: IValidatedEditOperationWithReverseRange[] = new Array(operations.length); + for (let i = 0; i < operations.length; i++) { + operationsWithReverseRanges[i] = { + ...operations[i], + reverseRange: reverseRanges[i] + }; + } this._mightContainRTL = mightContainRTL; this._mightContainUnusualLineTerminators = mightContainUnusualLineTerminators; this._mightContainNonBasicASCII = mightContainNonBasicASCII; - const contentChanges = this._doApplyEdits(operations); + const contentChanges = this._doApplyEdits(operationsWithReverseRanges); let trimAutoWhitespaceLineNumbers: number[] | null = null; if (recordTrimAutoWhitespace && newTrimAutoWhitespaceCandidates.length > 0) { @@ -476,7 +487,7 @@ export class PieceTreeTextBuffer extends Disposable implements ITextBuffer { }; } - private _doApplyEdits(operations: IValidatedEditOperation[]): IInternalModelContentChange[] { + private _doApplyEdits(operations: IValidatedEditOperationWithReverseRange[]): IInternalModelContentChange[] { operations.sort(PieceTreeTextBuffer._sortOpsDescending); const contentChanges: IInternalModelContentChange[] = []; @@ -509,6 +520,7 @@ export class PieceTreeTextBuffer extends Disposable implements ITextBuffer { contentChanges.push({ range: contentChangeRange, rangeLength: op.rangeLength, + rangeEndPosition: op.reverseRange.getEndPosition(), text: op.text, rangeOffset: op.rangeOffset, forceMoveMarkers: op.forceMoveMarkers diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 704afa2acf4e..cac9f3b882ed 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -454,12 +454,13 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._setValueFromTextBuffer(textBuffer, disposable); } - private _createContentChanged2(range: Range, rangeOffset: number, rangeLength: number, text: string, isUndoing: boolean, isRedoing: boolean, isFlush: boolean, isEolChange: boolean): IModelContentChangedEvent { + private _createContentChanged2(range: Range, rangeOffset: number, rangeLength: number, rangeEndPosition: Position, text: string, isUndoing: boolean, isRedoing: boolean, isFlush: boolean, isEolChange: boolean): IModelContentChangedEvent { return { changes: [{ range: range, rangeOffset: rangeOffset, rangeLength: rangeLength, + rangeEndPosition: rangeEndPosition, text: text, }], eol: this._buffer.getEOL(), @@ -500,7 +501,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati false, false ), - this._createContentChanged2(new Range(1, 1, endLineNumber, endColumn), 0, oldModelValueLength, this.getValue(), false, false, true, false) + this._createContentChanged2(new Range(1, 1, endLineNumber, endColumn), 0, oldModelValueLength, new Position(endLineNumber, endColumn), this.getValue(), false, false, true, false) ); } @@ -531,7 +532,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati false, false ), - this._createContentChanged2(new Range(1, 1, endLineNumber, endColumn), 0, oldModelValueLength, this.getValue(), false, false, false, true) + this._createContentChanged2(new Range(1, 1, endLineNumber, endColumn), 0, oldModelValueLength, new Position(endLineNumber, endColumn), this.getValue(), false, false, false, true) ); } diff --git a/src/vs/editor/common/model/tokenStore.ts b/src/vs/editor/common/model/tokenStore.ts new file mode 100644 index 000000000000..5d717aca1b8a --- /dev/null +++ b/src/vs/editor/common/model/tokenStore.ts @@ -0,0 +1,462 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from '../../../base/common/lifecycle.js'; +import { ITextModel } from '../model.js'; + +class ListNode implements IDisposable { + parent?: ListNode; + private readonly _children: Node[] = []; + get children(): ReadonlyArray { return this._children; } + + private _length: number = 0; + get length(): number { return this._length; } + + constructor(public readonly height: number) { } + + static create(node1: Node, node2: Node) { + const list = new ListNode(node1.height + 1); + list.appendChild(node1); + list.appendChild(node2); + return list; + } + + canAppendChild(): boolean { + return this._children.length < 3; + } + + appendChild(node: Node) { + if (!this.canAppendChild()) { + throw new Error('Cannot insert more than 3 children in a ListNode'); + } + this._children.push(node); + + this._length += node.length; + this._updateParentLength(node.length); + node.parent = this; + } + + private _updateParentLength(delta: number) { + let updateParent = this.parent; + while (updateParent) { + updateParent._length += delta; + updateParent = updateParent.parent; + } + } + + unappendChild(): Node { + const child = this._children.pop()!; + this._length -= child.length; + this._updateParentLength(-child.length); + return child; + } + + prependChild(node: Node) { + if (this._children.length >= 3) { + throw new Error('Cannot prepend more than 3 children in a ListNode'); + } + this._children.unshift(node); + + this._length += node.length; + this._updateParentLength(node.length); + node.parent = this; + } + + unprependChild(): Node { + const child = this._children.shift()!; + this._length -= child.length; + this._updateParentLength(-child.length); + return child; + } + + lastChild(): Node { + return this._children[this._children.length - 1]; + } + + dispose() { + this._children.splice(0, this._children.length); + } +} + +type Node = ListNode | LeafNode; + +interface LeafNode { + readonly length: number; + parent?: ListNode; + token: number; + needsRefresh?: boolean; + height: 0; +} + +export interface TokenUpdate { + readonly startOffsetInclusive: number; + readonly length: number; + readonly token: number; +} + +function isLeaf(node: Node): node is LeafNode { + return (node as LeafNode).token !== undefined; +} + +// Heavily inspired by https://github.com/microsoft/vscode/blob/4eb2658d592cb6114a7a393655574176cc790c5b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/concat23Trees.ts#L108-L109 +function append(node: Node, nodeToAppend: Node): Node { + let curNode = node; + const parents: ListNode[] = []; + let nodeToAppendOfCorrectHeight: Node | undefined; + while (true) { + if (nodeToAppend.height === curNode.height) { + nodeToAppendOfCorrectHeight = nodeToAppend; + break; + } + + if (isLeaf(curNode)) { + throw new Error('unexpected'); + } + parents.push(curNode); + curNode = curNode.lastChild(); + } + for (let i = parents.length - 1; i >= 0; i--) { + const parent = parents[i]; + if (nodeToAppendOfCorrectHeight) { + // Can we take the element? + if (parent.children.length >= 3) { + // we need to split to maintain (2,3)-tree property. + // Send the third element + the new element to the parent. + const newList = ListNode.create(parent.unappendChild()!, nodeToAppendOfCorrectHeight); + nodeToAppendOfCorrectHeight = newList; + } else { + parent.appendChild(nodeToAppendOfCorrectHeight); + nodeToAppendOfCorrectHeight = undefined; + } + } + } + if (nodeToAppendOfCorrectHeight) { + const newList = new ListNode(nodeToAppendOfCorrectHeight.height + 1); + newList.appendChild(node); + newList.appendChild(nodeToAppendOfCorrectHeight); + return newList; + } else { + return node; + } +} + +function prepend(list: Node, nodeToAppend: Node): Node { + let curNode = list; + const parents: ListNode[] = []; + while (nodeToAppend.height !== curNode.height) { + if (isLeaf(curNode)) { + throw new Error('unexpected'); + } + parents.push(curNode); + // assert 2 <= curNode.childrenFast.length <= 3 + curNode = curNode.children[0] as ListNode; + } + let nodeToPrependOfCorrectHeight: Node | undefined = nodeToAppend; + // assert nodeToAppendOfCorrectHeight!.listHeight === curNode.listHeight + for (let i = parents.length - 1; i >= 0; i--) { + const parent = parents[i]; + if (nodeToPrependOfCorrectHeight) { + // Can we take the element? + if (parent.children.length >= 3) { + // we need to split to maintain (2,3)-tree property. + // Send the third element + the new element to the parent. + nodeToPrependOfCorrectHeight = ListNode.create(nodeToPrependOfCorrectHeight, parent.unprependChild()); + } else { + parent.prependChild(nodeToPrependOfCorrectHeight); + nodeToPrependOfCorrectHeight = undefined; + } + } + } + if (nodeToPrependOfCorrectHeight) { + return ListNode.create(nodeToPrependOfCorrectHeight, list); + } else { + return list; + } +} + +function concat(node1: Node, node2: Node): Node { + if (node1.height === node2.height) { + return ListNode.create(node1, node2); + } + else if (node1.height > node2.height) { + // node1 is the tree we want to insert into + return append(node1, node2); + } else { + return prepend(node2, node1); + } +} + +export class TokenStore implements IDisposable { + private _root: Node; + get root(): Node { + return this._root; + } + + constructor(private readonly _textModel: ITextModel) { + this._root = { + length: this._textModel.getValueLength(), + token: 0, + height: 0 + }; + } + + /** + * + * @param update all the tokens for the document in sequence + */ + buildStore(tokens: TokenUpdate[]) { + this._root = this.createFromUpdates(tokens); + } + + private createFromUpdates(tokens: TokenUpdate[]): Node { + let newRoot: Node = { + length: tokens[0].length, + token: tokens[0].token, + height: 0 + }; + for (let j = 1; j < tokens.length; j++) { + newRoot = append(newRoot, { length: tokens[j].length, token: tokens[j].token, height: 0 }); + } + return newRoot; + } + + /** + * + * @param tokens tokens are in sequence in the document. + */ + update(length: number, tokens: TokenUpdate[]) { + if (tokens.length === 0) { + return; + } + this.replace(length, tokens[0].startOffsetInclusive, tokens); + } + + delete(length: number, startOffset: number) { + this.replace(length, startOffset, []); + } + + /** + * + * @param tokens tokens are in sequence in the document. + */ + private replace(length: number, updateOffsetStart: number, tokens: TokenUpdate[]) { + const firstUnchangedOffsetAfterUpdate = updateOffsetStart + length; + // Find the last unchanged node preceding the update + const precedingNodes: Node[] = []; + // Find the first unchanged node after the update + const postcedingNodes: Node[] = []; + const stack: { node: Node; offset: number }[] = [{ node: this._root, offset: 0 }]; + + while (stack.length > 0) { + const node = stack.pop()!; + const currentOffset = node.offset; + + if (currentOffset < updateOffsetStart && currentOffset + node.node.length <= updateOffsetStart) { + node.node.parent = undefined; + precedingNodes.push(node.node); + continue; + } else if (isLeaf(node.node) && (currentOffset < updateOffsetStart)) { + // We have a partial preceding node + precedingNodes.push({ length: updateOffsetStart - currentOffset, token: node.node.token, height: 0 }); + // Node could also be postceeding, so don't continue + } + + if ((updateOffsetStart <= currentOffset) && (currentOffset + node.node.length <= firstUnchangedOffsetAfterUpdate)) { + continue; + } + + if (currentOffset >= firstUnchangedOffsetAfterUpdate) { + node.node.parent = undefined; + postcedingNodes.push(node.node); + continue; + } else if (isLeaf(node.node) && (currentOffset + node.node.length >= firstUnchangedOffsetAfterUpdate)) { + // we have a partial postceeding node + postcedingNodes.push({ length: currentOffset + node.node.length - firstUnchangedOffsetAfterUpdate, token: node.node.token, height: 0 }); + continue; + } + + if (!isLeaf(node.node)) { + // Push children in reverse order to process them left-to-right when popping + let childOffset = currentOffset + node.node.length; + for (let i = node.node.children.length - 1; i >= 0; i--) { + childOffset -= node.node.children[i].length; + stack.push({ node: node.node.children[i], offset: childOffset }); + } + } + } + + let allNodes: Node[]; + if (tokens.length > 0) { + allNodes = precedingNodes.concat(this.createFromUpdates(tokens), postcedingNodes); + } else { + allNodes = precedingNodes.concat(postcedingNodes); + } + let newRoot: Node = allNodes[0]; + for (let i = 1; i < allNodes.length; i++) { + newRoot = concat(newRoot, allNodes[i]); + } + + this._root = newRoot; + } + + /** + * + * @param startOffsetInclusive + * @param endOffsetExclusive + * @param visitor Return true from visitor to exit early + * @returns + */ + private traverseInOrderInRange(startOffsetInclusive: number, endOffsetExclusive: number, visitor: (node: Node, offset: number) => boolean): void { + const stack: { node: Node; offset: number }[] = [{ node: this._root, offset: 0 }]; + + while (stack.length > 0) { + const { node, offset } = stack.pop()!; + const nodeEnd = offset + node.length; + + // Skip nodes that are completely before or after the range + if (nodeEnd <= startOffsetInclusive || offset >= endOffsetExclusive) { + continue; + } + + if (visitor(node, offset)) { + return; + } + + if (!isLeaf(node)) { + // Push children in reverse order to process them left-to-right when popping + let childOffset = offset + node.length; + for (let i = node.children.length - 1; i >= 0; i--) { + childOffset -= node.children[i].length; + stack.push({ node: node.children[i], offset: childOffset }); + } + } + } + } + + getTokenAt(offset: number): TokenUpdate | undefined { + let result: TokenUpdate | undefined; + this.traverseInOrderInRange(offset, this._root.length, (node, offset) => { + if (isLeaf(node)) { + result = { token: node.token, startOffsetInclusive: offset, length: node.length }; + return true; + } + return false; + }); + return result; + } + + getTokensInRange(startOffsetInclusive: number, endOffsetExclusive: number): TokenUpdate[] { + const result: { token: number; startOffsetInclusive: number; length: number }[] = []; + this.traverseInOrderInRange(startOffsetInclusive, endOffsetExclusive, (node, offset) => { + if (isLeaf(node)) { + let clippedLength = node.length; + let clippedOffset = offset; + if (offset < startOffsetInclusive) { + clippedLength -= (startOffsetInclusive - offset); + clippedOffset = startOffsetInclusive; + } else if (offset + node.length > endOffsetExclusive) { + clippedLength -= (offset + node.length - endOffsetExclusive); + } + result.push({ token: node.token, startOffsetInclusive: clippedOffset, length: clippedLength }); + } + return false; + }); + return result; + } + + markForRefresh(startOffsetInclusive: number, endOffsetExclusive: number): void { + this.traverseInOrderInRange(startOffsetInclusive, endOffsetExclusive, (node) => { + if (isLeaf(node)) { + node.needsRefresh = true; + } + return false; + }); + } + + rangeNeedsRefresh(startOffsetInclusive: number, endOffsetExclusive: number): boolean { + let needsRefresh = false; + this.traverseInOrderInRange(startOffsetInclusive, endOffsetExclusive, (node) => { + if (isLeaf(node) && node.needsRefresh) { + needsRefresh = true; + } + return false; + }); + return needsRefresh; + } + + public deepCopy(): TokenStore { + const newStore = new TokenStore(this._textModel); + newStore._root = this._copyNodeIterative(this._root); + return newStore; + } + + private _copyNodeIterative(root: Node): Node { + const newRoot = isLeaf(root) + ? { length: root.length, token: root.token, needsRefresh: root.needsRefresh, height: root.height } + : new ListNode(root.height); + + const stack: Array<[Node, Node]> = [[root, newRoot]]; + + while (stack.length > 0) { + const [oldNode, clonedNode] = stack.pop()!; + if (!isLeaf(oldNode)) { + for (const child of oldNode.children) { + const childCopy = isLeaf(child) + ? { length: child.length, token: child.token, needsRefresh: child.needsRefresh, height: child.height } + : new ListNode(child.height); + + (clonedNode as ListNode).appendChild(childCopy); + stack.push([child, childCopy]); + } + } + } + + return newRoot; + } + + /** + * Returns a string representation of the token tree using an iterative approach + */ + printTree(root: Node = this._root): string { + const result: string[] = []; + const stack: Array<[Node, number]> = [[root, 0]]; + + while (stack.length > 0) { + const [node, depth] = stack.pop()!; + const indent = ' '.repeat(depth); + + if (isLeaf(node)) { + result.push(`${indent}Leaf(length: ${node.length}, token: ${node.token})\n`); + } else { + result.push(`${indent}List(length: ${node.length})\n`); + // Push children in reverse order so they get processed left-to-right + for (let i = node.children.length - 1; i >= 0; i--) { + stack.push([node.children[i], depth + 1]); + } + } + } + + return result.join(''); + } + + dispose(): void { + const stack: Array<[Node, boolean]> = [[this._root, false]]; + while (stack.length > 0) { + const [node, visited] = stack.pop()!; + if (isLeaf(node)) { + node.parent = undefined; + } else if (!visited) { + stack.push([node, true]); + for (let i = node.children.length - 1; i >= 0; i--) { + stack.push([node.children[i], false]); + } + } else { + node.dispose(); + node.parent = undefined; + } + } + this._root = undefined!; + } +} diff --git a/src/vs/editor/common/model/treeSitterTokenStoreService.ts b/src/vs/editor/common/model/treeSitterTokenStoreService.ts new file mode 100644 index 000000000000..17d194b5bd5c --- /dev/null +++ b/src/vs/editor/common/model/treeSitterTokenStoreService.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from '../core/range.js'; +import { ITextModel } from '../model.js'; +import { TokenStore, TokenUpdate } from './tokenStore.js'; +import { InstantiationType, registerSingleton } from '../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; +import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; + +export interface ITreeSitterTokenizationStoreService { + readonly _serviceBrand: undefined; + setTokens(model: ITextModel, tokens: TokenUpdate[]): void; + getTokens(model: ITextModel, line: number): Uint32Array | undefined; + updateTokens(model: ITextModel, version: number, updates: { oldRangeLength: number; newTokens: TokenUpdate[] }[]): void; + markForRefresh(model: ITextModel, range: Range): void; + hasTokens(model: ITextModel, accurateForRange?: Range): boolean; +} + +export const ITreeSitterTokenizationStoreService = createDecorator('treeSitterTokenizationStoreService'); + +export interface TokenInformation { + tokens: Uint32Array; + needsRefresh?: boolean; +} + +class TreeSitterTokenizationStoreService implements ITreeSitterTokenizationStoreService, IDisposable { + readonly _serviceBrand: undefined; + + private readonly tokens = new Map(); + + constructor() { } + + setTokens(model: ITextModel, tokens: TokenUpdate[]): void { + const disposables = new DisposableStore(); + const store = disposables.add(new TokenStore(model)); + this.tokens.set(model, { store: store, accurateVersion: model.getVersionId(), disposables, guessVersion: 0 }); + + store.buildStore(tokens); + disposables.add(model.onDidChangeContent(e => { + const storeInfo = this.tokens.get(model); + if (!storeInfo) { + return; + } + + storeInfo.guessVersion = e.versionId; + for (const change of e.changes) { + storeInfo.store.markForRefresh(change.rangeOffset, change.rangeOffset + change.rangeLength); + if (change.text.length > change.rangeLength) { + const oldToken = storeInfo.store.getTokenAt(change.rangeOffset)!; + // Insert. Just grow the token at this position to include the insert. + const newToken = { startOffsetInclusive: oldToken.startOffsetInclusive, length: oldToken.length + change.text.length - change.rangeLength, token: oldToken.token }; + storeInfo.store.update(oldToken.length, [newToken]); + } else if (change.text.length < change.rangeLength) { + // Delete. Delete the tokens at the corresponding range. + const deletedCharCount = change.rangeLength - change.text.length; + storeInfo.store.delete(deletedCharCount, change.rangeOffset); + } + } + })); + disposables.add(model.onWillDispose(() => { + const storeInfo = this.tokens.get(model); + if (storeInfo) { + storeInfo.disposables.dispose(); + this.tokens.delete(model); + } + })); + } + + hasTokens(model: ITextModel, accurateForRange?: Range): boolean { + const tokens = this.tokens.get(model); + if (!tokens) { + return false; + } + if (!accurateForRange || (tokens.guessVersion === tokens.accurateVersion)) { + return true; + } + + return !tokens.store.rangeNeedsRefresh(model.getOffsetAt(accurateForRange.getStartPosition()), model.getOffsetAt(accurateForRange.getEndPosition())); + } + + getTokens(model: ITextModel, line: number): Uint32Array | undefined { + const tokens = this.tokens.get(model)?.store; + if (!tokens) { + return undefined; + } + const lineStartOffset = model.getOffsetAt({ lineNumber: line, column: 1 }); + const lineTokens = tokens.getTokensInRange(lineStartOffset, model.getOffsetAt({ lineNumber: line, column: model.getLineMaxColumn(line) }) + 1); + const result = new Uint32Array(lineTokens.length * 2); + for (let i = 0; i < lineTokens.length; i++) { + result[i * 2] = lineTokens[i].startOffsetInclusive - lineStartOffset + lineTokens[i].length; + result[i * 2 + 1] = lineTokens[i].token; + } + return result; + } + + updateTokens(model: ITextModel, version: number, updates: { oldRangeLength: number; newTokens: TokenUpdate[] }[]): void { + const existingTokens = this.tokens.get(model); + if (!existingTokens) { + return; + } + + existingTokens.accurateVersion = version; + for (const update of updates) { + const lastToken = update.newTokens[update.newTokens.length - 1]; + const oldRangeLength = (existingTokens.guessVersion >= version) ? (lastToken.startOffsetInclusive + lastToken.length - update.newTokens[0].startOffsetInclusive) : update.oldRangeLength; + existingTokens.store.update(oldRangeLength, update.newTokens); + } + } + + markForRefresh(model: ITextModel, range: Range): void { + const tree = this.tokens.get(model)?.store; + if (!tree) { + return; + } + + tree.markForRefresh(model.getOffsetAt(range.getStartPosition()), model.getOffsetAt(range.getEndPosition())); + } + + dispose(): void { + for (const [, value] of this.tokens) { + value.disposables.dispose(); + } + } +} + +registerSingleton(ITreeSitterTokenizationStoreService, TreeSitterTokenizationStoreService, InstantiationType.Delayed); diff --git a/src/vs/editor/common/model/treeSitterTokens.ts b/src/vs/editor/common/model/treeSitterTokens.ts index 7f8f91bb2762..eee41ee17df2 100644 --- a/src/vs/editor/common/model/treeSitterTokens.ts +++ b/src/vs/editor/common/model/treeSitterTokens.ts @@ -7,10 +7,11 @@ import { ILanguageIdCodec, ITreeSitterTokenizationSupport, TreeSitterTokenizatio import { LineTokens } from '../tokens/lineTokens.js'; import { StandardTokenType } from '../encodedTokenAttributes.js'; import { TextModel } from './textModel.js'; -import { ITreeSitterParserService } from '../services/treeSitterParserService.js'; import { IModelContentChangedEvent } from '../textModelEvents.js'; import { AbstractTokens } from './tokens.js'; import { IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js'; +import { ITreeSitterTokenizationStoreService } from './treeSitterTokenStoreService.js'; +import { Range } from '../core/range.js'; export class TreeSitterTokens extends AbstractTokens { private _tokenizationSupport: ITreeSitterTokenizationSupport | null = null; @@ -20,7 +21,7 @@ export class TreeSitterTokens extends AbstractTokens { constructor(languageIdCodec: ILanguageIdCodec, textModel: TextModel, languageId: () => string, - @ITreeSitterParserService private readonly _treeSitterService: ITreeSitterParserService) { + @ITreeSitterTokenizationStoreService private readonly _tokenStore: ITreeSitterTokenizationStoreService) { super(languageIdCodec, textModel, languageId); this._initialize(); @@ -42,7 +43,7 @@ export class TreeSitterTokens extends AbstractTokens { public getLineTokens(lineNumber: number): LineTokens { const content = this._textModel.getLineContent(lineNumber); if (this._tokenizationSupport) { - const rawTokens = this._tokenizationSupport.tokenizeEncoded(lineNumber, this._textModel); + const rawTokens = this._tokenStore.getTokens(this._textModel, lineNumber); if (rawTokens) { return new LineTokens(rawTokens, content, this._languageIdCodec); } @@ -77,16 +78,17 @@ export class TreeSitterTokens extends AbstractTokens { } public override forceTokenization(lineNumber: number): void { - // TODO @alexr00 implement + if (this._tokenizationSupport) { + this._tokenizationSupport.tokenizeEncoded(lineNumber, this._textModel); + } } public override hasAccurateTokensForLine(lineNumber: number): boolean { - // TODO @alexr00 update for background tokenization - return true; + return this._tokenStore.hasTokens(this._textModel, new Range(lineNumber, 1, lineNumber, this._textModel.getLineMaxColumn(lineNumber))); } public override isCheapToTokenize(lineNumber: number): boolean { - // TODO @alexr00 update for background tokenization + // TODO @alexr00 determine what makes it cheap to tokenize? return true; } @@ -99,8 +101,6 @@ export class TreeSitterTokens extends AbstractTokens { return null; } public override get hasTokens(): boolean { - // TODO @alexr00 once we have a token store, implement properly - const hasTree = this._treeSitterService.getParseResult(this._textModel) !== undefined; - return hasTree; + return this._tokenStore.hasTokens(this._textModel); } } diff --git a/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts similarity index 59% rename from src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts rename to src/vs/editor/common/services/treeSitter/treeSitterParserService.ts index 81d1736d775c..67bb129ac6c7 100644 --- a/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts +++ b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts @@ -5,12 +5,12 @@ import type { Parser } from '@vscode/tree-sitter-wasm'; import { AppResourcePath, FileAccess, nodeModulesAsarUnpackedPath, nodeModulesPath } from '../../../../base/common/network.js'; -import { EDITOR_EXPERIMENTAL_PREFER_TREESITTER, ITreeSitterParserService, ITreeSitterParseResult, ITextModelTreeSitter } from '../../../common/services/treeSitterParserService.js'; -import { IModelService } from '../../../common/services/model.js'; +import { EDITOR_EXPERIMENTAL_PREFER_TREESITTER, ITreeSitterParserService, ITreeSitterParseResult, ITextModelTreeSitter, RangeChange, TreeUpdateEvent, TreeParseUpdateEvent } from '../treeSitterParserService.js'; +import { IModelService } from '../model.js'; import { Disposable, DisposableMap, DisposableStore, dispose, IDisposable } from '../../../../base/common/lifecycle.js'; -import { ITextModel } from '../../../common/model.js'; +import { ITextModel } from '../../model.js'; import { IFileService } from '../../../../platform/files/common/files.js'; -import { IModelContentChange } from '../../../common/textModelEvents.js'; +import { IModelContentChange, IModelContentChangedEvent } from '../../textModelEvents.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -21,7 +21,9 @@ import { CancellationToken, cancelOnDispose } from '../../../../base/common/canc import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'; import { PromiseResult } from '../../../../base/common/observable.js'; -import { Range } from '../../../common/core/range.js'; +import { Range } from '../../core/range.js'; +import { Position } from '../../core/position.js'; +import { LimitedQueue } from '../../../../base/common/async.js'; const EDITOR_TREESITTER_TELEMETRY = 'editor.experimental.treeSitterTelemetry'; const MODULE_LOCATION_SUBPATH = `@vscode/tree-sitter-wasm/wasm`; @@ -32,9 +34,10 @@ function getModuleLocation(environmentService: IEnvironmentService): AppResource } export class TextModelTreeSitter extends Disposable implements ITextModelTreeSitter { - private _onDidChangeParseResult: Emitter = this._register(new Emitter()); - public readonly onDidChangeParseResult: Event = this._onDidChangeParseResult.event; + private _onDidChangeParseResult: Emitter = this._register(new Emitter()); + public readonly onDidChangeParseResult: Event = this._onDidChangeParseResult.event; private _parseResult: TreeSitterParseResult | undefined; + private _versionId: number = 0; get parseResult(): ITreeSitterParseResult | undefined { return this._parseResult; } @@ -53,7 +56,7 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit } } - private readonly _languageSessionDisposables = this._register(new DisposableStore()); + private readonly _parseSessionDisposables = this._register(new DisposableStore()); /** * Be very careful when making changes to this method as it is easy to introduce race conditions. */ @@ -62,10 +65,10 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit } public async parse(languageId: string = this.model.getLanguageId()): Promise { - this._languageSessionDisposables.clear(); + this._parseSessionDisposables.clear(); this._parseResult = undefined; - const token = cancelOnDispose(this._languageSessionDisposables); + const token = cancelOnDispose(this._parseSessionDisposables); let language: Parser.Language | undefined; try { language = await this._getLanguage(languageId, token); @@ -81,14 +84,20 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit return; } - const treeSitterTree = this._languageSessionDisposables.add(new TreeSitterParseResult(new Parser(), language, this._logService, this._telemetryService)); - this._languageSessionDisposables.add(this.model.onDidChangeContent(e => this._onDidChangeContent(treeSitterTree, e.changes))); - await this._onDidChangeContent(treeSitterTree, []); + const treeSitterTree = this._parseSessionDisposables.add(new TreeSitterParseResult(new Parser(), language, this._logService, this._telemetryService)); + this._parseResult = treeSitterTree; + this._parseSessionDisposables.add(treeSitterTree.onDidUpdate(e => { + if (e.ranges && (e.versionId > this._versionId)) { + this._versionId = e.versionId; + this._onDidChangeParseResult.fire({ ranges: e.ranges, versionId: e.versionId }); + } + })); + this._parseSessionDisposables.add(this.model.onDidChangeContent(e => this._onDidChangeContent(treeSitterTree, e))); + await this._onDidChangeContent(treeSitterTree, undefined); if (token.isCancellationRequested) { return; } - this._parseResult = treeSitterTree; return this._parseResult; } @@ -113,13 +122,8 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit }); } - private async _onDidChangeContent(treeSitterTree: TreeSitterParseResult, changes: IModelContentChange[]) { - const diff = await treeSitterTree.onDidChangeContent(this.model, changes); - if (!diff || diff.length > 0) { - // Tree sitter is 0 based, text model is 1 based - const ranges = diff ? diff.map(r => new Range(r.startPosition.row + 1, r.startPosition.column + 1, r.endPosition.row + 1, r.endPosition.column + 1)) : [this.model.getFullModelRange()]; - this._onDidChangeParseResult.fire(ranges); - } + private async _onDidChangeContent(treeSitterTree: TreeSitterParseResult, change: IModelContentChangedEvent | undefined) { + return treeSitterTree.onDidChangeContent(this.model, change?.changes ?? []); } } @@ -128,18 +132,38 @@ const enum TelemetryParseType { Incremental = 'incrementalParse' } +interface ChangedRange { + newNodeId: number; + newStartPosition: Position; + newEndPosition: Position; + newStartIndex: number; + newEndIndex: number; + oldStartIndex: number; + oldEndIndex: number; +} + export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResult { private _tree: Parser.Tree | undefined; + private _lastFullyParsed: Parser.Tree | undefined; + private _lastFullyParsedWithEdits: Parser.Tree | undefined; + private readonly _onDidUpdate: Emitter = new Emitter(); + public readonly onDidUpdate: Event = this._onDidUpdate.event; + private _versionId: number = 0; + private _editVersion: number = 0; + get versionId() { + return this._versionId; + } private _isDisposed: boolean = false; constructor(public readonly parser: Parser, public /** exposed for tests **/ readonly language: Parser.Language, private readonly _logService: ILogService, private readonly _telemetryService: ITelemetryService) { - this.parser.setTimeoutMicros(50 * 1000); // 50 ms + this.parser.setTimeoutMicros(5 * 1000); // 5 ms this.parser.setLanguage(language); } dispose(): void { this._isDisposed = true; + this._onDidUpdate.dispose(); this._tree?.delete(); this.parser?.delete(); } @@ -150,48 +174,213 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul } get isDisposed() { return this._isDisposed; } - private _onDidChangeContentQueue: Promise = Promise.resolve(); - public async onDidChangeContent(model: ITextModel, changes: IModelContentChange[]): Promise { - const oldTree = this.tree?.copy(); - this._applyEdits(model, changes); - return new Promise(resolve => { - this._onDidChangeContentQueue = this._onDidChangeContentQueue.then(async () => { - if (this.isDisposed) { - // No need to continue the queue if we are disposed - return; + private findChangedNodes(newTree: Parser.Tree, oldTree: Parser.Tree, version: number): ChangedRange[] { + const newCursor = newTree.walk(); + const oldCursor = oldTree.walk(); + const gotoNextSibling = () => { + const n = newCursor.gotoNextSibling(); + const o = oldCursor.gotoNextSibling(); + if (n !== o) { + throw new Error('Trees are out of sync'); + } + return n && o; + }; + const gotoParent = () => { + const n = newCursor.gotoParent(); + const o = oldCursor.gotoParent(); + if (n !== o) { + throw new Error('Trees are out of sync'); + } + return n && o; + }; + const gotoNthChild = (index: number) => { + const n = newCursor.gotoFirstChild(); + const o = oldCursor.gotoFirstChild(); + if (n !== o) { + throw new Error('Trees are out of sync'); + } + if (index === 0) { + return n && o; + } + for (let i = 1; i <= index; i++) { + const nn = newCursor.gotoNextSibling(); + const oo = oldCursor.gotoNextSibling(); + if (nn !== oo) { + throw new Error('Trees are out of sync'); + } + if (!nn || !oo) { + return false; } - await this._parseAndUpdateTree(model); - resolve((this.tree && oldTree) ? oldTree.getChangedRanges(this.tree) : undefined); + } + return n && o; + }; - }).catch((e) => { - this._logService.error('Error parsing tree-sitter tree', e); - }); + const changedRanges: ChangedRange[] = []; + let next = true; + const nextSiblingOrParentSibling = () => { + do { + if (newCursor.currentNode.nextSibling) { + return gotoNextSibling(); + } + if (newCursor.currentNode.parent) { + gotoParent(); + } + } while (newCursor.currentNode.nextSibling || newCursor.currentNode.parent); + return false; + }; + + const getClosestPreviousNodes = (): { old: Parser.SyntaxNode; new: Parser.SyntaxNode } | undefined => { + // Go up parents until the end of the parent is before the start of the current. + const newFindPrev = newTree.walk(); + newFindPrev.resetTo(newCursor); + const oldFindPrev = oldTree.walk(); + oldFindPrev.resetTo(oldCursor); + const startingNode = newCursor.currentNode; + do { + if (newFindPrev.currentNode.previousSibling) { + newFindPrev.gotoPreviousSibling(); + oldFindPrev.gotoPreviousSibling(); + } else { + newFindPrev.gotoParent(); + oldFindPrev.gotoParent(); + } + + } while (newFindPrev.currentNode.startIndex === startingNode.startIndex && (newFindPrev.currentNode.parent || newFindPrev.currentNode.previousSibling) && (newFindPrev.currentNode.id !== startingNode.id)); + if ((newFindPrev.currentNode.id !== startingNode.id) && newFindPrev.currentNode.endIndex < startingNode.startIndex) { + return { old: oldFindPrev.currentNode, new: newFindPrev.currentNode }; + } else { + return undefined; + } + }; + + do { + if (newCursor.currentNode.hasChanges) { + // Check if only one of the children has changes. + // If it's only one, then we go to that child. + // If it's more then, we need to go to each child + // If it's none, then we've found one of our ranges + const newChildren = newCursor.currentNode.children; + const indexChangedChildren: number[] = []; + const changedChildren = newChildren.filter((c, index) => { + if (c.hasChanges) { + indexChangedChildren.push(index); + } + return c.hasChanges; + }); + if (changedChildren.length >= 1) { + next = gotoNthChild(indexChangedChildren[0]); + } else if (changedChildren.length === 0) { + const newNode = newCursor.currentNode; + const oldNode = oldCursor.currentNode; + const newEndPosition = new Position(newNode.endPosition.row + 1, newNode.endPosition.column + 1); + const oldEndIndex = oldNode.endIndex; + + // Fill holes between nodes. + const closestPrev = getClosestPreviousNodes(); + const newStartPosition = new Position(closestPrev ? closestPrev.new.endPosition.row + 1 : newNode.startPosition.row + 1, closestPrev ? closestPrev.new.endPosition.column + 1 : newNode.startPosition.column + 1); + const newStartIndex = closestPrev ? closestPrev.new.endIndex : newNode.startIndex; + const oldStartIndex = closestPrev ? closestPrev.old.endIndex : oldNode.startIndex; + + changedRanges.push({ newStartPosition, newEndPosition, oldStartIndex, oldEndIndex, newNodeId: newNode.id, newStartIndex, newEndIndex: newNode.endIndex }); + next = nextSiblingOrParentSibling(); + } + } else { + next = nextSiblingOrParentSibling(); + } + } while (next); + + if (changedRanges.length === 0 && newTree.rootNode.hasChanges) { + return [{ newStartPosition: new Position(newTree.rootNode.startPosition.row + 1, newTree.rootNode.startPosition.column + 1), newEndPosition: new Position(newTree.rootNode.endPosition.row + 1, newTree.rootNode.endPosition.column + 1), oldStartIndex: oldTree.rootNode.startIndex, oldEndIndex: oldTree.rootNode.endIndex, newStartIndex: newTree.rootNode.startIndex, newEndIndex: newTree.rootNode.endIndex, newNodeId: newTree.rootNode.id }]; + } else { + return changedRanges; + } + } + + private calculateRangeChange(changedNodes: ChangedRange[] | undefined): RangeChange[] | undefined { + if (!changedNodes) { + return undefined; + } + + // Collapse conginguous ranges + const ranges: RangeChange[] = []; + for (let i = 0; i < changedNodes.length; i++) { + const node = changedNodes[i]; + + // Check if contiguous with previous + const prevNode = changedNodes[i - 1]; + if ((i > 0) && prevNode.newEndPosition.equals(node.newStartPosition)) { + const prevRangeChange = ranges[ranges.length - 1]; + prevRangeChange.newRange = new Range(prevRangeChange.newRange.startLineNumber, prevRangeChange.newRange.startColumn, node.newEndPosition.lineNumber, node.newEndPosition.column); + prevRangeChange.oldRangeLength = node.oldEndIndex - prevNode.oldStartIndex; + prevRangeChange.newRangeEndOffset = node.newEndIndex; + } else { + ranges.push({ newRange: Range.fromPositions(node.newStartPosition, node.newEndPosition), oldRangeLength: node.oldEndIndex - node.oldStartIndex, newRangeStartOffset: node.newStartIndex, newRangeEndOffset: node.newEndIndex }); + } + } + return ranges; + } + + private _onDidChangeContentQueue: LimitedQueue = new LimitedQueue(); + public async onDidChangeContent(model: ITextModel, changes: IModelContentChange[]): Promise { + const version = model.getVersionId(); + if (version === this._editVersion) { + return; + } + + this._applyEdits(changes, version); + + this._onDidChangeContentQueue.queue(async () => { + if (this.isDisposed) { + // No need to continue the queue if we are disposed + return; + } + + let ranges: RangeChange[] | undefined; + if (this._lastFullyParsedWithEdits && this._lastFullyParsed) { + ranges = this.calculateRangeChange(this.findChangedNodes(this._lastFullyParsedWithEdits, this._lastFullyParsed, version)); + } + + const completed = await this._parseAndUpdateTree(model, version); + if (completed && (version === model.getVersionId())) { + if (!ranges) { + ranges = [{ newRange: model.getFullModelRange(), oldRangeLength: model.getValueLength(), newRangeStartOffset: 0, newRangeEndOffset: model.getValueLength() }]; + } + this._onDidUpdate.fire({ ranges, versionId: version }); + } }); } - private _newEdits = true; - private _applyEdits(model: ITextModel, changes: IModelContentChange[]) { + private _applyEdits(changes: IModelContentChange[], version: number) { for (const change of changes) { - const newEndOffset = change.rangeOffset + change.text.length; - const newEndPosition = model.getPositionAt(newEndOffset); - - this.tree?.edit({ + this._tree?.edit({ + startIndex: change.rangeOffset, + oldEndIndex: change.rangeOffset + change.rangeLength, + newEndIndex: change.rangeOffset + change.text.length, + startPosition: { row: change.range.startLineNumber - 1, column: change.range.startColumn - 1 }, + oldEndPosition: { row: change.range.endLineNumber - 1, column: change.range.endColumn - 1 }, + newEndPosition: { row: change.rangeEndPosition.lineNumber - 1, column: change.rangeEndPosition.column - 1 } + }); + this._lastFullyParsedWithEdits?.edit({ startIndex: change.rangeOffset, oldEndIndex: change.rangeOffset + change.rangeLength, newEndIndex: change.rangeOffset + change.text.length, startPosition: { row: change.range.startLineNumber - 1, column: change.range.startColumn - 1 }, oldEndPosition: { row: change.range.endLineNumber - 1, column: change.range.endColumn - 1 }, - newEndPosition: { row: newEndPosition.lineNumber - 1, column: newEndPosition.column - 1 } + newEndPosition: { row: change.rangeEndPosition.lineNumber - 1, column: change.rangeEndPosition.column - 1 } }); - this._newEdits = true; } + this._editVersion = version; } - private async _parseAndUpdateTree(model: ITextModel) { + private async _parseAndUpdateTree(model: ITextModel, version: number): Promise { const tree = await this._parse(model); - if (!this._newEdits) { + if (tree) { this.tree = tree; + this._lastFullyParsed = tree.copy(); + this._lastFullyParsedWithEdits = tree.copy(); + this._versionId = version; } + return tree; } private _parse(model: ITextModel): Promise { @@ -204,14 +393,15 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul private async _parseAndYield(model: ITextModel, parseType: TelemetryParseType): Promise { const language = model.getLanguageId(); - let tree: Parser.Tree | undefined; let time: number = 0; let passes: number = 0; - this._newEdits = false; + const inProgressVersion = this._editVersion; + let newTree: Parser.Tree | undefined; + do { const timer = performance.now(); try { - tree = this.parser.parse((index: number, position?: Parser.Point) => this._parseCallback(model, index), this.tree); + newTree = this.parser.parse((index: number, position?: Parser.Point) => this._parseCallback(model, index), this.tree); } catch (e) { // parsing can fail when the timeout is reached, will resume upon next loop } finally { @@ -222,12 +412,9 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul // Even if the model changes and edits are applied, the tree parsing will continue correctly after the await. await new Promise(resolve => setTimeout0(resolve)); - if (model.isDisposed() || this.isDisposed) { - return; - } - } while (!tree && !this._newEdits); // exit if there a new edits, as anhy parsing done while there are new edits is throw away work + } while (!model.isDisposed() && !this.isDisposed && !newTree && inProgressVersion === model.getVersionId()); this.sendParseTimeTelemetry(parseType, language, time, passes); - return tree; + return (newTree && (inProgressVersion === model.getVersionId())) ? newTree : undefined; } private _parseCallback(textModel: ITextModel, index: number): string | null { @@ -357,8 +544,10 @@ export class TreeSitterTextModelService extends Disposable implements ITreeSitte private readonly _treeSitterLanguages: TreeSitterLanguages; public readonly onDidAddLanguage: Event<{ id: string; language: Parser.Language }>; - private _onDidUpdateTree: Emitter<{ textModel: ITextModel; ranges: Range[] }> = this._register(new Emitter()); - public readonly onDidUpdateTree: Event<{ textModel: ITextModel; ranges: Range[] }> = this._onDidUpdateTree.event; + private _onDidUpdateTree: Emitter = this._register(new Emitter()); + public readonly onDidUpdateTree: Event = this._onDidUpdateTree.event; + + public isTest: boolean = false; constructor(@IModelService private readonly _modelService: IModelService, @IFileService fileService: IFileService, @@ -387,10 +576,11 @@ export class TreeSitterTextModelService extends Disposable implements ITreeSitte return textModelTreeSitter?.textModelTreeSitter.parseResult; } + /** + * For testing + */ async getTree(content: string, languageId: string): Promise { - await this._init; - - const language = await this._treeSitterLanguages.getLanguage(languageId); + const language = await this.getLanguage(languageId); const Parser = await this._treeSitterImporter.getParserClass(); if (language) { const parser = new Parser(); @@ -400,12 +590,26 @@ export class TreeSitterTextModelService extends Disposable implements ITreeSitte return undefined; } + /** + * For testing + */ + async getLanguage(languageId: string): Promise { + await this._init; + return this._treeSitterLanguages.getLanguage(languageId); + } + private async _doInitParser() { const Parser = await this._treeSitterImporter.getParserClass(); const environmentService = this._environmentService; + const isTest = this.isTest; await Parser.init({ locateFile(_file: string, _folder: string) { - return FileAccess.asBrowserUri(`${getModuleLocation(environmentService)}/${FILENAME_TREESITTER_WASM}`).toString(true); + const location: AppResourcePath = `${getModuleLocation(environmentService)}/${FILENAME_TREESITTER_WASM}`; + if (isTest) { + return FileAccess.asFileUri(location).toString(true); + } else { + return FileAccess.asBrowserUri(location).toString(true); + } } }); return true; @@ -469,20 +673,22 @@ export class TreeSitterTextModelService extends Disposable implements ITreeSitte this._modelService.getModels().forEach(model => this._createTextModelTreeSitter(model)); } - public getTextModelTreeSitter(model: ITextModel): ITextModelTreeSitter { - return new TextModelTreeSitter(model, this._treeSitterLanguages, this._treeSitterImporter, this._logService, this._telemetryService, false); + public async getTextModelTreeSitter(model: ITextModel, parseImmediately: boolean = false): Promise { + await this.getLanguage(model.getLanguageId()); + return this._createTextModelTreeSitter(model, parseImmediately); } - private _createTextModelTreeSitter(model: ITextModel) { - const textModelTreeSitter = new TextModelTreeSitter(model, this._treeSitterLanguages, this._treeSitterImporter, this._logService, this._telemetryService); + private _createTextModelTreeSitter(model: ITextModel, parseImmediately: boolean = true): ITextModelTreeSitter { + const textModelTreeSitter = new TextModelTreeSitter(model, this._treeSitterLanguages, this._treeSitterImporter, this._logService, this._telemetryService, parseImmediately); const disposables = new DisposableStore(); disposables.add(textModelTreeSitter); - disposables.add(textModelTreeSitter.onDidChangeParseResult((ranges) => this._onDidUpdateTree.fire({ textModel: model, ranges }))); + disposables.add(textModelTreeSitter.onDidChangeParseResult(change => this._onDidUpdateTree.fire({ textModel: model, ranges: change.ranges ?? [], versionId: change.versionId }))); this._textModelTreeSitters.set(model, { textModelTreeSitter, disposables, dispose: disposables.dispose.bind(disposables) }); + return textModelTreeSitter; } private _addGrammar(languageId: string, grammarName: string) { diff --git a/src/vs/editor/common/services/treeSitterParserService.ts b/src/vs/editor/common/services/treeSitterParserService.ts index a19f3093be07..4d0ddb3f971e 100644 --- a/src/vs/editor/common/services/treeSitterParserService.ts +++ b/src/vs/editor/common/services/treeSitterParserService.ts @@ -13,22 +13,41 @@ export const EDITOR_EXPERIMENTAL_PREFER_TREESITTER = 'editor.experimental.prefer export const ITreeSitterParserService = createDecorator('treeSitterParserService'); +export interface RangeChange { + newRange: Range; + oldRangeLength: number; + newRangeStartOffset: number; + newRangeEndOffset: number; +} + +export interface TreeParseUpdateEvent { + ranges: RangeChange[] | undefined; + versionId: number; +} + +export interface TreeUpdateEvent { + textModel: ITextModel; + ranges: RangeChange[]; + versionId: number; +} + export interface ITreeSitterParserService { readonly _serviceBrand: undefined; onDidAddLanguage: Event<{ id: string; language: Parser.Language }>; getOrInitLanguage(languageId: string): Parser.Language | undefined; getParseResult(textModel: ITextModel): ITreeSitterParseResult | undefined; getTree(content: string, languageId: string): Promise; - onDidUpdateTree: Event<{ textModel: ITextModel; ranges: Range[] }>; + onDidUpdateTree: Event; /** * For testing purposes so that the time to parse can be measured. */ - getTextModelTreeSitter(textModel: ITextModel): ITextModelTreeSitter | undefined; + getTextModelTreeSitter(model: ITextModel, parseImmediately?: boolean): Promise; } export interface ITreeSitterParseResult { readonly tree: Parser.Tree | undefined; readonly language: Parser.Language; + versionId: number; } export interface ITextModelTreeSitter { diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index 768563c4be60..8cc19576d3ee 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Position } from './core/position.js'; import { IRange } from './core/range.js'; import { Selection } from './core/selection.js'; import { IModelDecoration, InjectedTextOptions } from './model.js'; @@ -34,7 +35,7 @@ export interface IModelLanguageConfigurationChangedEvent { export interface IModelContentChange { /** - * The range that got replaced. + * The old range that got replaced. */ readonly range: IRange; /** @@ -45,6 +46,10 @@ export interface IModelContentChange { * The length of the range that got replaced. */ readonly rangeLength: number; + /** + * The new end position of the range that got replaced. + */ + readonly rangeEndPosition: Position; /** * The new text for the range. */ diff --git a/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts b/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts index be9b21fbdd90..20a2f082f3c2 100644 --- a/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts +++ b/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts @@ -6,21 +6,20 @@ import type { Parser } from '@vscode/tree-sitter-wasm'; import { Event } from '../../../base/common/event.js'; import { ITextModel } from '../../common/model.js'; -import { ITextModelTreeSitter, ITreeSitterParseResult, ITreeSitterParserService } from '../../common/services/treeSitterParserService.js'; -import { Range } from '../../common/core/range.js'; +import { ITextModelTreeSitter, ITreeSitterParseResult, ITreeSitterParserService, TreeUpdateEvent } from '../../common/services/treeSitterParserService.js'; /** * The monaco build doesn't like the dynamic import of tree sitter in the real service. * We use a dummy sertive here to make the build happy. */ export class StandaloneTreeSitterParserService implements ITreeSitterParserService { - getTextModelTreeSitter(textModel: ITextModel): ITextModelTreeSitter | undefined { + async getTextModelTreeSitter(model: ITextModel, parseImmediately?: boolean): Promise { return undefined; } async getTree(content: string, languageId: string): Promise { return undefined; } - onDidUpdateTree: Event<{ textModel: ITextModel; ranges: Range[] }> = Event.None; + onDidUpdateTree: Event = Event.None; readonly _serviceBrand: undefined; onDidAddLanguage: Event<{ id: string; language: Parser.Language }> = Event.None; diff --git a/src/vs/editor/test/browser/services/treeSitterParserService.test.ts b/src/vs/editor/test/browser/services/treeSitterParserService.test.ts index d90cca2af992..5475195b7db4 100644 --- a/src/vs/editor/test/browser/services/treeSitterParserService.test.ts +++ b/src/vs/editor/test/browser/services/treeSitterParserService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { TextModelTreeSitter, TreeSitterImporter, TreeSitterLanguages } from '../../../browser/services/treeSitter/treeSitterParserService.js'; +import { TextModelTreeSitter, TreeSitterImporter, TreeSitterLanguages } from '../../../common/services/treeSitter/treeSitterParserService.js'; import type { Parser } from '@vscode/tree-sitter-wasm'; import { createTextModel } from '../../common/testTextModel.js'; import { timeout } from '../../../../base/common/async.js'; diff --git a/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts b/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts index e8406d868f87..501d92ea85ca 100644 --- a/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts +++ b/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts @@ -82,11 +82,11 @@ suite("CodeEditorWidget", () => { assert.deepStrictEqual(log.getAndClearEntries(), [ 'handle change: editor.onDidType "abc"', - 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', - 'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}', - 'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"rangeEndPosition":{"lineNumber":1,"column":2},"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"rangeEndPosition":{"lineNumber":1,"column":3},"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}', + 'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"rangeEndPosition":{"lineNumber":1,"column":4},"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}', 'handle change: editor.selections {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', - "running derived: selection: [1,4 -> 1,4], value: 4", + 'running derived: selection: [1,4 -> 1,4], value: 4', ]); })); @@ -96,11 +96,11 @@ suite("CodeEditorWidget", () => { assert.deepStrictEqual(log.getAndClearEntries(), [ 'handle change: editor.onDidType "abc"', - 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', - 'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}', - 'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"rangeEndPosition":{"lineNumber":1,"column":2},"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"rangeEndPosition":{"lineNumber":1,"column":3},"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}', + 'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"rangeEndPosition":{"lineNumber":1,"column":4},"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}', 'handle change: editor.selections {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', - "running derived: selection: [1,4 -> 1,4], value: 4", + 'running derived: selection: [1,4 -> 1,4], value: 4', ]); editor.setPosition(new Position(1, 5), "test"); @@ -134,7 +134,7 @@ suite("CodeEditorWidget", () => { ">>> before get", "<<< after get", 'handle change: editor.onDidType "a"', - 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"rangeEndPosition":{"lineNumber":1,"column":2},"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', 'handle change: editor.selections {"selection":"[1,2 -> 1,2]","modelVersionId":2,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', "running derived: selection: [1,2 -> 1,2], value: 2", ]); @@ -172,7 +172,7 @@ suite("CodeEditorWidget", () => { "running derived: selection: [1,2 -> 1,2], value: 2", "<<< after get", 'handle change: editor.onDidType "a"', - 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"rangeEndPosition":{"lineNumber":1,"column":2},"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', 'handle change: editor.selections {"selection":"[1,2 -> 1,2]","modelVersionId":2,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', "running derived: selection: [1,2 -> 1,2], value: 2", ]); diff --git a/src/vs/editor/test/common/model/tokenStore.test.ts b/src/vs/editor/test/common/model/tokenStore.test.ts new file mode 100644 index 000000000000..7e3b3b52a424 --- /dev/null +++ b/src/vs/editor/test/common/model/tokenStore.test.ts @@ -0,0 +1,701 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { TextModel } from '../../../common/model/textModel.js'; +import { TokenStore } from '../../../common/model/tokenStore.js'; + +suite('TokenStore', () => { + let textModel: TextModel; + ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { + textModel = { + getValueLength: () => 11 + } as TextModel; + }); + + test('constructs with empty model', () => { + const store = new TokenStore(textModel); + assert.ok(store.root); + assert.strictEqual(store.root.length, textModel.getValueLength()); + }); + + test('builds store with single token', () => { + const store = new TokenStore(textModel); + store.buildStore([{ + startOffsetInclusive: 0, + length: 5, + token: 1 + }]); + assert.strictEqual(store.root.length, 5); + }); + + test('builds store with multiple tokens', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 4, token: 3 } + ]); + assert.ok(store.root); + assert.strictEqual(store.root.length, 10); + }); + + test('creates balanced tree structure', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 2, token: 1 }, + { startOffsetInclusive: 2, length: 2, token: 2 }, + { startOffsetInclusive: 4, length: 2, token: 3 }, + { startOffsetInclusive: 6, length: 2, token: 4 } + ]); + + const root = store.root as any; + assert.ok(root.children); + assert.strictEqual(root.children.length, 2); + assert.strictEqual(root.children[0].length, 4); + assert.strictEqual(root.children[1].length, 4); + }); + + test('creates deep tree structure', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 1, token: 1 }, + { startOffsetInclusive: 1, length: 1, token: 2 }, + { startOffsetInclusive: 2, length: 1, token: 3 }, + { startOffsetInclusive: 3, length: 1, token: 4 }, + { startOffsetInclusive: 4, length: 1, token: 5 }, + { startOffsetInclusive: 5, length: 1, token: 6 }, + { startOffsetInclusive: 6, length: 1, token: 7 }, + { startOffsetInclusive: 7, length: 1, token: 8 } + ]); + + const root = store.root as any; + assert.ok(root.children); + assert.strictEqual(root.children.length, 2); + assert.ok(root.children[0].children); + assert.strictEqual(root.children[0].children.length, 2); + assert.ok(root.children[0].children[0].children); + assert.strictEqual(root.children[0].children[0].children.length, 2); + }); + + test('updates single token in middle', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + + store.update(3, [ + { startOffsetInclusive: 3, length: 3, token: 4 } + ]); + + const tokens = store.root as any; + assert.strictEqual(tokens.children[0].token, 1); + assert.strictEqual(tokens.children[1].token, 4); + assert.strictEqual(tokens.children[2].token, 3); + }); + + test('updates multiple consecutive tokens', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + + store.update(6, [ + { startOffsetInclusive: 3, length: 3, token: 4 }, + { startOffsetInclusive: 6, length: 3, token: 5 } + ]); + + const tokens = store.root as any; + assert.strictEqual(tokens.children[0].token, 1); + assert.strictEqual(tokens.children[1].token, 4); + assert.strictEqual(tokens.children[2].token, 5); + }); + + test('updates tokens at start of document', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + + store.update(3, [ + { startOffsetInclusive: 0, length: 3, token: 4 } + ]); + + const tokens = store.root as any; + assert.strictEqual(tokens.children[0].token, 4); + assert.strictEqual(tokens.children[1].token, 2); + assert.strictEqual(tokens.children[2].token, 3); + }); + + test('updates tokens at end of document', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + + store.update(3, [ + { startOffsetInclusive: 6, length: 3, token: 4 } + ]); + + const tokens = store.root as any; + assert.strictEqual(tokens.children[0].token, 1); + assert.strictEqual(tokens.children[1].token, 2); + assert.strictEqual(tokens.children[2].token, 4); + }); + + test('updates length of tokens', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + + store.update(6, [ + { startOffsetInclusive: 3, length: 5, token: 4 } + ]); + + const tokens = store.root as any; + assert.strictEqual(tokens.children[0].token, 1); + assert.strictEqual(tokens.children[0].length, 3); + assert.strictEqual(tokens.children[1].token, 4); + assert.strictEqual(tokens.children[1].length, 5); + }); + + test('update deeply nested tree with new token length in the middle', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 1, token: 1 }, + { startOffsetInclusive: 1, length: 1, token: 2 }, + { startOffsetInclusive: 2, length: 1, token: 3 }, + { startOffsetInclusive: 3, length: 1, token: 4 }, + { startOffsetInclusive: 4, length: 1, token: 5 }, + { startOffsetInclusive: 5, length: 1, token: 6 }, + { startOffsetInclusive: 6, length: 1, token: 7 }, + { startOffsetInclusive: 7, length: 1, token: 8 } + ]); + + // Update token in the middle (position 3-4) to span 3-6 + store.update(3, [ + { startOffsetInclusive: 3, length: 3, token: 9 } + ]); + + const root = store.root as any; + // Verify the structure remains balanced + assert.strictEqual(root.children.length, 3); + assert.strictEqual(root.children[0].children.length, 2); + + // Verify the lengths are updated correctly + assert.strictEqual(root.children[0].length, 2); // First 2 tokens + assert.strictEqual(root.children[1].length, 4); // Token 3 + our new longer token + assert.strictEqual(root.children[2].length, 2); // Last 2 tokens + }); + + test('getTokensInRange returns tokens in middle of document', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + + const tokens = store.getTokensInRange(3, 6); + assert.deepStrictEqual(tokens, [{ startOffsetInclusive: 3, length: 3, token: 2 }]); + }); + + test('getTokensInRange returns tokens at start of document', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + + const tokens = store.getTokensInRange(0, 3); + assert.deepStrictEqual(tokens, [{ startOffsetInclusive: 0, length: 3, token: 1 }]); + }); + + test('getTokensInRange returns tokens at end of document', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + + const tokens = store.getTokensInRange(6, 9); + assert.deepStrictEqual(tokens, [{ startOffsetInclusive: 6, length: 3, token: 3 }]); + }); + + test('getTokensInRange returns multiple tokens across nodes', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 1, token: 1 }, + { startOffsetInclusive: 1, length: 1, token: 2 }, + { startOffsetInclusive: 2, length: 1, token: 3 }, + { startOffsetInclusive: 3, length: 1, token: 4 }, + { startOffsetInclusive: 4, length: 1, token: 5 }, + { startOffsetInclusive: 5, length: 1, token: 6 } + ]); + + const tokens = store.getTokensInRange(2, 5); + assert.deepStrictEqual(tokens, [ + { startOffsetInclusive: 2, length: 1, token: 3 }, + { startOffsetInclusive: 3, length: 1, token: 4 }, + { startOffsetInclusive: 4, length: 1, token: 5 } + ]); + }); + + test('Realistic scenario one', () => { + // inspired by this snippet, with the update adding a space in the constructor's curly braces: + // /* + // */ + // class XY { + // constructor() {} + // } + + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 164164 }, + { startOffsetInclusive: 3, length: 1, token: 32836 }, + { startOffsetInclusive: 4, length: 3, token: 164164 }, + { startOffsetInclusive: 7, length: 2, token: 32836 }, + { startOffsetInclusive: 9, length: 5, token: 196676 }, + { startOffsetInclusive: 14, length: 1, token: 32836 }, + { startOffsetInclusive: 15, length: 2, token: 557124 }, + { startOffsetInclusive: 17, length: 4, token: 32836 }, + { startOffsetInclusive: 21, length: 1, token: 32836 }, + { startOffsetInclusive: 22, length: 11, token: 196676 }, + { startOffsetInclusive: 33, length: 7, token: 32836 }, + { startOffsetInclusive: 40, length: 3, token: 32836 } + ]); + + store.update(33, [ + { startOffsetInclusive: 9, length: 5, token: 196676 }, + { startOffsetInclusive: 14, length: 1, token: 32836 }, + { startOffsetInclusive: 15, length: 2, token: 557124 }, + { startOffsetInclusive: 17, length: 4, token: 32836 }, + { startOffsetInclusive: 21, length: 1, token: 32836 }, + { startOffsetInclusive: 22, length: 11, token: 196676 }, + { startOffsetInclusive: 33, length: 8, token: 32836 }, + { startOffsetInclusive: 41, length: 3, token: 32836 } + ]); + + }); + test('Realistic scenario two', () => { + // inspired by this snippet, with the update deleteing the space in the body of class x + // class x { + // + // } + // class y { + + // } + + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 5, token: 196676 }, + { startOffsetInclusive: 5, length: 1, token: 32836 }, + { startOffsetInclusive: 6, length: 1, token: 557124 }, + { startOffsetInclusive: 7, length: 4, token: 32836 }, + { startOffsetInclusive: 11, length: 3, token: 32836 }, + { startOffsetInclusive: 14, length: 3, token: 32836 }, + { startOffsetInclusive: 17, length: 5, token: 196676 }, + { startOffsetInclusive: 22, length: 1, token: 32836 }, + { startOffsetInclusive: 23, length: 1, token: 557124 }, + { startOffsetInclusive: 24, length: 4, token: 32836 }, + { startOffsetInclusive: 28, length: 2, token: 32836 }, + { startOffsetInclusive: 30, length: 1, token: 32836 } + ]); + const tokens0 = store.getTokensInRange(0, 16); + assert.deepStrictEqual(tokens0, [ + { token: 196676, startOffsetInclusive: 0, length: 5 }, + { token: 32836, startOffsetInclusive: 5, length: 1 }, + { token: 557124, startOffsetInclusive: 6, length: 1 }, + { token: 32836, startOffsetInclusive: 7, length: 4 }, + { token: 32836, startOffsetInclusive: 11, length: 3 }, + { token: 32836, startOffsetInclusive: 14, length: 2 } + ]); + + store.update(14, [ + { startOffsetInclusive: 0, length: 5, token: 196676 }, + { startOffsetInclusive: 5, length: 1, token: 32836 }, + { startOffsetInclusive: 6, length: 1, token: 557124 }, + { startOffsetInclusive: 7, length: 4, token: 32836 }, + { startOffsetInclusive: 11, length: 2, token: 32836 }, + { startOffsetInclusive: 13, length: 3, token: 32836 } + ]); + + const tokens = store.getTokensInRange(0, 16); + assert.deepStrictEqual(tokens, [ + { token: 196676, startOffsetInclusive: 0, length: 5 }, + { token: 32836, startOffsetInclusive: 5, length: 1 }, + { token: 557124, startOffsetInclusive: 6, length: 1 }, + { token: 32836, startOffsetInclusive: 7, length: 4 }, + { token: 32836, startOffsetInclusive: 11, length: 2 }, + { token: 32836, startOffsetInclusive: 13, length: 3 } + ]); + }); + test('Realistic scenario three', () => { + // inspired by this snippet, with the update adding a space after the { in the constructor + // /*-- + // --*/ + // class TreeViewPane { + // constructor( + // options: IViewletViewOptions, + // ) { + // } + // } + + + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 5, token: 164164 }, + { startOffsetInclusive: 5, length: 1, token: 32836 }, + { startOffsetInclusive: 6, length: 5, token: 164164 }, + { startOffsetInclusive: 11, length: 2, token: 32836 }, + { startOffsetInclusive: 13, length: 5, token: 196676 }, + { startOffsetInclusive: 18, length: 1, token: 32836 }, + { startOffsetInclusive: 19, length: 12, token: 557124 }, + { startOffsetInclusive: 31, length: 4, token: 32836 }, + { startOffsetInclusive: 35, length: 1, token: 32836 }, + { startOffsetInclusive: 36, length: 11, token: 196676 }, + { startOffsetInclusive: 47, length: 3, token: 32836 }, + { startOffsetInclusive: 50, length: 2, token: 32836 }, + { startOffsetInclusive: 52, length: 7, token: 327748 }, + { startOffsetInclusive: 59, length: 1, token: 98372 }, + { startOffsetInclusive: 60, length: 1, token: 32836 }, + { startOffsetInclusive: 61, length: 19, token: 557124 }, + { startOffsetInclusive: 80, length: 1, token: 32836 }, + { startOffsetInclusive: 81, length: 2, token: 32836 }, + { startOffsetInclusive: 83, length: 6, token: 32836 }, + { startOffsetInclusive: 89, length: 4, token: 32836 }, + { startOffsetInclusive: 93, length: 3, token: 32836 } + ]); + const tokens0 = store.getTokensInRange(36, 59); + assert.deepStrictEqual(tokens0, [ + { token: 196676, startOffsetInclusive: 36, length: 11 }, + { token: 32836, startOffsetInclusive: 47, length: 3 }, + { token: 32836, startOffsetInclusive: 50, length: 2 }, + { token: 327748, startOffsetInclusive: 52, length: 7 } + ]); + + store.update(82, [ + { startOffsetInclusive: 13, length: 5, token: 196676 }, + { startOffsetInclusive: 18, length: 1, token: 32836 }, + { startOffsetInclusive: 19, length: 12, token: 557124 }, + { startOffsetInclusive: 31, length: 4, token: 32836 }, + { startOffsetInclusive: 35, length: 1, token: 32836 }, + { startOffsetInclusive: 36, length: 11, token: 196676 }, + { startOffsetInclusive: 47, length: 3, token: 32836 }, + { startOffsetInclusive: 50, length: 2, token: 32836 }, + { startOffsetInclusive: 52, length: 7, token: 327748 }, + { startOffsetInclusive: 59, length: 1, token: 98372 }, + { startOffsetInclusive: 60, length: 1, token: 32836 }, + { startOffsetInclusive: 61, length: 19, token: 557124 }, + { startOffsetInclusive: 80, length: 1, token: 32836 }, + { startOffsetInclusive: 81, length: 2, token: 32836 }, + { startOffsetInclusive: 83, length: 7, token: 32836 }, + { startOffsetInclusive: 90, length: 4, token: 32836 }, + { startOffsetInclusive: 94, length: 3, token: 32836 } + ]); + + const tokens = store.getTokensInRange(36, 59); + assert.deepStrictEqual(tokens, [ + { token: 196676, startOffsetInclusive: 36, length: 11 }, + { token: 32836, startOffsetInclusive: 47, length: 3 }, + { token: 32836, startOffsetInclusive: 50, length: 2 }, + { token: 327748, startOffsetInclusive: 52, length: 7 } + ]); + }); + test('Realistic scenario four', () => { + // inspired by this snippet, with the update adding a new line after the return true; + // function x() { + // return true; + // } + + // class Y { + // private z = false; + // } + + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 8, token: 196676 }, + { startOffsetInclusive: 8, length: 1, token: 32836 }, + { startOffsetInclusive: 9, length: 1, token: 524356 }, + { startOffsetInclusive: 10, length: 6, token: 32836 }, + { startOffsetInclusive: 16, length: 1, token: 32836 }, + { startOffsetInclusive: 17, length: 6, token: 589892 }, + { startOffsetInclusive: 23, length: 1, token: 32836 }, + { startOffsetInclusive: 24, length: 4, token: 196676 }, + { startOffsetInclusive: 28, length: 1, token: 32836 }, + { startOffsetInclusive: 29, length: 2, token: 32836 }, + { startOffsetInclusive: 31, length: 3, token: 32836 }, // This is the closing curly brace + newline chars + { startOffsetInclusive: 34, length: 2, token: 32836 }, + { startOffsetInclusive: 36, length: 5, token: 196676 }, + { startOffsetInclusive: 41, length: 1, token: 32836 }, + { startOffsetInclusive: 42, length: 1, token: 557124 }, + { startOffsetInclusive: 43, length: 4, token: 32836 }, + { startOffsetInclusive: 47, length: 1, token: 32836 }, + { startOffsetInclusive: 48, length: 7, token: 196676 }, + { startOffsetInclusive: 55, length: 1, token: 32836 }, + { startOffsetInclusive: 56, length: 1, token: 327748 }, + { startOffsetInclusive: 57, length: 1, token: 32836 }, + { startOffsetInclusive: 58, length: 1, token: 98372 }, + { startOffsetInclusive: 59, length: 1, token: 32836 }, + { startOffsetInclusive: 60, length: 5, token: 196676 }, + { startOffsetInclusive: 65, length: 1, token: 32836 }, + { startOffsetInclusive: 66, length: 2, token: 32836 }, + { startOffsetInclusive: 68, length: 1, token: 32836 } + ]); + const tokens0 = store.getTokensInRange(36, 59); + assert.deepStrictEqual(tokens0, [ + { startOffsetInclusive: 36, length: 5, token: 196676 }, + { startOffsetInclusive: 41, length: 1, token: 32836 }, + { startOffsetInclusive: 42, length: 1, token: 557124 }, + { startOffsetInclusive: 43, length: 4, token: 32836 }, + { startOffsetInclusive: 47, length: 1, token: 32836 }, + { startOffsetInclusive: 48, length: 7, token: 196676 }, + { startOffsetInclusive: 55, length: 1, token: 32836 }, + { startOffsetInclusive: 56, length: 1, token: 327748 }, + { startOffsetInclusive: 57, length: 1, token: 32836 }, + { startOffsetInclusive: 58, length: 1, token: 98372 } + ]); + + // insert a tab + new line after `return true;` (like hitting enter after the ;) + store.update(32, [ + { startOffsetInclusive: 0, length: 8, token: 196676 }, + { startOffsetInclusive: 8, length: 1, token: 32836 }, + { startOffsetInclusive: 9, length: 1, token: 524356 }, + { startOffsetInclusive: 10, length: 6, token: 32836 }, + { startOffsetInclusive: 16, length: 1, token: 32836 }, + { startOffsetInclusive: 17, length: 6, token: 589892 }, + { startOffsetInclusive: 23, length: 1, token: 32836 }, + { startOffsetInclusive: 24, length: 4, token: 196676 }, + { startOffsetInclusive: 28, length: 1, token: 32836 }, + { startOffsetInclusive: 29, length: 2, token: 32836 }, + { startOffsetInclusive: 31, length: 3, token: 32836 }, // This is the new line, which consists of 3 characters: \t\r\n + { startOffsetInclusive: 34, length: 2, token: 32836 } + ]); + + const tokens1 = store.getTokensInRange(36, 59); + assert.deepStrictEqual(tokens1, [ + { startOffsetInclusive: 36, length: 2, token: 32836 }, + { startOffsetInclusive: 38, length: 2, token: 32836 }, + { startOffsetInclusive: 40, length: 5, token: 196676 }, + { startOffsetInclusive: 45, length: 1, token: 32836 }, + { startOffsetInclusive: 46, length: 1, token: 557124 }, + { startOffsetInclusive: 47, length: 4, token: 32836 }, + { startOffsetInclusive: 51, length: 1, token: 32836 }, + { startOffsetInclusive: 52, length: 7, token: 196676 } + ]); + + // Delete the tab character + store.update(37, [ + { startOffsetInclusive: 0, length: 8, token: 196676 }, + { startOffsetInclusive: 8, length: 1, token: 32836 }, + { startOffsetInclusive: 9, length: 1, token: 524356 }, + { startOffsetInclusive: 10, length: 6, token: 32836 }, + { startOffsetInclusive: 16, length: 1, token: 32836 }, + { startOffsetInclusive: 17, length: 6, token: 589892 }, + { startOffsetInclusive: 23, length: 1, token: 32836 }, + { startOffsetInclusive: 24, length: 4, token: 196676 }, + { startOffsetInclusive: 28, length: 1, token: 32836 }, + { startOffsetInclusive: 29, length: 2, token: 32836 }, + { startOffsetInclusive: 31, length: 2, token: 32836 }, // This is the changed line: \t\r\n to \r\n + { startOffsetInclusive: 33, length: 3, token: 32836 } + ]); + + const tokens2 = store.getTokensInRange(36, 59); + assert.deepStrictEqual(tokens2, [ + { startOffsetInclusive: 36, length: 1, token: 32836 }, + { startOffsetInclusive: 37, length: 2, token: 32836 }, + { startOffsetInclusive: 39, length: 5, token: 196676 }, + { startOffsetInclusive: 44, length: 1, token: 32836 }, + { startOffsetInclusive: 45, length: 1, token: 557124 }, + { startOffsetInclusive: 46, length: 4, token: 32836 }, + { startOffsetInclusive: 50, length: 1, token: 32836 }, + { startOffsetInclusive: 51, length: 7, token: 196676 }, + { startOffsetInclusive: 58, length: 1, token: 32836 } + ]); + + }); + + test('Insert new line and remove tabs (split tokens)', () => { + // class A { + // a() { + // } + // } + // + // interface I { + // + // } + + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 5, token: 196676 }, + { startOffsetInclusive: 5, length: 1, token: 32836 }, + { startOffsetInclusive: 6, length: 1, token: 557124 }, + { startOffsetInclusive: 7, length: 3, token: 32836 }, + { startOffsetInclusive: 10, length: 1, token: 32836 }, + { startOffsetInclusive: 11, length: 1, token: 524356 }, + { startOffsetInclusive: 12, length: 5, token: 32836 }, + { startOffsetInclusive: 17, length: 3, token: 32836 }, // This is the closing curly brace line of a() + { startOffsetInclusive: 20, length: 2, token: 32836 }, + { startOffsetInclusive: 22, length: 1, token: 32836 }, + { startOffsetInclusive: 23, length: 9, token: 196676 }, + { startOffsetInclusive: 32, length: 1, token: 32836 }, + { startOffsetInclusive: 33, length: 1, token: 557124 }, + { startOffsetInclusive: 34, length: 3, token: 32836 }, + { startOffsetInclusive: 37, length: 1, token: 32836 }, + { startOffsetInclusive: 38, length: 1, token: 32836 } + ]); + + const tokens0 = store.getTokensInRange(23, 39); + assert.deepStrictEqual(tokens0, [ + { startOffsetInclusive: 23, length: 9, token: 196676 }, + { startOffsetInclusive: 32, length: 1, token: 32836 }, + { startOffsetInclusive: 33, length: 1, token: 557124 }, + { startOffsetInclusive: 34, length: 3, token: 32836 }, + { startOffsetInclusive: 37, length: 1, token: 32836 }, + { startOffsetInclusive: 38, length: 1, token: 32836 } + ]); + + // Insert a new line after a() { }, which will add 2 tabs + store.update(21, [ + { startOffsetInclusive: 0, length: 5, token: 196676 }, + { startOffsetInclusive: 5, length: 1, token: 32836 }, + { startOffsetInclusive: 6, length: 1, token: 557124 }, + { startOffsetInclusive: 7, length: 3, token: 32836 }, + { startOffsetInclusive: 10, length: 1, token: 32836 }, + { startOffsetInclusive: 11, length: 1, token: 524356 }, + { startOffsetInclusive: 12, length: 5, token: 32836 }, + { startOffsetInclusive: 17, length: 3, token: 32836 }, + { startOffsetInclusive: 20, length: 3, token: 32836 }, + { startOffsetInclusive: 23, length: 1, token: 32836 } + ]); + + const tokens1 = store.getTokensInRange(26, 42); + assert.deepStrictEqual(tokens1, [ + { startOffsetInclusive: 26, length: 9, token: 196676 }, + { startOffsetInclusive: 35, length: 1, token: 32836 }, + { startOffsetInclusive: 36, length: 1, token: 557124 }, + { startOffsetInclusive: 37, length: 3, token: 32836 }, + { startOffsetInclusive: 40, length: 1, token: 32836 }, + { startOffsetInclusive: 41, length: 1, token: 32836 } + ]); + + // Insert another new line at the cursor, which will also cause the 2 tabs to be deleted + store.update(24, [ + { startOffsetInclusive: 0, length: 5, token: 196676 }, + { startOffsetInclusive: 5, length: 1, token: 32836 }, + { startOffsetInclusive: 6, length: 1, token: 557124 }, + { startOffsetInclusive: 7, length: 3, token: 32836 }, + { startOffsetInclusive: 10, length: 1, token: 32836 }, + { startOffsetInclusive: 11, length: 1, token: 524356 }, + { startOffsetInclusive: 12, length: 5, token: 32836 }, + { startOffsetInclusive: 17, length: 3, token: 32836 }, + { startOffsetInclusive: 20, length: 1, token: 32836 }, + { startOffsetInclusive: 21, length: 2, token: 32836 }, + { startOffsetInclusive: 23, length: 1, token: 32836 } + ]); + + const tokens2 = store.getTokensInRange(26, 42); + assert.deepStrictEqual(tokens2, [ + { startOffsetInclusive: 26, length: 9, token: 196676 }, + { startOffsetInclusive: 35, length: 1, token: 32836 }, + { startOffsetInclusive: 36, length: 1, token: 557124 }, + { startOffsetInclusive: 37, length: 3, token: 32836 }, + { startOffsetInclusive: 40, length: 1, token: 32836 }, + { startOffsetInclusive: 41, length: 1, token: 32836 } + ]); + }); + + test('delete removes tokens in the middle', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 3, token: 3 } + ]); + store.delete(3, 3); // delete 3 chars starting at offset 3 + const tokens = store.getTokensInRange(0, 9); + assert.deepStrictEqual(tokens, [ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 3 } + ]); + }); + + test('delete merges partially affected token', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 5, token: 1 }, + { startOffsetInclusive: 5, length: 5, token: 2 } + ]); + store.delete(3, 4); // removes 4 chars within token 1 and partially token 2 + const tokens = store.getTokensInRange(0, 10); + assert.deepStrictEqual(tokens, [ + { startOffsetInclusive: 0, length: 4, token: 1 }, + // token 2 is now shifted left by 4 + { startOffsetInclusive: 4, length: 3, token: 2 } + ]); + }); + + test('replace a token with a slightly larger token', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 5, token: 1 }, + { startOffsetInclusive: 5, length: 1, token: 2 }, + { startOffsetInclusive: 6, length: 1, token: 2 }, + { startOffsetInclusive: 7, length: 17, token: 2 }, + { startOffsetInclusive: 24, length: 1, token: 2 }, + { startOffsetInclusive: 25, length: 5, token: 2 }, + { startOffsetInclusive: 30, length: 1, token: 2 }, + { startOffsetInclusive: 31, length: 1, token: 2 }, + { startOffsetInclusive: 32, length: 5, token: 2 } + ]); + store.update(17, [{ startOffsetInclusive: 7, length: 19, token: 0 }]); // removes 4 chars within token 1 and partially token 2 + const tokens = store.getTokensInRange(0, 39); + assert.deepStrictEqual(tokens, [ + { startOffsetInclusive: 0, length: 5, token: 1 }, + { startOffsetInclusive: 5, length: 1, token: 2 }, + { startOffsetInclusive: 6, length: 1, token: 2 }, + { startOffsetInclusive: 7, length: 19, token: 0 }, + { startOffsetInclusive: 26, length: 1, token: 2 }, + { startOffsetInclusive: 27, length: 5, token: 2 }, + { startOffsetInclusive: 32, length: 1, token: 2 }, + { startOffsetInclusive: 33, length: 1, token: 2 }, + { startOffsetInclusive: 34, length: 5, token: 2 } + ]); + }); + + test('replace a character from a large token', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 2, token: 1 }, + { startOffsetInclusive: 2, length: 5, token: 2 }, + { startOffsetInclusive: 7, length: 1, token: 3 } + ]); + store.delete(1, 3); + const tokens = store.getTokensInRange(0, 7); + assert.deepStrictEqual(tokens, [ + { startOffsetInclusive: 0, length: 2, token: 1 }, + { startOffsetInclusive: 2, length: 1, token: 2 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 1, token: 3 } + ]); + }); +}); + diff --git a/src/vs/editor/test/common/services/testTreeSitterService.ts b/src/vs/editor/test/common/services/testTreeSitterService.ts index fd54b59ee5c4..e2ca50462ecc 100644 --- a/src/vs/editor/test/common/services/testTreeSitterService.ts +++ b/src/vs/editor/test/common/services/testTreeSitterService.ts @@ -6,17 +6,16 @@ import type { Parser } from '@vscode/tree-sitter-wasm'; import { Event } from '../../../../base/common/event.js'; import { ITextModel } from '../../../common/model.js'; -import { ITreeSitterParserService, ITreeSitterParseResult, ITextModelTreeSitter } from '../../../common/services/treeSitterParserService.js'; -import { Range } from '../../../common/core/range.js'; +import { ITreeSitterParserService, ITreeSitterParseResult, ITextModelTreeSitter, TreeUpdateEvent } from '../../../common/services/treeSitterParserService.js'; export class TestTreeSitterParserService implements ITreeSitterParserService { - getTextModelTreeSitter(textModel: ITextModel): ITextModelTreeSitter | undefined { + async getTextModelTreeSitter(model: ITextModel, parseImmediately?: boolean): Promise { throw new Error('Method not implemented.'); } getTree(content: string, languageId: string): Promise { throw new Error('Method not implemented.'); } - onDidUpdateTree: Event<{ textModel: ITextModel; ranges: Range[] }> = Event.None; + onDidUpdateTree: Event = Event.None; onDidAddLanguage: Event<{ id: string; language: Parser.Language }> = Event.None; _serviceBrand: undefined; getOrInitLanguage(languageId: string): Parser.Language | undefined { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 99ebbfcc9477..23f4e2deb13c 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2903,7 +2903,7 @@ declare namespace monaco.editor { export interface IModelContentChange { /** - * The range that got replaced. + * The old range that got replaced. */ readonly range: IRange; /** @@ -2914,6 +2914,10 @@ declare namespace monaco.editor { * The length of the range that got replaced. */ readonly rangeLength: number; + /** + * The new end position of the range that got replaced. + */ + readonly rangeEndPosition: Position; /** * The new text for the range. */ diff --git a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts index 926b8fdc6d29..1c05433c1170 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts @@ -104,6 +104,7 @@ suite('ExtHostDocumentData', () => { range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, rangeOffset: undefined!, rangeLength: undefined!, + rangeEndPosition: undefined!, text: '\t ' }], eol: undefined!, @@ -163,6 +164,7 @@ suite('ExtHostDocumentData', () => { range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 }, rangeOffset: undefined!, rangeLength: undefined!, + rangeEndPosition: undefined!, text: '' }], eol: undefined!, @@ -183,6 +185,7 @@ suite('ExtHostDocumentData', () => { range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 }, rangeOffset: undefined!, rangeLength: undefined!, + rangeEndPosition: undefined!, text: 'is could be' }], eol: undefined!, @@ -203,6 +206,7 @@ suite('ExtHostDocumentData', () => { range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 }, rangeOffset: undefined!, rangeLength: undefined!, + rangeEndPosition: undefined!, text: 'is could be\na line with number' }], eol: undefined!, @@ -226,6 +230,7 @@ suite('ExtHostDocumentData', () => { range: { startLineNumber: 1, startColumn: 3, endLineNumber: 2, endColumn: 6 }, rangeOffset: undefined!, rangeLength: undefined!, + rangeEndPosition: undefined!, text: '' }], eol: undefined!, @@ -416,6 +421,7 @@ suite('ExtHostDocumentData updates line mapping', () => { range: range, rangeOffset: undefined!, rangeLength: undefined!, + rangeEndPosition: undefined!, text: text }], eol: eol!, diff --git a/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts index 965bb16223ed..b5b3933c529a 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts @@ -294,6 +294,7 @@ suite('ExtHostDocumentSaveParticipant', () => { documents.$acceptModelChanged(resource, { changes: [{ range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, + rangeEndPosition: undefined!, rangeOffset: undefined!, rangeLength: undefined!, text: 'bar' @@ -329,6 +330,7 @@ suite('ExtHostDocumentSaveParticipant', () => { changes: [{ range, text, + rangeEndPosition: undefined!, rangeOffset: undefined!, rangeLength: undefined!, }], diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.contribution.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.contribution.ts index 119455c01c5e..e5ded2ea8ca7 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.contribution.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.contribution.ts @@ -5,9 +5,9 @@ import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; -import { TreeSitterTextModelService } from '../../../../editor/browser/services/treeSitter/treeSitterParserService.js'; +import { TreeSitterTextModelService } from '../../../../editor/common/services/treeSitter/treeSitterParserService.js'; import { ITreeSitterParserService } from '../../../../editor/common/services/treeSitterParserService.js'; -import { ITreeSitterTokenizationFeature } from './treeSitterTokenizationFeature.js'; +import { ITreeSitterTokenizationFeature } from '../common/treeSitterTokenizationFeature.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; import { URI } from '../../../../base/common/uri.js'; @@ -45,7 +45,7 @@ CommandsRegistry.registerCommand('_workbench.colorizeTreeSitterTokens', async (a throw new Error(`Cannot resolve tokenizer for language ${textModel.getLanguageId()}`); } - const textModelTreeSitter = treeSitterParserService.getTextModelTreeSitter(textModel); + const textModelTreeSitter = await treeSitterParserService.getTextModelTreeSitter(textModel); if (!textModelTreeSitter) { throw new Error(`Cannot resolve tree sitter parser for language ${textModel.getLanguageId()}`); } diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts b/src/vs/workbench/services/treeSitter/common/treeSitterTokenizationFeature.ts similarity index 61% rename from src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts rename to src/vs/workbench/services/treeSitter/common/treeSitterTokenizationFeature.ts index 24c4a1698c5c..b48982d364a2 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts +++ b/src/vs/workbench/services/treeSitter/common/treeSitterTokenizationFeature.ts @@ -11,7 +11,6 @@ import { ILanguageIdCodec, ITreeSitterTokenizationSupport, LazyTokenizationSuppo import { ITextModel } from '../../../../editor/common/model.js'; import { EDITOR_EXPERIMENTAL_PREFER_TREESITTER, ITreeSitterParserService, ITreeSitterParseResult } from '../../../../editor/common/services/treeSitterParserService.js'; import { IModelTokensChangedEvent } from '../../../../editor/common/textModelEvents.js'; -import { ColumnRange } from '../../../../editor/contrib/inlineCompletions/browser/utils.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; @@ -20,6 +19,10 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { ColorThemeData, findMetadata } from '../../themes/common/colorThemeData.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; +import { ITreeSitterTokenizationStoreService } from '../../../../editor/common/model/treeSitterTokenStoreService.js'; +import { LanguageId } from '../../../../editor/common/encodedTokenAttributes.js'; +import { TokenUpdate } from '../../../../editor/common/model/tokenStore.js'; +import { Range } from '../../../../editor/common/core/range.js'; const ALLOWED_SUPPORT = ['typescript']; type TreeSitterQueries = string; @@ -30,9 +33,9 @@ export interface ITreeSitterTokenizationFeature { _serviceBrand: undefined; } -class TreeSitterTokenizationFeature extends Disposable implements ITreeSitterTokenizationFeature { +export class TreeSitterTokenizationFeature extends Disposable implements ITreeSitterTokenizationFeature { public _serviceBrand: undefined; - private readonly _tokenizersRegistrations: DisposableMap = new DisposableMap(); + private readonly _tokenizersRegistrations: DisposableMap = this._register(new DisposableMap()); constructor( @ILanguageService private readonly _languageService: ILanguageService, @@ -86,7 +89,7 @@ class TreeSitterTokenizationFeature extends Disposable implements ITreeSitterTok } } -class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTokenizationSupport { +export class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTokenizationSupport { private _query: Parser.Query | undefined; private readonly _onDidChangeTokens: Emitter<{ textModel: ITextModel; changes: IModelTokensChangedEvent }> = new Emitter(); public readonly onDidChangeTokens: Event<{ textModel: ITextModel; changes: IModelTokensChangedEvent }> = this._onDidChangeTokens.event; @@ -99,21 +102,83 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok private readonly _languageIdCodec: ILanguageIdCodec, @ITreeSitterParserService private readonly _treeSitterService: ITreeSitterParserService, @IThemeService private readonly _themeService: IThemeService, + @ITreeSitterTokenizationStoreService private readonly _tokenizationStoreService: ITreeSitterTokenizationStoreService ) { super(); this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => this.reset())); this._register(this._treeSitterService.onDidUpdateTree((e) => { const maxLine = e.textModel.getLineCount(); - this._onDidChangeTokens.fire({ - textModel: e.textModel, - changes: { - semanticTokensApplied: false, - ranges: e.ranges.map(range => ({ fromLineNumber: range.startLineNumber, toLineNumber: range.endLineNumber < maxLine ? range.endLineNumber : maxLine })), + const ranges = e.ranges.map(range => ({ fromLineNumber: range.newRange.startLineNumber, toLineNumber: range.newRange.endLineNumber < maxLine ? range.newRange.endLineNumber : maxLine })); + + // First time we see a tree we need to build a token store. + if (!this._tokenizationStoreService.hasTokens(e.textModel)) { + const lineEndOffset = e.textModel.getValueLength(); + const editorEndPosition = e.textModel.getPositionAt(lineEndOffset); + const tokens = this.getTokensInRange(e.textModel, new Range(1, 1, editorEndPosition.lineNumber, editorEndPosition.column), 0, lineEndOffset); + if (tokens) { + this._tokenizationStoreService.setTokens(e.textModel, tokens); + + this._onDidChangeTokens.fire({ + textModel: e.textModel, + changes: { + semanticTokensApplied: false, + ranges + } + }); } - }); + } else { + // Mark the range for refresh immediately + for (const range of e.ranges) { + this._tokenizationStoreService.markForRefresh(e.textModel, range.newRange); + } + + const captures = e.ranges.map(range => this._getTreeAndCaptures(range.newRange, e.textModel)); + // Don't block + new Promise(resolve => { + const tokenUpdates = e.ranges.map((range, index) => { + const updates = this.getTokensInRange(e.textModel, range.newRange, range.newRangeStartOffset, range.newRangeEndOffset, captures[index]); + if (updates) { + return { oldRangeLength: range.oldRangeLength, newTokens: updates }; + } + return { oldRangeLength: range.oldRangeLength, newTokens: [] }; + }); + this._tokenizationStoreService.updateTokens(e.textModel, e.versionId, tokenUpdates); + this._onDidChangeTokens.fire({ + textModel: e.textModel, + changes: { + semanticTokensApplied: false, + ranges + } + }); + resolve(); + }); + } })); } + private _rangeTokensAsUpdates(rangeOffset: number, lineTokens: { endOffset: number; metadata: number }[]) { + const updates: TokenUpdate[] = []; + let lastEnd = 0; + for (const token of lineTokens) { + if (token.endOffset <= lastEnd) { + continue; + } + updates.push({ startOffsetInclusive: rangeOffset + lastEnd, length: token.endOffset - lastEnd, token: token.metadata }); + lastEnd = token.endOffset; + } + return updates; + } + + public getTokensInRange(textModel: ITextModel, range: Range, rangeStartOffset: number, rangeEndOffset: number, captures?: { tree: ITreeSitterParseResult | undefined; captures: Parser.QueryCapture[] }): TokenUpdate[] | undefined { + const languageId = this._languageIdCodec.encodeLanguageId(this._languageId); + + const tokens = captures ? this._tokenizeCaptures(captures.tree, captures.captures, languageId, rangeStartOffset, rangeEndOffset) : this._tokenize(languageId, range, rangeStartOffset, rangeEndOffset, textModel); + if (tokens?.endOffsetsAndMetadata) { + return this._rangeTokensAsUpdates(rangeStartOffset, tokens.endOffsetsAndMetadata); + } + return undefined; + } + private _getTree(textModel: ITextModel): ITreeSitterParseResult | undefined { return this._treeSitterService.getParseResult(textModel); } @@ -140,23 +205,23 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok captureAtPosition(lineNumber: number, column: number, textModel: ITextModel): Parser.QueryCapture[] { const tree = this._getTree(textModel); - const captures = this._captureAtRange(lineNumber, new ColumnRange(column, column + 1), tree?.tree); + const captures = this._captureAtRange(new Range(lineNumber, column, lineNumber, column + 1), tree?.tree); return captures; } captureAtPositionTree(lineNumber: number, column: number, tree: Parser.Tree): Parser.QueryCapture[] { - const captures = this._captureAtRange(lineNumber, new ColumnRange(column, column + 1), tree); + const captures = this._captureAtRange(new Range(lineNumber, column, lineNumber, column + 1), tree); return captures; } - private _captureAtRange(lineNumber: number, columnRange: ColumnRange, tree: Parser.Tree | undefined): Parser.QueryCapture[] { + private _captureAtRange(range: Range, tree: Parser.Tree | undefined): Parser.QueryCapture[] { const query = this._ensureQuery(); if (!tree || !query) { return []; } // Tree sitter row is 0 based, column is 0 based - return query.captures(tree.rootNode, { startPosition: { row: lineNumber - 1, column: columnRange.startColumn - 1 }, endPosition: { row: lineNumber - 1, column: columnRange.endColumnExclusive - 1 } }); + return query.captures(tree.rootNode, { startPosition: { row: range.startLineNumber - 1, column: range.startColumn - 1 }, endPosition: { row: range.endLineNumber - 1, column: range.endColumn - 1 } }); } /** @@ -174,40 +239,44 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok return this._tokenizeEncoded(lineNumber, textModel); } - private _tokenizeEncoded(lineNumber: number, textModel: ITextModel): { result: Uint32Array; captureTime: number; metadataTime: number } | undefined { - const stopwatch = StopWatch.create(); - const lineLength = textModel.getLineMaxColumn(lineNumber); + private _getTreeAndCaptures(range: Range, textModel: ITextModel): { tree: ITreeSitterParseResult | undefined; captures: Parser.QueryCapture[] } { const tree = this._getTree(textModel); - const captures = this._captureAtRange(lineNumber, new ColumnRange(1, lineLength), tree?.tree); - const encodedLanguageId = this._languageIdCodec.encodeLanguageId(this._languageId); + const captures = this._captureAtRange(range, tree?.tree); + return { tree, captures }; + } + + private _tokenize(encodedLanguageId: LanguageId, range: Range, rangeStartOffset: number, rangeEndOffset: number, textModel: ITextModel): { endOffsetsAndMetadata: { endOffset: number; metadata: number }[]; captureTime: number; metadataTime: number } | undefined { + const { tree, captures } = this._getTreeAndCaptures(range, textModel); + return this._tokenizeCaptures(tree, captures, encodedLanguageId, rangeStartOffset, rangeEndOffset); + } + + private _tokenizeCaptures(tree: ITreeSitterParseResult | undefined, captures: Parser.QueryCapture[], encodedLanguageId: LanguageId, rangeStartOffset: number, rangeEndOffset: number): { endOffsetsAndMetadata: { endOffset: number; metadata: number }[]; captureTime: number; metadataTime: number } | undefined { + const stopwatch = StopWatch.create(); + const rangeLength = rangeEndOffset - rangeStartOffset; if (captures.length === 0) { if (tree) { stopwatch.stop(); - const result = new Uint32Array(2); - result[0] = lineLength; - result[1] = findMetadata(this._colorThemeData, [], encodedLanguageId); - return { result, captureTime: stopwatch.elapsed(), metadataTime: 0 }; + const endOffsetsAndMetadata = [{ endOffset: rangeLength, metadata: findMetadata(this._colorThemeData, [], encodedLanguageId) }]; + return { endOffsetsAndMetadata, captureTime: stopwatch.elapsed(), metadataTime: 0 }; } return undefined; } - const endOffsetsAndScopes: { endOffset: number; scopes: string[] }[] = Array(captures.length); + const endOffsetsAndScopes: { endOffset: number; scopes: string[]; metadata?: number }[] = Array(captures.length); endOffsetsAndScopes.fill({ endOffset: 0, scopes: [] }); let tokenIndex = 0; - const lineStartOffset = textModel.getOffsetAt({ lineNumber: lineNumber, column: 1 }); const increaseSizeOfTokensByOneToken = () => { endOffsetsAndScopes.push({ endOffset: 0, scopes: [] }); }; - for (let captureIndex = 0; captureIndex < captures.length; captureIndex++) { const capture = captures[captureIndex]; - const tokenEndIndex = capture.node.endIndex < lineStartOffset + lineLength ? capture.node.endIndex : lineStartOffset + lineLength; - const tokenStartIndex = capture.node.startIndex < lineStartOffset ? lineStartOffset : capture.node.startIndex; + const tokenEndIndex = capture.node.endIndex < rangeStartOffset + rangeLength ? capture.node.endIndex : rangeStartOffset + rangeLength; + const tokenStartIndex = capture.node.startIndex < rangeStartOffset ? rangeStartOffset : capture.node.startIndex; - const lineRelativeOffset = tokenEndIndex - lineStartOffset; + const lineRelativeOffset = tokenEndIndex - rangeStartOffset; // Not every character will get captured, so we need to make sure that our current capture doesn't bleed toward the start of the line and cover characters that it doesn't apply to. // We do this by creating a new token in the array if the previous token ends before the current token starts. let previousTokenEnd: number; @@ -215,7 +284,7 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok if (captureIndex > 0) { previousTokenEnd = endOffsetsAndScopes[(tokenIndex - 1)].endOffset; } else { - previousTokenEnd = tokenStartIndex - lineStartOffset - 1; + previousTokenEnd = tokenStartIndex - rangeStartOffset - 1; } const intermediateTokenOffset = lineRelativeOffset - currentTokenLength; if ((previousTokenEnd >= 0) && (previousTokenEnd < intermediateTokenOffset)) { @@ -270,25 +339,49 @@ class TreeSitterTokenizationSupport extends Disposable implements ITreeSitterTok } // Account for uncaptured characters at the end of the line - if (endOffsetsAndScopes[tokenIndex - 1].endOffset < lineLength - 1) { - increaseSizeOfTokensByOneToken(); - endOffsetsAndScopes[tokenIndex] = { endOffset: lineLength - 1, scopes: endOffsetsAndScopes[tokenIndex].scopes }; - tokenIndex++; + if ((endOffsetsAndScopes[tokenIndex - 1].endOffset < rangeLength)) { + if (rangeLength - endOffsetsAndScopes[tokenIndex - 1].endOffset > 0) { + increaseSizeOfTokensByOneToken(); + endOffsetsAndScopes[tokenIndex] = { endOffset: rangeLength, scopes: endOffsetsAndScopes[tokenIndex].scopes }; + tokenIndex++; + } } const captureTime = stopwatch.elapsed(); stopwatch.reset(); - - const tokens: Uint32Array = new Uint32Array((tokenIndex) * 2); - for (let i = 0; i < tokenIndex; i++) { + for (let i = 0; i < endOffsetsAndScopes.length; i++) { const token = endOffsetsAndScopes[i]; - if (token.endOffset === 0 && token.scopes.length === 0) { + if (token.endOffset === 0 && token.scopes.length === 0 && i !== 0) { + endOffsetsAndScopes.splice(i, endOffsetsAndScopes.length - i); break; } - tokens[i * 2] = token.endOffset; - tokens[i * 2 + 1] = findMetadata(this._colorThemeData, token.scopes, encodedLanguageId); + token.metadata = findMetadata(this._colorThemeData, token.scopes, encodedLanguageId); } + const metadataTime = stopwatch.elapsed(); - return { result: tokens, captureTime, metadataTime }; + return { endOffsetsAndMetadata: endOffsetsAndScopes as { endOffset: number; scopes: string[]; metadata: number }[], captureTime, metadataTime }; + } + + private _tokenizeEncoded(lineNumber: number, textModel: ITextModel): { result: Uint32Array; captureTime: number; metadataTime: number } | undefined { + const encodedLanguageId = this._languageIdCodec.encodeLanguageId(this._languageId); + const lineOffset = textModel.getOffsetAt({ lineNumber: lineNumber, column: 1 }); + const maxLine = textModel.getLineCount(); + const lineEndOffset = (lineNumber + 1 <= maxLine) ? textModel.getOffsetAt({ lineNumber: lineNumber + 1, column: 1 }) : textModel.getValueLength(); + const lineLength = lineEndOffset - lineOffset; + + const result = this._tokenize(encodedLanguageId, new Range(lineNumber, 1, lineNumber, lineLength), lineOffset, lineEndOffset, textModel); + if (!result) { + return undefined; + } + + const tokens: Uint32Array = new Uint32Array((result.endOffsetsAndMetadata.length) * 2); + + for (let i = 0; i < result.endOffsetsAndMetadata.length; i++) { + const token = result.endOffsetsAndMetadata[i]; + tokens[i * 2] = token.endOffset; + tokens[i * 2 + 1] = token.metadata; + } + + return { result: tokens, captureTime: result.captureTime, metadataTime: result.metadataTime }; } override dispose() { diff --git a/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts b/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts new file mode 100644 index 000000000000..61a9cf9944ec --- /dev/null +++ b/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts @@ -0,0 +1,371 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TestInstantiationService } from '../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js'; +import { TreeSitterTextModelService } from '../../../editor/common/services/treeSitter/treeSitterParserService.js'; +import { IModelService } from '../../../editor/common/services/model.js'; +import { Event } from '../../../base/common/event.js'; +import { URI } from '../../../base/common/uri.js'; +import { IFileService } from '../../../platform/files/common/files.js'; +import { ILogService, NullLogService } from '../../../platform/log/common/log.js'; +import { ITelemetryData, ITelemetryService, TelemetryLevel } from '../../../platform/telemetry/common/telemetry.js'; +import { ClassifiedEvent, OmitMetadata, IGDPRProperty, StrictPropertyCheck } from '../../../platform/telemetry/common/gdprTypings.js'; +import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../platform/configuration/test/common/testConfigurationService.js'; +import { IEnvironmentService } from '../../../platform/environment/common/environment.js'; +import { ModelService } from '../../../editor/common/services/modelService.js'; +import { TreeSitterTokenizationFeature, TreeSitterTokenizationSupport } from '../../services/treeSitter/common/treeSitterTokenizationFeature.js'; +import { ITreeSitterParserService, TreeUpdateEvent } from '../../../editor/common/services/treeSitterParserService.js'; +import { TreeSitterTokenizationRegistry } from '../../../editor/common/languages.js'; +import { FileService } from '../../../platform/files/common/fileService.js'; +import { Schemas } from '../../../base/common/network.js'; +import { DiskFileSystemProvider } from '../../../platform/files/node/diskFileSystemProvider.js'; +import { ILanguageService } from '../../../editor/common/languages/language.js'; +import { LanguageService } from '../../../editor/common/services/languageService.js'; +import { TestColorTheme, TestThemeService } from '../../../platform/theme/test/common/testThemeService.js'; +import { IThemeService } from '../../../platform/theme/common/themeService.js'; +import { ITextResourcePropertiesService } from '../../../editor/common/services/textResourceConfiguration.js'; +import { TestTextResourcePropertiesService } from '../common/workbenchTestServices.js'; +import { TestLanguageConfigurationService } from '../../../editor/test/common/modes/testLanguageConfigurationService.js'; +import { ILanguageConfigurationService } from '../../../editor/common/languages/languageConfigurationRegistry.js'; +import { IUndoRedoService } from '../../../platform/undoRedo/common/undoRedo.js'; +import { UndoRedoService } from '../../../platform/undoRedo/common/undoRedoService.js'; +import { TestDialogService } from '../../../platform/dialogs/test/common/testDialogService.js'; +import { TestNotificationService } from '../../../platform/notification/test/common/testNotificationService.js'; +import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; +import { ProbeScope, TokenStyle } from '../../../platform/theme/common/tokenClassificationRegistry.js'; +import { TextMateThemingRuleDefinitions } from '../../services/themes/common/colorThemeData.js'; +import { Color } from '../../../base/common/color.js'; +import { ITreeSitterTokenizationStoreService } from '../../../editor/common/model/treeSitterTokenStoreService.js'; +import { Range } from '../../../editor/common/core/range.js'; +import { ITextModel } from '../../../editor/common/model.js'; +import { TokenUpdate } from '../../../editor/common/model/tokenStore.js'; + +class MockTelemetryService implements ITelemetryService { + _serviceBrand: undefined; + telemetryLevel: TelemetryLevel = TelemetryLevel.NONE; + sessionId: string = ''; + machineId: string = ''; + sqmId: string = ''; + devDeviceId: string = ''; + firstSessionDate: string = ''; + sendErrorTelemetry: boolean = false; + publicLog(eventName: string, data?: ITelemetryData): void { + } + publicLog2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck): void { + } + publicLogError(errorEventName: string, data?: ITelemetryData): void { + } + publicLogError2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck): void { + } + setExperimentProperty(name: string, value: string): void { + } +} + +class MockTokenStoreService implements ITreeSitterTokenizationStoreService { + _serviceBrand: undefined; + setTokens(model: ITextModel, tokens: TokenUpdate[]): void { + } + getTokens(model: ITextModel, line: number): Uint32Array | undefined { + return undefined; + } + updateTokens(model: ITextModel, version: number, updates: { oldRangeLength: number; newTokens: TokenUpdate[] }[]): void { + } + markForRefresh(model: ITextModel, range: Range): void { + } + hasTokens(model: ITextModel, accurateForRange?: Range): boolean { + return true; + } + +} + +class TestTreeSitterColorTheme extends TestColorTheme { + public resolveScopes(scopes: ProbeScope[], definitions?: TextMateThemingRuleDefinitions): TokenStyle | undefined { + return new TokenStyle(Color.red, undefined, undefined, undefined, undefined); + } + public getTokenColorIndex(): { get: () => number } { + return { get: () => 10 }; + } +} + +suite('Tree Sitter TokenizationFeature', function () { + + let instantiationService: TestInstantiationService; + let modelService: IModelService; + let fileService: IFileService; + let textResourcePropertiesService: ITextResourcePropertiesService; + let languageConfigurationService: ILanguageConfigurationService; + const telemetryService: ITelemetryService = new MockTelemetryService(); + const logService: ILogService = new NullLogService(); + const configurationService: IConfigurationService = new TestConfigurationService({ 'editor.experimental.preferTreeSitter': ['typescript'] }); + const themeService: IThemeService = new TestThemeService(new TestTreeSitterColorTheme()); + let languageService: ILanguageService; + const environmentService: IEnvironmentService = {} as IEnvironmentService; + const tokenStoreService: ITreeSitterTokenizationStoreService = new MockTokenStoreService(); + let treeSitterParserService: TreeSitterTextModelService; + let treeSitterTokenizationSupport: TreeSitterTokenizationSupport; + + let disposables: DisposableStore; + + setup(async () => { + disposables = new DisposableStore(); + instantiationService = disposables.add(new TestInstantiationService()); + instantiationService.set(IEnvironmentService, environmentService); + instantiationService.set(IConfigurationService, configurationService); + instantiationService.set(ILogService, logService); + instantiationService.set(ITelemetryService, telemetryService); + instantiationService.set(ITreeSitterTokenizationStoreService, tokenStoreService); + languageService = disposables.add(instantiationService.createInstance(LanguageService)); + instantiationService.set(ILanguageService, languageService); + instantiationService.set(IThemeService, themeService); + textResourcePropertiesService = instantiationService.createInstance(TestTextResourcePropertiesService); + instantiationService.set(ITextResourcePropertiesService, textResourcePropertiesService); + languageConfigurationService = disposables.add(instantiationService.createInstance(TestLanguageConfigurationService)); + instantiationService.set(ILanguageConfigurationService, languageConfigurationService); + + fileService = disposables.add(instantiationService.createInstance(FileService)); + const diskFileSystemProvider = disposables.add(new DiskFileSystemProvider(logService)); + disposables.add(fileService.registerProvider(Schemas.file, diskFileSystemProvider)); + + instantiationService.set(IFileService, fileService); + + const dialogService = new TestDialogService(); + const notificationService = new TestNotificationService(); + const undoRedoService = new UndoRedoService(dialogService, notificationService); + instantiationService.set(IUndoRedoService, undoRedoService); + modelService = new ModelService( + configurationService, + textResourcePropertiesService, + undoRedoService, + instantiationService + ); + instantiationService.set(IModelService, modelService); + treeSitterParserService = disposables.add(instantiationService.createInstance(TreeSitterTextModelService)); + treeSitterParserService.isTest = true; + instantiationService.set(ITreeSitterParserService, treeSitterParserService); + disposables.add(instantiationService.createInstance(TreeSitterTokenizationFeature)); + treeSitterTokenizationSupport = disposables.add(await TreeSitterTokenizationRegistry.getOrCreate('typescript') as TreeSitterTokenizationSupport); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + function tokensContentSize(tokens: TokenUpdate[]) { + return tokens[tokens.length - 1].startOffsetInclusive + tokens[tokens.length - 1].length; + } + + async function getModelAndPrepTree(content: string) { + const model = disposables.add(modelService.createModel(content, { languageId: 'typescript', onDidChange: Event.None }, URI.file('file.ts'))); + const tree = disposables.add(await treeSitterParserService.getTextModelTreeSitter(model)); + const treeParseResult = new Promise(resolve => { + const disposable = treeSitterParserService.onDidUpdateTree(e => { + if (e.textModel === model) { + disposable.dispose(); + resolve(); + } + }); + }); + await tree.parse(); + await treeParseResult; + + assert.ok(tree); + return model; + } + + function verifyTokens(tokens: TokenUpdate[] | undefined) { + assert.ok(tokens); + for (let i = 1; i < tokens.length; i++) { + const previousToken: TokenUpdate = tokens[i - 1]; + const token: TokenUpdate = tokens[i]; + assert.deepStrictEqual(previousToken.startOffsetInclusive + previousToken.length, token.startOffsetInclusive); + } + } + + test('File single line file', async () => { + const content = `console.log('x');`; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 1, 18), 0, 17); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 7); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with new lines at beginning and end', async () => { + const content = ` +console.log('x'); +`; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 3, 1), 0, 19); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 9); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with new lines at beginning and end \\r\\n', async () => { + const content = '\r\nconsole.log(\'x\');\r\n'; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 3, 1), 0, 21); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 9); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with empty lines in the middle', async () => { + const content = ` +console.log('x'); + +console.log('7'); +`; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 5, 1), 0, 38); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 17); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with empty lines in the middle \\r\\n', async () => { + const content = '\r\nconsole.log(\'x\');\r\n\r\nconsole.log(\'7\');\r\n'; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 5, 1), 0, 42); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 17); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with non-empty lines that match no scopes', async () => { + const content = `console.log('x'); +; +{ +} +`; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 5, 1), 0, 24); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 10); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with non-empty lines that match no scopes \\r\\n', async () => { + const content = 'console.log(\'x\');\r\n;\r\n{\r\n}\r\n'; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 5, 1), 0, 28); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 10); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with tree-sitter token that spans multiple lines', async () => { + const content = `/** +**/ + +console.log('x'); + +`; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 6, 1), 0, 28); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 10); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with tree-sitter token that spans multiple lines \\r\\n', async () => { + const content = '/**\r\n**/\r\n\r\nconsole.log(\'x\');\r\n\r\n'; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 6, 1), 0, 33); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 10); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with tabs', async () => { + const content = `function x() { + return true; +} + +class Y { + private z = false; +}`; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 7, 1), 0, 63); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 22); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('File with tabs \\r\\n', async () => { + const content = 'function x() {\r\n\treturn true;\r\n}\r\n\r\nclass Y {\r\n\tprivate z = false;\r\n}'; + const model = await getModelAndPrepTree(content); + const tokens = treeSitterTokenizationSupport.getTokensInRange(model, new Range(1, 1, 7, 1), 0, 69); + verifyTokens(tokens); + assert.deepStrictEqual(tokens?.length, 22); + assert.deepStrictEqual(tokensContentSize(tokens), content.length); + modelService.destroyModel(model.uri); + }); + + test('Three changes come back to back ', async () => { + const content = `/** +**/ +class x { +} + + + + +class y { +}`; + const model = await getModelAndPrepTree(content); + + let updateListener: IDisposable | undefined; + let change: TreeUpdateEvent | undefined; + + const updatePromise = new Promise(resolve => { + updateListener = treeSitterParserService.onDidUpdateTree(async e => { + change = e; + resolve(); + }); + }); + + const edit1 = new Promise(resolve => { + model.applyEdits([{ range: new Range(7, 1, 8, 1), text: '' }]); + resolve(); + }); + const edit2 = new Promise(resolve => { + model.applyEdits([{ range: new Range(6, 1, 7, 1), text: '' }]); + resolve(); + }); + const edit3 = new Promise(resolve => { + model.applyEdits([{ range: new Range(5, 1, 6, 1), text: '' }]); + resolve(); + }); + Promise.all([edit1, edit2, edit3]); + await updatePromise; + assert.ok(change); + + assert.strictEqual(change.versionId, 4); + assert.strictEqual(change.ranges[0].newRangeStartOffset, 7); + assert.strictEqual(change.ranges[0].newRangeEndOffset, 26); + assert.strictEqual(change.ranges[0].newRange.startLineNumber, 2); + assert.strictEqual(change.ranges[0].newRange.endLineNumber, 6); + assert.strictEqual(change.ranges[0].oldRangeLength, 22); + + updateListener?.dispose(); + modelService.destroyModel(model.uri); + }); +}); From 15006ee21fcb7b346cff59293d3c5165ad538f63 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:14:57 +0100 Subject: [PATCH 0715/3587] Git - when doing a git rename close the old file and open the new one (#238368) --- extensions/git/src/commands.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 01b9ef29fea6..abb00af22bab 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1341,6 +1341,10 @@ export class CommandCenter { } await repository.move(from, to); + + // Close active editor and open the renamed file + await commands.executeCommand('workbench.action.closeActiveEditor'); + await commands.executeCommand('vscode.open', Uri.file(path.join(repository.root, to)), { viewColumn: ViewColumn.Active }); } @command('git.stage') From 4f81afa0e8cd7f11debf8ac0d7e3305c95af5478 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 21 Jan 2025 07:45:32 -0800 Subject: [PATCH 0716/3587] Extract common parts into base render strategy class Part of #227108 --- .../gpu/renderStrategy/baseRenderStrategy.ts | 41 +++++++++++++ .../fullFileRenderStrategy.ts | 59 ++++++++----------- .../fullFileRenderStrategy.wgsl.ts | 6 +- .../viewParts/viewLinesGpu/viewLinesGpu.ts | 2 +- 4 files changed, 69 insertions(+), 39 deletions(-) create mode 100644 src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts rename src/vs/editor/browser/gpu/{ => renderStrategy}/fullFileRenderStrategy.ts (90%) rename src/vs/editor/browser/gpu/{ => renderStrategy}/fullFileRenderStrategy.wgsl.ts (94%) diff --git a/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts new file mode 100644 index 000000000000..7f068ba19de7 --- /dev/null +++ b/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts @@ -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 { MandatoryMutableDisposable } from '../../../../base/common/lifecycle.js'; +import { EditorOption } from '../../../common/config/editorOptions.js'; +import { ViewEventHandler } from '../../../common/viewEventHandler.js'; +import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; +import type { ViewContext } from '../../../common/viewModel/viewContext.js'; +import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions.js'; +import type { IGpuRenderStrategy } from '../gpu.js'; +import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; +import type { ViewGpuContext } from '../viewGpuContext.js'; + +export abstract class BaseRenderStrategy extends ViewEventHandler implements IGpuRenderStrategy { + protected readonly _glyphRasterizer: MandatoryMutableDisposable; + get glyphRasterizer() { return this._glyphRasterizer.value; } + + abstract wgsl: string; + abstract bindGroupEntries: GPUBindGroupEntry[]; + + constructor( + protected readonly _context: ViewContext, + protected readonly _viewGpuContext: ViewGpuContext, + protected readonly _device: GPUDevice, + ) { + super(); + + this._context.addEventHandler(this); + + const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); + const fontSize = this._context.configuration.options.get(EditorOption.fontSize); + + this._glyphRasterizer = this._register(new MandatoryMutableDisposable(new GlyphRasterizer(fontSize, fontFamily, this._viewGpuContext.devicePixelRatio.get()))); + } + + abstract reset(): void; + abstract update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number; + abstract draw?(pass: GPURenderPassEncoder, viewportData: ViewportData): void; +} diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts similarity index 90% rename from src/vs/editor/browser/gpu/fullFileRenderStrategy.ts rename to src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index d0c86d3da873..ba0238b9fd2f 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -3,27 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveWindow } from '../../../base/browser/dom.js'; -import { BugIndicatingError } from '../../../base/common/errors.js'; -import { MandatoryMutableDisposable } from '../../../base/common/lifecycle.js'; -import { EditorOption } from '../../common/config/editorOptions.js'; -import { CursorColumns } from '../../common/core/cursorColumns.js'; -import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; -import { ViewEventHandler } from '../../common/viewEventHandler.js'; -import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLineMappingChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js'; -import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; -import type { InlineDecoration, ViewLineRenderingData } from '../../common/viewModel.js'; -import type { ViewContext } from '../../common/viewModel/viewContext.js'; -import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js'; -import type { ITextureAtlasPageGlyph } from './atlas/atlas.js'; +import { getActiveWindow } from '../../../../base/browser/dom.js'; +import { Color } from '../../../../base/common/color.js'; +import { BugIndicatingError } from '../../../../base/common/errors.js'; +import { EditorOption } from '../../../common/config/editorOptions.js'; +import { CursorColumns } from '../../../common/core/cursorColumns.js'; +import type { IViewLineTokens } from '../../../common/tokens/lineTokens.js'; +import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLineMappingChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../../common/viewEvents.js'; +import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; +import type { InlineDecoration, ViewLineRenderingData } from '../../../common/viewModel.js'; +import type { ViewContext } from '../../../common/viewModel/viewContext.js'; +import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions.js'; +import type { ITextureAtlasPageGlyph } from '../atlas/atlas.js'; +import { createContentSegmenter, type IContentSegmenter } from '../contentSegmenter.js'; import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js'; -import { BindingId, type IGpuRenderStrategy } from './gpu.js'; -import { GPULifecycle } from './gpuDisposable.js'; -import { quadVertices } from './gpuUtils.js'; -import { GlyphRasterizer } from './raster/glyphRasterizer.js'; -import { ViewGpuContext } from './viewGpuContext.js'; -import { Color } from '../../../base/common/color.js'; -import { createContentSegmenter, type IContentSegmenter } from './contentSegmenter.js'; +import { BindingId } from '../gpu.js'; +import { GPULifecycle } from '../gpuDisposable.js'; +import { quadVertices } from '../gpuUtils.js'; +import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; +import { ViewGpuContext } from '../viewGpuContext.js'; +import { BaseRenderStrategy } from './baseRenderStrategy.js'; const enum Constants { IndicesPerCell = 6, @@ -47,13 +46,10 @@ type QueuedBufferEvent = ( ViewZonesChangedEvent ); -export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRenderStrategy { +export class FullFileRenderStrategy extends BaseRenderStrategy { readonly wgsl: string = fullFileRenderStrategyWgsl; - private readonly _glyphRasterizer: MandatoryMutableDisposable; - get glyphRasterizer() { return this._glyphRasterizer.value; } - private _cellBindBuffer!: GPUBuffer; /** @@ -81,18 +77,11 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } constructor( - private readonly _context: ViewContext, - private readonly _viewGpuContext: ViewGpuContext, - private readonly _device: GPUDevice, + context: ViewContext, + viewGpuContext: ViewGpuContext, + device: GPUDevice, ) { - super(); - - this._context.addEventHandler(this); - - const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); - const fontSize = this._context.configuration.options.get(EditorOption.fontSize); - - this._glyphRasterizer = this._register(new MandatoryMutableDisposable(new GlyphRasterizer(fontSize, fontFamily, this._viewGpuContext.devicePixelRatio.get()))); + super(context, viewGpuContext, device); const bufferSize = this._viewGpuContext.maxGpuLines * this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.wgsl.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.wgsl.ts similarity index 94% rename from src/vs/editor/browser/gpu/fullFileRenderStrategy.wgsl.ts rename to src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.wgsl.ts index 7aab399457b3..0e105241f11f 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.wgsl.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.wgsl.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TextureAtlas } from './atlas/textureAtlas.js'; -import { TextureAtlasPage } from './atlas/textureAtlasPage.js'; -import { BindingId } from './gpu.js'; +import { TextureAtlas } from '../atlas/textureAtlas.js'; +import { TextureAtlasPage } from '../atlas/textureAtlasPage.js'; +import { BindingId } from '../gpu.js'; export const fullFileRenderStrategyWgsl = /*wgsl*/ ` struct GlyphInfo { diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index 196c1f45dd3c..113904c6a005 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -14,7 +14,7 @@ import { Range } from '../../../common/core/range.js'; import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; import type { ViewContext } from '../../../common/viewModel/viewContext.js'; import { TextureAtlasPage } from '../../gpu/atlas/textureAtlasPage.js'; -import { FullFileRenderStrategy } from '../../gpu/fullFileRenderStrategy.js'; +import { FullFileRenderStrategy } from '../../gpu/renderStrategy/fullFileRenderStrategy.js'; import { BindingId, type IGpuRenderStrategy } from '../../gpu/gpu.js'; import { GPULifecycle } from '../../gpu/gpuDisposable.js'; import { quadVertices } from '../../gpu/gpuUtils.js'; From 943efdf6cea8ff31abab24e7bc87f29cf2f04870 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:08:32 +0100 Subject: [PATCH 0717/3587] Support reusing active inline edits and deletions (#238372) * Support reusing active inline edits * support also deletions --- .../browser/model/inlineCompletionsSource.ts | 53 ++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index cea455d54158..f585567164c1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -152,13 +152,20 @@ export class InlineCompletionsSource extends Disposable { return false; } + // Reuse Inline Edit if possible + if (activeInlineCompletion && activeInlineCompletion.isInlineEdit && activeInlineCompletion.canBeReused(this._textModel, position)) { + updatedCompletions.dispose(); + return false; + } + const endTime = new Date(); this._debounceValue.update(this._textModel, endTime.getTime() - startTime.getTime()); + // Reuse Inline Completion if possible const completions = new UpToDateInlineCompletions(updatedCompletions, request, this._textModel, this._versionId, this._invalidationDelay); - if (activeInlineCompletion) { + if (activeInlineCompletion && !activeInlineCompletion.isInlineEdit && activeInlineCompletion.canBeReused(this._textModel, position)) { const asInlineCompletion = activeInlineCompletion.toInlineCompletion(undefined); - if (activeInlineCompletion.canBeReused(this._textModel, position) && !updatedCompletions.has(asInlineCompletion)) { + if (!updatedCompletions.has(asInlineCompletion)) { completions.prepend(activeInlineCompletion.inlineCompletion, asInlineCompletion.range, true); } } @@ -347,6 +354,7 @@ export class InlineCompletionWithUpdatedRange { public get source() { return this.inlineCompletion.source; } public get sourceInlineCompletion() { return this.inlineCompletion.sourceInlineCompletion; } + public get isInlineEdit() { return this.inlineCompletion.sourceInlineCompletion.isInlineEdit; } private _invalidationTime: number | undefined = Date.now() + this._invalidationDelay.get(); @@ -396,19 +404,21 @@ export class InlineCompletionWithUpdatedRange { } const editUpdates = offsetEdit.edits.map(edit => acceptTextModelChange(edit, e.changes)); + const newEdits = editUpdates.filter(({ changeType }) => changeType !== 'fullyAccepted').map(({ edit }) => edit); - const emptyEdit = editUpdates.find(editUpdate => editUpdate.edit.isEmpty); - if (emptyEdit) { - // A change collided with one of our edits, so we will have to drop the completion - this._inlineEdit.set(new OffsetEdit([emptyEdit.edit]), tx); + const emptyEdit = newEdits.find(edit => edit.isEmpty); + if (emptyEdit || newEdits.length === 0) { + // Either a change collided with one of our edits, so we will have to drop the completion + // Or the completion has been typed by the user + this._inlineEdit.set(new OffsetEdit([emptyEdit ?? new SingleOffsetEdit(new OffsetRange(0, 0), '')]), tx); return; } - const partOfInlineEdit = editUpdates.some(({ changePartOfEdit }) => changePartOfEdit); - if (partOfInlineEdit) { + const changePartiallyAcceptsEdit = editUpdates.some(({ changeType }) => changeType === 'partiallyAccepted' || changeType === 'fullyAccepted'); + + if (changePartiallyAcceptsEdit) { this._invalidationTime = undefined; } - if (this._invalidationTime && this._invalidationTime < Date.now()) { // The completion has been shown for a while and the user // has been working on a different part of the document, so invalidate it @@ -416,31 +426,38 @@ export class InlineCompletionWithUpdatedRange { return; } - this._lastChangePartOfInlineEdit = partOfInlineEdit; - this._inlineEdit.set(new OffsetEdit(editUpdates.map(({ edit }) => edit)), tx); + this._lastChangePartOfInlineEdit = changePartiallyAcceptsEdit; + this._inlineEdit.set(new OffsetEdit(newEdits), tx); - function acceptTextModelChange(edit: SingleOffsetEdit, changes: readonly IModelContentChange[]): { edit: SingleOffsetEdit; changePartOfEdit: boolean } { + function acceptTextModelChange(edit: SingleOffsetEdit, changes: readonly IModelContentChange[]): { edit: SingleOffsetEdit; changeType: 'move' | 'partiallyAccepted' | 'fullyAccepted' } { let start = edit.replaceRange.start; let end = edit.replaceRange.endExclusive; let newText = edit.newText; - let changePartOfEdit = false; + let changeType: 'move' | 'partiallyAccepted' | 'fullyAccepted' = 'move'; for (let i = changes.length - 1; i >= 0; i--) { const change = changes[i]; - // user inserted text at the start of the completion + // Edit is an insertion: user inserted text at the start of the completion if (edit.replaceRange.isEmpty && change.rangeLength === 0 && change.rangeOffset === start && newText.startsWith(change.text)) { start += change.text.length; end = Math.max(start, end); newText = newText.substring(change.text.length); - changePartOfEdit = true; + changeType = newText.length === 0 ? 'fullyAccepted' : 'partiallyAccepted'; + continue; + } + + // Edit is a deletion: user deleted text inside the deletion range + if (!edit.replaceRange.isEmpty && change.text.length === 0 && change.rangeOffset >= start && change.rangeOffset + change.rangeLength <= end) { + end -= change.rangeLength; + changeType = start === end ? 'fullyAccepted' : 'partiallyAccepted'; continue; } - if (change.rangeOffset >= end) { + if (change.rangeOffset > end) { // the change happens after the completion range continue; } - if (change.rangeOffset + change.rangeLength <= start) { + if (change.rangeOffset + change.rangeLength < start) { // the change happens before the completion range start += change.text.length - change.rangeLength; end += change.text.length - change.rangeLength; @@ -452,7 +469,7 @@ export class InlineCompletionWithUpdatedRange { end = change.rangeOffset; newText = ''; } - return { edit: new SingleOffsetEdit(new OffsetRange(start, end), newText), changePartOfEdit }; + return { edit: new SingleOffsetEdit(new OffsetRange(start, end), newText), changeType }; } } From 0111f5de7fc7a97590344a79221caa26b5db879a Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:22:21 +0100 Subject: [PATCH 0718/3587] Remove word replacement and insertion settings (#238376) * remove word replacement/insertion setting * :lipstick: --- src/vs/editor/common/config/editorOptions.ts | 18 ----------------- .../browser/view/inlineEdits/view.ts | 20 +++++-------------- src/vs/monaco.d.ts | 2 -- 3 files changed, 5 insertions(+), 35 deletions(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index d13bae1abbca..60057bc930e4 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -4198,8 +4198,6 @@ export interface IInlineSuggestOptions { enabled?: boolean; useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; - useWordInsertionView?: 'never' | 'whenPossible'; - useWordReplacementView?: 'never' | 'whenPossible'; useGutterIndicator?: boolean; }; @@ -4233,8 +4231,6 @@ class InlineEditorSuggest extends BaseEditorOption s.edits.experimental.useMixedLinesDiff); private readonly _useInterleavedLinesDiff = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useInterleavedLinesDiff); - private readonly _useWordReplacementView = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useWordReplacementView); - private readonly _useWordInsertionView = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useWordInsertionView); private _previousView: { id: string; view: ReturnType; userJumpedToIt: boolean } | undefined; @@ -221,19 +219,11 @@ export class InlineEditsView extends Disposable { } if (diff.length === 1 && diff[0].original.length === 1 && diff[0].modified.length === 1) { - const canUseWordReplacementView = inner.every(m => ( - m.originalRange.isEmpty() && this._useWordInsertionView.read(reader) === 'whenPossible' || - !m.originalRange.isEmpty() && this._useWordReplacementView.read(reader) === 'whenPossible' - )); - - if (canUseWordReplacementView) { - const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); - - if (allInnerChangesNotTooLong && isSingleInnerEdit) { - return 'wordReplacements'; - } else { - return 'lineReplacement'; - } + const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); + if (allInnerChangesNotTooLong && isSingleInnerEdit) { + return 'wordReplacements'; + } else { + return 'lineReplacement'; } } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 23f4e2deb13c..7c6046b7f15e 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4613,8 +4613,6 @@ declare namespace monaco.editor { enabled?: boolean; useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; - useWordInsertionView?: 'never' | 'whenPossible'; - useWordReplacementView?: 'never' | 'whenPossible'; useGutterIndicator?: boolean; }; }; From 488421d693367cb2a062fc7154eaa95298ae50ae Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 21 Jan 2025 11:54:28 -0600 Subject: [PATCH 0719/3587] check for udf elt (#238384) fix #238383 --- .../suggest/browser/terminal.suggest.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index a43271a5e609..15175d7ceacf 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -158,7 +158,7 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo this.add(dom.addDisposableListener(this._ctx.instance.domElement, dom.EventType.FOCUS_OUT, (e) => { const focusedElement = e.relatedTarget as HTMLElement; - if (focusedElement.className === SuggestDetailsClassName) { + if (focusedElement?.className === SuggestDetailsClassName) { // Don't hide the suggest widget if the focus is moving to the details return; } From 1c8f3d1b48475a3fc19ef74aa44b1355938bb35c Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 21 Jan 2025 19:17:59 +0100 Subject: [PATCH 0720/3587] consolidate workbench feature logs under window (#238387) --- .../common/editSessionsLogService.ts | 3 +- .../services/notebookLoggingServiceImpl.ts | 3 +- .../views/browser/viewDescriptorService.ts | 3 +- .../views/common/viewContainerModel.ts | 28 +++---------------- 4 files changed, 10 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/contrib/editSessions/common/editSessionsLogService.ts b/src/vs/workbench/contrib/editSessions/common/editSessionsLogService.ts index 4bdae2d1cf40..08ff7d57671b 100644 --- a/src/vs/workbench/contrib/editSessions/common/editSessionsLogService.ts +++ b/src/vs/workbench/contrib/editSessions/common/editSessionsLogService.ts @@ -7,6 +7,7 @@ import { joinPath } from '../../../../base/common/resources.js'; import { localize } from '../../../../nls.js'; import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; import { AbstractLogger, ILogger, ILoggerService } from '../../../../platform/log/common/log.js'; +import { windowLogGroup } from '../../../services/log/common/logConstants.js'; import { IEditSessionsLogService, editSessionsLogId } from './editSessions.js'; export class EditSessionsLogService extends AbstractLogger implements IEditSessionsLogService { @@ -19,7 +20,7 @@ export class EditSessionsLogService extends AbstractLogger implements IEditSessi @IEnvironmentService environmentService: IEnvironmentService ) { super(); - this.logger = this._register(loggerService.createLogger(joinPath(environmentService.logsHome, `${editSessionsLogId}.log`), { id: editSessionsLogId, name: localize('cloudChangesLog', "Cloud Changes") })); + this.logger = this._register(loggerService.createLogger(joinPath(environmentService.logsHome, `${editSessionsLogId}.log`), { id: editSessionsLogId, name: localize('cloudChangesLog', "Cloud Changes"), group: windowLogGroup })); } trace(message: string, ...args: any[]): void { diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl.ts index 37aa5e99befc..93e248e3c745 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl.ts @@ -7,6 +7,7 @@ import * as nls from '../../../../../nls.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { INotebookLoggingService } from '../../common/notebookLoggingService.js'; import { ILogger, ILoggerService } from '../../../../../platform/log/common/log.js'; +import { windowLogGroup } from '../../../../services/log/common/logConstants.js'; const logChannelId = 'notebook.rendering'; @@ -20,7 +21,7 @@ export class NotebookLoggingService extends Disposable implements INotebookLoggi @ILoggerService loggerService: ILoggerService, ) { super(); - this._logger = this._register(loggerService.createLogger(logChannelId, { name: nls.localize('renderChannelName', "Notebook") })); + this._logger = this._register(loggerService.createLogger(logChannelId, { name: nls.localize('renderChannelName', "Notebook"), group: windowLogGroup })); } debug(category: string, output: string): void { diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index c667e719ae4d..c528489f5a01 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -23,6 +23,7 @@ import { IStringDictionary } from '../../../../base/common/collections.js'; import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { IViewsService } from '../common/viewsService.js'; +import { windowLogGroup } from '../../log/common/logConstants.js'; interface IViewsCustomizations { viewContainerLocations: IStringDictionary; @@ -79,7 +80,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor ) { super(); - this.logger = new Lazy(() => loggerService.createLogger(VIEWS_LOG_ID, { name: VIEWS_LOG_NAME, hidden: true })); + this.logger = new Lazy(() => loggerService.createLogger(VIEWS_LOG_ID, { name: VIEWS_LOG_NAME, group: windowLogGroup })); this.activeViewContextKeys = new Map>(); this.movableViewContextKeys = new Map>(); diff --git a/src/vs/workbench/services/views/common/viewContainerModel.ts b/src/vs/workbench/services/views/common/viewContainerModel.ts index e7d3665d9f9a..c109542cafeb 100644 --- a/src/vs/workbench/services/views/common/viewContainerModel.ts +++ b/src/vs/workbench/services/views/common/viewContainerModel.ts @@ -9,7 +9,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { Registry } from '../../../../platform/registry/common/platform.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { Event, Emitter } from '../../../../base/common/event.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { URI } from '../../../../base/common/uri.js'; import { coalesce, move } from '../../../../base/common/arrays.js'; import { isUndefined, isUndefinedOrNull } from '../../../../base/common/types.js'; @@ -17,29 +17,9 @@ import { isEqual } from '../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js'; -import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; -import { IOutputService } from '../../output/common/output.js'; import { CounterSet } from '../../../../base/common/map.js'; -import { localize2 } from '../../../../nls.js'; import { Lazy } from '../../../../base/common/lazy.js'; - -registerAction2(class extends Action2 { - constructor() { - super({ - id: '_workbench.output.showViewsLog', - title: localize2('showViewsLog', "Show Views Log"), - category: Categories.Developer, - f1: true - }); - } - async run(servicesAccessor: ServicesAccessor): Promise { - const loggerService = servicesAccessor.get(ILoggerService); - const outputService = servicesAccessor.get(IOutputService); - loggerService.setVisibility(VIEWS_LOG_ID, true); - outputService.showChannel(VIEWS_LOG_ID); - } -}); +import { windowLogGroup } from '../../log/common/logConstants.js'; export function getViewsStateStorageId(viewContainerStorageId: string): string { return `${viewContainerStorageId}.hidden`; } @@ -84,7 +64,7 @@ class ViewDescriptorsState extends Disposable { ) { super(); - this.logger = new Lazy(() => loggerService.createLogger(VIEWS_LOG_ID, { name: VIEWS_LOG_NAME, hidden: true })); + this.logger = new Lazy(() => loggerService.createLogger(VIEWS_LOG_ID, { name: VIEWS_LOG_NAME, group: windowLogGroup })); this.globalViewsStateStorageId = getViewsStateStorageId(viewContainerStorageId); this.workspaceViewsStateStorageId = viewContainerStorageId; @@ -354,7 +334,7 @@ export class ViewContainerModel extends Disposable implements IViewContainerMode ) { super(); - this.logger = new Lazy(() => loggerService.createLogger(VIEWS_LOG_ID, { name: VIEWS_LOG_NAME, hidden: true })); + this.logger = new Lazy(() => loggerService.createLogger(VIEWS_LOG_ID, { name: VIEWS_LOG_NAME, group: windowLogGroup })); this._register(Event.filter(contextKeyService.onDidChangeContext, e => e.affectsSome(this.contextKeys))(() => this.onDidChangeContext())); this.viewDescriptorsState = this._register(instantiationService.createInstance(ViewDescriptorsState, viewContainer.storageId || `${viewContainer.id}.state`, typeof viewContainer.title === 'string' ? viewContainer.title : viewContainer.title.original)); From 8e093a90dce147a3a06823e646e1f2b3ea1de6a4 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 21 Jan 2025 12:27:55 -0600 Subject: [PATCH 0721/3587] fix #237976 --- .../terminalContrib/suggest/browser/terminalCompletionService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index be3976432613..f18aa963cd1e 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -317,6 +317,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo label, provider, kind, + detail: getFriendlyFolderPath(stat.resource, resourceRequestConfig.pathSeparator), isDirectory, isFile: kind === TerminalCompletionItemKind.File, replacementIndex: cursorPosition - lastWord.length, From a67a8eed5d697a8ecb3c074ae7fa59322f089db0 Mon Sep 17 00:00:00 2001 From: Remy Suen Date: Tue, 21 Jan 2025 13:51:19 -0500 Subject: [PATCH 0722/3587] Fix typo in the help text of the icon extension point Signed-off-by: Remy Suen --- src/vs/workbench/services/themes/common/iconExtensionPoint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/services/themes/common/iconExtensionPoint.ts b/src/vs/workbench/services/themes/common/iconExtensionPoint.ts index 08a9b0591139..e8715a898c55 100644 --- a/src/vs/workbench/services/themes/common/iconExtensionPoint.ts +++ b/src/vs/workbench/services/themes/common/iconExtensionPoint.ts @@ -60,7 +60,7 @@ const iconConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint Date: Tue, 21 Jan 2025 13:02:07 -0600 Subject: [PATCH 0723/3587] For files, don't add end path sep --- .../suggest/browser/terminalCompletionService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index f18aa963cd1e..4022d1612c41 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -317,7 +317,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo label, provider, kind, - detail: getFriendlyFolderPath(stat.resource, resourceRequestConfig.pathSeparator), + detail: getFriendlyFolderPath(stat.resource, resourceRequestConfig.pathSeparator, kind === TerminalCompletionItemKind.File), isDirectory, isFile: kind === TerminalCompletionItemKind.File, replacementIndex: cursorPosition - lastWord.length, @@ -352,10 +352,10 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo } } -function getFriendlyFolderPath(uri: URI, pathSeparator: string): string { +function getFriendlyFolderPath(uri: URI, pathSeparator: string, isFile?: boolean): string { let path = uri.fsPath; // Ensure folders end with the path separator to differentiate presentation from files - if (!path.endsWith(pathSeparator)) { + if (!isFile && !path.endsWith(pathSeparator)) { path += pathSeparator; } // Ensure drive is capitalized on Windows From f2bfc6d15c969e2df6d2435dd84da73c497c99f1 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 21 Jan 2025 11:07:36 -0800 Subject: [PATCH 0724/3587] More specific editFile tool response (#238392) --- src/vs/workbench/contrib/chat/browser/tools/tools.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/tools.ts b/src/vs/workbench/contrib/chat/browser/tools/tools.ts index bcf466e85171..d8a4e4459ddd 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/tools.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/tools.ts @@ -161,7 +161,7 @@ class EditTool implements IToolImpl { await this.textFileService.save(uri); return { - content: [{ kind: 'text', value: 'Success' }] + content: [{ kind: 'text', value: 'The file was edited successfully' }] }; } } From 2083967ffaae014a21556e379052f1bd8893a9ea Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 21 Jan 2025 20:22:24 +0100 Subject: [PATCH 0725/3587] ability to set log level for compound logs (#238394) --- .../contrib/logs/common/defaultLogLevels.ts | 13 ---- .../contrib/logs/common/logs.contribution.ts | 9 --- .../contrib/logs/common/logsActions.ts | 33 ++++++----- .../output/browser/output.contribution.ts | 5 +- .../contrib/output/browser/outputServices.ts | 59 ++++++++++++++++--- .../services/output/common/output.ts | 19 ++++++ 6 files changed, 90 insertions(+), 48 deletions(-) diff --git a/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts b/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts index 1302428ebdfb..4d6ba9f5e01d 100644 --- a/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts +++ b/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts @@ -38,8 +38,6 @@ export interface IDefaultLogLevelsService { getDefaultLogLevel(extensionId?: string): Promise; setDefaultLogLevel(logLevel: LogLevel, extensionId?: string): Promise; - - migrateLogLevels(): void; } class DefaultLogLevelsService extends Disposable implements IDefaultLogLevelsService { @@ -148,17 +146,6 @@ class DefaultLogLevelsService extends Disposable implements IDefaultLogLevelsSer return !isUndefined(result.default) || result.extensions?.length ? result : undefined; } - async migrateLogLevels(): Promise { - const logLevels = await this._readLogLevelsFromArgv(); - const regex = /^([^.]+\..+):(.+)$/; - if (logLevels.some(extensionLogLevel => regex.test(extensionLogLevel))) { - const argvLogLevel = await this._parseLogLevelsFromArgv(); - if (argvLogLevel) { - await this._writeLogLevelsToArgv(argvLogLevel); - } - } - } - private async _readLogLevelsFromArgv(): Promise { try { const content = await this.fileService.readFile(this.environmentService.argvResource); diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 0d9082b7a747..05b5f7a03299 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -196,13 +196,4 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { } } -class LogLevelMigration implements IWorkbenchContribution { - constructor( - @IDefaultLogLevelsService defaultLogLevelsService: IDefaultLogLevelsService - ) { - defaultLogLevelsService.migrateLogLevels(); - } -} - Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LogOutputChannels, LifecyclePhase.Restored); -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LogLevelMigration, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/logs/common/logsActions.ts b/src/vs/workbench/contrib/logs/common/logsActions.ts index 4f94dffcd072..23bf63df8916 100644 --- a/src/vs/workbench/contrib/logs/common/logsActions.ts +++ b/src/vs/workbench/contrib/logs/common/logsActions.ts @@ -12,15 +12,14 @@ import { IFileService } from '../../../../platform/files/common/files.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { dirname, basename, isEqual } from '../../../../base/common/resources.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { IOutputChannelDescriptor, IOutputService, isSingleSourceOutputChannelDescriptor } from '../../../services/output/common/output.js'; -import { extensionTelemetryLogChannelId, telemetryLogId } from '../../../../platform/telemetry/common/telemetryUtils.js'; +import { IOutputChannelDescriptor, IOutputService, isMultiSourceOutputChannelDescriptor, isSingleSourceOutputChannelDescriptor } from '../../../services/output/common/output.js'; import { IDefaultLogLevelsService } from './defaultLogLevels.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; type LogLevelQuickPickItem = IQuickPickItem & { level: LogLevel }; -type LogChannelQuickPickItem = IQuickPickItem & { id: string; resource: URI; extensionId?: string }; +type LogChannelQuickPickItem = IQuickPickItem & { id: string; channel: IOutputChannelDescriptor }; export class SetLogLevelAction extends Action { @@ -52,11 +51,20 @@ export class SetLogLevelAction extends Action { const extensionLogs: LogChannelQuickPickItem[] = [], logs: LogChannelQuickPickItem[] = []; const logLevel = this.loggerService.getLogLevel(); for (const channel of this.outputService.getChannelDescriptors()) { - if (!isSingleSourceOutputChannelDescriptor(channel) || !SetLogLevelAction.isLevelSettable(channel)) { + if (!this.outputService.canSetLogLevel(channel)) { continue; } - const channelLogLevel = this.loggerService.getLogLevel(channel.source.resource) ?? logLevel; - const item: LogChannelQuickPickItem = { id: channel.id, resource: channel.source.resource, label: channel.label, description: channelLogLevel !== logLevel ? this.getLabel(channelLogLevel) : undefined, extensionId: channel.extensionId }; + const sources = isSingleSourceOutputChannelDescriptor(channel) ? [channel.source] : isMultiSourceOutputChannelDescriptor(channel) ? channel.source : []; + if (!sources.length) { + continue; + } + const channelLogLevel = sources.reduce((prev, curr) => Math.min(prev, this.loggerService.getLogLevel(curr.resource) ?? logLevel), logLevel); + const item: LogChannelQuickPickItem = { + id: channel.id, + label: channel.label, + description: channelLogLevel !== logLevel ? this.getLabel(channelLogLevel) : undefined, + channel + }; if (channel.extensionId) { extensionLogs.push(item); } else { @@ -96,15 +104,10 @@ export class SetLogLevelAction extends Action { }); } - static isLevelSettable(channel: IOutputChannelDescriptor): boolean { - return channel.log && isSingleSourceOutputChannelDescriptor(channel) && channel.id !== telemetryLogId && channel.id !== extensionTelemetryLogChannelId; - } - private async setLogLevelForChannel(logChannel: LogChannelQuickPickItem): Promise { const defaultLogLevels = await this.defaultLogLevelsService.getDefaultLogLevels(); - const defaultLogLevel = defaultLogLevels.extensions.find(e => e[0] === logChannel.extensionId?.toLowerCase())?.[1] ?? defaultLogLevels.default; - const currentLogLevel = this.loggerService.getLogLevel(logChannel.resource) ?? defaultLogLevel; - const entries = this.getLogLevelEntries(defaultLogLevel, currentLogLevel, !!logChannel.extensionId); + const defaultLogLevel = defaultLogLevels.extensions.find(e => e[0] === logChannel.channel.extensionId?.toLowerCase())?.[1] ?? defaultLogLevels.default; + const entries = this.getLogLevelEntries(defaultLogLevel, this.outputService.getLogLevel(logChannel.channel) ?? defaultLogLevel, !!logChannel.channel.extensionId); return new Promise((resolve, reject) => { const disposables = new DisposableStore(); @@ -115,7 +118,7 @@ export class SetLogLevelAction extends Action { let selectedItem: LogLevelQuickPickItem | undefined; disposables.add(quickPick.onDidTriggerItemButton(e => { quickPick.hide(); - this.defaultLogLevelsService.setDefaultLogLevel((e.item).level, logChannel.extensionId); + this.defaultLogLevelsService.setDefaultLogLevel((e.item).level, logChannel.channel.extensionId); })); disposables.add(quickPick.onDidAccept(e => { selectedItem = quickPick.selectedItems[0] as LogLevelQuickPickItem; @@ -123,7 +126,7 @@ export class SetLogLevelAction extends Action { })); disposables.add(quickPick.onDidHide(() => { if (selectedItem) { - this.loggerService.setLogLevel(logChannel.resource, selectedItem.level); + this.outputService.setLogLevel(logChannel.channel, selectedItem.level); } disposables.dispose(); resolve(); diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 32d2ff9404f9..2e16d0fd7adf 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -474,13 +474,12 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { }); } async run(accessor: ServicesAccessor): Promise { - const loggerService = accessor.get(ILoggerService); const outputService = accessor.get(IOutputService); const channel = outputService.getActiveChannel(); if (channel) { const channelDescriptor = outputService.getChannelDescriptor(channel.id); - if (channelDescriptor && isSingleSourceOutputChannelDescriptor(channelDescriptor)) { - return loggerService.setLogLevel(channelDescriptor.source.resource, logLevel); + if (channelDescriptor) { + outputService.setLogLevel(channelDescriptor, logLevel); } } } diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index f3aaea6abcba..942316dbe333 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -14,7 +14,7 @@ import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, LOG_MIME, OUTPUT_MIME, import { OutputLinkProvider } from './outputLinkProvider.js'; import { ITextModelService, ITextModelContentProvider } from '../../../../editor/common/services/resolverService.js'; import { ITextModel } from '../../../../editor/common/model.js'; -import { ILogService, ILoggerService, LogLevelToString } from '../../../../platform/log/common/log.js'; +import { ILogService, ILoggerService, LogLevel, LogLevelToString } from '../../../../platform/log/common/log.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; import { IOutputChannelModel } from '../common/outputChannelModel.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; @@ -22,13 +22,13 @@ import { OutputViewPane } from './outputView.js'; import { IOutputChannelModelService } from '../common/outputChannelModelService.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { SetLogLevelAction } from '../../logs/common/logsActions.js'; import { IDefaultLogLevelsService } from '../../logs/common/defaultLogLevels.js'; import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { localize } from '../../../../nls.js'; import { joinPath } from '../../../../base/common/resources.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; +import { extensionTelemetryLogChannelId, telemetryLogId } from '../../../../platform/telemetry/common/telemetryUtils.js'; const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel'; @@ -269,7 +269,8 @@ export class OutputService extends Disposable implements IOutputService, ITextMo } })); - this._register(this.loggerService.onDidChangeLogLevel(_level => { + this._register(this.loggerService.onDidChangeLogLevel(() => { + this.resetLogLevelFilters(); this.setLevelContext(); this.setLevelIsDefaultContext(); })); @@ -326,6 +327,36 @@ export class OutputService extends Disposable implements IOutputService, ITextMo return this.activeChannel; } + canSetLogLevel(channel: IOutputChannelDescriptor): boolean { + return channel.log && channel.id !== telemetryLogId && channel.id !== extensionTelemetryLogChannelId; + } + + getLogLevel(channel: IOutputChannelDescriptor): LogLevel | undefined { + if (!channel.log) { + return undefined; + } + const sources = isSingleSourceOutputChannelDescriptor(channel) ? [channel.source] : isMultiSourceOutputChannelDescriptor(channel) ? channel.source : []; + if (sources.length === 0) { + return undefined; + } + + const logLevel = this.loggerService.getLogLevel(); + return sources.reduce((prev, curr) => Math.min(prev, this.loggerService.getLogLevel(curr.resource) ?? logLevel), LogLevel.Error); + } + + setLogLevel(channel: IOutputChannelDescriptor, logLevel: LogLevel): void { + if (!channel.log) { + return; + } + const sources = isSingleSourceOutputChannelDescriptor(channel) ? [channel.source] : isMultiSourceOutputChannelDescriptor(channel) ? channel.source : []; + if (sources.length === 0) { + return; + } + for (const source of sources) { + this.loggerService.setLogLevel(source.resource, logLevel); + } + } + registerCompoundLogChannel(descriptors: IOutputChannelDescriptor[]): string { const outputChannelRegistry = Registry.as(Extensions.OutputChannels); descriptors.sort((a, b) => a.label.localeCompare(b.label)); @@ -453,17 +484,29 @@ export class OutputService extends Disposable implements IOutputService, ITextMo return this.instantiationService.createInstance(OutputChannel, channelData); } + private resetLogLevelFilters(): void { + const descriptor = this.activeChannel?.outputChannelDescriptor; + const channelLogLevel = descriptor ? this.getLogLevel(descriptor) : undefined; + if (channelLogLevel !== undefined) { + this.filters.error = channelLogLevel <= LogLevel.Error; + this.filters.warning = channelLogLevel <= LogLevel.Warning; + this.filters.info = channelLogLevel <= LogLevel.Info; + this.filters.debug = channelLogLevel <= LogLevel.Debug; + this.filters.trace = channelLogLevel <= LogLevel.Trace; + } + } + private setLevelContext(): void { const descriptor = this.activeChannel?.outputChannelDescriptor; - const channelLogLevel = descriptor?.log && isSingleSourceOutputChannelDescriptor(descriptor) ? this.loggerService.getLogLevel(descriptor.source.resource) : undefined; + const channelLogLevel = descriptor ? this.getLogLevel(descriptor) : undefined; this.activeOutputChannelLevelContext.set(channelLogLevel !== undefined ? LogLevelToString(channelLogLevel) : ''); } private async setLevelIsDefaultContext(): Promise { const descriptor = this.activeChannel?.outputChannelDescriptor; - if (descriptor?.log && isSingleSourceOutputChannelDescriptor(descriptor)) { - const channelLogLevel = this.loggerService.getLogLevel(descriptor.source.resource); - const channelDefaultLogLevel = await this.defaultLogLevelsService.getDefaultLogLevel(descriptor.extensionId); + const channelLogLevel = descriptor ? this.getLogLevel(descriptor) : undefined; + if (channelLogLevel !== undefined) { + const channelDefaultLogLevel = await this.defaultLogLevelsService.getDefaultLogLevel(descriptor?.extensionId); this.activeOutputChannelLevelIsDefaultContext.set(channelDefaultLogLevel === channelLogLevel); } else { this.activeOutputChannelLevelIsDefaultContext.set(false); @@ -475,7 +518,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo const descriptor = channel?.outputChannelDescriptor; this.activeFileOutputChannelContext.set(!!descriptor && isSingleSourceOutputChannelDescriptor(descriptor)); this.activeLogOutputChannelContext.set(!!descriptor?.log); - this.activeOutputChannelLevelSettableContext.set(descriptor !== undefined && SetLogLevelAction.isLevelSettable(descriptor)); + this.activeOutputChannelLevelSettableContext.set(descriptor !== undefined && this.canSetLogLevel(descriptor)); this.setLevelIsDefaultContext(); this.setLevelContext(); diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index 67781e8ed168..99e31cdae792 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -120,6 +120,25 @@ export interface IOutputService { * Save the logs to a file. */ saveOutputAs(...channels: IOutputChannelDescriptor[]): Promise; + + /** + * Checks if the log level can be set for the given channel. + * @param channel + */ + canSetLogLevel(channel: IOutputChannelDescriptor): boolean; + + /** + * Returns the log level for the given channel. + * @param channel + */ + getLogLevel(channel: IOutputChannelDescriptor): LogLevel | undefined; + + /** + * Sets the log level for the given channel. + * @param channel + * @param logLevel + */ + setLogLevel(channel: IOutputChannelDescriptor, logLevel: LogLevel): void; } export enum OutputChannelUpdateMode { From 5a6c2fa66d074f011abe59725941ea23f4257236 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 21 Jan 2025 20:31:44 +0100 Subject: [PATCH 0726/3587] Git - FS stat should also throw if the file does not exist (#238395) --- extensions/git/src/fileSystemProvider.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/extensions/git/src/fileSystemProvider.ts b/extensions/git/src/fileSystemProvider.ts index 896d933cddfb..6e30118128db 100644 --- a/extensions/git/src/fileSystemProvider.ts +++ b/extensions/git/src/fileSystemProvider.ts @@ -140,14 +140,14 @@ export class GitFileSystemProvider implements FileSystemProvider { throw FileSystemError.FileNotFound(); } - let size = 0; try { const details = await repository.getObjectDetails(sanitizeRef(ref, path, repository), path); - size = details.size; + return { type: FileType.File, size: details.size, mtime: this.mtime, ctime: 0 }; } catch { - // noop + // File does not exist in git. This could be because the file is untracked or ignored + this.logger.warn(`[GitFileSystemProvider][stat] File not found - ${uri.toString()}`); + throw FileSystemError.FileNotFound(); } - return { type: FileType.File, size: size, mtime: this.mtime, ctime: 0 }; } readDirectory(): Thenable<[string, FileType][]> { @@ -193,10 +193,9 @@ export class GitFileSystemProvider implements FileSystemProvider { try { return await repository.buffer(sanitizeRef(ref, path, repository), path); - } catch (err) { + } catch { + // File does not exist in git. This could be because the file is untracked or ignored this.logger.warn(`[GitFileSystemProvider][readFile] File not found - ${uri.toString()}`); - // File does not exist in git. This could be - // because the file is untracked or ignored throw FileSystemError.FileNotFound(); } } From d2c2cd9111bce3b753ea5307db4cacda89554482 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 21 Jan 2025 13:37:14 -0600 Subject: [PATCH 0727/3587] rename, pass in kind --- .../suggest/browser/terminalCompletionService.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 4022d1612c41..08c0da1c90bb 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -262,7 +262,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo kind: TerminalCompletionItemKind.Folder, isDirectory: true, isFile: false, - detail: getFriendlyFolderPath(cwd, resourceRequestConfig.pathSeparator), + detail: getFriendlyPath(cwd, resourceRequestConfig.pathSeparator), replacementIndex: cursorPosition - lastWord.length, replacementLength: lastWord.length }); @@ -317,7 +317,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo label, provider, kind, - detail: getFriendlyFolderPath(stat.resource, resourceRequestConfig.pathSeparator, kind === TerminalCompletionItemKind.File), + detail: getFriendlyPath(stat.resource, resourceRequestConfig.pathSeparator, TerminalCompletionItemKind.File), isDirectory, isFile: kind === TerminalCompletionItemKind.File, replacementIndex: cursorPosition - lastWord.length, @@ -340,7 +340,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo label: lastWordFolder + '..' + resourceRequestConfig.pathSeparator, provider, kind: TerminalCompletionItemKind.Folder, - detail: getFriendlyFolderPath(parentDir, resourceRequestConfig.pathSeparator), + detail: getFriendlyPath(parentDir, resourceRequestConfig.pathSeparator), isDirectory: true, isFile: false, replacementIndex: cursorPosition - lastWord.length, @@ -352,10 +352,10 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo } } -function getFriendlyFolderPath(uri: URI, pathSeparator: string, isFile?: boolean): string { +function getFriendlyPath(uri: URI, pathSeparator: string, kind?: TerminalCompletionItemKind): string { let path = uri.fsPath; // Ensure folders end with the path separator to differentiate presentation from files - if (!isFile && !path.endsWith(pathSeparator)) { + if (kind !== TerminalCompletionItemKind.File && !path.endsWith(pathSeparator)) { path += pathSeparator; } // Ensure drive is capitalized on Windows From 6446430c4e458427120f57eed8e6e431826a41a6 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 21 Jan 2025 14:52:18 -0500 Subject: [PATCH 0728/3587] use -- and remove special case for hypen --- .../contrib/terminal/common/scripts/shellIntegration-rc.zsh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index 75ab0ceb44d5..efcf6ca86068 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -82,8 +82,6 @@ __vsc_escape_value() { token="\\x3b" elif [ "$byte" = $'\n' ]; then token="\x0a" - elif [ "$byte" = "-" ]; then - token="\\x2d" else token="$byte" fi @@ -91,7 +89,7 @@ __vsc_escape_value() { out+="$token" done - builtin print -r "$out" + builtin print -r -- "$out" } __vsc_in_command_execution="1" From 0ac5cce00886d3428490d60fc816c6aabadccd0c Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 21 Jan 2025 13:57:07 -0600 Subject: [PATCH 0729/3587] prevent `.zlogin` and `.zprofile` from running more than once (#238388) Fix #238296 --- .../terminal/common/scripts/shellIntegration-login.zsh | 6 ++++++ .../terminal/common/scripts/shellIntegration-profile.zsh | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh index 128bd648616c..8edbca365e4d 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh @@ -3,6 +3,12 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # --------------------------------------------------------------------------------------------- +# Prevent recursive sourcing +if [[ -n "$VSCODE_LOGIN_INITIALIZED" ]]; then + return +fi +export VSCODE_LOGIN_INITIALIZED=1 + ZDOTDIR=$USER_ZDOTDIR if [[ $options[norcs] = off && -o "login" && -f $ZDOTDIR/.zlogin ]]; then . $ZDOTDIR/.zlogin diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-profile.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-profile.zsh index c25ded3d7ebf..7401c10d3764 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-profile.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-profile.zsh @@ -2,6 +2,13 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # --------------------------------------------------------------------------------------------- + +# Prevent recursive sourcing +if [[ -n "$VSCODE_PROFILE_INITIALIZED" ]]; then + return +fi +export VSCODE_PROFILE_INITIALIZED=1 + if [[ $options[norcs] = off && -o "login" ]]; then if [[ -f $USER_ZDOTDIR/.zprofile ]]; then VSCODE_ZDOTDIR=$ZDOTDIR From 258261710a279b8e11548d88127e8e92c96b7bbf Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 21 Jan 2025 15:00:27 -0500 Subject: [PATCH 0730/3587] revert my comment on hypens --- .../contrib/terminal/common/scripts/shellIntegration-rc.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index efcf6ca86068..e9db9f42183c 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -75,7 +75,7 @@ __vsc_escape_value() { for (( i = 0; i < ${#str}; ++i )); do byte="${str:$i:1}" - # Escape backslashes, semi-colons, newlines, and hyphens. + # Escape backslashes, semi-colons, newlines if [ "$byte" = "\\" ]; then token="\\\\" elif [ "$byte" = ";" ]; then From ac0f552f5685d0f5d14e43eef8f70d66b8c68df2 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 21 Jan 2025 14:03:42 -0600 Subject: [PATCH 0731/3587] Add documentation to completion item (#238391) --- .../src/completions/code-insiders.ts | 1 + .../src/terminalSuggestMain.ts | 22 +++++++++++++------ .../suggest/browser/simpleCompletionItem.ts | 7 ++++++ .../browser/simpleSuggestWidgetDetails.ts | 6 ++--- ...e.proposed.terminalCompletionProvider.d.ts | 6 +++++ 5 files changed, 32 insertions(+), 10 deletions(-) diff --git a/extensions/terminal-suggest/src/completions/code-insiders.ts b/extensions/terminal-suggest/src/completions/code-insiders.ts index 8014c97e8c19..89d01dc536ff 100644 --- a/extensions/terminal-suggest/src/completions/code-insiders.ts +++ b/extensions/terminal-suggest/src/completions/code-insiders.ts @@ -7,6 +7,7 @@ import code from './code'; const codeInsidersCompletionSpec: Fig.Spec = { ...code, name: 'code-insiders', + description: 'Visual Studio Code Insiders', }; export default codeInsidersCompletionSpec; diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 66d0962d623c..6072cd048f4c 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -198,7 +198,12 @@ export async function resolveCwdFromPrefix(prefix: string, currentCwd?: vscode.U // If the prefix is not a folder, return the current cwd return currentCwd; } - +function getDescription(spec: Fig.Spec): string { + if ('description' in spec) { + return spec.description ?? ''; + } + return ''; +} function getLabel(spec: Fig.Spec | Fig.Arg | Fig.Suggestion | string): string[] | undefined { if (typeof spec === 'string') { @@ -213,12 +218,13 @@ function getLabel(spec: Fig.Spec | Fig.Arg | Fig.Suggestion | string): string[] return spec.name; } -function createCompletionItem(cursorPosition: number, prefix: string, commandResource: ICompletionResource, description?: string, kind?: vscode.TerminalCompletionItemKind): vscode.TerminalCompletionItem { +function createCompletionItem(cursorPosition: number, prefix: string, commandResource: ICompletionResource, detail?: string, documentation?: string | vscode.MarkdownString, kind?: vscode.TerminalCompletionItemKind): vscode.TerminalCompletionItem { const endsWithSpace = prefix.endsWith(' '); const lastWord = endsWithSpace ? '' : prefix.split(' ').at(-1) ?? ''; return { label: commandResource.label, - detail: description ?? commandResource.detail ?? '', + detail: detail ?? commandResource.detail ?? '', + documentation, replacementIndex: cursorPosition - lastWord.length, replacementLength: lastWord.length, kind: kind ?? vscode.TerminalCompletionItemKind.Method @@ -327,7 +333,8 @@ export async function getCompletionItemsFromSpecs( } for (const specLabel of specLabels) { - if (!availableCommands.find(command => command.label === specLabel) || (token && token.isCancellationRequested)) { + const availableCommand = availableCommands.find(command => command.label === specLabel); + if (!availableCommand || (token && token.isCancellationRequested)) { continue; } @@ -338,7 +345,7 @@ export async function getCompletionItemsFromSpecs( || !!firstCommand && specLabel.startsWith(firstCommand) ) { // push it to the completion items - items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: specLabel })); + items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: specLabel }, availableCommand.detail, getDescription(spec))); } if (!terminalContext.commandLine.startsWith(specLabel)) { @@ -374,7 +381,7 @@ export async function getCompletionItemsFromSpecs( const labels = new Set(items.map((i) => i.label)); for (const command of availableCommands) { if (!labels.has(command.label)) { - items.push(createCompletionItem(terminalContext.cursorPosition, prefix, command)); + items.push(createCompletionItem(terminalContext.cursorPosition, prefix, command, command.detail)); } } } @@ -445,6 +452,7 @@ function handleOptions(specLabel: string, spec: Fig.Spec, terminalContext: { com prefix, { label: optionLabel }, option.description, + undefined, vscode.TerminalCompletionItemKind.Flag ) ); @@ -505,7 +513,7 @@ function getCompletionItemsFromArgs(args: Fig.SingleOrArray | undefined } if (suggestionLabel && suggestionLabel.startsWith(currentPrefix.trim())) { const description = typeof suggestion !== 'string' ? suggestion.description : ''; - items.push(createCompletionItem(terminalContext.cursorPosition, wordBefore ?? '', { label: suggestionLabel }, description, vscode.TerminalCompletionItemKind.Argument)); + items.push(createCompletionItem(terminalContext.cursorPosition, wordBefore ?? '', { label: suggestionLabel }, description, undefined, vscode.TerminalCompletionItemKind.Argument)); } } } diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts index 466a979d1968..9af7f683e6bb 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { FuzzyScore } from '../../../../base/common/filters.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { isWindows } from '../../../../base/common/platform.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; @@ -24,6 +25,12 @@ export interface ISimpleCompletion { * The completion's detail which appears on the right of the list. */ detail?: string; + + /** + * A human-readable string that represents a doc-comment. + */ + documentation?: string | MarkdownString; + /** * Whether the completion is a file. Files with the same score will be sorted against each other * first by extension length and then certain extensions will get a boost based on the OS. diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts index 028169745497..3d52d75c938c 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts @@ -18,7 +18,7 @@ import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRend import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; export function canExpandCompletionItem(item: SimpleCompletionItem | undefined): boolean { - return !!item && Boolean(item.completion.detail && item.completion.detail !== item.completion.label); + return !!item && Boolean(item.completion.documentation || item.completion.detail && item.completion.detail !== item.completion.label); } export const SuggestDetailsClassName = 'suggest-details'; @@ -104,10 +104,10 @@ export class SimpleSuggestDetailsWidget { renderItem(item: SimpleCompletionItem, explainMode: boolean): void { this._renderDisposeable.clear(); - let { detail } = item.completion; + let { detail, documentation } = item.completion; let md = ''; - let documentation; + if (explainMode) { md += `score: ${item.score[0]}\n`; md += `prefix: ${item.word ?? '(no prefix)'}\n`; diff --git a/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts b/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts index 095fdcc867a5..16b4809790b5 100644 --- a/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts @@ -40,6 +40,12 @@ declare module 'vscode' { */ detail?: string; + + /** + * A human-readable string that represents a doc-comment. + */ + documentation?: string | MarkdownString; + /** * The completion's kind. Note that this will map to an icon. */ From 1816564dbfed7a4fe57a60c20ab32c8c4241fff6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:04:32 -0800 Subject: [PATCH 0732/3587] Move line/col limits into render strat --- .../renderStrategy/fullFileRenderStrategy.ts | 40 ++++++++++++------- src/vs/editor/browser/gpu/viewGpuContext.ts | 18 ++++----- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index ba0238b9fd2f..bcfc2a2d6c78 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -48,6 +48,16 @@ type QueuedBufferEvent = ( export class FullFileRenderStrategy extends BaseRenderStrategy { + /** + * The hard cap for line count that can be rendered by the GPU renderer. + */ + static readonly maxSupportedLines = 3000; + + /** + * The hard cap for line columns that can be rendered by the GPU renderer. + */ + static readonly maxSupportedColumns = 200; + readonly wgsl: string = fullFileRenderStrategyWgsl; private _cellBindBuffer!: GPUBuffer; @@ -83,7 +93,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { ) { super(context, viewGpuContext, device); - const bufferSize = this._viewGpuContext.maxGpuLines * this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; + const bufferSize = FullFileRenderStrategy.maxSupportedLines * FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco full file cell buffer', size: bufferSize, @@ -270,7 +280,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { // Update cell data const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]); - const lineIndexCount = this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell; + const lineIndexCount = FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell; const upToDateLines = this._upToDateLines[this._activeDoubleBufferIndex]; let dirtyLineStart = 3000; @@ -294,9 +304,9 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { } case ViewEventType.ViewLinesDeleted: { // Shift content below deleted line up - const deletedLineContentStartIndex = (e.fromLineNumber - 1) * this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell; - const deletedLineContentEndIndex = (e.toLineNumber) * this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell; - const nullContentStartIndex = (this._finalRenderedLine - (e.toLineNumber - e.fromLineNumber + 1)) * this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell; + const deletedLineContentStartIndex = (e.fromLineNumber - 1) * FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell; + const deletedLineContentEndIndex = (e.toLineNumber) * FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell; + const nullContentStartIndex = (this._finalRenderedLine - (e.toLineNumber - e.fromLineNumber + 1)) * FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell; cellBuffer.set(cellBuffer.subarray(deletedLineContentEndIndex), deletedLineContentStartIndex); // Zero out content on lines that are no longer valid @@ -315,8 +325,8 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { // Only attempt to render lines that the GPU renderer can handle if (!this._viewGpuContext.canRender(viewLineOptions, viewportData, y)) { - fillStartIndex = ((y - 1) * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell; - fillEndIndex = (y * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell; + fillStartIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell; + fillEndIndex = (y * FullFileRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell; cellBuffer.fill(0, fillStartIndex, fillEndIndex); dirtyLineStart = Math.min(dirtyLineStart, y); @@ -354,7 +364,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { for (x = tokenStartIndex; x < tokenEndIndex; x++) { // TODO: This needs to move to a dynamic long line rendering strategy - if (x > this._viewGpuContext.maxGpuCols) { + if (x > FullFileRenderStrategy.maxSupportedColumns) { break; } segment = contentSegmenter.getSegmentAtIndex(x); @@ -422,7 +432,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { if (chars === ' ' || chars === '\t') { // Zero out glyph to ensure it doesn't get rendered - cellIndex = ((y - 1) * this._viewGpuContext.maxGpuCols + x) * Constants.IndicesPerCell; + cellIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell; cellBuffer.fill(0, cellIndex, cellIndex + CellBufferInfo.FloatsPerEntry); // Adjust xOffset for tab stops if (chars === '\t') { @@ -454,7 +464,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { glyph.fontBoundingBoxAscent ); - cellIndex = ((y - 1) * this._viewGpuContext.maxGpuCols + x) * Constants.IndicesPerCell; + cellIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell; cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.round(absoluteOffsetX); cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = absoluteOffsetY; cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex; @@ -468,8 +478,8 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { } // Clear to end of line - fillStartIndex = ((y - 1) * this._viewGpuContext.maxGpuCols + tokenEndIndex) * Constants.IndicesPerCell; - fillEndIndex = (y * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell; + fillStartIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + tokenEndIndex) * Constants.IndicesPerCell; + fillEndIndex = (y * FullFileRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell; cellBuffer.fill(0, fillStartIndex, fillEndIndex); upToDateLines.add(y); @@ -478,8 +488,8 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { const visibleObjectCount = (viewportData.endLineNumber - viewportData.startLineNumber + 1) * lineIndexCount; // Only write when there is changed data - dirtyLineStart = Math.min(dirtyLineStart, this._viewGpuContext.maxGpuLines); - dirtyLineEnd = Math.min(dirtyLineEnd, this._viewGpuContext.maxGpuLines); + dirtyLineStart = Math.min(dirtyLineStart, FullFileRenderStrategy.maxSupportedLines); + dirtyLineEnd = Math.min(dirtyLineEnd, FullFileRenderStrategy.maxSupportedLines); if (dirtyLineStart <= dirtyLineEnd) { // Write buffer and swap it out to unblock writes this._device.queue.writeBuffer( @@ -507,7 +517,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { quadVertices.length / 2, this._visibleObjectCount, undefined, - (viewportData.startLineNumber - 1) * this._viewGpuContext.maxGpuCols + (viewportData.startLineNumber - 1) * FullFileRenderStrategy.maxSupportedColumns ); } diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index f9759a4b0dd8..f2fac0bcc89b 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -24,24 +24,20 @@ import { Event } from '../../../base/common/event.js'; import { EditorOption, type IEditorOptions } from '../../common/config/editorOptions.js'; import { InlineDecorationType } from '../../common/viewModel.js'; import { DecorationStyleCache } from './css/decorationStyleCache.js'; - -const enum GpuRenderLimits { - maxGpuLines = 3000, - maxGpuCols = 200, -} +import { FullFileRenderStrategy } from './renderStrategy/fullFileRenderStrategy.js'; export class ViewGpuContext extends Disposable { /** * The temporary hard cap for lines rendered by the GPU renderer. This can be removed once more * dynamic allocation is implemented in https://github.com/microsoft/vscode/issues/227091 */ - readonly maxGpuLines = GpuRenderLimits.maxGpuLines; + readonly maxGpuLines = FullFileRenderStrategy.maxSupportedLines; /** * The temporary hard cap for line columns rendered by the GPU renderer. This can be removed * once more dynamic allocation is implemented in https://github.com/microsoft/vscode/issues/227108 */ - readonly maxGpuCols = GpuRenderLimits.maxGpuCols; + readonly maxGpuCols = FullFileRenderStrategy.maxSupportedColumns; readonly canvas: FastDomNode; readonly ctx: GPUCanvasContext; @@ -167,8 +163,8 @@ export class ViewGpuContext extends Disposable { // Check if the line has simple attributes that aren't supported if ( data.containsRTL || - data.maxColumn > GpuRenderLimits.maxGpuCols || - lineNumber >= GpuRenderLimits.maxGpuLines + data.maxColumn > this.maxGpuCols || + lineNumber >= this.maxGpuLines ) { return false; } @@ -213,7 +209,7 @@ export class ViewGpuContext extends Disposable { if (data.containsRTL) { reasons.push('containsRTL'); } - if (data.maxColumn > GpuRenderLimits.maxGpuCols) { + if (data.maxColumn > this.maxGpuCols) { reasons.push('maxColumn > maxGpuCols'); } if (data.inlineDecorations.length > 0) { @@ -256,7 +252,7 @@ export class ViewGpuContext extends Disposable { reasons.push(`inlineDecorations with unsupported CSS selectors (${problemSelectors.map(e => `\`${e}\``).join(', ')})`); } } - if (lineNumber >= GpuRenderLimits.maxGpuLines) { + if (lineNumber >= this.maxGpuLines) { reasons.push('lineNumber >= maxGpuLines'); } return reasons; From c30c5259c099c41d85a0d216ab07a5f12511fdde Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 21 Jan 2025 15:10:40 -0500 Subject: [PATCH 0733/3587] leave og --- .../contrib/terminal/common/scripts/shellIntegration-rc.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index e9db9f42183c..6482665b969f 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -75,7 +75,7 @@ __vsc_escape_value() { for (( i = 0; i < ${#str}; ++i )); do byte="${str:$i:1}" - # Escape backslashes, semi-colons, newlines + # Escape backslashes, semi-colons and newlines if [ "$byte" = "\\" ]; then token="\\\\" elif [ "$byte" = ";" ]; then From dc9707e723a5bc9c72d97f915c9e3f8f8135e9e9 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 21 Jan 2025 14:16:23 -0600 Subject: [PATCH 0734/3587] update all test expectations --- .../browser/terminalCompletionService.test.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index 174e1ea1e5e2..bda41f033703 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -111,7 +111,7 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: '.', detail: '/test/' }, - { label: './folder1/' }, + { label: './folder1/', detail: '/test/folder1/' }, { label: '../', detail: '/' }, ], { replacementIndex: 1, replacementLength: 0 }); }); @@ -127,7 +127,7 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: './', detail: '/test/' }, - { label: './folder1/' }, + { label: './folder1/', detail: '/test/folder1/' }, { label: './../', detail: '/' }, ], { replacementIndex: 1, replacementLength: 2 }); }); @@ -143,7 +143,7 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: './', detail: '/test/' }, - { label: './folder1/' }, + { label: './folder1/', detail: '/test/folder1/' }, { label: './../', detail: '/' }, ], { replacementIndex: 3, replacementLength: 2 }); }); @@ -158,7 +158,7 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: './', detail: '/test/' }, - { label: './folder1/' }, + { label: './folder1/', detail: '/test/folder1/' }, { label: './../', detail: '/' }, ], { replacementIndex: 3, replacementLength: 3 }); }); @@ -187,10 +187,10 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: './', detail: '/test/' }, - { label: './.hiddenFile', kind: TerminalCompletionItemKind.File }, - { label: './.hiddenFolder/' }, - { label: './folder1/' }, - { label: './file1.txt', kind: TerminalCompletionItemKind.File }, + { label: './.hiddenFile', detail: '/test/.hiddenFile', kind: TerminalCompletionItemKind.File }, + { label: './.hiddenFolder/', detail: '/test/.hiddenFolder/' }, + { label: './folder1/', detail: '/test/folder1/' }, + { label: './file1.txt', detail: '/test/file1.txt', kind: TerminalCompletionItemKind.File }, { label: './../', detail: '/' }, ], { replacementIndex: 0, replacementLength: 2 }); }); @@ -207,10 +207,10 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: './', detail: '/test/' }, - { label: './.hiddenFile', kind: TerminalCompletionItemKind.File }, - { label: './.hiddenFolder/' }, - { label: './folder1/' }, - { label: './file1.txt', kind: TerminalCompletionItemKind.File }, + { label: './.hiddenFile', detail: '/test/.hiddenFile', kind: TerminalCompletionItemKind.File }, + { label: './.hiddenFolder/', detail: '/test/.hiddenFolder/' }, + { label: './folder1/', detail: '/test/folder1/' }, + { label: './file1.txt', detail: '/test/file1.txt', kind: TerminalCompletionItemKind.File }, { label: './../', detail: '/' }, ], { replacementIndex: 0, replacementLength: 3 }); }); @@ -280,8 +280,8 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: './', detail: '/test/' }, - { label: './FolderA/' }, - { label: './foldera/' }, + { label: './FolderA/', detail: '/test/FolderA/' }, + { label: './foldera/', detail: '/test/foldera/' }, { label: './../', detail: '/' } ], { replacementIndex: 0, replacementLength: 8 }); }); @@ -303,8 +303,8 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: '.', detail: '/test/' }, - { label: './folder1/' }, - { label: './folder2/' }, + { label: './folder1/', detail: '/test/folder1/' }, + { label: './folder2/', detail: '/test/folder2/' }, { label: '../', detail: '/' } ], { replacementIndex: 0, replacementLength: 0 }); }); @@ -346,8 +346,8 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: './', detail: '/test/' }, - { label: './folder1/' }, - { label: './folder2/' }, + { label: './folder1/', detail: '/test/folder1/' }, + { label: './folder2/', detail: '/test/folder2/' }, { label: './../', detail: '/' } ], { replacementIndex: 1, replacementLength: 9 }); }); From 9542cdb3e92d0876a53336bb328e5289dea3448e Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 21 Jan 2025 14:30:39 -0600 Subject: [PATCH 0735/3587] don't request terminal completions if cursor index < 0 (#238385) fix #238380 --- .../suggest/browser/terminalCompletionService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index be3976432613..8e5f00aa194c 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -126,7 +126,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo } async provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean): Promise { - if (!this._providers || !this._providers.values) { + if (!this._providers || !this._providers.values || cursorPosition < 0) { return undefined; } From 5c4b473fbed4296327f3c6a7be4bbed7e4ac7fb4 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 21 Jan 2025 14:33:55 -0600 Subject: [PATCH 0736/3587] fix #237818 --- .../services/suggest/browser/simpleSuggestWidgetDetails.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts index 028169745497..ca905d16adab 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts @@ -177,7 +177,7 @@ export class SimpleSuggestDetailsWidget { this._body.scrollTop = 0; - this.layout(this._size.width, this._type.clientHeight + this._docs.clientHeight); + this.layout(this._size.width, this._type.clientHeight + this._docs.clientHeight + 20); this._onDidChangeContents.fire(this); } From b9781cc3a8a3a60111b184ec3c68aeff4ede2ae6 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 21 Jan 2025 14:34:51 -0600 Subject: [PATCH 0737/3587] fix another test --- .../suggest/test/browser/terminalCompletionService.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index bda41f033703..679710de5fc2 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -257,8 +257,8 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: '.\\', detail: 'C:\\test\\' }, - { label: '.\\FolderA\\' }, - { label: '.\\anotherFolder\\' }, + { label: '.\\FolderA\\', detail: 'C:\\test\\FolderA\\' }, + { label: '.\\anotherFolder\\', detail: 'C:\\test\\anotherFolder\\' }, { label: '.\\..\\', detail: 'C:\\' }, ], { replacementIndex: 0, replacementLength: 8 }); }); From d239bd342131aa61af5004ab7717d6ba0888164f Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:37:11 +0100 Subject: [PATCH 0738/3587] Engineering - update notebooks (#238398) --- .vscode/notebooks/endgame.github-issues | 2 +- .vscode/notebooks/my-endgame.github-issues | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 2e4c8d82aac0..ef066abaeafd 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"November 2024\"" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"January 2025\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index b0be17265cda..b16f0025f564 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\r\n\r\n$MILESTONE=milestone:\"November 2024\"\r\n\r\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:\"January 2025\"\n\n$MINE=assignee:@me" }, { "kind": 1, From b718610ff84a3e5e58cb95418d213dea79d31564 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 21 Jan 2025 14:48:45 -0600 Subject: [PATCH 0739/3587] show file/folder path in detail for terminal suggestions (#238389) fix #237976 --- .../browser/terminalCompletionService.ts | 9 +++-- .../browser/terminalCompletionService.test.ts | 40 +++++++++---------- .../browser/simpleSuggestWidgetDetails.ts | 2 +- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 8e5f00aa194c..669c035acd40 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -262,7 +262,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo kind: TerminalCompletionItemKind.Folder, isDirectory: true, isFile: false, - detail: getFriendlyFolderPath(cwd, resourceRequestConfig.pathSeparator), + detail: getFriendlyPath(cwd, resourceRequestConfig.pathSeparator), replacementIndex: cursorPosition - lastWord.length, replacementLength: lastWord.length }); @@ -317,6 +317,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo label, provider, kind, + detail: getFriendlyPath(stat.resource, resourceRequestConfig.pathSeparator, TerminalCompletionItemKind.File), isDirectory, isFile: kind === TerminalCompletionItemKind.File, replacementIndex: cursorPosition - lastWord.length, @@ -339,7 +340,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo label: lastWordFolder + '..' + resourceRequestConfig.pathSeparator, provider, kind: TerminalCompletionItemKind.Folder, - detail: getFriendlyFolderPath(parentDir, resourceRequestConfig.pathSeparator), + detail: getFriendlyPath(parentDir, resourceRequestConfig.pathSeparator), isDirectory: true, isFile: false, replacementIndex: cursorPosition - lastWord.length, @@ -351,10 +352,10 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo } } -function getFriendlyFolderPath(uri: URI, pathSeparator: string): string { +function getFriendlyPath(uri: URI, pathSeparator: string, kind?: TerminalCompletionItemKind): string { let path = uri.fsPath; // Ensure folders end with the path separator to differentiate presentation from files - if (!path.endsWith(pathSeparator)) { + if (kind !== TerminalCompletionItemKind.File && !path.endsWith(pathSeparator)) { path += pathSeparator; } // Ensure drive is capitalized on Windows diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index 174e1ea1e5e2..679710de5fc2 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -111,7 +111,7 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: '.', detail: '/test/' }, - { label: './folder1/' }, + { label: './folder1/', detail: '/test/folder1/' }, { label: '../', detail: '/' }, ], { replacementIndex: 1, replacementLength: 0 }); }); @@ -127,7 +127,7 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: './', detail: '/test/' }, - { label: './folder1/' }, + { label: './folder1/', detail: '/test/folder1/' }, { label: './../', detail: '/' }, ], { replacementIndex: 1, replacementLength: 2 }); }); @@ -143,7 +143,7 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: './', detail: '/test/' }, - { label: './folder1/' }, + { label: './folder1/', detail: '/test/folder1/' }, { label: './../', detail: '/' }, ], { replacementIndex: 3, replacementLength: 2 }); }); @@ -158,7 +158,7 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: './', detail: '/test/' }, - { label: './folder1/' }, + { label: './folder1/', detail: '/test/folder1/' }, { label: './../', detail: '/' }, ], { replacementIndex: 3, replacementLength: 3 }); }); @@ -187,10 +187,10 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: './', detail: '/test/' }, - { label: './.hiddenFile', kind: TerminalCompletionItemKind.File }, - { label: './.hiddenFolder/' }, - { label: './folder1/' }, - { label: './file1.txt', kind: TerminalCompletionItemKind.File }, + { label: './.hiddenFile', detail: '/test/.hiddenFile', kind: TerminalCompletionItemKind.File }, + { label: './.hiddenFolder/', detail: '/test/.hiddenFolder/' }, + { label: './folder1/', detail: '/test/folder1/' }, + { label: './file1.txt', detail: '/test/file1.txt', kind: TerminalCompletionItemKind.File }, { label: './../', detail: '/' }, ], { replacementIndex: 0, replacementLength: 2 }); }); @@ -207,10 +207,10 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: './', detail: '/test/' }, - { label: './.hiddenFile', kind: TerminalCompletionItemKind.File }, - { label: './.hiddenFolder/' }, - { label: './folder1/' }, - { label: './file1.txt', kind: TerminalCompletionItemKind.File }, + { label: './.hiddenFile', detail: '/test/.hiddenFile', kind: TerminalCompletionItemKind.File }, + { label: './.hiddenFolder/', detail: '/test/.hiddenFolder/' }, + { label: './folder1/', detail: '/test/folder1/' }, + { label: './file1.txt', detail: '/test/file1.txt', kind: TerminalCompletionItemKind.File }, { label: './../', detail: '/' }, ], { replacementIndex: 0, replacementLength: 3 }); }); @@ -257,8 +257,8 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: '.\\', detail: 'C:\\test\\' }, - { label: '.\\FolderA\\' }, - { label: '.\\anotherFolder\\' }, + { label: '.\\FolderA\\', detail: 'C:\\test\\FolderA\\' }, + { label: '.\\anotherFolder\\', detail: 'C:\\test\\anotherFolder\\' }, { label: '.\\..\\', detail: 'C:\\' }, ], { replacementIndex: 0, replacementLength: 8 }); }); @@ -280,8 +280,8 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: './', detail: '/test/' }, - { label: './FolderA/' }, - { label: './foldera/' }, + { label: './FolderA/', detail: '/test/FolderA/' }, + { label: './foldera/', detail: '/test/foldera/' }, { label: './../', detail: '/' } ], { replacementIndex: 0, replacementLength: 8 }); }); @@ -303,8 +303,8 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: '.', detail: '/test/' }, - { label: './folder1/' }, - { label: './folder2/' }, + { label: './folder1/', detail: '/test/folder1/' }, + { label: './folder2/', detail: '/test/folder2/' }, { label: '../', detail: '/' } ], { replacementIndex: 0, replacementLength: 0 }); }); @@ -346,8 +346,8 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: './', detail: '/test/' }, - { label: './folder1/' }, - { label: './folder2/' }, + { label: './folder1/', detail: '/test/folder1/' }, + { label: './folder2/', detail: '/test/folder2/' }, { label: './../', detail: '/' } ], { replacementIndex: 1, replacementLength: 9 }); }); diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts index 3d52d75c938c..fdea7625c931 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts @@ -177,7 +177,7 @@ export class SimpleSuggestDetailsWidget { this._body.scrollTop = 0; - this.layout(this._size.width, this._type.clientHeight + this._docs.clientHeight); + this.layout(this._size.width, this._type.clientHeight + this._docs.clientHeight + 20); this._onDidChangeContents.fire(this); } From 574c6c694514bdf6346c887d184c5c67f0dcb0e1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:49:26 -0800 Subject: [PATCH 0740/3587] Working viewport render strategy --- .../renderStrategy/fullFileRenderStrategy.ts | 5 + .../renderStrategy/viewportRenderStrategy.ts | 416 ++++++++++++++++++ src/vs/editor/browser/gpu/viewGpuContext.ts | 2 +- .../viewParts/viewLinesGpu/viewLinesGpu.ts | 6 +- 4 files changed, 426 insertions(+), 3 deletions(-) create mode 100644 src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts diff --git a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index bcfc2a2d6c78..6b4a8a2ce396 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -46,6 +46,11 @@ type QueuedBufferEvent = ( ViewZonesChangedEvent ); +/** + * A render strategy that tracks a large buffer, uploading only dirty lines as they change and + * leveraging heavy caching. This is the most performant strategy but has limitations around long + * lines and too many lines. + */ export class FullFileRenderStrategy extends BaseRenderStrategy { /** diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts new file mode 100644 index 000000000000..9ec402b0dbd8 --- /dev/null +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -0,0 +1,416 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveWindow } from '../../../../base/browser/dom.js'; +import { Color } from '../../../../base/common/color.js'; +import { BugIndicatingError } from '../../../../base/common/errors.js'; +import { EditorOption } from '../../../common/config/editorOptions.js'; +import { CursorColumns } from '../../../common/core/cursorColumns.js'; +import type { IViewLineTokens } from '../../../common/tokens/lineTokens.js'; +import { type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLineMappingChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../../common/viewEvents.js'; +import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; +import type { InlineDecoration, ViewLineRenderingData } from '../../../common/viewModel.js'; +import type { ViewContext } from '../../../common/viewModel/viewContext.js'; +import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions.js'; +import type { ITextureAtlasPageGlyph } from '../atlas/atlas.js'; +import { createContentSegmenter, type IContentSegmenter } from '../contentSegmenter.js'; +import { BindingId } from '../gpu.js'; +import { GPULifecycle } from '../gpuDisposable.js'; +import { quadVertices } from '../gpuUtils.js'; +import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; +import { ViewGpuContext } from '../viewGpuContext.js'; +import { BaseRenderStrategy } from './baseRenderStrategy.js'; +import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js'; + +const enum Constants { + IndicesPerCell = 6, +} + +const enum CellBufferInfo { + FloatsPerEntry = 6, + BytesPerEntry = CellBufferInfo.FloatsPerEntry * 4, + Offset_X = 0, + Offset_Y = 1, + Offset_Unused1 = 2, + Offset_Unused2 = 3, + GlyphIndex = 4, + TextureIndex = 5, +} + +// TODO: Handle these dynamically +const viewportWidth = 500; +const viewportHeight = 35; + +/** + * A render strategy that uploads the content of the entire viewport every frame. + */ +export class ViewportRenderStrategy extends BaseRenderStrategy { + readonly wgsl: string = fullFileRenderStrategyWgsl; + + private _cellBindBuffer!: GPUBuffer; + + /** + * The cell value buffers, these hold the cells and their glyphs. It's double buffers such that + * the thread doesn't block when one is being uploaded to the GPU. + */ + private _cellValueBuffers!: [ArrayBuffer, ArrayBuffer]; + private _activeDoubleBufferIndex: 0 | 1 = 0; + + private _visibleObjectCount: number = 0; + + private _scrollOffsetBindBuffer: GPUBuffer; + private _scrollOffsetValueBuffer: Float32Array; + private _scrollInitialized: boolean = false; + + get bindGroupEntries(): GPUBindGroupEntry[] { + return [ + { binding: BindingId.Cells, resource: { buffer: this._cellBindBuffer } }, + { binding: BindingId.ScrollOffset, resource: { buffer: this._scrollOffsetBindBuffer } } + ]; + } + + constructor( + context: ViewContext, + viewGpuContext: ViewGpuContext, + device: GPUDevice, + ) { + super(context, viewGpuContext, device); + + const bufferSize = viewportHeight * viewportWidth * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; + this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco full file cell buffer', + size: bufferSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + })).object; + this._cellValueBuffers = [ + new ArrayBuffer(bufferSize), + new ArrayBuffer(bufferSize), + ]; + + const scrollOffsetBufferSize = 2; + this._scrollOffsetBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco scroll offset buffer', + size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + })).object; + this._scrollOffsetValueBuffer = new Float32Array(scrollOffsetBufferSize); + } + + // #region Event handlers + + // The primary job of these handlers is to: + // 1. Invalidate the up to date line cache, which will cause the line to be re-rendered when + // it's _within the viewport_. + // 2. Pass relevant events on to the render function so it can force certain line ranges to be + // re-rendered even if they're not in the viewport. For example when a view zone is added, + // there are lines that used to be visible but are no longer, so those ranges must be + // cleared and uploaded to the GPU. + + public override onConfigurationChanged(e: ViewConfigurationChangedEvent): boolean { + const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); + const fontSize = this._context.configuration.options.get(EditorOption.fontSize); + const devicePixelRatio = this._viewGpuContext.devicePixelRatio.get(); + if ( + this._glyphRasterizer.value.fontFamily !== fontFamily || + this._glyphRasterizer.value.fontSize !== fontSize || + this._glyphRasterizer.value.devicePixelRatio !== devicePixelRatio + ) { + this._glyphRasterizer.value = new GlyphRasterizer(fontSize, fontFamily, devicePixelRatio); + } + + return true; + } + + public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean { + return true; + } + + public override onTokensChanged(e: ViewTokensChangedEvent): boolean { + return true; + } + + public override onLinesDeleted(e: ViewLinesDeletedEvent): boolean { + return true; + } + + public override onLinesInserted(e: ViewLinesInsertedEvent): boolean { + return true; + } + + public override onLinesChanged(e: ViewLinesChangedEvent): boolean { + return true; + } + + public override onScrollChanged(e?: ViewScrollChangedEvent): boolean { + const dpr = getActiveWindow().devicePixelRatio; + this._scrollOffsetValueBuffer[0] = (e?.scrollLeft ?? this._context.viewLayout.getCurrentScrollLeft()) * dpr; + this._scrollOffsetValueBuffer[1] = (e?.scrollTop ?? this._context.viewLayout.getCurrentScrollTop()) * dpr; + this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer); + return true; + } + + public override onThemeChanged(e: ViewThemeChangedEvent): boolean { + return true; + } + + public override onLineMappingChanged(e: ViewLineMappingChangedEvent): boolean { + return true; + } + + public override onZonesChanged(e: ViewZonesChangedEvent): boolean { + return true; + } + + // #endregion + + reset() { + for (const bufferIndex of [0, 1]) { + // Zero out buffer and upload to GPU to prevent stale rows from rendering + const buffer = new Float32Array(this._cellValueBuffers[bufferIndex]); + buffer.fill(0, 0, buffer.length); + this._device.queue.writeBuffer(this._cellBindBuffer, 0, buffer.buffer, 0, buffer.byteLength); + } + } + + update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number { + // IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the + // loop. This is done so we don't need to trust the JIT compiler to do this optimization to + // avoid potential additional blocking time in garbage collector which is a common cause of + // dropped frames. + + let chars = ''; + let segment: string | undefined; + let charWidth = 0; + let y = 0; + let x = 0; + let absoluteOffsetX = 0; + let absoluteOffsetY = 0; + let tabXOffset = 0; + let glyph: Readonly; + let cellIndex = 0; + + let tokenStartIndex = 0; + let tokenEndIndex = 0; + let tokenMetadata = 0; + + let decorationStyleSetBold: boolean | undefined; + let decorationStyleSetColor: number | undefined; + let decorationStyleSetOpacity: number | undefined; + + let lineData: ViewLineRenderingData; + let decoration: InlineDecoration; + let fillStartIndex = 0; + let fillEndIndex = 0; + + let tokens: IViewLineTokens; + + const dpr = getActiveWindow().devicePixelRatio; + let contentSegmenter: IContentSegmenter; + + if (!this._scrollInitialized) { + this.onScrollChanged(); + this._scrollInitialized = true; + } + + // Zero out cell buffer + const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]); + cellBuffer.fill(0); + + const lineIndexCount = viewportWidth * Constants.IndicesPerCell; + + for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) { + + // Only attempt to render lines that the GPU renderer can handle + if (!this._viewGpuContext.canRender(viewLineOptions, viewportData, y)) { + continue; + } + + lineData = viewportData.getViewLineRenderingData(y); + tabXOffset = 0; + + contentSegmenter = createContentSegmenter(lineData, viewLineOptions); + charWidth = viewLineOptions.spaceWidth * dpr; + absoluteOffsetX = 0; + + tokens = lineData.tokens; + tokenStartIndex = lineData.minColumn - 1; + tokenEndIndex = 0; + for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) { + tokenEndIndex = tokens.getEndOffset(tokenIndex); + if (tokenEndIndex <= tokenStartIndex) { + // The faux indent part of the line should have no token type + continue; + } + + tokenMetadata = tokens.getMetadata(tokenIndex); + + for (x = tokenStartIndex; x < tokenEndIndex; x++) { + // TODO: This needs to move to a dynamic long line rendering strategy + if (x > viewportWidth) { + break; + } + segment = contentSegmenter.getSegmentAtIndex(x); + if (segment === undefined) { + continue; + } + chars = segment; + + if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) { + charWidth = this._glyphRasterizer.value.getTextMetrics(chars).width; + } + + decorationStyleSetColor = undefined; + decorationStyleSetBold = undefined; + decorationStyleSetOpacity = undefined; + + // Apply supported inline decoration styles to the cell metadata + for (decoration of lineData.inlineDecorations) { + // This is Range.strictContainsPosition except it works at the cell level, + // it's also inlined to avoid overhead. + if ( + (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) || + (y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) || + (y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1) + ) { + continue; + } + + const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.inlineClassName); + for (const rule of rules) { + for (const r of rule.style) { + const value = rule.styleMap.get(r)?.toString() ?? ''; + switch (r) { + case 'color': { + // TODO: This parsing and error handling should move into canRender so fallback + // to DOM works + const parsedColor = Color.Format.CSS.parse(value); + if (!parsedColor) { + throw new BugIndicatingError('Invalid color format ' + value); + } + decorationStyleSetColor = parsedColor.toNumber32Bit(); + break; + } + case 'font-weight': { + const parsedValue = parseCssFontWeight(value); + if (parsedValue >= 400) { + decorationStyleSetBold = true; + // TODO: Set bold (https://github.com/microsoft/vscode/issues/237584) + } else { + decorationStyleSetBold = false; + // TODO: Set normal (https://github.com/microsoft/vscode/issues/237584) + } + break; + } + case 'opacity': { + const parsedValue = parseCssOpacity(value); + decorationStyleSetOpacity = parsedValue; + break; + } + default: throw new BugIndicatingError('Unexpected inline decoration style'); + } + } + } + } + + if (chars === ' ' || chars === '\t') { + // Zero out glyph to ensure it doesn't get rendered + cellIndex = ((y - 1) * viewportWidth + x) * Constants.IndicesPerCell; + cellBuffer.fill(0, cellIndex, cellIndex + CellBufferInfo.FloatsPerEntry); + // Adjust xOffset for tab stops + if (chars === '\t') { + // Find the pixel offset between the current position and the next tab stop + const offsetBefore = x + tabXOffset; + tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize); + absoluteOffsetX += charWidth * (tabXOffset - offsetBefore); + // Convert back to offset excluding x and the current character + tabXOffset -= x + 1; + } else { + absoluteOffsetX += charWidth; + } + continue; + } + + const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity); + glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer.value, chars, tokenMetadata, decorationStyleSetId); + + absoluteOffsetY = Math.round( + // Top of layout box (includes line height) + viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] * dpr + + + // Delta from top of layout box (includes line height) to top of the inline box (no line height) + Math.floor((viewportData.lineHeight * dpr - (glyph.fontBoundingBoxAscent + glyph.fontBoundingBoxDescent)) / 2) + + + // Delta from top of inline box (no line height) to top of glyph origin. If the glyph was drawn + // with a top baseline for example, this ends up drawing the glyph correctly using the alphabetical + // baseline. + glyph.fontBoundingBoxAscent + ); + + cellIndex = ((y - viewportData.startLineNumber) * viewportWidth + x) * Constants.IndicesPerCell; + cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.round(absoluteOffsetX); + cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = absoluteOffsetY; + cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex; + cellBuffer[cellIndex + CellBufferInfo.TextureIndex] = glyph.pageIndex; + + // Adjust the x pixel offset for the next character + absoluteOffsetX += charWidth; + } + + tokenStartIndex = tokenEndIndex; + } + + // Clear to end of line + fillStartIndex = ((y - viewportData.startLineNumber) * viewportWidth + tokenEndIndex) * Constants.IndicesPerCell; + fillEndIndex = ((y - viewportData.startLineNumber) * viewportWidth) * Constants.IndicesPerCell; + cellBuffer.fill(0, fillStartIndex, fillEndIndex); + } + + const visibleObjectCount = (viewportData.endLineNumber - viewportData.startLineNumber + 1) * lineIndexCount; + + // This render strategy always uploads the whole viewport + this._device.queue.writeBuffer( + this._cellBindBuffer, + 0, + cellBuffer.buffer, + 0, + (viewportData.endLineNumber - viewportData.startLineNumber) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT + ); + + this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1; + + this._visibleObjectCount = visibleObjectCount; + return visibleObjectCount; + } + + draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void { + if (this._visibleObjectCount <= 0) { + throw new BugIndicatingError('Attempt to draw 0 objects'); + } + pass.draw( + quadVertices.length / 2, + this._visibleObjectCount, + ); + } +} + +function parseCssFontWeight(value: string) { + switch (value) { + case 'lighter': + case 'normal': return 400; + case 'bolder': + case 'bold': return 700; + } + return parseInt(value); +} + +function parseCssOpacity(value: string): number { + if (value.endsWith('%')) { + return parseFloat(value.substring(0, value.length - 1)) / 100; + } + if (value.match(/^\d+(?:\.\d*)/)) { + return parseFloat(value); + } + return 1; +} diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index f2fac0bcc89b..a1915e600dba 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -31,7 +31,7 @@ export class ViewGpuContext extends Disposable { * The temporary hard cap for lines rendered by the GPU renderer. This can be removed once more * dynamic allocation is implemented in https://github.com/microsoft/vscode/issues/227091 */ - readonly maxGpuLines = FullFileRenderStrategy.maxSupportedLines; + readonly maxGpuLines = 10000;//FullFileRenderStrategy.maxSupportedLines; /** * The temporary hard cap for line columns rendered by the GPU renderer. This can be removed diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index 113904c6a005..a9fa075b556d 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -14,7 +14,6 @@ import { Range } from '../../../common/core/range.js'; import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; import type { ViewContext } from '../../../common/viewModel/viewContext.js'; import { TextureAtlasPage } from '../../gpu/atlas/textureAtlasPage.js'; -import { FullFileRenderStrategy } from '../../gpu/renderStrategy/fullFileRenderStrategy.js'; import { BindingId, type IGpuRenderStrategy } from '../../gpu/gpu.js'; import { GPULifecycle } from '../../gpu/gpuDisposable.js'; import { quadVertices } from '../../gpu/gpuUtils.js'; @@ -26,6 +25,8 @@ import type * as viewEvents from '../../../common/viewEvents.js'; import { CursorColumns } from '../../../common/core/cursorColumns.js'; import { TextureAtlas } from '../../gpu/atlas/textureAtlas.js'; import { createContentSegmenter, type IContentSegmenter } from '../../gpu/contentSegmenter.js'; +import { ViewportRenderStrategy } from '../../gpu/renderStrategy/viewportRenderStrategy.js'; +import { FullFileRenderStrategy } from '../../gpu/renderStrategy/fullFileRenderStrategy.js'; const enum GlyphStorageBufferInfo { FloatsPerEntry = 2 + 2 + 2, @@ -188,7 +189,8 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { // #region Storage buffers - this._renderStrategy = this._register(this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._viewGpuContext, this._device)); + // this._renderStrategy = this._register(this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._viewGpuContext, this._device)); + this._renderStrategy = this._register(this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device)); this._glyphStorageBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco glyph storage buffer', From 6e3d47802dfcbc4230c5387dbd42675cdd64d9c2 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:10:04 +0100 Subject: [PATCH 0741/3587] SCM - fix `scm.historyProviderCount` context key (#238402) --- src/vs/workbench/contrib/scm/common/scmService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index 14c69f73b0d7..bd3c282b499e 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -388,7 +388,7 @@ export class SCMService implements ISCMService { const historyProviderCount = () => { return Array.from(this._repositories.values()) - .filter(r => !!r.provider.historyProvider).length; + .filter(r => !!r.provider.historyProvider.get()).length; }; disposables.add(toDisposable(() => { From 8843616d9c7fa6b989aff5edbcee0151458d06ff Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 21 Jan 2025 13:39:02 -0800 Subject: [PATCH 0742/3587] bump typescript version for building vscode --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3669fef0d86f..5470da4c37a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,7 +154,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^5.8.0-dev.20250114", + "typescript": "^5.8.0-dev.20250121", "typescript-eslint": "^8.8.0", "util": "^0.12.4", "webpack": "^5.94.0", @@ -17611,9 +17611,9 @@ "dev": true }, "node_modules/typescript": { - "version": "5.8.0-dev.20250114", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.0-dev.20250114.tgz", - "integrity": "sha512-DGtuEPL692JPjTQHFmP810EklYi8ndHgCWt61kRjQfaO25LFpxuB9ZNVs79u15t9JpkeIB6WLKpjY/JiRCzYMw==", + "version": "5.8.0-dev.20250121", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.0-dev.20250121.tgz", + "integrity": "sha512-cRbtTDQVu4ZV28kEzT6EgGd1mT75C92i/5pFcQHVqa60S2HhbFbY+4Wpo0ebumER/YMoC63sfTzruaR2Z6kOJw==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 8aa946734a94..a8de9c8ac2d1 100644 --- a/package.json +++ b/package.json @@ -212,7 +212,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^5.8.0-dev.20250114", + "typescript": "^5.8.0-dev.20250121", "typescript-eslint": "^8.8.0", "util": "^0.12.4", "webpack": "^5.94.0", From 4ff9a0ec074b98780cf79538bc52955af862e6cb Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 21 Jan 2025 16:02:57 -0600 Subject: [PATCH 0743/3587] show spec info w priority over path --- extensions/terminal-suggest/src/terminalSuggestMain.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 6072cd048f4c..8ba754efab23 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -136,7 +136,6 @@ export async function activate(context: vscode.ExtensionContext) { const result = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, terminal.shellIntegration?.cwd, token); if (result.cwd && (result.filesRequested || result.foldersRequested)) { - // const cwd = resolveCwdFromPrefix(prefix, terminal.shellIntegration?.cwd) ?? terminal.shellIntegration?.cwd; return new vscode.TerminalCompletionList(result.items, { filesRequested: result.filesRequested, foldersRequested: result.foldersRequested, cwd: result.cwd, pathSeparator: isWindows ? '\\' : '/' }); } return result.items; @@ -345,7 +344,7 @@ export async function getCompletionItemsFromSpecs( || !!firstCommand && specLabel.startsWith(firstCommand) ) { // push it to the completion items - items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: specLabel }, availableCommand.detail, getDescription(spec))); + items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: specLabel }, getDescription(spec), availableCommand.detail)); } if (!terminalContext.commandLine.startsWith(specLabel)) { From 61e52c3265eb2ede6a71033c0999bd434405ec1c Mon Sep 17 00:00:00 2001 From: Aman Karmani Date: Tue, 21 Jan 2025 14:21:13 -0800 Subject: [PATCH 0744/3587] tsb: fix for deleted and re-added source file not being re-generated --- build/lib/tsb/builder.js | 1 + build/lib/tsb/builder.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/build/lib/tsb/builder.js b/build/lib/tsb/builder.js index e7a2519d1c92..df7bd6f25e5b 100644 --- a/build/lib/tsb/builder.js +++ b/build/lib/tsb/builder.js @@ -44,6 +44,7 @@ function createTypeScriptBuilder(config, projectFile, cmd) { } if (!file.contents) { host.removeScriptSnapshot(file.path); + delete lastBuildVersion[normalize(file.path)]; } else { host.addScriptSnapshot(file.path, new VinylScriptSnapshot(file)); diff --git a/build/lib/tsb/builder.ts b/build/lib/tsb/builder.ts index 509284d0cdcb..5d7a882978c9 100644 --- a/build/lib/tsb/builder.ts +++ b/build/lib/tsb/builder.ts @@ -65,6 +65,7 @@ export function createTypeScriptBuilder(config: IConfiguration, projectFile: str if (!file.contents) { host.removeScriptSnapshot(file.path); + delete lastBuildVersion[normalize(file.path)]; } else { host.addScriptSnapshot(file.path, new VinylScriptSnapshot(file)); } From fd52e5736a41211c4f1217f2be33951bc106477f Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Tue, 21 Jan 2025 16:07:18 -0800 Subject: [PATCH 0745/3587] enforce foreground color for codicon in notebook list (#238416) --- src/vs/workbench/contrib/notebook/browser/media/notebook.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 26e4ba6612f5..db5cc0157382 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -335,7 +335,7 @@ } .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row .codicon:not(.suggest-icon) { - color: inherit; + color: var(--vscode-icon-foreground); } .monaco-workbench .notebookOverlay > .cell-list-container .notebook-overview-ruler-container { From 7e17ab7219c7d77b6ceb619f6beb51e75156b70b Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:19:47 -0800 Subject: [PATCH 0746/3587] filter out image attachments in chat history (#238419) filter out image attachments in history --- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 1cefed6c759c..0585b3012a47 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -381,7 +381,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.getInputState = (): IChatInputState => { return { ...getContribsInputState(), - chatContextAttachments: this._attachmentModel.attachments, + chatContextAttachments: this._attachmentModel.attachments.filter(attachment => !attachment.isImage), }; }; this.inputEditorMaxHeight = this.options.renderStyle === 'compact' ? INPUT_EDITOR_MAX_HEIGHT / 3 : INPUT_EDITOR_MAX_HEIGHT; From 649d3e29c37aa7a4a7746b43c3c5fa511a530ab1 Mon Sep 17 00:00:00 2001 From: Aman Karmani Date: Tue, 21 Jan 2025 16:46:16 -0800 Subject: [PATCH 0747/3587] build: update to include more tsc boilerplate --- build/lib/bundle.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/lib/bundle.ts b/build/lib/bundle.ts index 58995b7d5d1b..47a686d00477 100644 --- a/build/lib/bundle.ts +++ b/build/lib/bundle.ts @@ -358,7 +358,7 @@ function removeAllDuplicateTSBoilerplate(destFiles: IConcatFile[]): IConcatFile[ } export function removeAllTSBoilerplate(source: string) { - const seen = new Array(BOILERPLATE.length).fill(true, 0, 10); + const seen = new Array(BOILERPLATE.length).fill(true, 0, BOILERPLATE.length); return removeDuplicateTSBoilerplate(source, seen); } @@ -374,6 +374,8 @@ const BOILERPLATE = [ { start: /^var __createBinding/, end: /^}\)\);$/ }, { start: /^var __setModuleDefault/, end: /^}\);$/ }, { start: /^var __importStar/, end: /^};$/ }, + { start: /^var __addDisposableResource/, end: /^};$/ }, + { start: /^var __disposeResources/, end: /^}\);$/ }, ]; function removeDuplicateTSBoilerplate(source: string, SEEN_BOILERPLATE: boolean[] = []): string { From 085e4e398a3471ca2d52c0c97b5a8d6ee6ce6087 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Tue, 21 Jan 2025 16:47:59 -0800 Subject: [PATCH 0748/3587] Layout the container on show (#238421) Decent enough fix until I do the "vm scaling" fix. Fixes https://github.com/microsoft/vscode/issues/238196 --- .../browser/quickInputController.ts | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 68c9a9cd8089..1196dddd8923 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -343,7 +343,8 @@ export class QuickInputController extends Disposable { node: headerContainer, includeChildren: false } - ] + ], + this.viewState )); // DnD update layout @@ -666,6 +667,7 @@ export class QuickInputController extends Disposable { ui.container.style.display = ''; this.updateLayout(); + this.dndController?.layoutContainer(); ui.inputBox.setFocus(); this.quickInputTypeContext.set(controller.type); } @@ -904,18 +906,32 @@ class QuickInputDragAndDropController extends Disposable { private _container: HTMLElement, private readonly _quickInputContainer: HTMLElement, private _quickInputDragAreas: { node: HTMLElement; includeChildren: boolean }[], + initialViewState: QuickInputViewState | undefined, @ILayoutService private readonly _layoutService: ILayoutService, @IContextKeyService private readonly _contextKeyService: IContextKeyService ) { super(); this._registerLayoutListener(); this.registerMouseListeners(); + this.dndViewState.set({ ...initialViewState, done: true }, undefined); } reparentUI(container: HTMLElement): void { this._container = container; } + layoutContainer(dimension = this._layoutService.activeContainerDimension): void { + const state = this.dndViewState.get(); + const dragAreaRect = this._quickInputContainer.getBoundingClientRect(); + if (state?.top && state?.left) { + const a = Math.round(state.left * 1e2) / 1e2; + const b = dimension.width; + const c = dragAreaRect.width; + const d = a * b - c / 2; + this._layout(state.top * dimension.height, d); + } + } + setAlignment(alignment: 'top' | 'center' | { top: number; left: number }, done = true): void { if (alignment === 'top') { this.dndViewState.set({ @@ -938,20 +954,7 @@ class QuickInputDragAndDropController extends Disposable { } private _registerLayoutListener() { - this._layoutService.onDidLayoutContainer((e) => { - if (e.container !== this._container) { - return; - } - const state = this.dndViewState.get(); - const dragAreaRect = this._quickInputContainer.getBoundingClientRect(); - if (state?.top && state?.left) { - const a = Math.round(state.left * 1e2) / 1e2; - const b = e.dimension.width; - const c = dragAreaRect.width; - const d = a * b - c / 2; - this._layout(state.top * e.dimension.height, d); - } - }); + this._register(Event.filter(this._layoutService.onDidLayoutContainer, e => e.container === this._container)((e) => this.layoutContainer(e.dimension))); } private registerMouseListeners(): void { From ce19b614c54a99c90805a0aac48cff16bf5c4fb4 Mon Sep 17 00:00:00 2001 From: Aman Karmani Date: Tue, 21 Jan 2025 17:09:00 -0800 Subject: [PATCH 0749/3587] build: re-generate --- build/lib/bundle.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/lib/bundle.js b/build/lib/bundle.js index 627b9966700e..7f7e55963acb 100644 --- a/build/lib/bundle.js +++ b/build/lib/bundle.js @@ -224,7 +224,7 @@ function removeAllDuplicateTSBoilerplate(destFiles) { return destFiles; } function removeAllTSBoilerplate(source) { - const seen = new Array(BOILERPLATE.length).fill(true, 0, 10); + const seen = new Array(BOILERPLATE.length).fill(true, 0, BOILERPLATE.length); return removeDuplicateTSBoilerplate(source, seen); } // Taken from typescript compiler => emitFiles @@ -239,6 +239,8 @@ const BOILERPLATE = [ { start: /^var __createBinding/, end: /^}\)\);$/ }, { start: /^var __setModuleDefault/, end: /^}\);$/ }, { start: /^var __importStar/, end: /^};$/ }, + { start: /^var __addDisposableResource/, end: /^};$/ }, + { start: /^var __disposeResources/, end: /^}\);$/ }, ]; function removeDuplicateTSBoilerplate(source, SEEN_BOILERPLATE = []) { const lines = source.split(/\r\n|\n|\r/); From 9eb1f787c699459b2bf37a73c395166b0174d3e8 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 21 Jan 2025 17:13:41 -0800 Subject: [PATCH 0750/3587] Add "experimental" label to agent UI bits (#238424) --- .../contrib/chat/browser/actions/chatExecuteActions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 9b44062187db..82fdf00c6394 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -90,7 +90,7 @@ export class ToggleAgentModeAction extends Action2 { constructor() { super({ id: ToggleAgentModeAction.ID, - title: localize2('interactive.toggleAgent.label', "Toggle Agent Mode"), + title: localize2('interactive.toggleAgent.label', "Toggle Agent Mode (Experimental)"), f1: true, category: CHAT_CATEGORY, precondition: ContextKeyExpr.and( @@ -100,7 +100,7 @@ export class ToggleAgentModeAction extends Action2 { toggled: { condition: ChatContextKeys.Editing.agentMode, icon: Codicon.tools, - tooltip: localize('agentEnabled', "Agent Mode Enabled"), + tooltip: localize('agentEnabled', "Agent Mode Enabled (Experimental)"), }, tooltip: localize('agentDisabled', "Agent Mode Disabled"), keybinding: { From b69a02f8b42e3147207374625b1d244a544d74bb Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 21 Jan 2025 20:32:12 -0600 Subject: [PATCH 0751/3587] Restrict list scroll to option, `scrollToActiveElement` (#238425) Restrict list scroll to option --- src/vs/base/browser/ui/list/listView.ts | 9 +++++++-- src/vs/platform/list/browser/listService.ts | 1 + src/vs/workbench/contrib/chat/browser/chatWidget.ts | 1 + .../contrib/preferences/browser/settingsTree.ts | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 4e065bf3539d..0befe20b8c01 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -87,6 +87,7 @@ export interface IListViewOptions extends IListViewOptionsUpdate { readonly transformOptimization?: boolean; readonly alwaysConsumeMouseWheel?: boolean; readonly initialSize?: Dimension; + readonly scrollToActiveElement?: boolean; } const DefaultOptions = { @@ -445,7 +446,9 @@ export class ListView implements IListView { const element = (e.target as HTMLElement); const scrollValue = element.scrollTop; element.scrollTop = 0; - this.setScrollTop(this.scrollTop + scrollValue); + if (options.scrollToActiveElement) { + this.setScrollTop(this.scrollTop + scrollValue); + } })); this.disposables.add(addDisposableListener(this.domNode, 'dragover', e => this.onDragOver(this.toDragEvent(e)))); @@ -465,7 +468,9 @@ export class ListView implements IListView { this.dnd = options.dnd ?? this.disposables.add(DefaultOptions.dnd); this.layout(options.initialSize?.height, options.initialSize?.width); - this._setupFocusObserver(container); + if (options.scrollToActiveElement) { + this._setupFocusObserver(container); + } } private _setupFocusObserver(container: HTMLElement): void { diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 4bedb6c7de09..a92048240590 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -865,6 +865,7 @@ export interface IWorkbenchObjectTreeOptions extends IObjectTree readonly accessibilityProvider: IListAccessibilityProvider; readonly overrideStyles?: IStyleOverride; readonly selectionNavigation?: boolean; + readonly scrollToActiveElement?: boolean; } export class WorkbenchObjectTree, TFilterData = void> extends ObjectTree { diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 0da2466b9d9c..88d43a4181e6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -673,6 +673,7 @@ export class ChatWidget extends Disposable implements IChatWidget { keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: ChatTreeItem) => isRequestVM(e) ? e.message : isResponseVM(e) ? e.response.value : '' }, // TODO setRowLineHeight: false, filter: this.viewOptions.filter ? { filter: this.viewOptions.filter.bind(this.viewOptions), } : undefined, + scrollToActiveElement: true, overrideStyles: { listFocusBackground: this.styles.listBackground, listInactiveFocusBackground: this.styles.listBackground, diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index eb600aa17e98..e44793b308e2 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -2560,6 +2560,7 @@ export class SettingsTree extends WorkbenchObjectTree { { horizontalScrolling: false, supportDynamicHeights: true, + scrollToActiveElement: true, identityProvider: { getId(e) { return e.id; From 8dd608208b21d4b5052b51a9785151f682548024 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 21 Jan 2025 18:34:59 -0800 Subject: [PATCH 0752/3587] [instructions]: restrict file list in the picker to `.prompt.md` files only (#238433) --- .../chatAttachmentModel/chatInstructionsFileLocator.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts index e31f5e0fe832..a0305c9598bc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts @@ -8,6 +8,7 @@ import { dirname, extUri } from '../../../../../base/common/resources.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js'; +import { PROMP_SNIPPET_FILE_EXTENSION } from '../../common/promptSyntax/contentProviders/promptContentsProviderBase.js'; /** * Configuration setting name for the prompt instructions source folder paths. @@ -19,11 +20,6 @@ const PROMPT_FILES_LOCATION_SETTING_NAME = 'chat.experimental.prompt-files.locat */ const PROMPT_FILES_DEFAULT_LOCATION = ['.copilot/prompts']; -/** - * Extension of the prompt instructions files. - */ -const INSTRUCTIONS_FILE_EXTENSION = '.md'; - /** * Class to locate prompt instructions files. */ @@ -164,7 +160,7 @@ export class ChatInstructionsFileLocator { continue; } - if (!name.endsWith(INSTRUCTIONS_FILE_EXTENSION)) { + if (!name.endsWith(PROMP_SNIPPET_FILE_EXTENSION)) { continue; } From 27c4a2945328727a6475394cd6205f3ee25b9a49 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 21 Jan 2025 18:55:04 -0800 Subject: [PATCH 0753/3587] rename attachments picker item (#238428) [instructions]: rename `Prompt Instructions` to `Instructions` in the attachments picker --- .../contrib/chat/browser/actions/chatContextActions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index b1e1de068893..2535ac2888dd 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -827,12 +827,12 @@ export class AttachContextAction extends Action2 { } // if the `prompt instructions` feature is enabled, add - // the `Prompt Instructions` attachment type to the list + // the `Instructions` attachment type to the list if (widget.attachmentModel.promptInstructions.featureEnabled) { quickPickItems.push({ kind: 'prompt-instructions', id: 'prompt-instructions', - label: localize('chatContext.promptInstructions', 'Prompt Instructions'), + label: localize('chatContext.promptInstructions', 'Instructions'), iconClass: ThemeIcon.asClassName(Codicon.lightbulbSparkle), }); } From d226a2a497b928d78aa654f74c8af5317d3becfb Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 21 Jan 2025 20:03:40 -0800 Subject: [PATCH 0754/3587] [prompt settings]: remove the `chat.experimental.prompt-instructions.enable` config setting (#238430) [settings]: remove the `chat.experimental.prompt-instructions.enabled` configuration settings in favor of `chat.experimental.prompt-snippets` --- .../chatInstructionAttachmentsModel.ts | 25 ++----------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts index d981e0e655a4..e57cd6fc284b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts @@ -8,15 +8,10 @@ import { Emitter } from '../../../../../base/common/event.js'; import { ChatInstructionsFileLocator } from './chatInstructionsFileLocator.js'; import { ChatInstructionsAttachmentModel } from './chatInstructionsAttachment.js'; import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; +import { BasePromptParser } from '../../common/promptSyntax/parsers/basePromptParser.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -/** - * Configuration setting name for the `prompt instructions` feature. - * Set to `true` to enable the feature for yourself. - */ -const PROMPT_INSTRUCTIONS_SETTING_NAME = 'chat.experimental.prompt-instructions.enabled'; - /** * Model for a collection of prompt instruction attachments. * See {@linkcode ChatInstructionsAttachmentModel}. @@ -141,24 +136,8 @@ export class ChatInstructionAttachmentsModel extends Disposable { /** * Checks if the prompt instructions feature is enabled in the user settings. - * The setting can be set to `true`, `'true'`, `True`, `TRUE`, etc. - * All other values are treated as the `false` value, which is the `default`. */ public get featureEnabled(): boolean { - const value = this.configService.getValue(PROMPT_INSTRUCTIONS_SETTING_NAME); - - if (!value) { - return false; - } - - if (value === true) { - return true; - } - - if (typeof value === 'string' && value.toLowerCase().trim() === 'true') { - return true; - } - - return false; + return BasePromptParser.promptSnippetsEnabled(this.configService); } } From d6d62da51b4570efd0f20bae25e391643ffd9536 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 21 Jan 2025 23:07:01 -0800 Subject: [PATCH 0755/3587] Plumb a requestID and location through the tool invocation to the codemapper request (#238429) To drive codemapper telemetry --- src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts | 4 +++- src/vs/workbench/api/common/extHostCodeMapper.ts | 2 ++ src/vs/workbench/api/common/extHostLanguageModelTools.ts | 7 ++++++- src/vs/workbench/contrib/chat/browser/tools/tools.ts | 4 +++- .../workbench/contrib/chat/common/chatCodeMapperService.ts | 3 ++- .../contrib/chat/common/languageModelToolsService.ts | 1 + src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts | 4 ++++ src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts | 2 ++ 8 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts index eea3e819b976..f447d2ca7552 100644 --- a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts +++ b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts @@ -33,7 +33,9 @@ export class MainThreadChatCodemapper extends Disposable implements MainThreadCo this._responseMap.set(requestId, response); const extHostRequest: ICodeMapperRequestDto = { requestId, - codeBlocks: uiRequest.codeBlocks + codeBlocks: uiRequest.codeBlocks, + chatRequestId: uiRequest.chatRequestId, + location: uiRequest.location }; try { return await this._proxy.$mapCode(handle, extHostRequest, token).then((result) => result ?? undefined); diff --git a/src/vs/workbench/api/common/extHostCodeMapper.ts b/src/vs/workbench/api/common/extHostCodeMapper.ts index 4a421e6f23d7..ffa51e9a7598 100644 --- a/src/vs/workbench/api/common/extHostCodeMapper.ts +++ b/src/vs/workbench/api/common/extHostCodeMapper.ts @@ -42,6 +42,8 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape }; const request: vscode.MappedEditsRequest = { + location: internalRequest.location, + chatRequestId: internalRequest.chatRequestId, codeBlocks: internalRequest.codeBlocks.map(block => { return { code: block.code, diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index c1bf5e1b1be2..74c4d0aff4d7 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -68,6 +68,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape parameters: options.input, tokenBudget: options.tokenizationOptions?.tokenBudget, context: options.toolInvocationToken as IToolInvocationContext | undefined, + chatRequestId: options.chatRequestId, }, token); return typeConvert.LanguageModelToolResult.to(result); } finally { @@ -100,7 +101,11 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape throw new Error(`Unknown tool ${dto.toolId}`); } - const options: vscode.LanguageModelToolInvocationOptions = { input: dto.parameters, toolInvocationToken: dto.context as vscode.ChatParticipantToolToken | undefined }; + const options: vscode.LanguageModelToolInvocationOptions = { + input: dto.parameters, + toolInvocationToken: dto.context as vscode.ChatParticipantToolToken | undefined, + chatRequestId: dto.chatRequestId, + }; if (dto.tokenBudget !== undefined) { options.tokenizationOptions = { tokenBudget: dto.tokenBudget, diff --git a/src/vs/workbench/contrib/chat/browser/tools/tools.ts b/src/vs/workbench/contrib/chat/browser/tools/tools.ts index d8a4e4459ddd..b1a209f23625 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/tools.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/tools.ts @@ -134,7 +134,9 @@ class EditTool implements IToolImpl { } const result = await this.codeMapperService.mapCode({ - codeBlocks: [{ code: parameters.code, resource: uri, markdownBeforeBlock: parameters.explanation }] + codeBlocks: [{ code: parameters.code, resource: uri, markdownBeforeBlock: parameters.explanation }], + location: 'tool', + chatRequestId: invocation.chatRequestId }, { textEdit: (target, edits) => { model.acceptResponseProgress(request, { kind: 'textEdit', uri: target, edits }); diff --git a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts index 809a37714805..386758af35e2 100644 --- a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts +++ b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts @@ -21,6 +21,8 @@ export interface ICodeMapperCodeBlock { export interface ICodeMapperRequest { readonly codeBlocks: ICodeMapperCodeBlock[]; + readonly chatRequestId?: string; + readonly location?: string; } export interface ICodeMapperResult { @@ -69,4 +71,3 @@ export class CodeMapperService implements ICodeMapperService { return undefined; } } - diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index f69be4fed5c6..bc80d7c62866 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -34,6 +34,7 @@ export interface IToolInvocation { parameters: Object; tokenBudget?: number; context: IToolInvocationContext | undefined; + chatRequestId?: string; } export interface IToolInvocationContext { diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index d124c4dde5af..4f08ef3a4a47 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -118,4 +118,8 @@ declare module 'vscode' { export interface LanguageModelIgnoredFileProvider { provideFileIgnored(uri: Uri, token: CancellationToken): ProviderResult; } + + export interface LanguageModelToolInvocationOptions { + chatRequestId?: string; + } } diff --git a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts index b677ab05a49f..ac72b52ff37f 100644 --- a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts @@ -73,6 +73,8 @@ declare module 'vscode' { */ export interface MappedEditsRequest { readonly codeBlocks: { code: string; resource: Uri; markdownBeforeBlock?: string }[]; + readonly location?: string; + readonly chatRequestId?: string; } export interface MappedEditsResponseStream { From 63c32041550f526569094bdd78a938a37c94d0f4 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 22 Jan 2025 08:50:30 +0100 Subject: [PATCH 0756/3587] Fix deletion decoration logic in inline diff view (#238377) fix deletion decoration --- .../browser/view/inlineEdits/inlineDiffView.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts index 796e21e260f0..a910c8854d73 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts @@ -15,7 +15,7 @@ import { EditorOption } from '../../../../../common/config/editorOptions.js'; import { Range } from '../../../../../common/core/range.js'; import { AbstractText } from '../../../../../common/core/textEdit.js'; import { DetailedLineRangeMapping } from '../../../../../common/diff/rangeMapping.js'; -import { IModelDeltaDecoration, ITextModel } from '../../../../../common/model.js'; +import { EndOfLinePreference, IModelDeltaDecoration, ITextModel } from '../../../../../common/model.js'; import { ModelDecorationOptions } from '../../../../../common/model/textModel.js'; import { InlineDecoration, InlineDecorationType } from '../../../../../common/viewModel.js'; import { classNames } from './utils.js'; @@ -184,6 +184,7 @@ export class OriginalEditorInlineDiffView extends Disposable { for (const i of m.innerChanges || []) { // Don't show empty markers outside the line range if (m.original.contains(i.originalRange.startLineNumber)) { + const replacedText = this._originalEditor.getModel()?.getValueInRange(i.originalRange, EndOfLinePreference.LF); originalDecorations.push({ range: i.originalRange, options: { @@ -193,7 +194,7 @@ export class OriginalEditorInlineDiffView extends Disposable { 'inlineCompletions-char-delete', i.originalRange.isSingleLine() && diff.mode === 'ghostText' && 'single-line-inline', i.originalRange.isEmpty() && 'empty', - ((i.originalRange.isEmpty() || diff.mode === 'deletion') && showEmptyDecorations && !useInlineDiff) && 'diff-range-empty' + ((i.originalRange.isEmpty() || diff.mode === 'deletion' && replacedText === '\n') && showEmptyDecorations && !useInlineDiff) && 'diff-range-empty' ), inlineClassName: useInlineDiff ? classNames('strike-through', 'inlineCompletions') : null, zIndex: 1 From 3aece83761acf67703acbb429cd3f5513033d1f5 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 22 Jan 2025 09:13:23 +0100 Subject: [PATCH 0757/3587] wip - inline chat via editing session --- src/vs/workbench/contrib/chat/browser/chat.ts | 1 + .../contrib/chat/browser/chatInputPart.ts | 3 +- .../contrib/chat/browser/chatWidget.ts | 3 +- .../contrib/chat/common/chatService.ts | 2 +- .../chat/test/common/mockChatService.ts | 2 +- .../browser/inlineChat.contribution.ts | 4 + .../browser/inlineChatController.ts | 6 +- .../browser/inlineChatController2.ts | 249 ++++++++++++++++++ .../browser/inlineChatSessionService.ts | 13 + .../browser/inlineChatSessionServiceImpl.ts | 66 ++++- .../browser/inlineChatZoneWidget.ts | 6 +- .../contrib/inlineChat/common/inlineChat.ts | 1 + 12 files changed, 341 insertions(+), 15 deletions(-) create mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index ef6da957b264..080f2b23c151 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -187,6 +187,7 @@ export interface IChatWidgetViewOptions { defaultElementHeight?: number; editorOverflowWidgetsDomNode?: HTMLElement; enableImplicitContext?: boolean; + enableWorkingSet?: boolean; } export interface IChatViewViewContext { diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 0585b3012a47..16f62acef2b6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -125,6 +125,7 @@ interface IChatInputPartOptions { }; editorOverflowWidgetsDomNode?: HTMLElement; enableImplicitContext?: boolean; + enableWorkingSet?: boolean; } export interface IWorkingSetEntry { @@ -1138,7 +1139,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge async renderChatEditingSessionState(chatEditingSession: IChatEditingSession | null, chatWidget?: IChatWidget) { dom.setVisibility(Boolean(chatEditingSession), this.chatEditingSessionWidgetContainer); - if (!chatEditingSession) { + if (!chatEditingSession || this.options.enableWorkingSet === false) { dom.clearNode(this.chatEditingSessionWidgetContainer); this._chatEditsDisposables.clear(); this._chatEditList = undefined; diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 88d43a4181e6..ad10fd0adea0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -763,7 +763,8 @@ 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 + enableImplicitContext: this.viewOptions.enableImplicitContext, + enableWorkingSet: this.viewOptions.enableWorkingSet }, this.styles, () => this.collectInputState() diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 20089cb5315a..e6974becb638 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -446,7 +446,7 @@ export interface IChatService { isEnabled(location: ChatAgentLocation): boolean; hasSessions(): boolean; - startSession(location: ChatAgentLocation, token: CancellationToken): ChatModel | undefined; + startSession(location: ChatAgentLocation, token: CancellationToken): ChatModel; getSession(sessionId: string): IChatModel | undefined; getOrRestoreSession(sessionId: string): IChatModel | undefined; loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModel | undefined; diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index 31fbb654b635..841b65ccc085 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -26,7 +26,7 @@ export class MockChatService implements IChatService { getProviderInfos(): IChatProviderInfo[] { throw new Error('Method not implemented.'); } - startSession(location: ChatAgentLocation, token: CancellationToken): ChatModel | undefined { + startSession(location: ChatAgentLocation, token: CancellationToken): ChatModel { throw new Error('Method not implemented.'); } addSession(session: IChatModel): void { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 3572a0801b35..c6c0e99cdca2 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -25,7 +25,11 @@ import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { InlineChatAccessibilityHelp } from './inlineChatAccessibilityHelp.js'; import { InlineChatExpandLineAction, InlineChatHintsController, HideInlineChatHintAction, ShowInlineChatHintAction } from './inlineChatCurrentLine.js'; +import { InlineChatController2, StartSessionAction2, StopSessionAction2 } from './inlineChatController2.js'; +registerEditorContribution(InlineChatController2.ID, InlineChatController2, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors +registerAction2(StartSessionAction2); +registerAction2(StopSessionAction2); // --- browser diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index cbd575f7fde4..f5c49deac93d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -188,7 +188,7 @@ export class InlineChatController implements IEditorContribution { } } - const zone = _instaService.createInstance(InlineChatZoneWidget, location, this._editor); + const zone = _instaService.createInstance(InlineChatZoneWidget, location, undefined, this._editor); this._store.add(zone); this._store.add(zone.widget.chatWidget.onDidClear(async () => { const r = this.joinCurrentRun(); @@ -1145,10 +1145,6 @@ export class InlineChatController implements IEditorContribution { const uri = this._editor.getModel().uri; const chatModel = this._chatService.startSession(ChatAgentLocation.Editor, token); - if (!chatModel) { - return false; - } - const editSession = await this._chatEditingService.createAdhocEditingSession(chatModel.sessionId); // diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts new file mode 100644 index 000000000000..b83695ed642f --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts @@ -0,0 +1,249 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { autorun, observableFromEvent } from '../../../../base/common/observable.js'; +import { assertType } from '../../../../base/common/types.js'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; +import { EditorAction2, ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; +import { ObservableCodeEditor, observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; +import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diffEditor/embeddedDiffEditorWidget.js'; +import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; +import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; +import { localize2 } from '../../../../nls.js'; +import { IAction2Options, MenuId } from '../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; +import { ChatAgentLocation } from '../../chat/common/chatAgents.js'; +import { isRequestVM } from '../../chat/common/chatViewModel.js'; +import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; +import { CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_POSSIBLE, CTX_INLINE_CHAT_VISIBLE } from '../common/inlineChat.js'; +import { IInlineChatSessionService } from './inlineChatSessionService.js'; +import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; + +export class InlineChatController2 implements IEditorContribution { + + static readonly ID = 'editor.contrib.inlineChatController2'; + + static get(editor: ICodeEditor): InlineChatController2 | undefined { + return editor.getContribution(InlineChatController2.ID) ?? undefined; + } + + private readonly _store = new DisposableStore(); + + private readonly _editorObs: ObservableCodeEditor; + + // private readonly _session: IObservable; + + // private readonly _zone: InlineChatZoneWidget; + + constructor( + private readonly _editor: ICodeEditor, + @IInstantiationService private readonly _instaService: IInstantiationService, + @INotebookEditorService private readonly _notebookEditorService: INotebookEditorService, + @IInlineChatSessionService private readonly _inlineChatSessions: IInlineChatSessionService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + + const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); + + const location: IChatWidgetLocationOptions = { + location: ChatAgentLocation.Editor, + resolveData: () => { + assertType(this._editor.hasModel()); + + return { + type: ChatAgentLocation.Editor, + selection: this._editor.getSelection(), + document: this._editor.getModel().uri, + wholeRange: this._editor.getSelection(), + }; + } + }; + + // inline chat in notebooks + // check if this editor is part of a notebook editor + // and iff so, use the notebook location but keep the resolveData + // talk about editor data + for (const notebookEditor of this._notebookEditorService.listNotebookEditors()) { + for (const [, codeEditor] of notebookEditor.codeEditors) { + if (codeEditor === this._editor) { + location.location = ChatAgentLocation.Notebook; + break; + } + } + } + + const zone = this._instaService.createInstance(InlineChatZoneWidget, + location, + { + enableWorkingSet: false, + filter: item => isRequestVM(item) + }, + this._editor + ); + + + this._editorObs = observableCodeEditor(_editor); + + const sessionObs = observableFromEvent(this, _inlineChatSessions.onDidChangeSessions, () => { + const model = _editor.getModel(); + if (!model) { + return undefined; + } + return _inlineChatSessions.getSession2(_editor, model.uri); + }); + + this._store.add(autorun(r => { + const position = this._editorObs.cursorPosition.read(r); + const value = sessionObs.read(r); + if (!value || !position) { + zone.hide(); + ctxInlineChatVisible.reset(); + } else { + zone.widget.setChatModel(value.chatModel); + ctxInlineChatVisible.set(true); + if (!zone.position) { + zone.show(position); + } + } + })); + + } + + dispose(): void { + this._store.dispose(); + } + + async start() { + assertType(this._editor.hasModel()); + const textModel = this._editor.getModel(); + await this._inlineChatSessions.createSession2(this._editor, textModel.uri, CancellationToken.None); + } +} + +export class StartSessionAction2 extends EditorAction2 { + + constructor() { + super({ + id: 'inlineChat2.start', + title: localize2('start', "Inline Chat"), + precondition: ContextKeyExpr.and( + CTX_INLINE_CHAT_HAS_AGENT2, + CTX_INLINE_CHAT_POSSIBLE, + EditorContextKeys.writable, + EditorContextKeys.editorSimpleInput.negate() + ), + f1: true, + category: AbstractInlineChatAction.category, + keybinding: { + when: EditorContextKeys.focus, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyI + }, + menu: { + id: MenuId.ChatCommandCenter, + group: 'd_inlineChat', + order: 10, + } + }); + } + + override async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]) { + const inlineChatSessions = accessor.get(IInlineChatSessionService); + if (!editor.hasModel()) { + return; + } + const textModel = editor.getModel(); + await inlineChatSessions.createSession2(editor, textModel.uri, CancellationToken.None); + } +} + +abstract class AbstractInlineChatAction extends EditorAction2 { + + static readonly category = localize2('cat', "Inline Chat"); + + constructor(desc: IAction2Options) { + super({ + ...desc, + category: AbstractInlineChatAction.category, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT2, desc.precondition) + }); + } + + override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ..._args: any[]) { + const editorService = accessor.get(IEditorService); + const logService = accessor.get(ILogService); + + let ctrl = InlineChatController2.get(editor); + if (!ctrl) { + const { activeTextEditorControl } = editorService; + if (isCodeEditor(activeTextEditorControl)) { + editor = activeTextEditorControl; + } else if (isDiffEditor(activeTextEditorControl)) { + editor = activeTextEditorControl.getModifiedEditor(); + } + ctrl = InlineChatController2.get(editor); + } + + if (!ctrl) { + logService.warn('[IE] NO controller found for action', this.desc.id, editor.getModel()?.uri); + return; + } + + if (editor instanceof EmbeddedCodeEditorWidget) { + editor = editor.getParentEditor(); + } + if (!ctrl) { + for (const diffEditor of accessor.get(ICodeEditorService).listDiffEditors()) { + if (diffEditor.getOriginalEditor() === editor || diffEditor.getModifiedEditor() === editor) { + if (diffEditor instanceof EmbeddedDiffEditorWidget) { + this.runEditorCommand(accessor, diffEditor.getParentEditor(), ..._args); + } + } + } + return; + } + this.runInlineChatCommand(accessor, ctrl, editor, ..._args); + } + + abstract runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController2, editor: ICodeEditor, ...args: any[]): void; +} + +export class StopSessionAction2 extends AbstractInlineChatAction { + constructor() { + super({ + id: 'inlineChat2.stop', + title: localize2('stop', "Stop"), + f1: true, + category: AbstractInlineChatAction.category, + precondition: ContextKeyExpr.and( + CTX_INLINE_CHAT_VISIBLE, + ), + keybinding: { + when: EditorContextKeys.focus, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Escape + }, + }); + } + + runInlineChatCommand(accessor: ServicesAccessor, _ctrl: InlineChatController2, editor: ICodeEditor, ...args: any[]): void { + const inlineChatSessions = accessor.get(IInlineChatSessionService); + if (!editor.hasModel()) { + return; + } + const textModel = editor.getModel(); + inlineChatSessions.getSession2(editor, textModel.uri)?.dispose(); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts index 6c4541c633ed..80c212a5517e 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts @@ -10,6 +10,8 @@ import { IActiveCodeEditor, ICodeEditor } from '../../../../editor/browser/edito import { IRange } from '../../../../editor/common/core/range.js'; import { IValidEditOperation } from '../../../../editor/common/model.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IChatEditingSession } from '../../chat/common/chatEditingService.js'; +import { IChatModel } from '../../chat/common/chatModel.js'; import { Session, StashedSession } from './inlineChatSession.js'; export interface ISessionKeyComputer { @@ -27,6 +29,12 @@ export interface IInlineChatSessionEndEvent extends IInlineChatSessionEvent { readonly endedByExternalCause: boolean; } +export interface IInlineChatSession2 { + readonly chatModel: IChatModel; + readonly editingSession: IChatEditingSession; + dispose(): void; +} + export interface IInlineChatSessionService { _serviceBrand: undefined; @@ -50,4 +58,9 @@ export interface IInlineChatSessionService { registerSessionKeyComputer(scheme: string, value: ISessionKeyComputer): IDisposable; dispose(): void; + + + createSession2(editor: ICodeEditor, uri: URI, token: CancellationToken): Promise; + getSession2(editor: ICodeEditor, uri: URI): IInlineChatSession2 | undefined; + onDidChangeSessions: Event; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 284212f668f2..60b9f7fa3294 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -22,14 +22,15 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { DEFAULT_EDITOR_ASSOCIATION } from '../../../common/editor.js'; import { ChatAgentLocation, IChatAgentService } from '../../chat/common/chatAgents.js'; import { IChatService } from '../../chat/common/chatService.js'; -import { CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_POSSIBLE } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_POSSIBLE } from '../common/inlineChat.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js'; import { HunkData, Session, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession.js'; -import { IInlineChatSessionEndEvent, IInlineChatSessionEvent, IInlineChatSessionService, ISessionKeyComputer } from './inlineChatSessionService.js'; +import { IInlineChatSession2, IInlineChatSessionEndEvent, IInlineChatSessionEvent, IInlineChatSessionService, ISessionKeyComputer } from './inlineChatSessionService.js'; import { isEqual } from '../../../../base/common/resources.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; +import { IChatEditingService } from '../../chat/common/chatEditingService.js'; type SessionData = { @@ -79,7 +80,8 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { @ITextFileService private readonly _textFileService: ITextFileService, @ILanguageService private readonly _languageService: ILanguageService, @IChatService private readonly _chatService: IChatService, - @IChatAgentService private readonly _chatAgentService: IChatAgentService + @IChatAgentService private readonly _chatAgentService: IChatAgentService, + @IChatEditingService private readonly _chatEditingService: IChatEditingService, ) { } dispose() { @@ -311,6 +313,49 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { this._keyComputers.set(scheme, value); return toDisposable(() => this._keyComputers.delete(scheme)); } + + // ---- NEW + + private readonly _sessions2 = new Map(); + + private readonly _onDidChangeSessions = this._store.add(new Emitter()); + readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; + + + async createSession2(editor: ICodeEditor, uri: URI, token: CancellationToken): Promise { + + const key = this._key(editor, uri); + if (this._sessions2.has(key)) { + throw new Error('Session already exists'); + } + + this._onWillStartSession.fire(editor as IActiveCodeEditor); + + const chatModel = this._chatService.startSession(ChatAgentLocation.EditingSession, token); + + const editingSession = await this._chatEditingService.createAdhocEditingSession(chatModel.sessionId); + editingSession.addFileToWorkingSet(uri); + + const result: IInlineChatSession2 = { + chatModel, + editingSession, + dispose: () => { + if (this._sessions2.delete(key)) { + editingSession.dispose(); + chatModel.dispose(); + this._onDidChangeSessions.fire(this); + } + } + }; + this._sessions2.set(key, result); + this._onDidChangeSessions.fire(this); + return result; + } + + getSession2(editor: ICodeEditor, uri: URI): IInlineChatSession2 | undefined { + const key = this._key(editor, uri); + return this._sessions2.get(key); + } } export class InlineChatEnabler { @@ -318,6 +363,7 @@ export class InlineChatEnabler { static Id = 'inlineChat.enabler'; private readonly _ctxHasProvider: IContextKey; + private readonly _ctxHasProvider2: IContextKey; private readonly _ctxPossible: IContextKey; private readonly _store = new DisposableStore(); @@ -328,11 +374,21 @@ export class InlineChatEnabler { @IEditorService editorService: IEditorService, ) { this._ctxHasProvider = CTX_INLINE_CHAT_HAS_AGENT.bindTo(contextKeyService); + this._ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService); this._ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService); const updateAgent = () => { - const hasEditorAgent = Boolean(chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)); - this._ctxHasProvider.set(hasEditorAgent); + const agent = chatAgentService.getDefaultAgent(ChatAgentLocation.Editor); + if (agent?.locations.length === 1) { + this._ctxHasProvider.set(true); + this._ctxHasProvider2.reset(); + } else if (agent?.locations.includes(ChatAgentLocation.EditingSession)) { + this._ctxHasProvider.reset(); + this._ctxHasProvider2.set(true); + } else { + this._ctxHasProvider.reset(); + this._ctxHasProvider2.reset(); + } }; this._store.add(chatAgentService.onDidChangeAgents(updateAgent)); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index 9d61a980d52a..138b51910c44 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -19,6 +19,7 @@ import { localize } from '../../../../nls.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { IChatWidgetViewOptions } from '../../chat/browser/chat.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; import { isResponseVM } from '../../chat/common/chatViewModel.js'; import { ACTION_REGENERATE_RESPONSE, ACTION_REPORT_ISSUE, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET_SECONDARY, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; @@ -47,6 +48,7 @@ export class InlineChatZoneWidget extends ZoneWidget { constructor( location: IChatWidgetLocationOptions, + options: IChatWidgetViewOptions | undefined, editor: ICodeEditor, @IInstantiationService private readonly _instaService: IInstantiationService, @ILogService private _logService: ILogService, @@ -80,13 +82,15 @@ export class InlineChatZoneWidget extends ZoneWidget { menus: { telemetrySource: 'interactiveEditorWidget-toolbar', }, + ...options, rendererOptions: { renderTextEditsAsSummary: (uri) => { // render when dealing with the current file in the editor return isEqual(uri, editor.getModel()?.uri); }, renderDetectedCommandsWithRequest: true, - } + ...options?.rendererOptions + }, } }); this._disposables.add(this.widget); diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 19f27bf3ad6a..a5574bfadff4 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -75,6 +75,7 @@ export const enum InlineChatResponseType { export const CTX_INLINE_CHAT_POSSIBLE = new RawContextKey('inlineChatPossible', false, localize('inlineChatHasPossible', "Whether a provider for inline chat exists and whether an editor for inline chat is open")); export const CTX_INLINE_CHAT_HAS_AGENT = new RawContextKey('inlineChatHasProvider', false, localize('inlineChatHasProvider', "Whether a provider for interactive editors exists")); +export const CTX_INLINE_CHAT_HAS_AGENT2 = new RawContextKey('inlineChatHasEditsAgent', false, localize('inlineChatHasEditsAgent', "Whether an agent for inliine for interactive editors exists")); export const CTX_INLINE_CHAT_VISIBLE = new RawContextKey('inlineChatVisible', false, localize('inlineChatVisible', "Whether the interactive editor input is visible")); export const CTX_INLINE_CHAT_FOCUSED = new RawContextKey('inlineChatFocused', false, localize('inlineChatFocused', "Whether the interactive editor input is focused")); export const CTX_INLINE_CHAT_EDITING = new RawContextKey('inlineChatEditing', true, localize('inlineChatEditing', "Whether the user is currently editing or generating code in the inline chat")); From 70f22f586e3b4c6537e221a55a798e9b3dc8d4b9 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 22 Jan 2025 09:51:42 +0100 Subject: [PATCH 0758/3587] Merge Go To and Accept in gutter menu (#238446) closes https://github.com/microsoft/vscode-copilot/issues/11923 --- .../view/inlineEdits/gutterIndicatorMenu.ts | 18 +++++----- .../view/inlineEdits/gutterIndicatorView.ts | 35 +++++++------------ 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts index afbcc2b58d5c..089c058b6d40 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorMenu.ts @@ -21,7 +21,7 @@ import { ChildNode, FirstFnArg, LiveElement, n } from './utils.js'; export class GutterIndicatorMenuContent { constructor( private readonly _menuTitle: IObservable, - private readonly _selectionOverride: IObservable<'jump' | 'accept' | undefined>, + private readonly _tabAction: IObservable<'jump' | 'accept' | 'inactive'>, private readonly _close: (focusEditor: boolean) => void, private readonly _extensionCommands: IObservable, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @@ -36,18 +36,17 @@ export class GutterIndicatorMenuContent { private _createHoverContent() { const activeElement = observableValue('active', undefined); - const activeElementOrDefault = derived(reader => this._selectionOverride.read(reader) ?? activeElement.read(reader)); - const createOptionArgs = (options: { id: string; title: string; icon: ThemeIcon; commandId: string; commandArgs?: unknown[] }): FirstFnArg => { + const createOptionArgs = (options: { id: string; title: string; icon: IObservable | ThemeIcon; commandId: string | IObservable; commandArgs?: unknown[] }): FirstFnArg => { return { title: options.title, icon: options.icon, - keybinding: this._getKeybinding(options.commandArgs ? undefined : options.commandId), - isActive: activeElementOrDefault.map(v => v === options.id), + keybinding: typeof options.commandId === 'string' ? this._getKeybinding(options.commandArgs ? undefined : options.commandId) : derived(reader => typeof options.commandId === 'string' ? undefined : this._getKeybinding(options.commandArgs ? undefined : options.commandId.read(reader)).read(reader)), + isActive: activeElement.map(v => v === options.id), onHoverChange: v => activeElement.set(v ? options.id : undefined, undefined), onAction: () => { this._close(true); - return this._commandService.executeCommand(options.commandId, ...(options.commandArgs ?? [])); + return this._commandService.executeCommand(typeof options.commandId === 'string' ? options.commandId : options.commandId.get(), ...(options.commandArgs ?? [])); }, }; }; @@ -55,8 +54,7 @@ export class GutterIndicatorMenuContent { // TODO make this menu contributable! return hoverContent([ header(this._menuTitle), - option(createOptionArgs({ id: 'jump', title: localize('goto', "Go To"), icon: Codicon.arrowRight, commandId: new JumpToNextInlineEdit().id })), - option(createOptionArgs({ id: 'accept', title: localize('accept', "Accept"), icon: Codicon.check, commandId: new AcceptInlineCompletion().id })), + option(createOptionArgs({ id: 'gotoAndAccept', title: `${localize('goto', "Go To")} / ${localize('accept', "Accept")}`, icon: this._tabAction.map(action => action === 'accept' ? Codicon.check : Codicon.arrowRight), commandId: this._tabAction.map(action => action === 'accept' ? new AcceptInlineCompletion().id : new JumpToNextInlineEdit().id) })), option(createOptionArgs({ id: 'reject', title: localize('reject', "Reject"), icon: Codicon.close, commandId: new HideInlineCompletion().id })), separator(), this._extensionCommands?.map(c => c && c.length > 0 ? [ @@ -100,7 +98,7 @@ function header(title: string | IObservable) { function option(props: { title: string; - icon: ThemeIcon; + icon: IObservable | ThemeIcon; keybinding: IObservable; isActive?: IObservable; onHoverChange?: (isHovered: boolean) => void; @@ -123,7 +121,7 @@ function option(props: { fontSize: 16, display: 'flex', } - }, [renderIcon(props.icon)]), + }, [ThemeIcon.isThemeIcon(props.icon) ? renderIcon(props.icon) : props.icon.map(icon => renderIcon(icon))]), n.elem('span', {}, [props.title]), n.div({ style: { marginLeft: 'auto', opacity: '0.6' }, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index 9fa6bb711e44..7dc1c30042ac 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -157,30 +157,9 @@ export class InlineEditsGutterIndicator extends Disposable { return 'inactive' as const; }); - private readonly _onClickAction = derived(this, reader => { - if (this._layout.map(d => d && d.docked).read(reader)) { - return { - selectionOverride: 'accept' as const, - action: () => { - this._editorObs.editor.focus(); - this._model.get()?.accept(); - } - }; - } else { - return { - selectionOverride: 'jump' as const, - action: () => { - this._editorObs.editor.focus(); - this._model.get()?.jump(); - } - }; - } - }); - private readonly _iconRef = n.ref(); private _hoverVisible: boolean = false; private readonly _isHoveredOverIcon = observableValue(this, false); - private readonly _hoverSelectionOverride = derived(this, reader => this._isHoveredOverIcon.read(reader) ? this._onClickAction.read(reader).selectionOverride : undefined); private _showHover(): void { if (this._hoverVisible) { @@ -200,7 +179,7 @@ export class InlineEditsGutterIndicator extends Disposable { const content = disposableStore.add(this._instantiationService.createInstance( GutterIndicatorMenuContent, displayName, - this._hoverSelectionOverride, + this._tabAction, (focusEditor) => { if (focusEditor) { this._editorObs.editor.focus(); @@ -232,7 +211,17 @@ export class InlineEditsGutterIndicator extends Disposable { private readonly _indicator = n.div({ class: 'inline-edits-view-gutter-indicator', - onclick: () => this._onClickAction.get().action(), + onclick: () => { + const model = this._model.get(); + if (!model) { return; } + const docked = this._layout.map(l => l && l.docked).get(); + this._editorObs.editor.focus(); + if (docked) { + model.accept(); + } else { + model.jump(); + } + }, tabIndex: 0, style: { position: 'absolute', From 9b637ac69e65779aabbda7103d342c92e4c0b40d Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 22 Jan 2025 09:52:52 +0100 Subject: [PATCH 0759/3587] remove `chat.editing.alwaysSaveWithGeneratedChanges` setting and things around it (#238447) fyi @isidorn --- .../contrib/chat/browser/chat.contribution.ts | 9 - .../contrib/chat/browser/chatEditorSaving.ts | 408 ------------------ .../contrib/chat/browser/chatInputPart.ts | 3 +- .../browser/inlineChat.contribution.ts | 3 - .../browser/inlineChatController.ts | 6 - .../browser/inlineChatSavingService.ts | 16 - .../browser/inlineChatSavingServiceImpl.ts | 197 --------- .../contrib/inlineChat/common/inlineChat.ts | 1 - .../test/browser/inlineChatController.test.ts | 7 - .../test/browser/inlineChatSession.test.ts | 8 +- 10 files changed, 2 insertions(+), 656 deletions(-) delete mode 100644 src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts delete mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingService.ts delete mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl.ts diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 9006925683d1..37c4dbb7403a 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -60,7 +60,6 @@ import { registerChatEditorActions } from './chatEditorActions.js'; import { ChatEditorController } from './chatEditorController.js'; import { ChatEditorInput, ChatEditorInputSerializer } from './chatEditorInput.js'; import { ChatInputBoxContentProvider } from './chatEdinputInputContentProvider.js'; -import { ChatEditorAutoSaveDisabler, ChatEditorSaving } from './chatEditorSaving.js'; import { agentSlashCommandToMarkdown, agentToMarkdown } from './chatMarkdownDecorationsRenderer.js'; import { ChatCompatibilityNotifier, ChatExtensionPointHandler } from './chatParticipant.contribution.js'; import { ChatPasteProvidersFeature } from './chatPasteProviders.js'; @@ -122,12 +121,6 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('chat.commandCenter.enabled', "Controls whether the command center shows a menu for actions to control Copilot (requires {0}).", '`#window.commandCenter#`'), default: true }, - 'chat.editing.alwaysSaveWithGeneratedChanges': { - type: 'boolean', - scope: ConfigurationScope.APPLICATION, - markdownDescription: nls.localize('chat.editing.alwaysSaveWithGeneratedChanges', "Whether files that have changes made by chat can be saved without confirmation."), - default: false, - }, 'chat.editing.autoAcceptDelay': { type: 'number', markdownDescription: nls.localize('chat.editing.autoAcceptDelay', "Delay after which changes made by chat are automatically accepted. Values are in seconds, `0` means disabled and `100` seconds is the maximum."), @@ -319,8 +312,6 @@ registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNo registerWorkbenchContribution2(ChatCommandCenterRendering.ID, ChatCommandCenterRendering, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatImplicitContextContribution.ID, ChatImplicitContextContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatRelatedFilesContribution.ID, ChatRelatedFilesContribution, WorkbenchPhase.Eventually); -registerWorkbenchContribution2(ChatEditorSaving.ID, ChatEditorSaving, WorkbenchPhase.AfterRestored); -registerWorkbenchContribution2(ChatEditorAutoSaveDisabler.ID, ChatEditorAutoSaveDisabler, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatViewsWelcomeHandler.ID, ChatViewsWelcomeHandler, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(ChatGettingStartedContribution.ID, ChatGettingStartedContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatSetupContribution.ID, ChatSetupContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts b/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts deleted file mode 100644 index d93c639d9d1b..000000000000 --- a/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts +++ /dev/null @@ -1,408 +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 { DeferredPromise, RunOnceScheduler } from '../../../../base/common/async.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { CancellationError } from '../../../../base/common/errors.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { ResourceSet } from '../../../../base/common/map.js'; -import { autorun, autorunWithStore } from '../../../../base/common/observable.js'; -import { assertType } from '../../../../base/common/types.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; -import { localize } from '../../../../nls.js'; -import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { ILabelService } from '../../../../platform/label/common/label.js'; -import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { IEditorIdentifier, SaveReason } from '../../../common/editor.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { AutoSaveMode, IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; -import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; -import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; -import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; -import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IModifiedFileEntry, WorkingSetEntryState } from '../common/chatEditingService.js'; -import { IChatModel } from '../common/chatModel.js'; -import { IChatService } from '../common/chatService.js'; -import { ChatEditingModifiedFileEntry } from './chatEditing/chatEditingModifiedFileEntry.js'; - - -const STORAGE_KEY_AUTOSAVE_DISABLED = 'chat.editing.autosaveDisabled'; - -export class ChatEditorAutoSaveDisabler extends Disposable implements IWorkbenchContribution { - - static readonly ID: string = 'workbench.chat.autoSaveDisabler'; - - private _autosaveDisabledUris: string[] = []; - - constructor( - @IConfigurationService configService: IConfigurationService, - @IChatEditingService chatEditingService: IChatEditingService, - @IFilesConfigurationService fileConfigService: IFilesConfigurationService, - @ILifecycleService lifecycleService: ILifecycleService, - @IStorageService storageService: IStorageService - ) { - super(); - - // on shutdown remember all files that have auto save disabled - this._store.add(lifecycleService.onWillShutdown((e) => { - storageService.store(STORAGE_KEY_AUTOSAVE_DISABLED, this._autosaveDisabledUris, StorageScope.WORKSPACE, StorageTarget.MACHINE); - })); - - const alwaysSaveConfig = observableConfigValue(ChatEditorSaving._config, false, configService); - - // as quickly as possible disable auto save for all files that were modified before the last shutdown - if (!alwaysSaveConfig.get()) { - const autoSaveDisabled = storageService.getObject(STORAGE_KEY_AUTOSAVE_DISABLED, StorageScope.WORKSPACE, []); - if (Array.isArray(autoSaveDisabled) && autoSaveDisabled.length > 0) { - - const initializingStore = new DisposableStore(); - for (const uriString of autoSaveDisabled) { - initializingStore.add(fileConfigService.disableAutoSave(URI.parse(uriString))); - } - chatEditingService.getOrRestoreEditingSession().finally(() => { - // by now the session is restored and the auto save handlers are in place - initializingStore.dispose(); - }); - - } - } - - // listen to session changes and update auto save settings accordingly - const saveConfig = this._store.add(new MutableDisposable()); - this._store.add(autorun(reader => { - const store = new DisposableStore(); - const autoSaveDisabled: string[] = []; - try { - if (alwaysSaveConfig.read(reader)) { - return; - } - const session = chatEditingService.currentEditingSessionObs.read(reader); - if (session) { - const entries = session.entries.read(reader); - for (const entry of entries) { - if (entry.state.read(reader) === WorkingSetEntryState.Modified) { - autoSaveDisabled.push(entry.modifiedURI.toString()); - store.add(fileConfigService.disableAutoSave(entry.modifiedURI)); - } - } - } - } finally { - saveConfig.value = store; // disposes the previous store, after we have added the new one - this._autosaveDisabledUris = autoSaveDisabled; - } - })); - } -} - - -export class ChatEditorSaving extends Disposable implements IWorkbenchContribution { - - static readonly ID: string = 'workbench.chat.editorSaving'; - - static readonly _config = 'chat.editing.alwaysSaveWithGeneratedChanges'; - - constructor( - @IConfigurationService configService: IConfigurationService, - @IChatEditingService chatEditingService: IChatEditingService, - @IChatAgentService chatAgentService: IChatAgentService, - @IFilesConfigurationService fileConfigService: IFilesConfigurationService, - @ITextFileService textFileService: ITextFileService, - @ILabelService labelService: ILabelService, - @IDialogService dialogService: IDialogService, - @IChatService private readonly _chatService: IChatService, - ) { - super(); - - // --- report that save happened - this._store.add(autorunWithStore((r, store) => { - const session = chatEditingService.currentEditingSessionObs.read(r); - if (!session) { - return; - } - const chatSession = this._chatService.getSession(session.chatSessionId); - if (!chatSession) { - return; - } - store.add(textFileService.files.onDidSave(e => { - const entry = session.getEntry(e.model.resource); - if (entry && entry.state.get() === WorkingSetEntryState.Modified) { - this._reportSavedWhenReady(chatSession, entry); - } - })); - })); - - const alwaysSaveConfig = observableConfigValue(ChatEditorSaving._config, false, configService); - this._store.add(autorunWithStore((r, store) => { - - const alwaysSave = alwaysSaveConfig.read(r); - - if (alwaysSave) { - return; - } - - const saveJobs = new class { - - private _deferred?: DeferredPromise; - private readonly _soon = new RunOnceScheduler(() => this._prompt(), 0); - private readonly _uris = new ResourceSet(); - - add(uri: URI) { - this._uris.add(uri); - this._soon.schedule(); - this._deferred ??= new DeferredPromise(); - return this._deferred.p; - } - - private async _prompt() { - - // this might have changed in the meantime and there is checked again and acted upon - const alwaysSave = configService.getValue(ChatEditorSaving._config); - if (alwaysSave) { - return; - } - - const uri = Iterable.first(this._uris); - if (!uri) { - // bogous? - return; - } - - const agentName = chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession)?.fullName ?? localize('chat', "chat"); - const filelabel = labelService.getUriBasenameLabel(uri); - - const message = this._uris.size === 1 - ? localize('message.1', "Do you want to save the changes {0} made in {1}?", agentName, filelabel) - : localize('message.2', "Do you want to save the changes {0} made to {1} files?", agentName, this._uris.size); - - const result = await dialogService.confirm({ - message, - detail: localize('detail2', "AI-generated changes may be incorrect and should be reviewed before saving.", agentName), - primaryButton: localize('save', "Save"), - cancelButton: localize('discard', "Cancel"), - checkbox: { - label: localize('config', "Always save with AI-generated changes without asking"), - checked: false - } - }); - - this._uris.clear(); - - if (result.confirmed && result.checkboxChecked) { - // remember choice - await configService.updateValue(ChatEditorSaving._config, true); - } - - if (!result.confirmed) { - // cancel the save - this._deferred?.error(new CancellationError()); - } else { - this._deferred?.complete(); - } - this._deferred = undefined; - } - }; - - store.add(textFileService.files.addSaveParticipant({ - participate: async (workingCopy, context, progress, token) => { - - if (context.reason !== SaveReason.EXPLICIT) { - // all saves that we are concerned about are explicit - // because we have disabled auto-save for them - return; - } - - const session = await chatEditingService.getOrRestoreEditingSession(); - if (!session || session.isToolsAgentSession) { - // For now, don't prompt when in agent mode - return; - } - const entry = session.getEntry(workingCopy.resource); - if (!entry || entry.state.get() !== WorkingSetEntryState.Modified) { - return; - } - - return saveJobs.add(entry.modifiedURI); - } - })); - })); - - // autosave: OFF & alwaysSaveWithAIChanges - save files after accept - this._store.add(autorun(r => { - const saveConfig = fileConfigService.getAutoSaveMode(undefined); - if (saveConfig.mode !== AutoSaveMode.OFF) { - return; - } - if (!alwaysSaveConfig.read(r)) { - return; - } - const session = chatEditingService.currentEditingSessionObs.read(r); - if (!session) { - return; - } - for (const entry of session.entries.read(r)) { - if (entry.state.read(r) === WorkingSetEntryState.Accepted) { - textFileService.save(entry.modifiedURI); - } - } - })); - } - - private _reportSaved(entry: IModifiedFileEntry) { - assertType(entry instanceof ChatEditingModifiedFileEntry); - - this._chatService.notifyUserAction({ - action: { kind: 'chatEditingSessionAction', uri: entry.modifiedURI, hasRemainingEdits: false, outcome: 'saved' }, - agentId: entry.telemetryInfo.agentId, - command: entry.telemetryInfo.command, - sessionId: entry.telemetryInfo.sessionId, - requestId: entry.telemetryInfo.requestId, - result: entry.telemetryInfo.result - }); - } - - private _reportSavedWhenReady(session: IChatModel, entry: IModifiedFileEntry) { - if (!session.requestInProgress) { - this._reportSaved(entry); - return; - } - // wait until no more request is pending - const d = session.onDidChange(e => { - if (!session.requestInProgress) { - this._reportSaved(entry); - this._store.delete(d); - d.dispose(); - } - }); - this._store.add(d); - } -} - -export class ChatEditingSaveAllAction extends Action2 { - static readonly ID = 'chatEditing.saveAllFiles'; - - constructor() { - super({ - id: ChatEditingSaveAllAction.ID, - title: localize('save.allFiles', 'Save All'), - precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), - icon: Codicon.saveAll, - menu: [ - { - when: ContextKeyExpr.equals('resourceScheme', CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME), - id: MenuId.EditorTitle, - order: 2, - group: 'navigation', - }, - { - id: MenuId.ChatEditingWidgetToolbar, - group: 'navigation', - order: 2, - // Show the option to save without accepting if the user hasn't configured the setting to always save with generated changes - when: ContextKeyExpr.and( - applyingChatEditsFailedContextKey.negate(), - hasUndecidedChatEditingResourceContextKey, - ContextKeyExpr.equals(`config.${ChatEditorSaving._config}`, false), - ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession) - ) - } - ], - keybinding: { - primary: KeyMod.CtrlCmd | KeyCode.KeyS, - when: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey, ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), ChatContextKeys.inChatInput), - weight: KeybindingWeight.WorkbenchContrib, - }, - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const chatEditingService = accessor.get(IChatEditingService); - const editorService = accessor.get(IEditorService); - const configService = accessor.get(IConfigurationService); - const chatAgentService = accessor.get(IChatAgentService); - const dialogService = accessor.get(IDialogService); - const labelService = accessor.get(ILabelService); - - const currentEditingSession = chatEditingService.currentEditingSession; - if (!currentEditingSession) { - return; - } - - const editors: IEditorIdentifier[] = []; - for (const modifiedFileEntry of currentEditingSession.entries.get()) { - if (modifiedFileEntry.state.get() === WorkingSetEntryState.Modified) { - const modifiedFile = modifiedFileEntry.modifiedURI; - const matchingEditors = editorService.findEditors(modifiedFile); - if (matchingEditors.length === 0) { - continue; - } - const matchingEditor = matchingEditors[0]; - if (matchingEditor.editor.isDirty()) { - editors.push(matchingEditor); - } - } - } - - if (editors.length === 0) { - return; - } - - const alwaysSave = configService.getValue(ChatEditorSaving._config); - if (!alwaysSave) { - const agentName = chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession)?.fullName; - - let message: string; - if (editors.length === 1) { - const resource = editors[0].editor.resource; - if (resource) { - const filelabel = labelService.getUriBasenameLabel(resource); - message = agentName - ? localize('message.batched.oneFile.1', "Do you want to save the changes {0} made in {1}?", agentName, filelabel) - : localize('message.batched.oneFile.2', "Do you want to save the changes chat made in {0}?", filelabel); - } else { - message = agentName - ? localize('message.batched.oneFile.3', "Do you want to save the changes {0} made in 1 file?", agentName) - : localize('message.batched.oneFile.4', "Do you want to save the changes chat made in 1 file?"); - } - } else { - message = agentName - ? localize('message.batched.multiFile.1', "Do you want to save the changes {0} made in {1} files?", agentName, editors.length) - : localize('message.batched.multiFile.2', "Do you want to save the changes chat made in {0} files?", editors.length); - } - - - const result = await dialogService.confirm({ - message, - detail: localize('detail2', "AI-generated changes may be incorrect and should be reviewed before saving.", agentName), - primaryButton: localize('save all', "Save All"), - cancelButton: localize('discard', "Cancel"), - checkbox: { - label: localize('config', "Always save with AI-generated changes without asking"), - checked: false - } - }); - - if (!result.confirmed) { - return; - } - - if (result.checkboxChecked) { - await configService.updateValue(ChatEditorSaving._config, true); - } - } - - // Skip our own chat editing save blocking participant, since we already showed our own batched dialog - await editorService.save(editors, { reason: SaveReason.EXPLICIT, skipSaveParticipants: true }); - } -} -registerAction2(ChatEditingSaveAllAction); diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 0585b3012a47..8a676d3e3230 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -99,7 +99,6 @@ import { IDisposableReference } from './chatContentParts/chatCollections.js'; import { CollapsibleListPool, IChatCollapsibleListItem } from './chatContentParts/chatReferencesContentPart.js'; import { ChatDragAndDrop, EditsDragAndDrop } from './chatDragAndDrop.js'; import { ChatEditingRemoveAllFilesAction, ChatEditingShowChangesAction } from './chatEditing/chatEditingActions.js'; -import { ChatEditingSaveAllAction } from './chatEditorSaving.js'; import { ChatFollowups } from './chatFollowups.js'; import { IChatViewState } from './chatWidget.js'; import { ChatFileReference } from './contrib/chatDynamicVariables/chatFileReference.js'; @@ -1310,7 +1309,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge arg: { sessionId: chatEditingSession.chatSessionId }, }, buttonConfigProvider: (action) => { - if (action.id === ChatEditingShowChangesAction.ID || action.id === ChatEditingSaveAllAction.ID || action.id === ChatEditingRemoveAllFilesAction.ID) { + if (action.id === ChatEditingShowChangesAction.ID || action.id === ChatEditingRemoveAllFilesAction.ID) { return { showIcon: true, showLabel: false, isSecondary: true }; } return undefined; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 3572a0801b35..ee316323294a 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -13,9 +13,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { InlineChatNotebookContribution } from './inlineChatNotebook.js'; import { IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from '../../../common/contributions.js'; -import { InlineChatSavingServiceImpl } from './inlineChatSavingServiceImpl.js'; import { InlineChatAccessibleView } from './inlineChatAccessibleView.js'; -import { IInlineChatSavingService } from './inlineChatSavingService.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { InlineChatEnabler, InlineChatSessionServiceImpl } from './inlineChatSessionServiceImpl.js'; import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; @@ -30,7 +28,6 @@ import { InlineChatExpandLineAction, InlineChatHintsController, HideInlineChatHi // --- browser registerSingleton(IInlineChatSessionService, InlineChatSessionServiceImpl, InstantiationType.Delayed); -registerSingleton(IInlineChatSavingService, InlineChatSavingServiceImpl, InstantiationType.Delayed); registerEditorContribution(INLINE_CHAT_ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index cbd575f7fde4..6d8f16a122ca 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -46,7 +46,6 @@ import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGr import { IChatService } from '../../chat/common/chatService.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js'; -import { IInlineChatSavingService } from './inlineChatSavingService.js'; import { HunkInformation, Session, StashedSession } from './inlineChatSession.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { InlineChatError } from './inlineChatSessionServiceImpl.js'; @@ -139,7 +138,6 @@ export class InlineChatController implements IEditorContribution { private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instaService: IInstantiationService, @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, - @IInlineChatSavingService private readonly _inlineChatSavingService: IInlineChatSavingService, @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, @ILogService private readonly _logService: ILogService, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -964,7 +962,6 @@ export class InlineChatController implements IEditorContribution { stop: () => this._session!.hunkData.ignoreTextModelNChanges = false, }; - this._inlineChatSavingService.markChanged(this._session); if (opts) { await this._strategy.makeProgressiveChanges(editOperations, editsObserver, opts, undoStopBefore); } else { @@ -1127,9 +1124,6 @@ export class InlineChatController implements IEditorContribution { unstashLastSession(): Session | undefined { const result = this._stashedSession.value?.unstash(); - if (result) { - this._inlineChatSavingService.markChanged(result); - } return result; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingService.ts deleted file mode 100644 index ebe92f44f050..000000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingService.ts +++ /dev/null @@ -1,16 +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 { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { Session } from './inlineChatSession.js'; - - -export const IInlineChatSavingService = createDecorator('IInlineChatSavingService '); - -export interface IInlineChatSavingService { - _serviceBrand: undefined; - - markChanged(session: Session): void; - -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl.ts deleted file mode 100644 index 53ae97f28060..000000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl.ts +++ /dev/null @@ -1,197 +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 { Queue } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { DisposableStore, MutableDisposable, combinedDisposable, dispose } from '../../../../base/common/lifecycle.js'; -import { localize } from '../../../../nls.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; -import { SaveReason } from '../../../common/editor.js'; -import { Session } from './inlineChatSession.js'; -import { IInlineChatSessionService } from './inlineChatSessionService.js'; -import { InlineChatConfigKeys } from '../common/inlineChat.js'; -import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; -import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; -import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; -import { IInlineChatSavingService } from './inlineChatSavingService.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { CellUri } from '../../notebook/common/notebookCommon.js'; -import { IWorkingCopyFileService } from '../../../services/workingCopy/common/workingCopyFileService.js'; -import { URI } from '../../../../base/common/uri.js'; -import { Event } from '../../../../base/common/event.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { ILabelService } from '../../../../platform/label/common/label.js'; -import { CancellationError } from '../../../../base/common/errors.js'; - -interface SessionData { - readonly resourceUri: URI; - readonly dispose: () => void; - readonly session: Session; - readonly groupCandidate: IEditorGroup; -} - -// TODO@jrieken this duplicates a config key -const key = 'chat.editing.alwaysSaveWithGeneratedChanges'; - -export class InlineChatSavingServiceImpl implements IInlineChatSavingService { - - declare readonly _serviceBrand: undefined; - - private readonly _store = new DisposableStore(); - private readonly _saveParticipant = this._store.add(new MutableDisposable()); - private readonly _sessionData = new Map(); - - constructor( - @IFilesConfigurationService private readonly _fileConfigService: IFilesConfigurationService, - @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, - @ITextFileService private readonly _textFileService: ITextFileService, - @IInlineChatSessionService _inlineChatSessionService: IInlineChatSessionService, - @IConfigurationService private readonly _configService: IConfigurationService, - @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, - @IDialogService private readonly _dialogService: IDialogService, - @ILabelService private readonly _labelService: ILabelService, - ) { - this._store.add(Event.any(_inlineChatSessionService.onDidEndSession, _inlineChatSessionService.onDidStashSession)(e => { - this._sessionData.get(e.session)?.dispose(); - })); - - this._store.add(_configService.onDidChangeConfiguration(e => { - if (!e.affectsConfiguration(key) && !e.affectsConfiguration(InlineChatConfigKeys.AcceptedOrDiscardBeforeSave)) { - return; - } - if (this._isDisabled()) { - dispose(this._sessionData.values()); - this._sessionData.clear(); - } - })); - } - - dispose(): void { - this._store.dispose(); - dispose(this._sessionData.values()); - } - - markChanged(session: Session): void { - - if (this._isDisabled()) { - return; - } - - if (!this._sessionData.has(session)) { - - let uri = session.targetUri; - - // notebooks: use the notebook-uri because saving happens on the notebook-level - if (uri.scheme === Schemas.vscodeNotebookCell) { - const data = CellUri.parse(uri); - if (!data) { - return; - } - uri = data?.notebook; - } - - if (this._sessionData.size === 0) { - this._installSaveParticpant(); - } - - const saveConfigOverride = this._fileConfigService.disableAutoSave(uri); - this._sessionData.set(session, { - resourceUri: uri, - groupCandidate: this._editorGroupService.activeGroup, - session, - dispose: () => { - saveConfigOverride.dispose(); - this._sessionData.delete(session); - if (this._sessionData.size === 0) { - this._saveParticipant.clear(); - } - } - }); - } - } - - private _installSaveParticpant(): void { - - const queue = new Queue(); - - const d1 = this._textFileService.files.addSaveParticipant({ - participate: (model, ctx, progress, token) => { - return queue.queue(() => this._participate(ctx.savedFrom ?? model.textEditorModel?.uri, ctx.reason, progress, token)); - } - }); - const d2 = this._workingCopyFileService.addSaveParticipant({ - participate: (workingCopy, ctx, progress, token) => { - return queue.queue(() => this._participate(ctx.savedFrom ?? workingCopy.resource, ctx.reason, progress, token)); - } - }); - this._saveParticipant.value = combinedDisposable(d1, d2, queue); - } - - private async _participate(uri: URI | undefined, reason: SaveReason, progress: IProgress, token: CancellationToken): Promise { - - - if (reason !== SaveReason.EXPLICIT) { - // all saves that we are concerned about are explicit - // because we have disabled auto-save for them - return; - } - - if (this._isDisabled()) { - // disabled - return; - } - - const sessions = new Map(); - for (const [session, data] of this._sessionData) { - if (uri?.toString() === data.resourceUri.toString()) { - sessions.set(session, data); - } - } - - if (sessions.size === 0) { - return; - } - - let message: string; - - if (sessions.size === 1) { - const session = Iterable.first(sessions.values())!.session; - const agentName = session.agent.fullName; - const filelabel = this._labelService.getUriBasenameLabel(session.textModelN.uri); - - message = localize('message.1', "Do you want to save the changes {0} made in {1}?", agentName, filelabel); - } else { - const labels = Array.from(Iterable.map(sessions.values(), i => this._labelService.getUriBasenameLabel(i.session.textModelN.uri))); - message = localize('message.2', "Do you want to save the changes inline chat made in {0}?", labels.join(', ')); - } - - const result = await this._dialogService.confirm({ - message, - detail: localize('detail', "AI-generated changes may be incorrect and should be reviewed before saving."), - primaryButton: localize('save', "Save"), - cancelButton: localize('discard', "Cancel"), - checkbox: { - label: localize('config', "Always save with AI-generated changes without asking"), - checked: false - } - }); - - if (!result.confirmed) { - // cancel the save - throw new CancellationError(); - } - - if (result.checkboxChecked) { - // remember choice - this._configService.updateValue(key, true); - } - } - - private _isDisabled() { - return this._configService.getValue(InlineChatConfigKeys.AcceptedOrDiscardBeforeSave) === true || this._configService.getValue(key); - } -} diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 19f27bf3ad6a..2270a8f9ef24 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -14,7 +14,6 @@ import { diffInserted, diffRemoved, editorWidgetBackground, editorWidgetBorder, export const enum InlineChatConfigKeys { FinishOnType = 'inlineChat.finishOnType', - AcceptedOrDiscardBeforeSave = 'inlineChat.acceptedOrDiscardBeforeSave', StartWithOverlayWidget = 'inlineChat.startWithOverlayWidget', HoldToSpeech = 'inlineChat.holdToSpeech', AccessibleDiffView = 'inlineChat.accessibleDiffView', diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index aaeb5de7b5a1..06791cc94323 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -34,7 +34,6 @@ import { IChatAccessibilityService, IChatWidget, IChatWidgetService } from '../. import { ChatAgentLocation, ChatAgentService, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../../../chat/common/chatAgents.js'; import { IChatResponseViewModel } from '../../../chat/common/chatViewModel.js'; import { InlineChatController, State } from '../../browser/inlineChatController.js'; -import { Session } from '../../browser/inlineChatSession.js'; import { CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, InlineChatConfigKeys, InlineChatResponseType } from '../../common/inlineChat.js'; import { TestViewsService, workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; @@ -61,7 +60,6 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { assertType } from '../../../../../base/common/types.js'; import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js'; -import { IInlineChatSavingService } from '../../browser/inlineChatSavingService.js'; import { IInlineChatSessionService } from '../../browser/inlineChatSessionService.js'; import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl.js'; import { TestWorkerService } from './testWorkerService.js'; @@ -167,11 +165,6 @@ suite('InlineChatController', function () { [IChatEditingService, new class extends mock() { override currentEditingSessionObs: IObservable = observableValue(this, null); }], - [IInlineChatSavingService, new class extends mock() { - override markChanged(session: Session): void { - // noop - } - }], [IEditorProgressService, new class extends mock() { override show(total: unknown, delay?: unknown): IProgressRunner { return { diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts index 880552dd7ba1..80b877eb95a8 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -26,8 +26,7 @@ import { IViewDescriptorService } from '../../../../common/views.js'; import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { IChatAccessibilityService, IChatWidgetService } from '../../../chat/browser/chat.js'; import { IChatResponseViewModel } from '../../../chat/common/chatViewModel.js'; -import { IInlineChatSavingService } from '../../browser/inlineChatSavingService.js'; -import { HunkState, Session } from '../../browser/inlineChatSession.js'; +import { HunkState } from '../../browser/inlineChatSession.js'; import { IInlineChatSessionService } from '../../browser/inlineChatSessionService.js'; import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl.js'; import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; @@ -95,11 +94,6 @@ suite('InlineChatSession', function () { [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], [ICommandService, new SyncDescriptor(TestCommandService)], [ILanguageModelToolsService, new MockLanguageModelToolsService()], - [IInlineChatSavingService, new class extends mock() { - override markChanged(session: Session): void { - // noop - } - }], [IEditorProgressService, new class extends mock() { override show(total: unknown, delay?: unknown): IProgressRunner { return { From b169dfcf405e6206600d4f0fd2fe873cb0333038 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:36:22 +0100 Subject: [PATCH 0760/3587] Git - add caching for commit details (#238451) --- extensions/git/src/blame.ts | 8 +- extensions/git/src/cache.ts | 485 ++++++++++++++++++++++++++++++++++++ 2 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 extensions/git/src/cache.ts diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index aafbbd1cd99f..ca0c6262625d 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -14,6 +14,7 @@ import { emojify, ensureEmojis } from './emoji'; import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation } from './staging'; import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; import { AvatarQuery, AvatarQueryCommit } from './api/git'; +import { LRUCache } from './cache'; const AVATAR_SIZE = 20; @@ -172,6 +173,7 @@ export class GitBlameController { this._onDidChangeBlameInformation.fire(); } + private readonly _commitInformationCache = new LRUCache(100); private readonly _repositoryBlameCache = new GitBlameInformationCache(); private _editorDecoration: GitBlameEditorDecoration | undefined; @@ -216,7 +218,11 @@ export class GitBlameController { if (repository) { try { // Commit details - commitInformation = await repository.getCommit(blameInformation.hash); + commitInformation = this._commitInformationCache.get(blameInformation.hash); + if (!commitInformation) { + commitInformation = await repository.getCommit(blameInformation.hash); + this._commitInformationCache.set(blameInformation.hash, commitInformation); + } // Avatar const avatarQuery = { diff --git a/extensions/git/src/cache.ts b/extensions/git/src/cache.ts new file mode 100644 index 000000000000..df0c0df5561a --- /dev/null +++ b/extensions/git/src/cache.ts @@ -0,0 +1,485 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +interface Item { + previous: Item | undefined; + next: Item | undefined; + key: K; + value: V; +} + +const enum Touch { + None = 0, + AsOld = 1, + AsNew = 2 +} + +class LinkedMap implements Map { + + readonly [Symbol.toStringTag] = 'LinkedMap'; + + private _map: Map>; + private _head: Item | undefined; + private _tail: Item | undefined; + private _size: number; + + private _state: number; + + constructor() { + this._map = new Map>(); + this._head = undefined; + this._tail = undefined; + this._size = 0; + this._state = 0; + } + + clear(): void { + this._map.clear(); + this._head = undefined; + this._tail = undefined; + this._size = 0; + this._state++; + } + + isEmpty(): boolean { + return !this._head && !this._tail; + } + + get size(): number { + return this._size; + } + + get first(): V | undefined { + return this._head?.value; + } + + get last(): V | undefined { + return this._tail?.value; + } + + has(key: K): boolean { + return this._map.has(key); + } + + get(key: K, touch: Touch = Touch.None): V | undefined { + const item = this._map.get(key); + if (!item) { + return undefined; + } + if (touch !== Touch.None) { + this.touch(item, touch); + } + return item.value; + } + + set(key: K, value: V, touch: Touch = Touch.None): this { + let item = this._map.get(key); + if (item) { + item.value = value; + if (touch !== Touch.None) { + this.touch(item, touch); + } + } else { + item = { key, value, next: undefined, previous: undefined }; + switch (touch) { + case Touch.None: + this.addItemLast(item); + break; + case Touch.AsOld: + this.addItemFirst(item); + break; + case Touch.AsNew: + this.addItemLast(item); + break; + default: + this.addItemLast(item); + break; + } + this._map.set(key, item); + this._size++; + } + return this; + } + + delete(key: K): boolean { + return !!this.remove(key); + } + + remove(key: K): V | undefined { + const item = this._map.get(key); + if (!item) { + return undefined; + } + this._map.delete(key); + this.removeItem(item); + this._size--; + return item.value; + } + + shift(): V | undefined { + if (!this._head && !this._tail) { + return undefined; + } + if (!this._head || !this._tail) { + throw new Error('Invalid list'); + } + const item = this._head; + this._map.delete(item.key); + this.removeItem(item); + this._size--; + return item.value; + } + + forEach(callbackfn: (value: V, key: K, map: LinkedMap) => void, thisArg?: any): void { + const state = this._state; + let current = this._head; + while (current) { + if (thisArg) { + callbackfn.bind(thisArg)(current.value, current.key, this); + } else { + callbackfn(current.value, current.key, this); + } + if (this._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + current = current.next; + } + } + + keys(): IterableIterator { + const map = this; + const state = this._state; + let current = this._head; + const iterator: IterableIterator = { + [Symbol.iterator]() { + return iterator; + }, + next(): IteratorResult { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + if (current) { + const result = { value: current.key, done: false }; + current = current.next; + return result; + } else { + return { value: undefined, done: true }; + } + } + }; + return iterator; + } + + values(): IterableIterator { + const map = this; + const state = this._state; + let current = this._head; + const iterator: IterableIterator = { + [Symbol.iterator]() { + return iterator; + }, + next(): IteratorResult { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + if (current) { + const result = { value: current.value, done: false }; + current = current.next; + return result; + } else { + return { value: undefined, done: true }; + } + } + }; + return iterator; + } + + entries(): IterableIterator<[K, V]> { + const map = this; + const state = this._state; + let current = this._head; + const iterator: IterableIterator<[K, V]> = { + [Symbol.iterator]() { + return iterator; + }, + next(): IteratorResult<[K, V]> { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + if (current) { + const result: IteratorResult<[K, V]> = { value: [current.key, current.value], done: false }; + current = current.next; + return result; + } else { + return { value: undefined, done: true }; + } + } + }; + return iterator; + } + + [Symbol.iterator](): IterableIterator<[K, V]> { + return this.entries(); + } + + protected trimOld(newSize: number) { + if (newSize >= this.size) { + return; + } + if (newSize === 0) { + this.clear(); + return; + } + let current = this._head; + let currentSize = this.size; + while (current && currentSize > newSize) { + this._map.delete(current.key); + current = current.next; + currentSize--; + } + this._head = current; + this._size = currentSize; + if (current) { + current.previous = undefined; + } + this._state++; + } + + protected trimNew(newSize: number) { + if (newSize >= this.size) { + return; + } + if (newSize === 0) { + this.clear(); + return; + } + let current = this._tail; + let currentSize = this.size; + while (current && currentSize > newSize) { + this._map.delete(current.key); + current = current.previous; + currentSize--; + } + this._tail = current; + this._size = currentSize; + if (current) { + current.next = undefined; + } + this._state++; + } + + private addItemFirst(item: Item): void { + // First time Insert + if (!this._head && !this._tail) { + this._tail = item; + } else if (!this._head) { + throw new Error('Invalid list'); + } else { + item.next = this._head; + this._head.previous = item; + } + this._head = item; + this._state++; + } + + private addItemLast(item: Item): void { + // First time Insert + if (!this._head && !this._tail) { + this._head = item; + } else if (!this._tail) { + throw new Error('Invalid list'); + } else { + item.previous = this._tail; + this._tail.next = item; + } + this._tail = item; + this._state++; + } + + private removeItem(item: Item): void { + if (item === this._head && item === this._tail) { + this._head = undefined; + this._tail = undefined; + } + else if (item === this._head) { + // This can only happen if size === 1 which is handled + // by the case above. + if (!item.next) { + throw new Error('Invalid list'); + } + item.next.previous = undefined; + this._head = item.next; + } + else if (item === this._tail) { + // This can only happen if size === 1 which is handled + // by the case above. + if (!item.previous) { + throw new Error('Invalid list'); + } + item.previous.next = undefined; + this._tail = item.previous; + } + else { + const next = item.next; + const previous = item.previous; + if (!next || !previous) { + throw new Error('Invalid list'); + } + next.previous = previous; + previous.next = next; + } + item.next = undefined; + item.previous = undefined; + this._state++; + } + + private touch(item: Item, touch: Touch): void { + if (!this._head || !this._tail) { + throw new Error('Invalid list'); + } + if ((touch !== Touch.AsOld && touch !== Touch.AsNew)) { + return; + } + + if (touch === Touch.AsOld) { + if (item === this._head) { + return; + } + + const next = item.next; + const previous = item.previous; + + // Unlink the item + if (item === this._tail) { + // previous must be defined since item was not head but is tail + // So there are more than on item in the map + previous!.next = undefined; + this._tail = previous; + } + else { + // Both next and previous are not undefined since item was neither head nor tail. + next!.previous = previous; + previous!.next = next; + } + + // Insert the node at head + item.previous = undefined; + item.next = this._head; + this._head.previous = item; + this._head = item; + this._state++; + } else if (touch === Touch.AsNew) { + if (item === this._tail) { + return; + } + + const next = item.next; + const previous = item.previous; + + // Unlink the item. + if (item === this._head) { + // next must be defined since item was not tail but is head + // So there are more than on item in the map + next!.previous = undefined; + this._head = next; + } else { + // Both next and previous are not undefined since item was neither head nor tail. + next!.previous = previous; + previous!.next = next; + } + item.next = undefined; + item.previous = this._tail; + this._tail.next = item; + this._tail = item; + this._state++; + } + } + + toJSON(): [K, V][] { + const data: [K, V][] = []; + + this.forEach((value, key) => { + data.push([key, value]); + }); + + return data; + } + + fromJSON(data: [K, V][]): void { + this.clear(); + + for (const [key, value] of data) { + this.set(key, value); + } + } +} + +abstract class Cache extends LinkedMap { + + protected _limit: number; + protected _ratio: number; + + constructor(limit: number, ratio: number = 1) { + super(); + this._limit = limit; + this._ratio = Math.min(Math.max(0, ratio), 1); + } + + get limit(): number { + return this._limit; + } + + set limit(limit: number) { + this._limit = limit; + this.checkTrim(); + } + + get ratio(): number { + return this._ratio; + } + + set ratio(ratio: number) { + this._ratio = Math.min(Math.max(0, ratio), 1); + this.checkTrim(); + } + + override get(key: K, touch: Touch = Touch.AsNew): V | undefined { + return super.get(key, touch); + } + + peek(key: K): V | undefined { + return super.get(key, Touch.None); + } + + override set(key: K, value: V): this { + super.set(key, value, Touch.AsNew); + return this; + } + + protected checkTrim() { + if (this.size > this._limit) { + this.trim(Math.round(this._limit * this._ratio)); + } + } + + protected abstract trim(newSize: number): void; +} + +export class LRUCache extends Cache { + + constructor(limit: number, ratio: number = 1) { + super(limit, ratio); + } + + protected override trim(newSize: number) { + this.trimOld(newSize); + } + + override set(key: K, value: V): this { + super.set(key, value); + this.checkTrim(); + return this; + } +} From 36801b49c9090ae4750d25eeefe907c49f4a01ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:13:11 +0000 Subject: [PATCH 0761/3587] Bump undici from 7.2.0 to 7.2.3 Bumps [undici](https://github.com/nodejs/undici) from 7.2.0 to 7.2.3. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v7.2.0...v7.2.3) --- updated-dependencies: - dependency-name: undici dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5470da4c37a0..edd2ec9114e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17793,9 +17793,9 @@ "dev": true }, "node_modules/undici": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.2.0.tgz", - "integrity": "sha512-klt+0S55GBViA9nsq48/NSCo4YX5mjydjypxD7UmHh/brMu8h/Mhd/F7qAeoH2NOO8SDTk6kjnTFc4WpzmfYpQ==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.2.3.tgz", + "integrity": "sha512-2oSLHaDalSt2/O/wHA9M+/ZPAOcU2yrSP/cdBYJ+YxZskiPYDSqHbysLSlD7gq3JMqOoJI5O31RVU3BxX/MnAA==", "license": "MIT", "engines": { "node": ">=20.18.1" From 23c4dc3f8e604831e8235125cba954b826a70b29 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:18:09 +0100 Subject: [PATCH 0762/3587] Git - enable git blame status bar entry by default (#238454) --- extensions/git/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index bfff94c02cb4..d23ef7b949c9 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -3268,7 +3268,7 @@ }, "git.blame.statusBarItem.enabled": { "type": "boolean", - "default": false, + "default": true, "markdownDescription": "%config.blameStatusBarItem.enabled%" }, "git.blame.statusBarItem.template": { From 4a3edfd006c406653c2cb00df41816908907e133 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:23:54 +0100 Subject: [PATCH 0763/3587] StatusBar - keep disposing a status bar entry on the extension host side (#238453) * StatusBar - keep disposing a status bar entry on the extension host side * Update src/vs/workbench/api/common/extHostStatusBar.ts Co-authored-by: Benjamin Pasero * Update src/vs/workbench/api/common/extHostStatusBar.ts Co-authored-by: Benjamin Pasero * Fix argument names --------- Co-authored-by: Benjamin Pasero --- .../workbench/api/browser/mainThreadStatusBar.ts | 5 +++-- src/vs/workbench/api/common/extHost.protocol.ts | 2 +- src/vs/workbench/api/common/extHostStatusBar.ts | 15 ++++++--------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadStatusBar.ts b/src/vs/workbench/api/browser/mainThreadStatusBar.ts index 92d66d7a2f08..96059861a79a 100644 --- a/src/vs/workbench/api/browser/mainThreadStatusBar.ts +++ b/src/vs/workbench/api/browser/mainThreadStatusBar.ts @@ -36,8 +36,9 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { this._proxy.$acceptStaticEntries(entries); this._store.add(statusbarService.onDidChange(e => { - const added = e.added ? [asDto(e.added[0], e.added[1])] : []; - this._proxy.$acceptStaticEntries(added, e.removed); + if (e.added) { + this._proxy.$acceptStaticEntries([asDto(e.added[0], e.added[1])]); + } })); function asDto(entryId: string, item: { entry: IStatusbarEntry; alignment: StatusbarAlignment; priority: number }): StatusBarItemDto { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index dfe7ac5466f4..1c1d6a921988 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -692,7 +692,7 @@ export type StatusBarItemDto = { }; export interface ExtHostStatusBarShape { - $acceptStaticEntries(added?: StatusBarItemDto[], removed?: string): void; + $acceptStaticEntries(added?: StatusBarItemDto[]): void; $provideTooltip(entryId: string, cancellation: CancellationToken): Promise; } diff --git a/src/vs/workbench/api/common/extHostStatusBar.ts b/src/vs/workbench/api/common/extHostStatusBar.ts index b05a1879f104..c31d8ec28e0e 100644 --- a/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/src/vs/workbench/api/common/extHostStatusBar.ts @@ -60,9 +60,9 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private _timeoutHandle: any; private _accessibilityInformation?: vscode.AccessibilityInformation; - constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, staticItems: ReadonlyMap, extension: IExtensionDescription, id?: string, alignment?: ExtHostStatusBarAlignment, priority?: number); - constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, staticItems: ReadonlyMap, extension: IExtensionDescription | undefined, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number); - constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, staticItems: ReadonlyMap, extension?: IExtensionDescription, id?: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number) { + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, staticItems: ReadonlyMap, extension: IExtensionDescription, id?: string, alignment?: ExtHostStatusBarAlignment, priority?: number, _onDispose?: () => void); + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, staticItems: ReadonlyMap, extension: IExtensionDescription | undefined, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number, _onDispose?: () => void); + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, staticItems: ReadonlyMap, extension?: IExtensionDescription, id?: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number, private _onDispose?: () => void) { this.#proxy = proxy; this.#commands = commands; @@ -304,6 +304,7 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { public dispose(): void { this.hide(); + this._onDispose?.(); this._disposed = true; } } @@ -361,14 +362,10 @@ export class ExtHostStatusBar implements ExtHostStatusBarShape { this._statusMessage = new StatusBarMessage(this); } - $acceptStaticEntries(added: StatusBarItemDto[], removed?: string): void { + $acceptStaticEntries(added: StatusBarItemDto[]): void { for (const item of added) { this._existingItems.set(item.entryId, item); } - - if (removed) { - this._entries.delete(removed); - } } async $provideTooltip(entryId: string, cancellation: vscode.CancellationToken): Promise { @@ -384,7 +381,7 @@ export class ExtHostStatusBar implements ExtHostStatusBarShape { createStatusBarEntry(extension: IExtensionDescription | undefined, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem; createStatusBarEntry(extension: IExtensionDescription, id?: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem; createStatusBarEntry(extension: IExtensionDescription, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem { - const entry = new ExtHostStatusBarEntry(this._proxy, this._commands, this._existingItems, extension, id, alignment, priority); + const entry = new ExtHostStatusBarEntry(this._proxy, this._commands, this._existingItems, extension, id, alignment, priority, () => this._entries.delete(entry.entryId)); this._entries.set(entry.entryId, entry); return entry; From 92c3380c8f8a263c55e8786040bafe9176cacada Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 22 Jan 2025 03:30:15 -0800 Subject: [PATCH 0764/3587] Remove unused classification test (#237989) Remove unused test file This test has been commented out for 5 years now --- build/filters.js | 1 - .../node/classification/typescript-test.ts | 71 --------- .../node/classification/typescript.test.ts | 145 ------------------ 3 files changed, 217 deletions(-) delete mode 100644 src/vs/editor/test/node/classification/typescript-test.ts delete mode 100644 src/vs/editor/test/node/classification/typescript.test.ts diff --git a/build/filters.js b/build/filters.js index 70e175463dc5..17e74c3871a5 100644 --- a/build/filters.js +++ b/build/filters.js @@ -176,7 +176,6 @@ module.exports.copyrightFilter = [ '!extensions/typescript-language-features/node-maintainer/**', '!extensions/html-language-features/server/src/modes/typescript/*', '!extensions/*/server/bin/*', - '!src/vs/editor/test/node/classification/typescript-test.ts', ]; module.exports.tsFormattingFilter = [ diff --git a/src/vs/editor/test/node/classification/typescript-test.ts b/src/vs/editor/test/node/classification/typescript-test.ts deleted file mode 100644 index f1a8a21c22d8..000000000000 --- a/src/vs/editor/test/node/classification/typescript-test.ts +++ /dev/null @@ -1,71 +0,0 @@ -/// -/* eslint-disable */ -const x01 = "string"; -/// ^^^^^^^^ string - -const x02 = '\''; -/// ^^^^ string - -const x03 = '\n\'\t'; -/// ^^^^^^^^ string - -const x04 = 'this is\ -/// ^^^^^^^^^ string\ -a multiline string'; -/// <------------------- string - -const x05 = x01;// just some text -/// ^^^^^^^^^^^^^^^^^ comment - -const x06 = x05;/* multi -/// ^^^^^^^^ comment -line *comment */ -/// <---------------- comment - -const x07 = 4 / 5; - -const x08 = `howdy`; -/// ^^^^^^^ string - -const x09 = `\'\"\``; -/// ^^^^^^^^ string - -const x10 = `$[]`; -/// ^^^^^ string - -const x11 = `${x07 +/**/3}px`; -/// ^^^ string -/// ^^^^ comment -/// ^^^^ string - -const x12 = `${x07 + (function () { return 5; })()/**/}px`; -/// ^^^ string -/// ^^^^ comment -/// ^^^^ string - -const x13 = /([\w\-]+)?(#([\w\-]+))?((.([\w\-]+))*)/; -/// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ regex - -const x14 = /\./g; -/// ^^^^^ regex - - -const x15 = Math.abs(x07) / x07; // speed -/// ^^^^^^^^ comment - -const x16 = / x07; /.test('3'); -/// ^^^^^^^^ regex -/// ^^^ string - -const x17 = `.monaco-dialog-modal-block${true ? '.dimmed' : ''}`; -/// ^^^^^^^^^^^^^^^^^^^^^^ string -/// ^^^^^^^^^ string -/// ^^^^ string - -const x18 = Math.min((14 <= 0.5 ? 123 / (2 * 1) : ''.length / (2 - (2 * 1))), 1); -/// ^^ string - -const x19 = `${3 / '5'.length} km/h)`; -/// ^^^ string -/// ^^^ string -/// ^^^^^^^ string diff --git a/src/vs/editor/test/node/classification/typescript.test.ts b/src/vs/editor/test/node/classification/typescript.test.ts deleted file mode 100644 index 4166eadcd235..000000000000 --- a/src/vs/editor/test/node/classification/typescript.test.ts +++ /dev/null @@ -1,145 +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 { StandardTokenType } from '../../../common/encodedTokenAttributes.js'; -import * as fs from 'fs'; -// import { getPathFromAmdModule } from 'vs/base/test/node/testUtils'; -// import { parse } from 'vs/editor/common/modes/tokenization/typescript'; -import { toStandardTokenType } from '../../../common/languages/supports/tokenization.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; - -interface IParseFunc { - (text: string): number[]; -} - -interface IAssertion { - testLineNumber: number; - startOffset: number; - length: number; - tokenType: StandardTokenType; -} - -interface ITest { - content: string; - assertions: IAssertion[]; -} - -function parseTest(fileName: string): ITest { - interface ILineWithAssertions { - line: string; - assertions: ILineAssertion[]; - } - - interface ILineAssertion { - testLineNumber: number; - startOffset: number; - length: number; - expectedTokenType: StandardTokenType; - } - - const testContents = fs.readFileSync(fileName).toString(); - const lines = testContents.split(/\r\n|\n/); - const magicToken = lines[0]; - - let currentElement: ILineWithAssertions = { - line: lines[1], - assertions: [] - }; - - const parsedTest: ILineWithAssertions[] = []; - for (let i = 2; i < lines.length; i++) { - const line = lines[i]; - if (line.substr(0, magicToken.length) === magicToken) { - // this is an assertion line - const m1 = line.substr(magicToken.length).match(/^( +)([\^]+) (\w+)\\?$/); - if (m1) { - currentElement.assertions.push({ - testLineNumber: i + 1, - startOffset: magicToken.length + m1[1].length, - length: m1[2].length, - expectedTokenType: toStandardTokenType(m1[3]) - }); - } else { - const m2 = line.substr(magicToken.length).match(/^( +)<(-+) (\w+)\\?$/); - if (m2) { - currentElement.assertions.push({ - testLineNumber: i + 1, - startOffset: 0, - length: m2[2].length, - expectedTokenType: toStandardTokenType(m2[3]) - }); - } else { - throw new Error(`Invalid test line at line number ${i + 1}.`); - } - } - } else { - // this is a line to be parsed - parsedTest.push(currentElement); - currentElement = { - line: line, - assertions: [] - }; - } - } - parsedTest.push(currentElement); - - const assertions: IAssertion[] = []; - - let offset = 0; - for (let i = 0; i < parsedTest.length; i++) { - const parsedTestLine = parsedTest[i]; - for (let j = 0; j < parsedTestLine.assertions.length; j++) { - const assertion = parsedTestLine.assertions[j]; - assertions.push({ - testLineNumber: assertion.testLineNumber, - startOffset: offset + assertion.startOffset, - length: assertion.length, - tokenType: assertion.expectedTokenType - }); - } - offset += parsedTestLine.line.length + 1; - } - - const content: string = parsedTest.map(parsedTestLine => parsedTestLine.line).join('\n'); - - return { content, assertions }; -} - -// @ts-expect-error -function executeTest(fileName: string, parseFunc: IParseFunc): void { - const { content, assertions } = parseTest(fileName); - const actual = parseFunc(content); - - let actualIndex = 0; - const actualCount = actual.length / 3; - for (let i = 0; i < assertions.length; i++) { - const assertion = assertions[i]; - while (actualIndex < actualCount && actual[3 * actualIndex] + actual[3 * actualIndex + 1] <= assertion.startOffset) { - actualIndex++; - } - assert.ok( - actual[3 * actualIndex] <= assertion.startOffset, - `Line ${assertion.testLineNumber} : startOffset : ${actual[3 * actualIndex]} <= ${assertion.startOffset}` - ); - assert.ok( - actual[3 * actualIndex] + actual[3 * actualIndex + 1] >= assertion.startOffset + assertion.length, - `Line ${assertion.testLineNumber} : length : ${actual[3 * actualIndex]} + ${actual[3 * actualIndex + 1]} >= ${assertion.startOffset} + ${assertion.length}.` - ); - assert.strictEqual( - actual[3 * actualIndex + 2], - assertion.tokenType, - `Line ${assertion.testLineNumber} : tokenType`); - } -} - -suite('Classification', () => { - - ensureNoDisposablesAreLeakedInTestSuite(); - - test('TypeScript', () => { - // executeTest(getPathFromAmdModule(require, 'vs/editor/test/node/classification/typescript-test.ts').replace(/\bout\b/, 'src'), parse); - }); -}); From 69ce98f0d4487a47d4f5c352dc67d867a904230d Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 22 Jan 2025 06:45:23 -0600 Subject: [PATCH 0765/3587] show spec info w priority over path (#238408) --- extensions/terminal-suggest/src/terminalSuggestMain.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 6072cd048f4c..8ba754efab23 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -136,7 +136,6 @@ export async function activate(context: vscode.ExtensionContext) { const result = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, terminal.shellIntegration?.cwd, token); if (result.cwd && (result.filesRequested || result.foldersRequested)) { - // const cwd = resolveCwdFromPrefix(prefix, terminal.shellIntegration?.cwd) ?? terminal.shellIntegration?.cwd; return new vscode.TerminalCompletionList(result.items, { filesRequested: result.filesRequested, foldersRequested: result.foldersRequested, cwd: result.cwd, pathSeparator: isWindows ? '\\' : '/' }); } return result.items; @@ -345,7 +344,7 @@ export async function getCompletionItemsFromSpecs( || !!firstCommand && specLabel.startsWith(firstCommand) ) { // push it to the completion items - items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: specLabel }, availableCommand.detail, getDescription(spec))); + items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: specLabel }, getDescription(spec), availableCommand.detail)); } if (!terminalContext.commandLine.startsWith(specLabel)) { From 5f888fae91ed3f1e5b0437440947b7258c9e6e89 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 22 Jan 2025 13:58:32 +0100 Subject: [PATCH 0766/3587] make output parsing performant by exposing log entries in the output channel model (#238459) --- .../contrib/output/browser/outputServices.ts | 46 ++-- .../contrib/output/browser/outputView.ts | 123 +++++------ .../output/common/outputChannelModel.ts | 202 ++++++++++++++---- .../common/outputChannelModelService.ts | 57 ----- .../services/output/common/output.ts | 122 ++--------- src/vs/workbench/workbench.common.main.ts | 1 - 6 files changed, 275 insertions(+), 276 deletions(-) delete mode 100644 src/vs/workbench/contrib/output/common/outputChannelModelService.ts diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 942316dbe333..2cbd03b88259 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -10,16 +10,15 @@ import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js' import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; -import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, IOutputViewFilters, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT, IMultiSourceOutputChannelDescriptor, isSingleSourceOutputChannelDescriptor, HIDE_SOURCE_FILTER_CONTEXT, isMultiSourceOutputChannelDescriptor } from '../../../services/output/common/output.js'; +import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, IOutputViewFilters, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT, IMultiSourceOutputChannelDescriptor, isSingleSourceOutputChannelDescriptor, HIDE_SOURCE_FILTER_CONTEXT, isMultiSourceOutputChannelDescriptor, ILogEntry } from '../../../services/output/common/output.js'; import { OutputLinkProvider } from './outputLinkProvider.js'; import { ITextModelService, ITextModelContentProvider } from '../../../../editor/common/services/resolverService.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { ILogService, ILoggerService, LogLevel, LogLevelToString } from '../../../../platform/log/common/log.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; -import { IOutputChannelModel } from '../common/outputChannelModel.js'; +import { DelegatedOutputChannelModel, FileOutputChannelModel, IOutputChannelModel, MultiFileOutputChannelModel } from '../common/outputChannelModel.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { OutputViewPane } from './outputView.js'; -import { IOutputChannelModelService } from '../common/outputChannelModelService.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IDefaultLogLevelsService } from '../../logs/common/defaultLogLevels.js'; @@ -29,6 +28,8 @@ import { localize } from '../../../../nls.js'; import { joinPath } from '../../../../base/common/resources.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { extensionTelemetryLogChannelId, telemetryLogId } from '../../../../platform/telemetry/common/telemetryUtils.js'; +import { toLocalISOString } from '../../../../base/common/date.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel'; @@ -42,19 +43,31 @@ class OutputChannel extends Disposable implements IOutputChannel { constructor( readonly outputChannelDescriptor: IOutputChannelDescriptor, - @IOutputChannelModelService outputChannelModelService: IOutputChannelModelService, - @ILanguageService languageService: ILanguageService, + private readonly outputLocation: URI, + private readonly outputDirPromise: Promise, + @ILanguageService private readonly languageService: ILanguageService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this.id = outputChannelDescriptor.id; this.label = outputChannelDescriptor.label; this.uri = URI.from({ scheme: Schemas.outputChannel, path: this.id }); - this.model = this._register(outputChannelModelService.createOutputChannelModel( - this.id, - this.uri, - outputChannelDescriptor.languageId ? languageService.createById(outputChannelDescriptor.languageId) : languageService.createByMimeType(outputChannelDescriptor.log ? LOG_MIME : OUTPUT_MIME), - outputChannelDescriptor.source) - ); + this.model = this._register(this.createOutputChannelModel(this.uri, outputChannelDescriptor)); + } + + private createOutputChannelModel(uri: URI, outputChannelDescriptor: IOutputChannelDescriptor): IOutputChannelModel { + const language = outputChannelDescriptor.languageId ? this.languageService.createById(outputChannelDescriptor.languageId) : this.languageService.createByMimeType(outputChannelDescriptor.log ? LOG_MIME : OUTPUT_MIME); + if (isMultiSourceOutputChannelDescriptor(outputChannelDescriptor)) { + return this.instantiationService.createInstance(MultiFileOutputChannelModel, uri, language, [...outputChannelDescriptor.source]); + } + if (isSingleSourceOutputChannelDescriptor(outputChannelDescriptor)) { + return this.instantiationService.createInstance(FileOutputChannelModel, uri, language, outputChannelDescriptor.source); + } + return this.instantiationService.createInstance(DelegatedOutputChannelModel, this.id, uri, language, this.outputLocation, this.outputDirPromise); + } + + getLogEntries(): ReadonlyArray { + return this.model.getLogEntries(); } append(output: string): void { @@ -217,6 +230,8 @@ export class OutputService extends Disposable implements IOutputService, ITextMo private readonly activeOutputChannelLevelContext: IContextKey; private readonly activeOutputChannelLevelIsDefaultContext: IContextKey; + private readonly outputLocation: URI; + readonly filters: OutputViewFilters; constructor( @@ -231,6 +246,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo @IDefaultLogLevelsService private readonly defaultLogLevelsService: IDefaultLogLevelsService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IFileService private readonly fileService: IFileService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { super(); this.activeChannelIdInStorage = this.storageService.get(OUTPUT_ACTIVE_CHANNEL_KEY, StorageScope.WORKSPACE, ''); @@ -244,6 +260,8 @@ export class OutputService extends Disposable implements IOutputService, ITextMo this.activeOutputChannelLevelContext = CONTEXT_ACTIVE_OUTPUT_LEVEL.bindTo(contextKeyService); this.activeOutputChannelLevelIsDefaultContext = CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT.bindTo(contextKeyService); + this.outputLocation = joinPath(environmentService.windowLogsPath, `output_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); + // Register as text model content provider for output this._register(textModelService.registerTextModelContentProvider(Schemas.outputChannel, this)); this._register(instantiationService.createInstance(OutputLinkProvider)); @@ -475,13 +493,17 @@ export class OutputService extends Disposable implements IOutputService, ITextMo return channel; } + private outputFolderCreationPromise: Promise | null = null; private instantiateChannel(id: string): OutputChannel { const channelData = Registry.as(Extensions.OutputChannels).getChannel(id); if (!channelData) { this.logService.error(`Channel '${id}' is not registered yet`); throw new Error(`Channel '${id}' is not registered yet`); } - return this.instantiationService.createInstance(OutputChannel, channelData); + if (!this.outputFolderCreationPromise) { + this.outputFolderCreationPromise = this.fileService.createFolder(this.outputLocation).then(() => undefined); + } + return this.instantiationService.createInstance(OutputChannel, channelData, this.outputLocation, this.outputFolderCreationPromise); } private resetLogLevelFilters(): void { diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 250bf7afa2dd..6cf5141a650b 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -13,7 +13,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { IContextKeyService, IContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IEditorOpenContext } from '../../../common/editor.js'; import { AbstractTextResourceEditor } from '../../../browser/parts/editor/textResourceEditor.js'; -import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT, parseLogEntries, ILogEntry, parseLogEntryAt } from '../../../services/output/common/output.js'; +import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT, ILogEntry } from '../../../services/output/common/output.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; @@ -351,8 +351,6 @@ export class FilterController extends Disposable implements IEditorContribution private hiddenAreas: Range[] = []; private readonly decorationsCollection: IEditorDecorationsCollection; - private logEntries: ILogEntry[] | undefined; - constructor( private readonly editor: ICodeEditor, @IOutputService private readonly outputService: IOutputService @@ -365,7 +363,6 @@ export class FilterController extends Disposable implements IEditorContribution private onDidChangeModel(): void { this.modelDisposables.clear(); - this.logEntries = undefined; this.hiddenAreas = []; if (!this.editor.hasModel()) { @@ -373,7 +370,6 @@ export class FilterController extends Disposable implements IEditorContribution } const model = this.editor.getModel(); - this.computeLogEntries(model); this.filter(model); const computeEndLineNumber = () => { @@ -385,92 +381,89 @@ export class FilterController extends Disposable implements IEditorContribution this.modelDisposables.add(model.onDidChangeContent(e => { if (e.changes.every(e => e.range.startLineNumber > endLineNumber)) { - const filterFrom = this.logEntries?.length ?? endLineNumber + 1; - if (this.logEntries) { - this.computeLogEntriesIncremental(model, endLineNumber + 1); - } - this.filterIncremental(model, filterFrom); + this.filterIncremental(model, endLineNumber + 1); } else { - this.computeLogEntries(model); this.filter(model); } endLineNumber = computeEndLineNumber(); })); } - private computeLogEntries(model: ITextModel): void { - this.logEntries = undefined; - if (!parseLogEntryAt(model, 1)) { - return; - } - - this.logEntries = []; - this.computeLogEntriesIncremental(model, 1); - } - - private computeLogEntriesIncremental(model: ITextModel, fromLine: number): void { - if (this.logEntries) { - this.logEntries = this.logEntries.concat(parseLogEntries(model, fromLine)); - } - } - private filter(model: ITextModel): void { this.hiddenAreas = []; this.decorationsCollection.clear(); - this.filterIncremental(model, 0); + this.filterIncremental(model, 1); + } + + private filterIncremental(model: ITextModel, fromLineNumber: number): void { + const { findMatches, hiddenAreas } = this.computeDecorations(model, fromLineNumber); + this.hiddenAreas.push(...hiddenAreas); + this.editor.setHiddenAreas(this.hiddenAreas, this); + if (findMatches.length) { + this.decorationsCollection.append(findMatches); + } } - private filterIncremental(model: ITextModel, from: number): void { + private computeDecorations(model: ITextModel, fromLineNumber: number): { findMatches: IModelDeltaDecoration[]; hiddenAreas: Range[] } { const filters = this.outputService.filters; - const activeChannelId = this.outputService.getActiveChannel()?.id ?? ''; - const findMatchesDecorations: IModelDeltaDecoration[] = []; + const activeChannel = this.outputService.getActiveChannel(); + const findMatches: IModelDeltaDecoration[] = []; + const hiddenAreas: Range[] = []; - if (this.logEntries) { + const logEntries = activeChannel?.getLogEntries(); + if (activeChannel && logEntries?.length) { const hasLogLevelFilter = !filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error; - if (hasLogLevelFilter || filters.text || filters.sources.includes(activeChannelId)) { - for (let i = from; i < this.logEntries.length; i++) { - const entry = this.logEntries[i]; - if (hasLogLevelFilter && !this.shouldShowLogLevel(entry, filters)) { - this.hiddenAreas.push(entry.range); - continue; - } - if (!this.shouldShowSource(activeChannelId, entry, filters)) { - this.hiddenAreas.push(entry.range); - continue; - } - if (filters.text) { - const matches = model.findMatches(filters.text, entry.range, false, false, null, false); - if (matches.length) { - for (const match of matches) { - findMatchesDecorations.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION }); - } - } else { - this.hiddenAreas.push(entry.range); - } - } - } + + if (!hasLogLevelFilter && !filters.text && !filters.sources.includes(activeChannel.id)) { + return { findMatches, hiddenAreas }; } - } else { - if (filters.text) { - const lineCount = model.getLineCount(); - for (let lineNumber = from + 1; lineNumber <= lineCount; lineNumber++) { - const lineRange = new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber)); - const matches = model.findMatches(filters.text, lineRange, false, false, null, false); + + const fromLogLevelEntryIndex = logEntries.findIndex(entry => fromLineNumber >= entry.range.startLineNumber && fromLineNumber <= entry.range.endLineNumber); + if (fromLogLevelEntryIndex === -1) { + return { findMatches, hiddenAreas }; + } + + for (let i = fromLogLevelEntryIndex; i < logEntries.length; i++) { + const entry = logEntries[i]; + if (hasLogLevelFilter && !this.shouldShowLogLevel(entry, filters)) { + hiddenAreas.push(entry.range); + continue; + } + if (!this.shouldShowSource(activeChannel.id, entry, filters)) { + hiddenAreas.push(entry.range); + continue; + } + if (filters.text) { + const matches = model.findMatches(filters.text, entry.range, false, false, null, false); if (matches.length) { for (const match of matches) { - findMatchesDecorations.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION }); + findMatches.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION }); } } else { - this.hiddenAreas.push(lineRange); + hiddenAreas.push(entry.range); } } } + return { findMatches, hiddenAreas }; } - this.editor.setHiddenAreas(this.hiddenAreas, this); - if (findMatchesDecorations.length) { - this.decorationsCollection.append(findMatchesDecorations); + if (!filters.text) { + return { findMatches, hiddenAreas }; + } + + const lineCount = model.getLineCount(); + for (let lineNumber = fromLineNumber + 1; lineNumber <= lineCount; lineNumber++) { + const lineRange = new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber)); + const matches = model.findMatches(filters.text, lineRange, false, false, null, false); + if (matches.length) { + for (const match of matches) { + findMatches.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION }); + } + } else { + hiddenAreas.push(lineRange); + } } + return { findMatches, hiddenAreas }; } private shouldShowLogLevel(entry: ILogEntry, filters: IOutputViewFilters): boolean { diff --git a/src/vs/workbench/contrib/output/common/outputChannelModel.ts b/src/vs/workbench/contrib/output/common/outputChannelModel.ts index ed44b86a3c06..8d78deac91f1 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModel.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModel.ts @@ -19,16 +19,80 @@ import { EditOperation, ISingleEditOperation } from '../../../../editor/common/c import { Position } from '../../../../editor/common/core/position.js'; import { Range } from '../../../../editor/common/core/range.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; -import { ILogger, ILoggerService, ILogService } from '../../../../platform/log/common/log.js'; +import { ILogger, ILoggerService, ILogService, LogLevel } from '../../../../platform/log/common/log.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { ILogEntry, IOutputContentSource, LOG_MIME, logEntryIterator, OutputChannelUpdateMode } from '../../../services/output/common/output.js'; +import { ILogEntry, IOutputContentSource, LOG_MIME, OutputChannelUpdateMode } from '../../../services/output/common/output.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { TextModel } from '../../../../editor/common/model/textModel.js'; import { binarySearch, sortedDiff } from '../../../../base/common/arrays.js'; +const LOG_ENTRY_REGEX = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s(\[(info|trace|debug|error|warning)\])\s(\[(.*?)\]\s)?/; + +function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntry | null { + const lineContent = model.getLineContent(lineNumber); + const match = LOG_ENTRY_REGEX.exec(lineContent); + if (match) { + const timestamp = new Date(match[1]).getTime(); + const timestampRange = new Range(lineNumber, 1, lineNumber, match[1].length); + const logLevel = parseLogLevel(match[3]); + const logLevelRange = new Range(lineNumber, timestampRange.endColumn + 1, lineNumber, timestampRange.endColumn + 1 + match[2].length); + const source = match[5]; + const startLine = lineNumber; + let endLine = lineNumber; + + while (endLine < model.getLineCount()) { + const nextLineContent = model.getLineContent(endLine + 1); + if (model.getLineFirstNonWhitespaceColumn(endLine + 1) === 0 || LOG_ENTRY_REGEX.test(nextLineContent)) { + break; + } + endLine++; + } + const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)); + return { range, timestamp, timestampRange, logLevel, logLevelRange, source }; + } + return null; +} + +function* logEntryIterator(model: ITextModel, process: (logEntry: ILogEntry) => T): IterableIterator { + for (let lineNumber = 1; lineNumber <= model.getLineCount(); lineNumber++) { + const logEntry = parseLogEntryAt(model, lineNumber); + if (logEntry) { + yield process(logEntry); + lineNumber = logEntry.range.endLineNumber; + } + } +} + +function changeStartLineNumber(logEntry: ILogEntry, lineNumber: number): ILogEntry { + return { + ...logEntry, + range: new Range(lineNumber, logEntry.range.startColumn, lineNumber + logEntry.range.endLineNumber - logEntry.range.startLineNumber, logEntry.range.endColumn), + timestampRange: new Range(lineNumber, logEntry.timestampRange.startColumn, lineNumber, logEntry.timestampRange.endColumn), + logLevelRange: new Range(lineNumber, logEntry.logLevelRange.startColumn, lineNumber, logEntry.logLevelRange.endColumn), + }; +} + +function parseLogLevel(level: string): LogLevel { + switch (level.toLowerCase()) { + case 'trace': + return LogLevel.Trace; + case 'debug': + return LogLevel.Debug; + case 'info': + return LogLevel.Info; + case 'warning': + return LogLevel.Warning; + case 'error': + return LogLevel.Error; + default: + throw new Error(`Unknown log level: ${level}`); + } +} + export interface IOutputChannelModel extends IDisposable { readonly onDispose: Event; readonly source: IOutputContentSource | ReadonlyArray; + getLogEntries(): ReadonlyArray; append(output: string): void; update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void; updateChannelSources(sources: ReadonlyArray): void; @@ -44,6 +108,7 @@ interface IContentProvider { watch(): void; unwatch(): void; getContent(): Promise<{ readonly content: string; readonly consume: () => void }>; + getLogEntries(): ReadonlyArray; } class FileContentProvider extends Disposable implements IContentProvider { @@ -58,6 +123,7 @@ class FileContentProvider extends Disposable implements IContentProvider { private syncDelayer: ThrottledDelayer; private etag: string | undefined = ''; + private logEntries: ILogEntry[] = []; private startOffset: number = 0; private endOffset: number = 0; @@ -67,6 +133,7 @@ class FileContentProvider extends Disposable implements IContentProvider { constructor( { name, resource }: IOutputContentSource, @IFileService private readonly fileService: IFileService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService private readonly logService: ILogService, ) { super(); @@ -79,10 +146,12 @@ class FileContentProvider extends Disposable implements IContentProvider { reset(offset?: number): void { this.endOffset = this.startOffset = offset ?? this.startOffset; + this.logEntries = []; } resetToEnd(): void { this.startOffset = this.endOffset; + this.logEntries = []; } watch(): void { @@ -132,7 +201,11 @@ class FileContentProvider extends Disposable implements IContentProvider { } } - async getContent(): Promise<{ readonly name: string; readonly content: string; readonly consume: () => void }> { + getLogEntries(): ReadonlyArray { + return this.logEntries; + } + + async getContent(donotConsumeLogEntries?: boolean): Promise<{ readonly name: string; readonly content: string; readonly consume: () => void }> { try { if (!this.fileService.hasProvider(this.resource)) { return { @@ -141,16 +214,19 @@ class FileContentProvider extends Disposable implements IContentProvider { consume: () => { /* No Op */ } }; } - const content = await this.fileService.readFile(this.resource, { position: this.endOffset }); + const fileContent = await this.fileService.readFile(this.resource, { position: this.endOffset }); + const content = fileContent.value.toString(); + const logEntries = donotConsumeLogEntries ? [] : this.parseLogEntries(content, this.logEntries[this.logEntries.length - 1]); let consumed = false; return { name: this.name, - content: content.value.toString(), + content, consume: () => { if (!consumed) { consumed = true; - this.endOffset += content.value.byteLength; - this.etag = content.etag; + this.endOffset += fileContent.value.byteLength; + this.etag = fileContent.etag; + this.logEntries.push(...logEntries); } } }; @@ -165,6 +241,24 @@ class FileContentProvider extends Disposable implements IContentProvider { }; } } + + private parseLogEntries(content: string, lastLogEntry: ILogEntry | undefined): ILogEntry[] { + const model = this.instantiationService.createInstance(TextModel, content, LOG_MIME, TextModel.DEFAULT_CREATION_OPTIONS, null); + if (!parseLogEntryAt(model, 1)) { + return []; + } + try { + const logEntries: ILogEntry[] = []; + let logEntryStartLineNumber = lastLogEntry ? lastLogEntry.range.endLineNumber + 1 : 1; + for (const entry of logEntryIterator(model, (e) => changeStartLineNumber(e, logEntryStartLineNumber))) { + logEntries.push(entry); + logEntryStartLineNumber = entry.range.endLineNumber + 1; + } + return logEntries; + } finally { + model.dispose(); + } + } } class MultiFileContentProvider extends Disposable implements IContentProvider { @@ -173,6 +267,7 @@ class MultiFileContentProvider extends Disposable implements IContentProvider { readonly onDidAppend = this._onDidAppend.event; readonly onDidReset = Event.None; + private logEntries: ILogEntry[] = []; private readonly fileContentProviderItems: [FileContentProvider, DisposableStore][] = []; private watching: boolean = false; @@ -196,7 +291,7 @@ class MultiFileContentProvider extends Disposable implements IContentProvider { private createFileContentProvider(file: IOutputContentSource): [FileContentProvider, DisposableStore] { const disposables = new DisposableStore(); - const fileOutput = disposables.add(new FileContentProvider(file, this.fileService, this.logService)); + const fileOutput = disposables.add(new FileContentProvider(file, this.fileService, this.instantiationService, this.logService)); disposables.add(fileOutput.onDidAppend(() => this._onDidAppend.fire())); return [fileOutput, disposables]; } @@ -243,43 +338,60 @@ class MultiFileContentProvider extends Disposable implements IContentProvider { for (const [output] of this.fileContentProviderItems) { output.reset(); } + this.logEntries = []; } resetToEnd(): void { for (const [output] of this.fileContentProviderItems) { output.resetToEnd(); } + this.logEntries = []; + } + + getLogEntries(): ReadonlyArray { + return this.logEntries; } async getContent(): Promise<{ readonly content: string; readonly consume: () => void }> { - const outputs = await Promise.all(this.fileContentProviderItems.map(([output]) => output.getContent())); - const content = this.combineLogEntries(outputs); + const outputs = await Promise.all(this.fileContentProviderItems.map(([output]) => output.getContent(true))); + const { content, logEntries } = this.combineLogEntries(outputs, this.logEntries[this.logEntries.length - 1]); + let consumed = false; return { content, - consume: () => outputs.forEach(({ consume }) => consume()) + consume: () => { + if (!consumed) { + consumed = true; + outputs.forEach(({ consume }) => consume()); + this.logEntries.push(...logEntries); + } + } }; } - private combineLogEntries(outputs: { content: string; name: string }[]): string { + private combineLogEntries(outputs: { content: string; name: string }[], lastEntry: ILogEntry | undefined): { logEntries: ILogEntry[]; content: string } { outputs = outputs.filter(output => !!output.content); if (outputs.length === 0) { - return ''; + return { logEntries: [], content: '' }; } - const timestamps: number[] = []; + const logEntries: ILogEntry[] = []; const contents: string[] = []; - const process = (model: ITextModel, logEntry: ILogEntry, name: string): [number, string] => { + const process = (model: ITextModel, logEntry: ILogEntry, name: string): [ILogEntry, string] => { const lineContent = model.getValueInRange(logEntry.range); const content = name ? `${lineContent.substring(0, logEntry.logLevelRange.endColumn)} [${name}]${lineContent.substring(logEntry.logLevelRange.endColumn)}` : lineContent; - return [logEntry.timestamp, content]; + return [{ + ...logEntry, + source: name, + range: new Range(logEntry.range.startLineNumber, logEntry.logLevelRange.startColumn, logEntry.range.endLineNumber, name ? logEntry.range.endColumn + name.length + 3 : logEntry.range.endColumn), + }, content]; }; const model = this.instantiationService.createInstance(TextModel, outputs[0].content, LOG_MIME, TextModel.DEFAULT_CREATION_OPTIONS, null); try { - for (const [timestamp, content] of logEntryIterator(model, (e) => process(model, e, outputs[0].name))) { - timestamps.push(timestamp); + for (const [logEntry, content] of logEntryIterator(model, (e) => process(model, e, outputs[0].name))) { + logEntries.push(logEntry); contents.push(content); } } finally { @@ -293,50 +405,58 @@ class MultiFileContentProvider extends Disposable implements IContentProvider { const iterator = logEntryIterator(model, (e) => process(model, e, name)); let next = iterator.next(); while (!next.done) { - const [timestamp, content] = next.value; - const timestampsToAdd = [timestamp]; + const [logEntry, content] = next.value; + const logEntriesToAdd = [logEntry]; const contentsToAdd = [content]; let insertionIndex; // If the timestamp is greater than or equal to the last timestamp, // we can just append all the entries at the end - if (timestamp >= timestamps[timestamps.length - 1]) { - insertionIndex = timestamps.length; + if (logEntry.timestamp >= logEntries[logEntries.length - 1].timestamp) { + insertionIndex = logEntries.length; for (next = iterator.next(); !next.done; next = iterator.next()) { - timestampsToAdd.push(next.value[0]); + logEntriesToAdd.push(next.value[0]); contentsToAdd.push(next.value[1]); } } else { - if (timestamp <= timestamps[0]) { + if (logEntry.timestamp <= logEntries[0].timestamp) { // If the timestamp is less than or equal to the first timestamp // then insert at the beginning insertionIndex = 0; } else { // Otherwise, find the insertion index - const idx = binarySearch(timestamps, timestamp, (a, b) => a - b); + const idx = binarySearch(logEntries, logEntry, (a, b) => a.timestamp - b.timestamp); insertionIndex = idx < 0 ? ~idx : idx; } // Collect all entries that have a timestamp less than or equal to the timestamp at the insertion index - for (next = iterator.next(); !next.done && next.value[0] <= timestamps[insertionIndex]; next = iterator.next()) { - timestampsToAdd.push(next.value[0]); + for (next = iterator.next(); !next.done && next.value[0].timestamp <= logEntries[insertionIndex].timestamp; next = iterator.next()) { + logEntriesToAdd.push(next.value[0]); contentsToAdd.push(next.value[1]); } } contents.splice(insertionIndex, 0, ...contentsToAdd); - timestamps.splice(insertionIndex, 0, ...timestampsToAdd); + logEntries.splice(insertionIndex, 0, ...logEntriesToAdd); } } finally { model.dispose(); } } - // Add a newline at the end - contents.push(''); - return contents.join('\n'); + let content = ''; + const updatedLogEntries: ILogEntry[] = []; + let logEntryStartLineNumber = lastEntry ? lastEntry.range.endLineNumber + 1 : 1; + for (let i = 0; i < logEntries.length; i++) { + content += contents[i] + '\n'; + const updatedLogEntry = changeStartLineNumber(logEntries[i], logEntryStartLineNumber); + updatedLogEntries.push(updatedLogEntry); + logEntryStartLineNumber = updatedLogEntry.range.endLineNumber + 1; + } + + return { logEntries: updatedLogEntries, content }; } } @@ -373,8 +493,8 @@ export abstract class AbstractFileOutputChannelModel extends Disposable implemen this.modelDisposable.value = new DisposableStore(); this.model = this.modelService.createModel('', this.language, this.modelUri); const { content, consume } = await this.outputContentProvider.getContent(); - this.doAppendContent(this.model, content); consume(); + this.doAppendContent(this.model, content); this.modelDisposable.value.add(this.outputContentProvider.onDidReset(() => this.onDidContentChange(true, true))); this.modelDisposable.value.add(this.outputContentProvider.onDidAppend(() => this.onDidContentChange(false, false))); this.outputContentProvider.watch(); @@ -393,6 +513,10 @@ export abstract class AbstractFileOutputChannelModel extends Disposable implemen return this.loadModelPromise; } + getLogEntries(): readonly ILogEntry[] { + return this.outputContentProvider.getLogEntries(); + } + private onDidContentChange(reset: boolean, appendImmediately: boolean): void { if (reset && !this.modelUpdateInProgress) { this.doUpdate(OutputChannelUpdateMode.Clear, true); @@ -456,8 +580,8 @@ export abstract class AbstractFileOutputChannelModel extends Disposable implemen } /* Appned Content */ - this.doAppendContent(model, content); consume(); + this.doAppendContent(model, content); this.modelUpdateInProgress = false; }, immediate ? 0 : undefined).catch(error => { if (!isCancellationError(error)) { @@ -487,11 +611,11 @@ export abstract class AbstractFileOutputChannelModel extends Disposable implemen return; } + consume(); if (edits.length) { /* Apply Edits */ model.applyEdits(edits); } - consume(); this.modelUpdateInProgress = false; } @@ -543,10 +667,11 @@ export class FileOutputChannelModel extends AbstractFileOutputChannelModel imple readonly source: IOutputContentSource, @IFileService fileService: IFileService, @IModelService modelService: IModelService, + @IInstantiationService instantiationService: IInstantiationService, @ILogService logService: ILogService, @IEditorWorkerService editorWorkerService: IEditorWorkerService, ) { - const fileOutput = new FileContentProvider(source, fileService, logService); + const fileOutput = new FileContentProvider(source, fileService, instantiationService, logService); super(modelUri, language, fileOutput, modelService, editorWorkerService); this.fileOutput = this._register(fileOutput); } @@ -625,10 +750,11 @@ class OutputChannelBackedByFile extends FileOutputChannelModel implements IOutpu @IFileService fileService: IFileService, @IModelService modelService: IModelService, @ILoggerService loggerService: ILoggerService, + @IInstantiationService instantiationService: IInstantiationService, @ILogService logService: ILogService, @IEditorWorkerService editorWorkerService: IEditorWorkerService ) { - super(modelUri, language, { resource: file, name: '' }, fileService, modelService, logService, editorWorkerService); + super(modelUri, language, { resource: file, name: '' }, fileService, modelService, instantiationService, logService, editorWorkerService); // Donot rotate to check for the file reset this.logger = loggerService.createLogger(file, { logLevel: 'always', donotRotate: true, donotUseFormatters: true, hidden: true }); @@ -688,6 +814,10 @@ export class DelegatedOutputChannelModel extends Disposable implements IOutputCh return outputChannelModel; } + getLogEntries(): readonly ILogEntry[] { + return []; + } + append(output: string): void { this.outputChannelModel.then(outputChannelModel => outputChannelModel.append(output)); } diff --git a/src/vs/workbench/contrib/output/common/outputChannelModelService.ts b/src/vs/workbench/contrib/output/common/outputChannelModelService.ts deleted file mode 100644 index c3388fe8b721..000000000000 --- a/src/vs/workbench/contrib/output/common/outputChannelModelService.ts +++ /dev/null @@ -1,57 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; -import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; -import { toLocalISOString } from '../../../../base/common/date.js'; -import { joinPath } from '../../../../base/common/resources.js'; -import { DelegatedOutputChannelModel, FileOutputChannelModel, IOutputChannelModel, MultiFileOutputChannelModel } from './outputChannelModel.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ILanguageSelection } from '../../../../editor/common/languages/language.js'; -import { IOutputContentSource } from '../../../services/output/common/output.js'; - -export const IOutputChannelModelService = createDecorator('outputChannelModelService'); - -export interface IOutputChannelModelService { - readonly _serviceBrand: undefined; - - createOutputChannelModel(id: string, modelUri: URI, language: ILanguageSelection, source?: IOutputContentSource | ReadonlyArray): IOutputChannelModel; - -} - -export class OutputChannelModelService implements IOutputChannelModelService { - - declare readonly _serviceBrand: undefined; - - private readonly outputLocation: URI; - - constructor( - @IFileService private readonly fileService: IFileService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService - ) { - this.outputLocation = joinPath(environmentService.windowLogsPath, `output_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); - } - - createOutputChannelModel(id: string, modelUri: URI, language: ILanguageSelection, source?: IOutputContentSource | IOutputContentSource[]): IOutputChannelModel { - return source ? - Array.isArray(source) ? this.instantiationService.createInstance(MultiFileOutputChannelModel, modelUri, language, source) - : this.instantiationService.createInstance(FileOutputChannelModel, modelUri, language, source) - : this.instantiationService.createInstance(DelegatedOutputChannelModel, id, modelUri, language, this.outputLocation, this.outputDirPromise); - } - - private _outputDir: Promise | null = null; - private get outputDirPromise(): Promise { - if (!this._outputDir) { - this._outputDir = this.fileService.createFolder(this.outputLocation).then(() => undefined); - } - return this._outputDir; - } - -} - -registerSingleton(IOutputChannelModelService, OutputChannelModelService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index 99e31cdae792..8d916e2bcb9c 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -8,7 +8,6 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { URI } from '../../../../base/common/uri.js'; import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { ITextModel } from '../../../../editor/common/model.js'; import { LogLevel } from '../../../../platform/log/common/log.js'; import { Range } from '../../../../editor/common/core/range.js'; @@ -147,22 +146,36 @@ export enum OutputChannelUpdateMode { Clear } +export interface ILogEntry { + readonly range: Range; + readonly timestamp: number; + readonly timestampRange: Range; + readonly logLevel: LogLevel; + readonly logLevelRange: Range; + readonly source: string | undefined; +} + export interface IOutputChannel { /** * Identifier of the output channel. */ - id: string; + readonly id: string; /** * Label of the output channel to be displayed to the user. */ - label: string; + readonly label: string; /** * URI of the output channel. */ - uri: URI; + readonly uri: URI; + + /** + * Log entries of the output channel. + */ + getLogEntries(): readonly ILogEntry[]; /** * Appends output to the channel. @@ -305,104 +318,3 @@ class OutputChannelRegistry implements IOutputChannelRegistry { } Registry.add(Extensions.OutputChannels, new OutputChannelRegistry()); - -const LOG_ENTRY_REGEX = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s(\[(info|trace|debug|error|warning)\])\s(\[(.*?)\]\s)?/; - -export interface ILogEntry { - readonly range: Range; - readonly timestamp: number; - readonly timestampRange: Range; - readonly logLevel: LogLevel; - readonly logLevelRange: Range; - readonly source?: string; -} - -/** - * Parses log entries from a given text model starting from a specified line. - * - * @param model - The text model containing the log entries. - * @param fromLine - The line number to start parsing from (default is 1). - * @returns An array of log entries, each containing the log level and the range of lines it spans. - */ -export function parseLogEntries(model: ITextModel, fromLine: number = 1): ILogEntry[] { - const logEntries: ILogEntry[] = []; - for (let lineNumber = fromLine; lineNumber <= model.getLineCount(); lineNumber++) { - const logEntry = parseLogEntryAt(model, lineNumber); - if (logEntry) { - logEntries.push(logEntry); - lineNumber = logEntry.range.endLineNumber; - } - } - return logEntries; -} - - -/** - * Parses a log entry at the specified line number in the given text model. - * - * @param model - The text model containing the log entries. - * @param lineNumber - The line number at which to start parsing the log entry. - * @returns An object representing the parsed log entry, or `null` if no log entry is found at the specified line. - * - * The returned log entry object contains: - * - `timestamp`: The timestamp of the log entry as a number. - * - `logLevel`: The log level of the log entry. - * - `range`: The range of lines that the log entry spans. - */ -export function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntry | null { - const lineContent = model.getLineContent(lineNumber); - const match = LOG_ENTRY_REGEX.exec(lineContent); - if (match) { - const timestamp = new Date(match[1]).getTime(); - const timestampRange = new Range(lineNumber, 1, lineNumber, match[1].length); - const logLevel = parseLogLevel(match[3]); - const logLevelRange = new Range(lineNumber, timestampRange.endColumn + 1, lineNumber, timestampRange.endColumn + 1 + match[2].length); - const source = match[5]; - const startLine = lineNumber; - let endLine = lineNumber; - - while (endLine < model.getLineCount()) { - const nextLineContent = model.getLineContent(endLine + 1); - if (model.getLineFirstNonWhitespaceColumn(endLine + 1) === 0 || LOG_ENTRY_REGEX.test(nextLineContent)) { - break; - } - endLine++; - } - return { range: new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)), timestamp, timestampRange, logLevel, logLevelRange, source }; - } - return null; -} - -/** - * Iterator for log entries from a model with a processing function. - * - * @param model - The text model containing the log entries. - * @param process - A function to process each log entry. - * @returns An iterable iterator for processed log entries. - */ -export function* logEntryIterator(model: ITextModel, process: (logEntry: ILogEntry) => T): IterableIterator { - for (let lineNumber = 1; lineNumber <= model.getLineCount(); lineNumber++) { - const logEntry = parseLogEntryAt(model, lineNumber); - if (logEntry) { - yield process(logEntry); - lineNumber = logEntry.range.endLineNumber; - } - } -} - -function parseLogLevel(level: string): LogLevel { - switch (level.toLowerCase()) { - case 'trace': - return LogLevel.Trace; - case 'debug': - return LogLevel.Debug; - case 'info': - return LogLevel.Info; - case 'warning': - return LogLevel.Warning; - case 'error': - return LogLevel.Error; - default: - throw new Error(`Unknown log level: ${level}`); - } -} diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 90894ea4fac1..f43004533d7a 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -269,7 +269,6 @@ import './contrib/extensions/browser/extensions.contribution.js'; import './contrib/extensions/browser/extensionsViewlet.js'; // Output View -import './contrib/output/common/outputChannelModelService.js'; import './contrib/output/browser/output.contribution.js'; import './contrib/output/browser/outputView.js'; From 17483cdfb8c964c5a1ee2bd0e3c5ff67db8325b6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Jan 2025 05:42:03 -0800 Subject: [PATCH 0767/3587] Upgrade to viewport render strategy for large files --- src/vs/editor/browser/gpu/gpu.ts | 6 +- .../gpu/renderStrategy/baseRenderStrategy.ts | 4 +- .../renderStrategy/fullFileRenderStrategy.ts | 1 + .../renderStrategy/viewportRenderStrategy.ts | 23 +++--- src/vs/editor/browser/gpu/viewGpuContext.ts | 12 +-- .../viewParts/viewLinesGpu/viewLinesGpu.ts | 80 +++++++++++-------- 6 files changed, 70 insertions(+), 56 deletions(-) diff --git a/src/vs/editor/browser/gpu/gpu.ts b/src/vs/editor/browser/gpu/gpu.ts index b5d4d72e70af..7284d275c769 100644 --- a/src/vs/editor/browser/gpu/gpu.ts +++ b/src/vs/editor/browser/gpu/gpu.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type { IDisposable } from '../../../base/common/lifecycle.js'; import type { ViewConfigurationChangedEvent, ViewLinesChangedEvent, ViewLinesDeletedEvent, ViewLinesInsertedEvent, ViewScrollChangedEvent, ViewTokensChangedEvent } from '../../common/viewEvents.js'; import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js'; @@ -18,7 +19,8 @@ export const enum BindingId { ScrollOffset, } -export interface IGpuRenderStrategy { +export interface IGpuRenderStrategy extends IDisposable { + readonly type: string; readonly wgsl: string; readonly bindGroupEntries: GPUBindGroupEntry[]; readonly glyphRasterizer: IGlyphRasterizer; @@ -35,5 +37,5 @@ export interface IGpuRenderStrategy { */ reset(): void; update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number; - draw?(pass: GPURenderPassEncoder, viewportData: ViewportData): void; + draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void; } diff --git a/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts index 7f068ba19de7..a5a925977ea6 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts @@ -14,9 +14,11 @@ import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; import type { ViewGpuContext } from '../viewGpuContext.js'; export abstract class BaseRenderStrategy extends ViewEventHandler implements IGpuRenderStrategy { + protected readonly _glyphRasterizer: MandatoryMutableDisposable; get glyphRasterizer() { return this._glyphRasterizer.value; } + abstract type: string; abstract wgsl: string; abstract bindGroupEntries: GPUBindGroupEntry[]; @@ -37,5 +39,5 @@ export abstract class BaseRenderStrategy extends ViewEventHandler implements IGp abstract reset(): void; abstract update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number; - abstract draw?(pass: GPURenderPassEncoder, viewportData: ViewportData): void; + abstract draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void; } diff --git a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index 6b4a8a2ce396..1dae90695566 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -63,6 +63,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { */ static readonly maxSupportedColumns = 200; + readonly type = 'fullfile'; readonly wgsl: string = fullFileRenderStrategyWgsl; private _cellBindBuffer!: GPUBuffer; diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts index 9ec402b0dbd8..83ecdcc01d48 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -39,14 +39,19 @@ const enum CellBufferInfo { TextureIndex = 5, } -// TODO: Handle these dynamically -const viewportWidth = 500; const viewportHeight = 35; /** * A render strategy that uploads the content of the entire viewport every frame. */ export class ViewportRenderStrategy extends BaseRenderStrategy { + // TODO: Can this be removed? + /** + * The hard cap for line columns that can be rendered by the GPU renderer. + */ + static readonly maxSupportedColumns = 200; + + readonly type = 'viewport'; readonly wgsl: string = fullFileRenderStrategyWgsl; private _cellBindBuffer!: GPUBuffer; @@ -78,7 +83,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { ) { super(context, viewGpuContext, device); - const bufferSize = viewportHeight * viewportWidth * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; + const bufferSize = viewportHeight * ViewportRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco full file cell buffer', size: bufferSize, @@ -218,7 +223,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]); cellBuffer.fill(0); - const lineIndexCount = viewportWidth * Constants.IndicesPerCell; + const lineIndexCount = ViewportRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell; for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) { @@ -248,7 +253,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { for (x = tokenStartIndex; x < tokenEndIndex; x++) { // TODO: This needs to move to a dynamic long line rendering strategy - if (x > viewportWidth) { + if (x > ViewportRenderStrategy.maxSupportedColumns) { break; } segment = contentSegmenter.getSegmentAtIndex(x); @@ -316,7 +321,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { if (chars === ' ' || chars === '\t') { // Zero out glyph to ensure it doesn't get rendered - cellIndex = ((y - 1) * viewportWidth + x) * Constants.IndicesPerCell; + cellIndex = ((y - 1) * ViewportRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell; cellBuffer.fill(0, cellIndex, cellIndex + CellBufferInfo.FloatsPerEntry); // Adjust xOffset for tab stops if (chars === '\t') { @@ -348,7 +353,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { glyph.fontBoundingBoxAscent ); - cellIndex = ((y - viewportData.startLineNumber) * viewportWidth + x) * Constants.IndicesPerCell; + cellIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell; cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.round(absoluteOffsetX); cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = absoluteOffsetY; cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex; @@ -362,8 +367,8 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { } // Clear to end of line - fillStartIndex = ((y - viewportData.startLineNumber) * viewportWidth + tokenEndIndex) * Constants.IndicesPerCell; - fillEndIndex = ((y - viewportData.startLineNumber) * viewportWidth) * Constants.IndicesPerCell; + fillStartIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns + tokenEndIndex) * Constants.IndicesPerCell; + fillEndIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell; cellBuffer.fill(0, fillStartIndex, fillEndIndex); } diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index a1915e600dba..bbebfe5532c1 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -27,12 +27,6 @@ import { DecorationStyleCache } from './css/decorationStyleCache.js'; import { FullFileRenderStrategy } from './renderStrategy/fullFileRenderStrategy.js'; export class ViewGpuContext extends Disposable { - /** - * The temporary hard cap for lines rendered by the GPU renderer. This can be removed once more - * dynamic allocation is implemented in https://github.com/microsoft/vscode/issues/227091 - */ - readonly maxGpuLines = 10000;//FullFileRenderStrategy.maxSupportedLines; - /** * The temporary hard cap for line columns rendered by the GPU renderer. This can be removed * once more dynamic allocation is implemented in https://github.com/microsoft/vscode/issues/227108 @@ -163,8 +157,7 @@ export class ViewGpuContext extends Disposable { // Check if the line has simple attributes that aren't supported if ( data.containsRTL || - data.maxColumn > this.maxGpuCols || - lineNumber >= this.maxGpuLines + data.maxColumn > this.maxGpuCols ) { return false; } @@ -252,9 +245,6 @@ export class ViewGpuContext extends Disposable { reasons.push(`inlineDecorations with unsupported CSS selectors (${problemSelectors.map(e => `\`${e}\``).join(', ')})`); } } - if (lineNumber >= this.maxGpuLines) { - reasons.push('lineNumber >= maxGpuLines'); - } return reasons; } } diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index a9fa075b556d..c19be0e6ed08 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -27,6 +27,7 @@ import { TextureAtlas } from '../../gpu/atlas/textureAtlas.js'; import { createContentSegmenter, type IContentSegmenter } from '../../gpu/contentSegmenter.js'; import { ViewportRenderStrategy } from '../../gpu/renderStrategy/viewportRenderStrategy.js'; import { FullFileRenderStrategy } from '../../gpu/renderStrategy/fullFileRenderStrategy.js'; +import { MutableDisposable } from '../../../../base/common/lifecycle.js'; const enum GlyphStorageBufferInfo { FloatsPerEntry = 2 + 2 + 2, @@ -61,7 +62,8 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { private _initialized = false; - private _renderStrategy!: IGpuRenderStrategy; + private readonly _renderStrategy: MutableDisposable = new MutableDisposable(); + private _rebuildBindGroup?: () => void; constructor( context: ViewContext, @@ -106,7 +108,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { this._atlasGpuTextureVersions.length = 0; this._atlasGpuTextureVersions[0] = 0; this._atlasGpuTextureVersions[1] = 0; - this._renderStrategy.reset(); + this._renderStrategy.value!.reset(); })); const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); @@ -189,8 +191,8 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { // #region Storage buffers - // this._renderStrategy = this._register(this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._viewGpuContext, this._device)); - this._renderStrategy = this._register(this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device)); + this._renderStrategy.value = this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._viewGpuContext, this._device); + // this._renderStrategy.value = this._register(this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device)); this._glyphStorageBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco glyph storage buffer', @@ -227,7 +229,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { const module = this._device.createShaderModule({ label: 'Monaco shader module', - code: this._renderStrategy.wgsl, + code: this._renderStrategy.value!.wgsl, }); // #endregion Shader module @@ -272,25 +274,28 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { // #region Bind group - this._bindGroup = this._device.createBindGroup({ - label: 'Monaco bind group', - layout: this._pipeline.getBindGroupLayout(0), - entries: [ - // TODO: Pass in generically as array? - { binding: BindingId.GlyphInfo, resource: { buffer: this._glyphStorageBuffer } }, - { - binding: BindingId.TextureSampler, resource: this._device.createSampler({ - label: 'Monaco atlas sampler', - magFilter: 'nearest', - minFilter: 'nearest', - }) - }, - { binding: BindingId.Texture, resource: this._atlasGpuTexture.createView() }, - { binding: BindingId.LayoutInfoUniform, resource: { buffer: layoutInfoUniformBuffer } }, - { binding: BindingId.AtlasDimensionsUniform, resource: { buffer: atlasInfoUniformBuffer } }, - ...this._renderStrategy.bindGroupEntries - ], - }); + this._rebuildBindGroup = () => { + this._bindGroup = this._device.createBindGroup({ + label: 'Monaco bind group', + layout: this._pipeline.getBindGroupLayout(0), + entries: [ + // TODO: Pass in generically as array? + { binding: BindingId.GlyphInfo, resource: { buffer: this._glyphStorageBuffer } }, + { + binding: BindingId.TextureSampler, resource: this._device.createSampler({ + label: 'Monaco atlas sampler', + magFilter: 'nearest', + minFilter: 'nearest', + }) + }, + { binding: BindingId.Texture, resource: this._atlasGpuTexture.createView() }, + { binding: BindingId.LayoutInfoUniform, resource: { buffer: layoutInfoUniformBuffer } }, + { binding: BindingId.AtlasDimensionsUniform, resource: { buffer: atlasInfoUniformBuffer } }, + ...this._renderStrategy.value!.bindGroupEntries + ], + }); + }; + this._rebuildBindGroup(); // endregion Bind group @@ -307,6 +312,18 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { } } + private _refreshRenderStrategy(viewportData: ViewportData) { + if (this._renderStrategy.value?.type === 'viewport') { + return; + } + if (viewportData.endLineNumber < FullFileRenderStrategy.maxSupportedLines) { + return; + } + this._logService.trace(`File is larger than ${FullFileRenderStrategy.maxSupportedLines} lines, switching to viewport render strategy`); + this._renderStrategy.value = this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device); + this._rebuildBindGroup?.(); + } + private _updateAtlasStorageBufferAndTexture() { for (const [layerIndex, page] of ViewGpuContext.atlas.pages.entries()) { if (layerIndex >= TextureAtlas.maximumPageCount) { @@ -400,6 +417,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { public renderText(viewportData: ViewportData): void { if (this._initialized) { + this._refreshRenderStrategy(viewportData); return this._renderText(viewportData); } else { this._initViewportData = this._initViewportData ?? []; @@ -412,7 +430,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { const options = new ViewLineOptions(this._context.configuration, this._context.theme.type); - const visibleObjectCount = this._renderStrategy.update(viewportData, options); + this._renderStrategy.value!.update(viewportData, options); this._updateAtlasStorageBufferAndTexture(); @@ -429,11 +447,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { pass.setBindGroup(0, this._bindGroup); - if (this._renderStrategy?.draw) { - this._renderStrategy.draw(pass, viewportData); - } else { - pass.draw(quadVertices.length / 2, visibleObjectCount); - } + this._renderStrategy.value!.draw(pass, viewportData); pass.end(); @@ -541,7 +555,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { if (chars === undefined) { continue; } - resolvedStartCssPixelOffset += (this._renderStrategy.glyphRasterizer.getTextMetrics(chars).width / getActiveWindow().devicePixelRatio) - viewLineOptions.spaceWidth; + resolvedStartCssPixelOffset += (this._renderStrategy.value!.glyphRasterizer.getTextMetrics(chars).width / getActiveWindow().devicePixelRatio) - viewLineOptions.spaceWidth; } if (chars === '\t') { resolvedStartColumn = CursorColumns.nextRenderTabStop(resolvedStartColumn, lineData.tabSize); @@ -559,7 +573,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { if (chars === undefined) { continue; } - resolvedEndCssPixelOffset += (this._renderStrategy.glyphRasterizer.getTextMetrics(chars).width / getActiveWindow().devicePixelRatio) - viewLineOptions.spaceWidth; + resolvedEndCssPixelOffset += (this._renderStrategy.value!.glyphRasterizer.getTextMetrics(chars).width / getActiveWindow().devicePixelRatio) - viewLineOptions.spaceWidth; } if (chars === '\t') { resolvedEndColumn = CursorColumns.nextRenderTabStop(resolvedEndColumn, lineData.tabSize); @@ -641,7 +655,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { } else if (lineData.isBasicASCII && this._lastViewLineOptions.useMonospaceOptimizations) { charWidth = spaceWidthDevicePixels; } else { - charWidth = this._renderStrategy.glyphRasterizer.getTextMetrics(chars).width; + charWidth = this._renderStrategy.value!.glyphRasterizer.getTextMetrics(chars).width; } if (mouseContentHorizontalOffsetDevicePixels < widthSoFar + charWidth / 2) { From 299387b0718880da0979f0d0fbbe32c2d8301d1e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Jan 2025 05:57:59 -0800 Subject: [PATCH 0768/3587] Viewport rendering strategy up to 2000 cols Instead of dealing with scroll offset for now, just increase column cap significantly --- .../renderStrategy/viewportRenderStrategy.ts | 4 ++-- src/vs/editor/browser/gpu/viewGpuContext.ts | 7 +++---- .../viewParts/viewLinesGpu/viewLinesGpu.ts | 17 ++++++++++++++--- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts index 83ecdcc01d48..640651b54db8 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -39,7 +39,7 @@ const enum CellBufferInfo { TextureIndex = 5, } -const viewportHeight = 35; +const viewportHeight = 100; /** * A render strategy that uploads the content of the entire viewport every frame. @@ -49,7 +49,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { /** * The hard cap for line columns that can be rendered by the GPU renderer. */ - static readonly maxSupportedColumns = 200; + static readonly maxSupportedColumns = 2000; readonly type = 'viewport'; readonly wgsl: string = fullFileRenderStrategyWgsl; diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index bbebfe5532c1..181c468f12c5 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -24,14 +24,13 @@ import { Event } from '../../../base/common/event.js'; import { EditorOption, type IEditorOptions } from '../../common/config/editorOptions.js'; import { InlineDecorationType } from '../../common/viewModel.js'; import { DecorationStyleCache } from './css/decorationStyleCache.js'; -import { FullFileRenderStrategy } from './renderStrategy/fullFileRenderStrategy.js'; +import { ViewportRenderStrategy } from './renderStrategy/viewportRenderStrategy.js'; export class ViewGpuContext extends Disposable { /** - * The temporary hard cap for line columns rendered by the GPU renderer. This can be removed - * once more dynamic allocation is implemented in https://github.com/microsoft/vscode/issues/227108 + * The hard cap for line columns rendered by the GPU renderer. */ - readonly maxGpuCols = FullFileRenderStrategy.maxSupportedColumns; + readonly maxGpuCols = ViewportRenderStrategy.maxSupportedColumns; readonly canvas: FastDomNode; readonly ctx: GPUCanvasContext; diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index c19be0e6ed08..d92240e40258 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -28,6 +28,7 @@ import { createContentSegmenter, type IContentSegmenter } from '../../gpu/conten import { ViewportRenderStrategy } from '../../gpu/renderStrategy/viewportRenderStrategy.js'; import { FullFileRenderStrategy } from '../../gpu/renderStrategy/fullFileRenderStrategy.js'; import { MutableDisposable } from '../../../../base/common/lifecycle.js'; +import type { ViewLineRenderingData } from '../../../common/viewModel.js'; const enum GlyphStorageBufferInfo { FloatsPerEntry = 2 + 2 + 2, @@ -192,7 +193,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { // #region Storage buffers this._renderStrategy.value = this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._viewGpuContext, this._device); - // this._renderStrategy.value = this._register(this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device)); + // this._renderStrategy.value = this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device); this._glyphStorageBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco glyph storage buffer', @@ -316,14 +317,24 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { if (this._renderStrategy.value?.type === 'viewport') { return; } - if (viewportData.endLineNumber < FullFileRenderStrategy.maxSupportedLines) { + if (viewportData.endLineNumber < FullFileRenderStrategy.maxSupportedLines && this._viewportMaxColumn(viewportData) < FullFileRenderStrategy.maxSupportedColumns) { return; } - this._logService.trace(`File is larger than ${FullFileRenderStrategy.maxSupportedLines} lines, switching to viewport render strategy`); + this._logService.trace(`File is larger than ${FullFileRenderStrategy.maxSupportedLines} lines or ${FullFileRenderStrategy.maxSupportedColumns} columns, switching to viewport render strategy`); this._renderStrategy.value = this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device); this._rebuildBindGroup?.(); } + private _viewportMaxColumn(viewportData: ViewportData): number { + let maxColumn = 0; + let lineData: ViewLineRenderingData; + for (let i = viewportData.startLineNumber; i <= viewportData.endLineNumber; i++) { + lineData = viewportData.getViewLineRenderingData(i); + maxColumn = Math.max(maxColumn, lineData.maxColumn); + } + return maxColumn; + } + private _updateAtlasStorageBufferAndTexture() { for (const [layerIndex, page] of ViewGpuContext.atlas.pages.entries()) { if (layerIndex >= TextureAtlas.maximumPageCount) { From d8268820c0ffe30fb3d848d16a462cbc0945a405 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:09:23 +0100 Subject: [PATCH 0769/3587] Improve border color for inline edits (#238461) better side by side border color --- .../view/inlineEdits/sideBySideDiff.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index 5d34efa6ef8d..c0023b811da7 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -12,15 +12,14 @@ import { IObservable, autorun, constObservable, derived, derivedObservableWithCa import { MenuId, MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { diffInserted, diffRemoved } from '../../../../../../platform/theme/common/colorRegistry.js'; -import { darken, lighten, registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; +import { diffInserted, diffRemoved, editorHoverBorder } from '../../../../../../platform/theme/common/colorRegistry.js'; +import { registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { Point } from '../../../../../browser/point.js'; import { EmbeddedCodeEditorWidget } from '../../../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorOption } from '../../../../../common/config/editorOptions.js'; -import { editorLineHighlightBorder } from '../../../../../common/core/editorColorRegistry.js'; import { LineRange } from '../../../../../common/core/lineRange.js'; import { OffsetRange } from '../../../../../common/core/offsetRange.js'; import { Position } from '../../../../../common/core/position.js'; @@ -77,10 +76,10 @@ export const modifiedChangedTextOverlayColor = registerColor( export const originalBorder = registerColor( 'inlineEdit.originalBorder', { - light: darken(editorLineHighlightBorder, 0.15), - dark: lighten(editorLineHighlightBorder, 0.50), - hcDark: editorLineHighlightBorder, - hcLight: editorLineHighlightBorder + light: editorHoverBorder, + dark: editorHoverBorder, + hcDark: editorHoverBorder, + hcLight: editorHoverBorder }, localize('inlineEdit.originalBorder', 'Border color for the original text in inline edits.') ); @@ -88,10 +87,10 @@ export const originalBorder = registerColor( export const modifiedBorder = registerColor( 'inlineEdit.modifiedBorder', { - light: darken(editorLineHighlightBorder, 0.15), - dark: lighten(editorLineHighlightBorder, 0.50), - hcDark: editorLineHighlightBorder, - hcLight: editorLineHighlightBorder + light: editorHoverBorder, + dark: editorHoverBorder, + hcDark: editorHoverBorder, + hcLight: editorHoverBorder }, localize('inlineEdit.modifiedBorder', 'Border color for the modified text in inline edits.') ); From c953035b618222e297d98c1a6b44029e4474f801 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:23:28 +0100 Subject: [PATCH 0770/3587] Git - simplify git blame caching (#238462) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Git - simplify git blame caching * 💄 remove code that is not needed --- extensions/git/src/blame.ts | 98 +++++++++---------------------------- 1 file changed, 22 insertions(+), 76 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index ca0c6262625d..f29212c51a84 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -9,7 +9,7 @@ import { dispose, fromNow, getCommitShortHash, IDisposable } from './util'; import { Repository } from './repository'; import { throttle } from './decorators'; import { BlameInformation, Commit } from './git'; -import { fromGitUri, isGitUri } from './uri'; +import { fromGitUri, isGitUri, toGitUri } from './uri'; import { emojify, ensureEmojis } from './emoji'; import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation } from './staging'; import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; @@ -79,82 +79,34 @@ type BlameInformationTemplateTokens = { readonly authorDateAgo: string; }; -interface RepositoryBlameInformation { - /** - * Track the current HEAD of the repository so that we can clear cache entries - */ - HEAD: string; - - /** - * Outer map - maps resource scheme to resource blame information. Using the uri - * scheme as the key so that we can easily delete the cache entries for the "file" - * scheme as those entries are outdated when the HEAD of the repository changes. - * - * Inner map - maps commit + resource to blame information. - */ - readonly blameInformation: Map>; -} - interface LineBlameInformation { readonly lineNumber: number; readonly blameInformation: BlameInformation | string; } class GitBlameInformationCache { - private readonly _cache = new Map(); - - getRepositoryHEAD(repository: Repository): string | undefined { - return this._cache.get(repository)?.HEAD; - } - - setRepositoryHEAD(repository: Repository, commit: string): void { - const repositoryBlameInformation = this._cache.get(repository) ?? { - HEAD: commit, - blameInformation: new Map>() - } satisfies RepositoryBlameInformation; - - this._cache.set(repository, { - ...repositoryBlameInformation, - HEAD: commit - } satisfies RepositoryBlameInformation); - } - - deleteBlameInformation(repository: Repository, scheme?: string): boolean { - if (scheme === undefined) { - return this._cache.delete(repository); - } + private readonly _cache = new Map>(); - return this._cache.get(repository)?.blameInformation.delete(scheme) === true; + delete(repository: Repository): boolean { + return this._cache.delete(repository); } - getBlameInformation(repository: Repository, resource: Uri, commit: string): BlameInformation[] | undefined { - const blameInformationKey = this._getBlameInformationKey(resource, commit); - return this._cache.get(repository)?.blameInformation.get(resource.scheme)?.get(blameInformationKey); + get(repository: Repository, resource: Uri, commit: string): BlameInformation[] | undefined { + const key = this._getCacheKey(resource, commit); + return this._cache.get(repository)?.get(key); } - setBlameInformation(repository: Repository, resource: Uri, commit: string, blameInformation: BlameInformation[]): void { - if (!repository.HEAD?.commit) { - return; - } - + set(repository: Repository, resource: Uri, commit: string, blameInformation: BlameInformation[]): void { if (!this._cache.has(repository)) { - this._cache.set(repository, { - HEAD: repository.HEAD.commit, - blameInformation: new Map>() - } satisfies RepositoryBlameInformation); - } - - const repositoryBlameInformation = this._cache.get(repository)!; - if (!repositoryBlameInformation.blameInformation.has(resource.scheme)) { - repositoryBlameInformation.blameInformation.set(resource.scheme, new Map()); + this._cache.set(repository, new LRUCache(100)); } - const resourceSchemeBlameInformation = repositoryBlameInformation.blameInformation.get(resource.scheme)!; - resourceSchemeBlameInformation.set(this._getBlameInformationKey(resource, commit), blameInformation); + const key = this._getCacheKey(resource, commit); + this._cache.get(repository)!.set(key, blameInformation); } - private _getBlameInformationKey(resource: Uri, commit: string): string { - return `${commit}:${resource.toString()}`; + private _getCacheKey(resource: Uri, commit: string): string { + return toGitUri(resource, commit).toString(); } } @@ -173,6 +125,7 @@ export class GitBlameController { this._onDidChangeBlameInformation.fire(); } + private _HEAD: string | undefined; private readonly _commitInformationCache = new LRUCache(100); private readonly _repositoryBlameCache = new GitBlameInformationCache(); @@ -389,23 +342,16 @@ export class GitBlameController { } this._repositoryDisposables.delete(repository); - this._repositoryBlameCache.deleteBlameInformation(repository); + this._repositoryBlameCache.delete(repository); } private _onDidRunGitStatus(repository: Repository): void { - const repositoryHEAD = this._repositoryBlameCache.getRepositoryHEAD(repository); - if (!repositoryHEAD || !repository.HEAD?.commit) { + if (!repository.HEAD?.commit || this._HEAD === repository.HEAD.commit) { return; } - // If the HEAD of the repository changed we can remove the cache - // entries for the "file" scheme as those entries are outdated. - if (repositoryHEAD !== repository.HEAD.commit) { - this._repositoryBlameCache.deleteBlameInformation(repository, 'file'); - this._repositoryBlameCache.setRepositoryHEAD(repository, repository.HEAD.commit); - - this._updateTextEditorBlameInformation(window.activeTextEditor); - } + this._HEAD = repository.HEAD.commit; + this._updateTextEditorBlameInformation(window.activeTextEditor); } private async _getBlameInformation(resource: Uri, commit: string): Promise { @@ -414,18 +360,18 @@ export class GitBlameController { return undefined; } - const resourceBlameInformation = this._repositoryBlameCache.getBlameInformation(repository, resource, commit); + const resourceBlameInformation = this._repositoryBlameCache.get(repository, resource, commit); if (resourceBlameInformation) { return resourceBlameInformation; } - // Ensure that the emojis are loaded. We will - // use them when formatting the blame information. + // Ensure that the emojis are loaded as we will need + // access to them when formatting the blame information. await ensureEmojis(); // Get blame information for the resource and cache it const blameInformation = await repository.blame2(resource.fsPath, commit) ?? []; - this._repositoryBlameCache.setBlameInformation(repository, resource, commit, blameInformation); + this._repositoryBlameCache.set(repository, resource, commit, blameInformation); return blameInformation; } From 7fb4569b541a9619c1e1df8b577013e4a436dc28 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:35:29 -0500 Subject: [PATCH 0771/3587] Comment out env for bash until fix (#238463) comment out env for bash until fix --- .../common/scripts/shellIntegration-bash.sh | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index 3040617a5a6d..090f40212d2e 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -214,16 +214,16 @@ __vsc_update_cwd() { builtin printf '\e]633;P;Cwd=%s\a' "$(__vsc_escape_value "$__vsc_cwd")" } -__vsc_update_env() { - builtin printf '\e]633;EnvSingleStart;%s;\a' $__vsc_nonce - for var in $(compgen -v); do - if printenv "$var" >/dev/null 2>&1; then - value=$(builtin printf '%s' "${!var}") - builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$var" "$(__vsc_escape_value "$value")" $__vsc_nonce - fi - done - builtin printf '\e]633;EnvSingleEnd;%s;\a' $__vsc_nonce -} +# __vsc_update_env() { +# builtin printf '\e]633;EnvSingleStart;%s;\a' $__vsc_nonce +# for var in $(compgen -v); do +# if printenv "$var" >/dev/null 2>&1; then +# value=$(builtin printf '%s' "${!var}") +# builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$var" "$(__vsc_escape_value "$value")" $__vsc_nonce +# fi +# done +# builtin printf '\e]633;EnvSingleEnd;%s;\a' $__vsc_nonce +# } __vsc_command_output_start() { if [[ -z "${__vsc_first_prompt-}" ]]; then @@ -252,9 +252,9 @@ __vsc_command_complete() { fi __vsc_update_cwd - if [ "$__vsc_stable" = "0" ]; then - __vsc_update_env - fi + # if [ "$__vsc_stable" = "0" ]; then + # __vsc_update_env + # fi } __vsc_update_prompt() { # in command execution @@ -285,9 +285,9 @@ __vsc_precmd() { __vsc_first_prompt=1 __vsc_update_prompt - if [ "$__vsc_stable" = "0" ]; then - __vsc_update_env - fi + # if [ "$__vsc_stable" = "0" ]; then + # __vsc_update_env + # fi } __vsc_preexec() { From b53977c1b1361d8576755161130a586af7765f4d Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 22 Jan 2025 17:01:19 +0100 Subject: [PATCH 0772/3587] Tokenize viewport first (#238386) * Tokenize viewport first * Actually don't block --- src/vs/editor/common/languages.ts | 21 +- src/vs/editor/common/model/tokenStore.ts | 9 +- .../treeSitter/treeSitterParserService.ts | 2 +- src/vs/monaco.d.ts | 11 + ...eSitterTokenizationFeature.contribution.ts | 2 +- .../treeSitterTokenizationFeature.ts | 250 ++++++++++++++---- .../treeSitterTokenizationFeature.test.ts | 9 +- 7 files changed, 235 insertions(+), 69 deletions(-) rename src/vs/workbench/services/treeSitter/{common => browser}/treeSitterTokenizationFeature.ts (63%) diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index b622496c2573..95aa2bfee68a 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -28,6 +28,8 @@ import { ExtensionIdentifier } from '../../platform/extensions/common/extensions import { IMarkerData } from '../../platform/markers/common/markers.js'; import { IModelTokensChangedEvent } from './textModelEvents.js'; import type { Parser } from '@vscode/tree-sitter-wasm'; +import { ITextModel } from './model.js'; +import { TokenUpdate } from './model/tokenStore.js'; /** * @internal @@ -84,14 +86,29 @@ export class EncodedTokenizationResult { } } +export interface SyntaxNode { + startIndex: number; + endIndex: number; +} + +export interface QueryCapture { + name: string; + text?: string; + node: SyntaxNode; +} + /** * An intermediate interface for scaffolding the new tree sitter tokenization support. Not final. * @internal */ export interface ITreeSitterTokenizationSupport { + /** + * exposed for testing + */ + getTokensInRange(textModel: ITextModel, range: Range, rangeStartOffset: number, rangeEndOffset: number): TokenUpdate[] | undefined; tokenizeEncoded(lineNumber: number, textModel: model.ITextModel): Uint32Array | undefined; - captureAtPosition(lineNumber: number, column: number, textModel: model.ITextModel): Parser.QueryCapture[]; - captureAtPositionTree(lineNumber: number, column: number, tree: Parser.Tree): Parser.QueryCapture[]; + captureAtPosition(lineNumber: number, column: number, textModel: model.ITextModel): QueryCapture[]; + captureAtPositionTree(lineNumber: number, column: number, tree: Parser.Tree): QueryCapture[]; onDidChangeTokens: Event<{ textModel: model.ITextModel; changes: IModelTokensChangedEvent }>; tokenizeEncodedInstrumented(lineNumber: number, textModel: model.ITextModel): { result: Uint32Array; captureTime: number; metadataTime: number } | undefined; } diff --git a/src/vs/editor/common/model/tokenStore.ts b/src/vs/editor/common/model/tokenStore.ts index 5d717aca1b8a..b357069c5b4c 100644 --- a/src/vs/editor/common/model/tokenStore.ts +++ b/src/vs/editor/common/model/tokenStore.ts @@ -207,17 +207,18 @@ export class TokenStore implements IDisposable { * @param update all the tokens for the document in sequence */ buildStore(tokens: TokenUpdate[]) { - this._root = this.createFromUpdates(tokens); + this._root = this.createFromUpdates(tokens, true); } - private createFromUpdates(tokens: TokenUpdate[]): Node { + private createFromUpdates(tokens: TokenUpdate[], isNew?: boolean): Node { let newRoot: Node = { length: tokens[0].length, token: tokens[0].token, - height: 0 + height: 0, + needsRefresh: isNew }; for (let j = 1; j < tokens.length; j++) { - newRoot = append(newRoot, { length: tokens[j].length, token: tokens[j].token, height: 0 }); + newRoot = append(newRoot, { length: tokens[j].length, token: tokens[j].token, height: 0, needsRefresh: isNew }); } return newRoot; } diff --git a/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts index 67bb129ac6c7..5de94af33cf0 100644 --- a/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts +++ b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts @@ -158,7 +158,7 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul public /** exposed for tests **/ readonly language: Parser.Language, private readonly _logService: ILogService, private readonly _telemetryService: ITelemetryService) { - this.parser.setTimeoutMicros(5 * 1000); // 5 ms + this.parser.setTimeoutMicros(50 * 1000); // 50 ms this.parser.setLanguage(language); } dispose(): void { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 7c6046b7f15e..b80a2fc776b8 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -6907,6 +6907,17 @@ declare namespace monaco.languages { removeText?: number; } + export interface SyntaxNode { + startIndex: number; + endIndex: number; + } + + export interface QueryCapture { + name: string; + text?: string; + node: SyntaxNode; + } + /** * The state of the tokenizer between two lines. * It is useful to store flags such as in multiline comment, etc. diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.contribution.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.contribution.ts index e5ded2ea8ca7..21701ec617c5 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.contribution.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.contribution.ts @@ -7,7 +7,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; import { TreeSitterTextModelService } from '../../../../editor/common/services/treeSitter/treeSitterParserService.js'; import { ITreeSitterParserService } from '../../../../editor/common/services/treeSitterParserService.js'; -import { ITreeSitterTokenizationFeature } from '../common/treeSitterTokenizationFeature.js'; +import { ITreeSitterTokenizationFeature } from './treeSitterTokenizationFeature.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; import { URI } from '../../../../base/common/uri.js'; diff --git a/src/vs/workbench/services/treeSitter/common/treeSitterTokenizationFeature.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts similarity index 63% rename from src/vs/workbench/services/treeSitter/common/treeSitterTokenizationFeature.ts rename to src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts index b48982d364a2..ffac377bd1b0 100644 --- a/src/vs/workbench/services/treeSitter/common/treeSitterTokenizationFeature.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts @@ -7,9 +7,9 @@ import type { Parser } from '@vscode/tree-sitter-wasm'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; import { AppResourcePath, FileAccess } from '../../../../base/common/network.js'; -import { ILanguageIdCodec, ITreeSitterTokenizationSupport, LazyTokenizationSupport, TreeSitterTokenizationRegistry } from '../../../../editor/common/languages.js'; +import { ILanguageIdCodec, ITreeSitterTokenizationSupport, LazyTokenizationSupport, QueryCapture, TreeSitterTokenizationRegistry } from '../../../../editor/common/languages.js'; import { ITextModel } from '../../../../editor/common/model.js'; -import { EDITOR_EXPERIMENTAL_PREFER_TREESITTER, ITreeSitterParserService, ITreeSitterParseResult } from '../../../../editor/common/services/treeSitterParserService.js'; +import { EDITOR_EXPERIMENTAL_PREFER_TREESITTER, ITreeSitterParserService, ITreeSitterParseResult, TreeUpdateEvent, RangeChange } from '../../../../editor/common/services/treeSitterParserService.js'; import { IModelTokensChangedEvent } from '../../../../editor/common/textModelEvents.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../platform/files/common/files.js'; @@ -23,6 +23,9 @@ import { ITreeSitterTokenizationStoreService } from '../../../../editor/common/m import { LanguageId } from '../../../../editor/common/encodedTokenAttributes.js'; import { TokenUpdate } from '../../../../editor/common/model/tokenStore.js'; import { Range } from '../../../../editor/common/core/range.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { Position } from '../../../../editor/common/core/position.js'; +import { setTimeout0 } from '../../../../base/common/platform.js'; const ALLOWED_SUPPORT = ['typescript']; type TreeSitterQueries = string; @@ -33,6 +36,11 @@ export interface ITreeSitterTokenizationFeature { _serviceBrand: undefined; } +interface EndOffsetToken { + endOffset: number; + metadata: number; +} + export class TreeSitterTokenizationFeature extends Disposable implements ITreeSitterTokenizationFeature { public _serviceBrand: undefined; private readonly _tokenizersRegistrations: DisposableMap = this._register(new DisposableMap()); @@ -102,7 +110,8 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi private readonly _languageIdCodec: ILanguageIdCodec, @ITreeSitterParserService private readonly _treeSitterService: ITreeSitterParserService, @IThemeService private readonly _themeService: IThemeService, - @ITreeSitterTokenizationStoreService private readonly _tokenizationStoreService: ITreeSitterTokenizationStoreService + @ITreeSitterTokenizationStoreService private readonly _tokenizationStoreService: ITreeSitterTokenizationStoreService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, ) { super(); this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => this.reset())); @@ -112,54 +121,154 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi // First time we see a tree we need to build a token store. if (!this._tokenizationStoreService.hasTokens(e.textModel)) { - const lineEndOffset = e.textModel.getValueLength(); - const editorEndPosition = e.textModel.getPositionAt(lineEndOffset); - const tokens = this.getTokensInRange(e.textModel, new Range(1, 1, editorEndPosition.lineNumber, editorEndPosition.column), 0, lineEndOffset); - if (tokens) { - this._tokenizationStoreService.setTokens(e.textModel, tokens); - - this._onDidChangeTokens.fire({ - textModel: e.textModel, - changes: { - semanticTokensApplied: false, - ranges - } - }); - } + this._firstTreeUpdate(e.textModel, ranges); } else { // Mark the range for refresh immediately for (const range of e.ranges) { this._tokenizationStoreService.markForRefresh(e.textModel, range.newRange); } - - const captures = e.ranges.map(range => this._getTreeAndCaptures(range.newRange, e.textModel)); - // Don't block - new Promise(resolve => { - const tokenUpdates = e.ranges.map((range, index) => { - const updates = this.getTokensInRange(e.textModel, range.newRange, range.newRangeStartOffset, range.newRangeEndOffset, captures[index]); - if (updates) { - return { oldRangeLength: range.oldRangeLength, newTokens: updates }; - } - return { oldRangeLength: range.oldRangeLength, newTokens: [] }; - }); - this._tokenizationStoreService.updateTokens(e.textModel, e.versionId, tokenUpdates); - this._onDidChangeTokens.fire({ - textModel: e.textModel, - changes: { - semanticTokensApplied: false, - ranges - } - }); - resolve(); - }); + this._handleTreeUpdate(e, ranges); } })); } - private _rangeTokensAsUpdates(rangeOffset: number, lineTokens: { endOffset: number; metadata: number }[]) { + private _createEmptyTokens(captures: { tree: ITreeSitterParseResult | undefined; captures: QueryCapture[] }, modelEndOffset: number) { + const languageId = this._languageIdCodec.encodeLanguageId(this._languageId); + const emptyToken = this._emptyToken(languageId); + const capturedTokens = this._createTokensFromCaptures(captures.tree, captures.captures, 0, modelEndOffset); + if (!capturedTokens) { + return; + } + const emptyTokens: EndOffsetToken[] = capturedTokens.endOffsets.map(capture => ({ endOffset: capture.endOffset, metadata: emptyToken })); + return this._rangeTokensAsUpdates(0, emptyTokens); + } + + private _firstTreeUpdate(textModel: ITextModel, ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[]) { + const modelEndOffset = textModel.getValueLength(); + const editorEndPosition = textModel.getPositionAt(modelEndOffset); + const captures = this._getTreeAndCaptures(new Range(1, 1, editorEndPosition.lineNumber, editorEndPosition.column), textModel); + // Make empty tokens to populate the store + const tokens: TokenUpdate[] = this._createEmptyTokens(captures, modelEndOffset) ?? []; + + this._tokenizationStoreService.setTokens(textModel, tokens); + this._setViewPortTokens(textModel); + this._tokenizeEntireDocument(textModel, ranges); + + } + + private _tokenizeEntireDocument(textModel: ITextModel, ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[] = []) { + const modelEndOffset = textModel.getValueLength(); + + // Go in 10000 offset chunks to avoid long operations + const chunkSize = 10000; + let chunkEnd = modelEndOffset > chunkSize ? chunkSize : modelEndOffset; + let chunkStart = 0; + let chunkStartingPosition = new Position(1, 1); + const rangeChanges: RangeChange[] = []; + do { + const chunkEndPosition = textModel.getPositionAt(chunkEnd); + const chunkRange = Range.fromPositions(chunkStartingPosition, chunkEndPosition); + + rangeChanges.push({ + newRange: chunkRange, + newRangeStartOffset: chunkStart, + newRangeEndOffset: chunkEnd, + oldRangeLength: chunkEnd - chunkStart + }); + + chunkStart = chunkEnd; + if (chunkEnd < modelEndOffset && chunkEnd + chunkSize > modelEndOffset) { + chunkEnd = modelEndOffset; + } else { + chunkEnd = chunkEnd + chunkSize; + } + chunkStartingPosition = chunkEndPosition; + } while (chunkEnd <= modelEndOffset); + this._handleTreeUpdate({ ranges: rangeChanges, textModel, versionId: textModel.getVersionId() }, ranges); + } + + private _setViewPortTokens(textModel: ITextModel) { + const maxLine = textModel.getLineCount(); + const editor = this._codeEditorService.listCodeEditors().find(editor => editor.getModel() === textModel); + if (!editor) { + return; + } + + const viewPort = editor.getVisibleRangesPlusViewportAboveBelow(); + const ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[] = new Array(viewPort.length); + const rangeChanges: RangeChange[] = new Array(viewPort.length); + + for (let i = 0; i < viewPort.length; i++) { + const range = viewPort[i]; + ranges[i] = { fromLineNumber: range.startLineNumber, toLineNumber: range.endLineNumber < maxLine ? range.endLineNumber : maxLine }; + const newRangeStartOffset = textModel.getOffsetAt(range.getStartPosition()); + const newRangeEndOffset = textModel.getOffsetAt(range.getEndPosition()); + rangeChanges[i] = { + newRange: range, + newRangeStartOffset, + newRangeEndOffset, + oldRangeLength: newRangeEndOffset - newRangeStartOffset + }; + } + this._handleTreeUpdate({ ranges: rangeChanges, textModel, versionId: textModel.getVersionId() }, ranges); + } + + /** + * Do not await in this method, it will cause a race + */ + private _handleTreeUpdate(e: TreeUpdateEvent, ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[]) { + // Get the captures immediately too while the text model is correct + const captures = e.ranges.map(range => this._getTreeAndCaptures(range.newRange, e.textModel)); + // Don't block + this._updateTreeForRanges(e, ranges, captures); + } + + private _clipRangeChangeToFileLength(range: RangeChange, textModel: ITextModel) { + const valueLength = textModel.getValueLength(); + const startPosition = range.newRange.getStartPosition(); + const endPosition = range.newRangeEndOffset > valueLength ? textModel.getPositionAt(valueLength) : range.newRange.getEndPosition(); + const endOffset = textModel.getOffsetAt(endPosition); + const newRangeChange = { + oldRangeLength: endOffset - range.newRangeStartOffset, + newRangeEndOffset: endOffset, + newRangeStartOffset: textModel.getOffsetAt(startPosition), + newRange: Range.fromPositions(startPosition, endPosition) + }; + return newRangeChange; + } + + private async _updateTreeForRanges(e: TreeUpdateEvent, ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[], captures: { tree: ITreeSitterParseResult | undefined; captures: QueryCapture[] }[]) { + let tokenUpdate: { oldRangeLength: number; newTokens: TokenUpdate[] } | undefined; + for (let i = 0; i < e.ranges.length; i++) { + let capture = captures[i]; + let range = e.ranges[i]; + if (e.versionId < e.textModel.getVersionId()) { + // Our captures have become invalid and we need to re-capture + range = this._clipRangeChangeToFileLength(range, e.textModel); + capture = this._getTreeAndCaptures(range.newRange, e.textModel); + } + const updates = this.getTokensInRange(e.textModel, range.newRange, range.newRangeStartOffset, range.newRangeEndOffset, capture); + if (updates) { + tokenUpdate = { oldRangeLength: range.oldRangeLength, newTokens: updates }; + } else { + tokenUpdate = { oldRangeLength: range.oldRangeLength, newTokens: [] }; + } + this._tokenizationStoreService.updateTokens(e.textModel, e.versionId, [tokenUpdate]); + await new Promise(resolve => setTimeout0(resolve)); + } + this._onDidChangeTokens.fire({ + textModel: e.textModel, + changes: { + semanticTokensApplied: false, + ranges + } + }); + } + + private _rangeTokensAsUpdates(rangeOffset: number, endOffsetToken: EndOffsetToken[]) { const updates: TokenUpdate[] = []; let lastEnd = 0; - for (const token of lineTokens) { + for (const token of endOffsetToken) { if (token.endOffset <= lastEnd) { continue; } @@ -169,10 +278,10 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi return updates; } - public getTokensInRange(textModel: ITextModel, range: Range, rangeStartOffset: number, rangeEndOffset: number, captures?: { tree: ITreeSitterParseResult | undefined; captures: Parser.QueryCapture[] }): TokenUpdate[] | undefined { + public getTokensInRange(textModel: ITextModel, range: Range, rangeStartOffset: number, rangeEndOffset: number, captures?: { tree: ITreeSitterParseResult | undefined; captures: QueryCapture[] }): TokenUpdate[] | undefined { const languageId = this._languageIdCodec.encodeLanguageId(this._languageId); - const tokens = captures ? this._tokenizeCaptures(captures.tree, captures.captures, languageId, rangeStartOffset, rangeEndOffset) : this._tokenize(languageId, range, rangeStartOffset, rangeEndOffset, textModel); + const tokens = captures ? this._tokenizeCapturesWithMetadata(captures.tree, captures.captures, languageId, rangeStartOffset, rangeEndOffset) : this._tokenize(languageId, range, rangeStartOffset, rangeEndOffset, textModel); if (tokens?.endOffsetsAndMetadata) { return this._rangeTokensAsUpdates(rangeStartOffset, tokens.endOffsetsAndMetadata); } @@ -203,25 +312,34 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi this._colorThemeData = this._themeService.getColorTheme() as ColorThemeData; } - captureAtPosition(lineNumber: number, column: number, textModel: ITextModel): Parser.QueryCapture[] { + captureAtPosition(lineNumber: number, column: number, textModel: ITextModel): QueryCapture[] { const tree = this._getTree(textModel); const captures = this._captureAtRange(new Range(lineNumber, column, lineNumber, column + 1), tree?.tree); return captures; } - captureAtPositionTree(lineNumber: number, column: number, tree: Parser.Tree): Parser.QueryCapture[] { + captureAtPositionTree(lineNumber: number, column: number, tree: Parser.Tree): QueryCapture[] { const captures = this._captureAtRange(new Range(lineNumber, column, lineNumber, column + 1), tree); return captures; } - private _captureAtRange(range: Range, tree: Parser.Tree | undefined): Parser.QueryCapture[] { + private _captureAtRange(range: Range, tree: Parser.Tree | undefined): QueryCapture[] { const query = this._ensureQuery(); if (!tree || !query) { return []; } // Tree sitter row is 0 based, column is 0 based - return query.captures(tree.rootNode, { startPosition: { row: range.startLineNumber - 1, column: range.startColumn - 1 }, endPosition: { row: range.endLineNumber - 1, column: range.endColumn - 1 } }); + return query.captures(tree.rootNode, { startPosition: { row: range.startLineNumber - 1, column: range.startColumn - 1 }, endPosition: { row: range.endLineNumber - 1, column: range.endColumn - 1 } }).map(capture => ( + { + name: capture.name, + text: capture.node.text, + node: { + startIndex: capture.node.startIndex, + endIndex: capture.node.endIndex + } + } + )); } /** @@ -239,7 +357,7 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi return this._tokenizeEncoded(lineNumber, textModel); } - private _getTreeAndCaptures(range: Range, textModel: ITextModel): { tree: ITreeSitterParseResult | undefined; captures: Parser.QueryCapture[] } { + private _getTreeAndCaptures(range: Range, textModel: ITextModel): { tree: ITreeSitterParseResult | undefined; captures: QueryCapture[] } { const tree = this._getTree(textModel); const captures = this._captureAtRange(range, tree?.tree); return { tree, captures }; @@ -247,23 +365,23 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi private _tokenize(encodedLanguageId: LanguageId, range: Range, rangeStartOffset: number, rangeEndOffset: number, textModel: ITextModel): { endOffsetsAndMetadata: { endOffset: number; metadata: number }[]; captureTime: number; metadataTime: number } | undefined { const { tree, captures } = this._getTreeAndCaptures(range, textModel); - return this._tokenizeCaptures(tree, captures, encodedLanguageId, rangeStartOffset, rangeEndOffset); + return this._tokenizeCapturesWithMetadata(tree, captures, encodedLanguageId, rangeStartOffset, rangeEndOffset); } - private _tokenizeCaptures(tree: ITreeSitterParseResult | undefined, captures: Parser.QueryCapture[], encodedLanguageId: LanguageId, rangeStartOffset: number, rangeEndOffset: number): { endOffsetsAndMetadata: { endOffset: number; metadata: number }[]; captureTime: number; metadataTime: number } | undefined { + private _createTokensFromCaptures(tree: ITreeSitterParseResult | undefined, captures: QueryCapture[], rangeStartOffset: number, rangeEndOffset: number): { endOffsets: { endOffset: number; scopes: string[] }[]; captureTime: number } | undefined { const stopwatch = StopWatch.create(); const rangeLength = rangeEndOffset - rangeStartOffset; if (captures.length === 0) { if (tree) { stopwatch.stop(); - const endOffsetsAndMetadata = [{ endOffset: rangeLength, metadata: findMetadata(this._colorThemeData, [], encodedLanguageId) }]; - return { endOffsetsAndMetadata, captureTime: stopwatch.elapsed(), metadataTime: 0 }; + const endOffsetsAndMetadata = [{ endOffset: rangeLength, scopes: [] }]; + return { endOffsets: endOffsetsAndMetadata, captureTime: stopwatch.elapsed() }; } return undefined; } - const endOffsetsAndScopes: { endOffset: number; scopes: string[]; metadata?: number }[] = Array(captures.length); + const endOffsetsAndScopes: { endOffset: number; scopes: string[] }[] = Array(captures.length); endOffsetsAndScopes.fill({ endOffset: 0, scopes: [] }); let tokenIndex = 0; @@ -273,10 +391,11 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi for (let captureIndex = 0; captureIndex < captures.length; captureIndex++) { const capture = captures[captureIndex]; - const tokenEndIndex = capture.node.endIndex < rangeStartOffset + rangeLength ? capture.node.endIndex : rangeStartOffset + rangeLength; - const tokenStartIndex = capture.node.startIndex < rangeStartOffset ? rangeStartOffset : capture.node.startIndex; + const tokenEndIndex = capture.node.endIndex < rangeEndOffset ? ((capture.node.endIndex < rangeStartOffset) ? rangeStartOffset : capture.node.endIndex) : rangeEndOffset; + const tokenStartIndex = capture.node.startIndex < rangeStartOffset ? rangeStartOffset : ((capture.node.startIndex > tokenEndIndex) ? tokenEndIndex : capture.node.startIndex); const lineRelativeOffset = tokenEndIndex - rangeStartOffset; + // Not every character will get captured, so we need to make sure that our current capture doesn't bleed toward the start of the line and cover characters that it doesn't apply to. // We do this by creating a new token in the array if the previous token ends before the current token starts. let previousTokenEnd: number; @@ -346,19 +465,36 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi tokenIndex++; } } - const captureTime = stopwatch.elapsed(); - stopwatch.reset(); for (let i = 0; i < endOffsetsAndScopes.length; i++) { const token = endOffsetsAndScopes[i]; if (token.endOffset === 0 && token.scopes.length === 0 && i !== 0) { endOffsetsAndScopes.splice(i, endOffsetsAndScopes.length - i); break; } + } + const captureTime = stopwatch.elapsed(); + return { endOffsets: endOffsetsAndScopes as { endOffset: number; scopes: string[] }[], captureTime }; + + } + + private _tokenizeCapturesWithMetadata(tree: ITreeSitterParseResult | undefined, captures: QueryCapture[], encodedLanguageId: LanguageId, rangeStartOffset: number, rangeEndOffset: number): { endOffsetsAndMetadata: { endOffset: number; metadata: number }[]; captureTime: number; metadataTime: number } | undefined { + const stopwatch = StopWatch.create(); + const emptyTokens = this._createTokensFromCaptures(tree, captures, rangeStartOffset, rangeEndOffset); + if (!emptyTokens) { + return undefined; + } + const endOffsetsAndScopes: { endOffset: number; scopes: string[]; metadata?: number }[] = emptyTokens.endOffsets; + for (let i = 0; i < endOffsetsAndScopes.length; i++) { + const token = endOffsetsAndScopes[i]; token.metadata = findMetadata(this._colorThemeData, token.scopes, encodedLanguageId); } const metadataTime = stopwatch.elapsed(); - return { endOffsetsAndMetadata: endOffsetsAndScopes as { endOffset: number; scopes: string[]; metadata: number }[], captureTime, metadataTime }; + return { endOffsetsAndMetadata: endOffsetsAndScopes as { endOffset: number; scopes: string[]; metadata: number }[], captureTime: emptyTokens.captureTime, metadataTime }; + } + + private _emptyToken(encodedLanguageId: number) { + return findMetadata(this._colorThemeData, [], encodedLanguageId); } private _tokenizeEncoded(lineNumber: number, textModel: ITextModel): { result: Uint32Array; captureTime: number; metadataTime: number } | undefined { diff --git a/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts b/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts index 61a9cf9944ec..65c196380a05 100644 --- a/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts +++ b/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts @@ -18,9 +18,10 @@ import { IConfigurationService } from '../../../platform/configuration/common/co import { TestConfigurationService } from '../../../platform/configuration/test/common/testConfigurationService.js'; import { IEnvironmentService } from '../../../platform/environment/common/environment.js'; import { ModelService } from '../../../editor/common/services/modelService.js'; -import { TreeSitterTokenizationFeature, TreeSitterTokenizationSupport } from '../../services/treeSitter/common/treeSitterTokenizationFeature.js'; +// eslint-disable-next-line local/code-layering, local/code-import-patterns +import { TreeSitterTokenizationFeature } from '../../services/treeSitter/browser/treeSitterTokenizationFeature.js'; import { ITreeSitterParserService, TreeUpdateEvent } from '../../../editor/common/services/treeSitterParserService.js'; -import { TreeSitterTokenizationRegistry } from '../../../editor/common/languages.js'; +import { ITreeSitterTokenizationSupport, TreeSitterTokenizationRegistry } from '../../../editor/common/languages.js'; import { FileService } from '../../../platform/files/common/fileService.js'; import { Schemas } from '../../../base/common/network.js'; import { DiskFileSystemProvider } from '../../../platform/files/node/diskFileSystemProvider.js'; @@ -107,7 +108,7 @@ suite('Tree Sitter TokenizationFeature', function () { const environmentService: IEnvironmentService = {} as IEnvironmentService; const tokenStoreService: ITreeSitterTokenizationStoreService = new MockTokenStoreService(); let treeSitterParserService: TreeSitterTextModelService; - let treeSitterTokenizationSupport: TreeSitterTokenizationSupport; + let treeSitterTokenizationSupport: ITreeSitterTokenizationSupport; let disposables: DisposableStore; @@ -148,7 +149,7 @@ suite('Tree Sitter TokenizationFeature', function () { treeSitterParserService.isTest = true; instantiationService.set(ITreeSitterParserService, treeSitterParserService); disposables.add(instantiationService.createInstance(TreeSitterTokenizationFeature)); - treeSitterTokenizationSupport = disposables.add(await TreeSitterTokenizationRegistry.getOrCreate('typescript') as TreeSitterTokenizationSupport); + treeSitterTokenizationSupport = disposables.add(await TreeSitterTokenizationRegistry.getOrCreate('typescript') as (ITreeSitterTokenizationSupport & IDisposable)); }); teardown(() => { From b11d84d0cbae963e31cfcb9b3dd027b17e1a183e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 22 Jan 2025 17:20:08 +0100 Subject: [PATCH 0773/3587] chat setup - do not bring up confirm dialogs when reloading or closing window (#238468) --- .../contrib/chat/browser/chatSetup.ts | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index ffeea094bf47..7f6fda724de0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -64,6 +64,7 @@ import { ExtensionUrlHandlerOverrideRegistry } from '../../../services/extension import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; +import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -749,6 +750,8 @@ class ChatSetupController extends Disposable { private _step = ChatSetupStep.Initial; get step(): ChatSetupStep { return this._step; } + private willShutdown = false; + constructor( private readonly context: ChatSetupContext, private readonly requests: ChatSetupRequests, @@ -765,7 +768,8 @@ class ChatSetupController extends Disposable { @ICommandService private readonly commandService: ICommandService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, - @IDialogService private readonly dialogService: IDialogService + @IDialogService private readonly dialogService: IDialogService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, ) { super(); @@ -774,6 +778,7 @@ class ChatSetupController extends Disposable { private registerListeners(): void { this._register(this.context.onDidChange(() => this._onDidChange.fire())); + this._register(this.lifecycleService.onWillShutdown(() => this.willShutdown = true)); } private setStep(step: ChatSetupStep): void { @@ -866,7 +871,7 @@ class ChatSetupController extends Disposable { this.logService.error(`[chat setup] signIn: error ${e}`); } - if (!session) { + if (!session && !this.willShutdown) { const { confirmed } = await this.dialogService.confirm({ type: Severity.Error, message: localize('unknownSignInError', "Unable to sign in to {0}. Would you like to try again?", defaultChat.providerName), @@ -944,15 +949,17 @@ class ChatSetupController extends Disposable { } if (error) { - const { confirmed } = await this.dialogService.confirm({ - type: Severity.Error, - message: localize('unknownSetupError', "An error occurred while setting up Copilot. Would you like to try again?"), - detail: error && !isCancellationError(error) ? toErrorMessage(error) : undefined, - primaryButton: localize('retry', "Retry") - }); + if (!this.willShutdown) { + const { confirmed } = await this.dialogService.confirm({ + type: Severity.Error, + message: localize('unknownSetupError', "An error occurred while setting up Copilot. Would you like to try again?"), + detail: error && !isCancellationError(error) ? toErrorMessage(error) : undefined, + primaryButton: localize('retry', "Retry") + }); - if (confirmed) { - return this.doInstall(); + if (confirmed) { + return this.doInstall(); + } } throw error; From 75db9eb5095097daab33ecfbfc809f94a85ca284 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 22 Jan 2025 08:37:06 -0800 Subject: [PATCH 0774/3587] Remove paths from tsconfig.base.json --- src/bootstrap-window.ts | 6 +++--- src/tsconfig.base.json | 6 ------ src/vs/base/parts/sandbox/electron-sandbox/preload.ts | 2 +- .../electron-sandbox/processExplorer/processExplorer.ts | 6 +++--- src/vs/code/electron-sandbox/workbench/workbench.ts | 8 ++++---- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/bootstrap-window.ts b/src/bootstrap-window.ts index 3c1270026039..2f6c296a3aea 100644 --- a/src/bootstrap-window.ts +++ b/src/bootstrap-window.ts @@ -5,9 +5,9 @@ (function () { - type ISandboxConfiguration = import('vs/base/parts/sandbox/common/sandboxTypes.js').ISandboxConfiguration; - type ILoadResult = import('vs/platform/window/electron-sandbox/window.js').ILoadResult; - type ILoadOptions = import('vs/platform/window/electron-sandbox/window.js').ILoadOptions; + type ISandboxConfiguration = import('./vs/base/parts/sandbox/common/sandboxTypes.js').ISandboxConfiguration; + type ILoadResult = import('./vs/platform/window/electron-sandbox/window.js').ILoadResult; + type ILoadOptions = import('./vs/platform/window/electron-sandbox/window.js').ILoadOptions; type IMainWindowSandboxGlobals = import('./vs/base/parts/sandbox/electron-sandbox/globals.js').IMainWindowSandboxGlobals; const preloadGlobals: IMainWindowSandboxGlobals = (window as any).vscode; // defined by preload.ts diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index fc5247fab0c7..e354b0ed463c 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -12,12 +12,6 @@ "exactOptionalPropertyTypes": false, "useUnknownInCatchVariables": false, "forceConsistentCasingInFileNames": true, - "baseUrl": ".", - "paths": { - "vs/*": [ - "./vs/*" - ] - }, "target": "es2022", "useDefineForClassFields": false, "lib": [ diff --git a/src/vs/base/parts/sandbox/electron-sandbox/preload.ts b/src/vs/base/parts/sandbox/electron-sandbox/preload.ts index ba5476ffb2c3..7ff6c24d2c7f 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/preload.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/preload.ts @@ -9,7 +9,7 @@ const { ipcRenderer, webFrame, contextBridge, webUtils } = require('electron'); - type ISandboxConfiguration = import('vs/base/parts/sandbox/common/sandboxTypes.js').ISandboxConfiguration; + type ISandboxConfiguration = import('../common/sandboxTypes.js').ISandboxConfiguration; //#region Utilities diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer.ts b/src/vs/code/electron-sandbox/processExplorer/processExplorer.ts index 35db48125151..17be24c02f0a 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer.ts +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer.ts @@ -7,9 +7,9 @@ (async function () { - type IBootstrapWindow = import('vs/platform/window/electron-sandbox/window.js').IBootstrapWindow; - type IProcessExplorerMain = import('vs/code/electron-sandbox/processExplorer/processExplorerMain.js').IProcessExplorerMain; - type ProcessExplorerWindowConfiguration = import('vs/platform/process/common/process.js').ProcessExplorerWindowConfiguration; + type IBootstrapWindow = import('../../../platform/window/electron-sandbox/window.js').IBootstrapWindow; + type IProcessExplorerMain = import('./processExplorerMain.js').IProcessExplorerMain; + type ProcessExplorerWindowConfiguration = import('../../../platform/process/common/process.js').ProcessExplorerWindowConfiguration; const bootstrapWindow: IBootstrapWindow = (window as any).MonacoBootstrapWindow; // defined by bootstrap-window.ts diff --git a/src/vs/code/electron-sandbox/workbench/workbench.ts b/src/vs/code/electron-sandbox/workbench/workbench.ts index daddd15eb072..833f8118d7ab 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.ts +++ b/src/vs/code/electron-sandbox/workbench/workbench.ts @@ -10,10 +10,10 @@ // Add a perf entry right from the top performance.mark('code/didStartRenderer'); - type INativeWindowConfiguration = import('vs/platform/window/common/window.ts').INativeWindowConfiguration; - type IBootstrapWindow = import('vs/platform/window/electron-sandbox/window.js').IBootstrapWindow; - type IMainWindowSandboxGlobals = import('vs/base/parts/sandbox/electron-sandbox/globals.js').IMainWindowSandboxGlobals; - type IDesktopMain = import('vs/workbench/electron-sandbox/desktop.main.js').IDesktopMain; + type INativeWindowConfiguration = import('../../../platform/window/common/window.ts').INativeWindowConfiguration; + type IBootstrapWindow = import('../../../platform/window/electron-sandbox/window.js').IBootstrapWindow; + type IMainWindowSandboxGlobals = import('../../../base/parts/sandbox/electron-sandbox/globals.js').IMainWindowSandboxGlobals; + type IDesktopMain = import('../../../workbench/electron-sandbox/desktop.main.js').IDesktopMain; const bootstrapWindow: IBootstrapWindow = (window as any).MonacoBootstrapWindow; // defined by bootstrap-window.ts const preloadGlobals: IMainWindowSandboxGlobals = (window as any).vscode; // defined by preload.ts From 02ea21a23df243c33a77985d993814d5b8b91094 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 22 Jan 2025 09:30:53 -0800 Subject: [PATCH 0775/3587] add links provider for prompt files (#238427) * [prompt links]: add link background decorations * [prompt links]: cleanup and move files around * [prompt links]: add II of PromptLinkProvider, ObjectCache and TrckedDisposable utilities * [prompt links]: add all missing unit tests * [prompt links]: remove `TextModelDecoratorsProvider` * [prompt links]: address PR feedback --- src/vs/base/common/assert.ts | 20 +- src/vs/base/common/codecs/asyncDecoder.ts | 2 +- src/vs/base/common/codecs/baseDecoder.ts | 78 +++- src/vs/base/common/objectCache.ts | 152 ++++++++ src/vs/base/common/observableDisposable.ts | 103 +++++ src/vs/base/common/trackedDisposable.ts | 37 -- src/vs/base/test/common/assert.test.ts | 68 +++- src/vs/base/test/common/objectCache.test.ts | 332 +++++++++++++++++ .../test/common/observableDisposable.test.ts | 222 +++++++++++ .../markdownCodec/tokens/markdownLink.ts | 26 +- .../test/common/codecs/linesDecoder.test.ts | 284 +++++++++----- .../editor/test/common/utils/testDecoder.ts | 161 ++++++-- .../instructionsAttachment.ts | 14 +- .../contrib/chat/browser/chat.contribution.ts | 1 + .../chatInstructionsAttachment.ts | 159 +------- .../contrib/chat/browser/media/chat.css | 2 +- .../chat/common/promptFileReferenceErrors.ts | 10 +- .../codecs/tokens/fileReference.ts | 20 +- .../filePromptContentsProvider.ts | 8 +- .../promptContentsProviderBase.ts | 4 +- .../textModelContentsProvider.ts | 41 +- .../languageFeatures/promptLinkProvider.ts | 135 +++++++ .../promptSyntax/languageFeatures/types.d.ts | 37 ++ .../promptSyntax/parsers/basePromptParser.ts | 351 +++++++++++++++--- .../parsers/textModelPromptParser.ts | 4 +- .../common/promptSyntax/parsers/types.d.ts | 66 +++- .../codecs/tokens/fileReference.test.ts | 89 +++++ .../codecs/tokens/markdownLink.test.ts | 98 +++++ .../promptSyntax/promptFileReference.test.ts | 7 +- 29 files changed, 2107 insertions(+), 424 deletions(-) create mode 100644 src/vs/base/common/objectCache.ts create mode 100644 src/vs/base/common/observableDisposable.ts delete mode 100644 src/vs/base/common/trackedDisposable.ts create mode 100644 src/vs/base/test/common/objectCache.test.ts create mode 100644 src/vs/base/test/common/observableDisposable.test.ts create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/types.d.ts create mode 100644 src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/fileReference.test.ts create mode 100644 src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/markdownLink.test.ts diff --git a/src/vs/base/common/assert.ts b/src/vs/base/common/assert.ts index b185897f6147..56b74b3a3b78 100644 --- a/src/vs/base/common/assert.ts +++ b/src/vs/base/common/assert.ts @@ -29,9 +29,25 @@ export function assertNever(value: never, message = 'Unreachable'): never { throw new Error(message); } -export function assert(condition: boolean, message = 'unexpected state'): asserts condition { +/** + * Asserts that a condition is `truthy`. + * + * @throws provided {@linkcode messageOrError} if the {@linkcode condition} is `falsy`. + * + * @param condition The condition to assert. + * @param messageOrError An error message or error object to throw if condition is `falsy`. + */ +export function assert( + condition: boolean, + messageOrError: string | Error = 'unexpected state', +): asserts condition { if (!condition) { - throw new BugIndicatingError(`Assertion Failed: ${message}`); + // if error instance is provided, use it, otherwise create a new one + const errorToThrow = typeof messageOrError === 'string' + ? new BugIndicatingError(`Assertion Failed: ${messageOrError}`) + : messageOrError; + + throw errorToThrow; } } diff --git a/src/vs/base/common/codecs/asyncDecoder.ts b/src/vs/base/common/codecs/asyncDecoder.ts index efbcf9a7fe4e..e35daa773176 100644 --- a/src/vs/base/common/codecs/asyncDecoder.ts +++ b/src/vs/base/common/codecs/asyncDecoder.ts @@ -69,7 +69,7 @@ export class AsyncDecoder, K extends NonNullable< } // if no data available and stream ended, we're done - if (this.decoder.isEnded) { + if (this.decoder.ended) { this.dispose(); return null; diff --git a/src/vs/base/common/codecs/baseDecoder.ts b/src/vs/base/common/codecs/baseDecoder.ts index f28ab6c7d2c1..586ea61f56aa 100644 --- a/src/vs/base/common/codecs/baseDecoder.ts +++ b/src/vs/base/common/codecs/baseDecoder.ts @@ -7,8 +7,9 @@ import { assert } from '../assert.js'; import { Emitter } from '../event.js'; import { IDisposable } from '../lifecycle.js'; import { ReadableStream } from '../stream.js'; +import { DeferredPromise } from '../async.js'; import { AsyncDecoder } from './asyncDecoder.js'; -import { TrackedDisposable } from '../trackedDisposable.js'; +import { ObservableDisposable } from '../observableDisposable.js'; /** * Event names of {@link ReadableStream} stream. @@ -24,21 +25,27 @@ export type TStreamListenerNames = 'data' | 'error' | 'end'; export abstract class BaseDecoder< T extends NonNullable, K extends NonNullable = NonNullable, -> extends TrackedDisposable implements ReadableStream { +> extends ObservableDisposable implements ReadableStream { /** - * Flag that indicates if the decoder stream has ended. + * Private attribute to track if the stream has ended. */ - protected ended = false; + private _ended = false; protected readonly _onData = this._register(new Emitter()); - protected readonly _onEnd = this._register(new Emitter()); - protected readonly _onError = this._register(new Emitter()); + private readonly _onEnd = this._register(new Emitter()); + private readonly _onError = this._register(new Emitter()); /** * A store of currently registered event listeners. */ private readonly _listeners: Map> = new Map(); + /** + * This method is called when a new incomming data + * is received from the input stream. + */ + protected abstract onStreamData(data: K): void; + /** * @param stream The input stream to decode. */ @@ -53,10 +60,41 @@ export abstract class BaseDecoder< } /** - * This method is called when a new incomming data - * is received from the input stream. + * Private attribute to track if the stream has started. */ - protected abstract onStreamData(data: K): void; + private started = false; + + /** + * Promise that resolves when the stream has ended, either by + * receiving the `end` event or by a disposal, but not when + * the `error` event is received alone. + */ + private settledPromise = new DeferredPromise(); + + /** + * Promise that resolves when the stream has ended, either by + * receiving the `end` event or by a disposal, but not when + * the `error` event is received alone. + * + * @throws If the stream was not yet started to prevent this + * promise to block the consumer calls indefinitely. + */ + public get settled(): Promise { + // if the stream has not started yet, the promise might + // block the consumer calls indefinitely if they forget + // to call the `start()` method, or if the call happens + // after await on the `settled` promise; to forbid this + // confusion, we require the stream to be started first + assert( + this.started, + [ + 'Cannot get `settled` promise of a stream that has not been started.', + 'Please call `start()` first.', + ].join(' '), + ); + + return this.settledPromise.p; + } /** * Start receiveing data from the stream. @@ -64,15 +102,20 @@ export abstract class BaseDecoder< */ public start(): this { assert( - !this.ended, + !this._ended, 'Cannot start stream that has already ended.', ); - assert( !this.disposed, 'Cannot start stream that has already disposed.', ); + // if already started, nothing to do + if (this.started) { + return this; + } + this.started = true; + this.stream.on('data', this.tryOnStreamData); this.stream.on('error', this.onStreamError); this.stream.on('end', this.onStreamEnd); @@ -91,8 +134,8 @@ export abstract class BaseDecoder< * Check if the decoder has been ended hence has * no more data to produce. */ - public get isEnded(): boolean { - return this.ended; + public get ended(): boolean { + return this._ended; } /** @@ -263,12 +306,13 @@ export abstract class BaseDecoder< * This method is called when the input stream ends. */ protected onStreamEnd(): void { - if (this.ended) { + if (this._ended) { return; } - this.ended = true; + this._ended = true; this._onEnd.fire(); + this.settledPromise.complete(); } /** @@ -286,7 +330,7 @@ export abstract class BaseDecoder< */ public async consumeAll(): Promise { assert( - !this.ended, + !this._ended, 'Cannot consume all messages of the stream that has already ended.', ); @@ -309,7 +353,7 @@ export abstract class BaseDecoder< */ [Symbol.asyncIterator](): AsyncIterator { assert( - !this.ended, + !this._ended, 'Cannot iterate on messages of the stream that has already ended.', ); diff --git a/src/vs/base/common/objectCache.ts b/src/vs/base/common/objectCache.ts new file mode 100644 index 000000000000..e7be0b161e9c --- /dev/null +++ b/src/vs/base/common/objectCache.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableMap } from '../../base/common/lifecycle.js'; +import { ObservableDisposable, assertNotDisposed } from './observableDisposable.js'; + +/** + * Generic cache for object instances. Guarantees to return only non-disposed + * objects from the {@linkcode get} method. If a requested object is not yet + * in the cache or is disposed already, the {@linkcode factory} callback is + * called to create a new object. + * + * @throws if {@linkcode factory} callback returns a disposed object. + * + * ## Examples + * + * ```typescript + * // a class that will be used as a cache key; the key can be of any + * // non-nullable type, including primitives like `string` or `number`, + * // but in this case we use an object pointer as a key + * class KeyObject {} + * + * // a class for testing purposes + * class TestObject extends ObservableDisposable { + * constructor( + * public readonly id: KeyObject, + * ) {} + * }; + * + * // create an object cache instance providing it a factory function that + * // is responsible for creating new objects based on the provided key if + * // the cache does not contain the requested object yet or an existing + * // object is already disposed + * const cache = new ObjectCache((key) => { + * // create a new test object based on the provided key + * return new TestObject(key); + * }); + * + * // create two keys + * const key1 = new KeyObject(); + * const key2 = new KeyObject(); + * + * // get an object from the cache by its key + * const object1 = cache.get(key1); // returns a new test object + * + * // validate that the new object has the correct key + * assert( + * object1.id === key1, + * 'Object 1 must have correct ID.', + * ); + * + * // returns the same cached test object + * const object2 = cache.get(key1); + * + * // validate that the same exact object is returned from the cache + * assert( + * object1 === object2, + * 'Object 2 the same cached object as object 1.', + * ); + * + * // returns a new test object + * const object3 = cache.get(key2); + * + * // validate that the new object has the correct key + * assert( + * object3.id === key2, + * 'Object 3 must have correct ID.', + * ); + * + * assert( + * object3 !== object1, + * 'Object 3 must be a new object.', + * ); + * ``` + */ +export class ObjectCache< + TValue extends ObservableDisposable, + TKey extends NonNullable = string, +> extends Disposable { + private readonly cache: DisposableMap = + this._register(new DisposableMap()); + + constructor( + private readonly factory: (key: TKey) => TValue & { disposed: false }, + ) { + super(); + } + + /** + * Get an existing object from the cache. If a requested object is not yet + * in the cache or is disposed already, the {@linkcode factory} callback is + * called to create a new object. + * + * @throws if {@linkcode factory} callback returns a disposed object. + * @param key - ID of the object in the cache + */ + public get(key: TKey): TValue & { disposed: false } { + let object = this.cache.get(key); + + // if object is already disposed, remove it from the cache + if (object?.disposed) { + this.cache.deleteAndLeak(key); + object = undefined; + } + + // if object exists and is not disposed, return it + if (object) { + // must always hold true due to the check above + assertNotDisposed( + object, + 'Object must not be disposed.', + ); + + return object; + } + + // create a new object by calling the factory + object = this.factory(key); + + // newly created object must not be disposed + assertNotDisposed( + object, + 'Newly created object must not be disposed.', + ); + + // remove it from the cache automatically on dispose + object.onDispose(() => { + this.cache.deleteAndLeak(key); + }); + this.cache.set(key, object); + + return object; + } + + /** + * Remove an object from the cache by its key. + * + * @param key ID of the object to remove. + * @param dispose Whether the removed object must be disposed. + */ + public remove(key: TKey, dispose: boolean): this { + if (dispose) { + this.cache.deleteAndDispose(key); + return this; + } + + this.cache.deleteAndLeak(key); + return this; + } +} diff --git a/src/vs/base/common/observableDisposable.ts b/src/vs/base/common/observableDisposable.ts new file mode 100644 index 000000000000..3bce45ffa5a3 --- /dev/null +++ b/src/vs/base/common/observableDisposable.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from './event.js'; +import { Disposable } from './lifecycle.js'; + +/** + * Disposable object that tracks its {@linkcode disposed} state + * as a public attribute and provides the {@linkcode onDispose} + * event to subscribe to. + */ +export abstract class ObservableDisposable extends Disposable { + /** + * Private emitter for the `onDispose` event. + */ + private readonly _onDispose = this._register(new Emitter()); + + /** + * The event is fired when this object is disposed. + * Note! Executes the callback immediately if already disposed. + * + * @param callback The callback function to be called on updates. + */ + public onDispose(callback: () => void): this { + // if already disposed, execute the callback immediately + if (this.disposed) { + callback(); + + return this; + } + + // otherwise subscribe to the event + this._register(this._onDispose.event(callback)); + return this; + } + + /** + * Tracks disposed state of this object. + */ + private _disposed = false; + + /** + * Check if the current object was already disposed. + */ + public get disposed(): boolean { + return this._disposed; + } + + /** + * Dispose current object if not already disposed. + * @returns + */ + public override dispose(): void { + if (this.disposed) { + return; + } + + this._disposed = true; + this._onDispose.fire(); + super.dispose(); + } + + /** + * Assert that the current object was not yet disposed. + * + * @throws If the current object was already disposed. + * @param error Error message or error object to throw if assertion fails. + */ + public assertNotDisposed( + error: string | Error, + ): asserts this is TNotDisposed { + assertNotDisposed(this, error); + } +} + +/** + * Type for a non-disposed object `TObject`. + */ +type TNotDisposed = TObject & { disposed: false }; + +/** + * Asserts that a provided `object` is not `disposed` yet, + * e.g., its `disposed` property is `false`. + * + * @throws if the provided `object.disposed` equal to `false`. + * @param error Error message or error object to throw if assertion fails. + */ +export function assertNotDisposed( + object: TObject, + error: string | Error, +): asserts object is TNotDisposed { + if (!object.disposed) { + return; + } + + const errorToThrow = typeof error === 'string' + ? new Error(error) + : error; + + throw errorToThrow; +} diff --git a/src/vs/base/common/trackedDisposable.ts b/src/vs/base/common/trackedDisposable.ts deleted file mode 100644 index 7c848c552118..000000000000 --- a/src/vs/base/common/trackedDisposable.ts +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from '../../base/common/lifecycle.js'; - -/** - * Disposable class that tracks its own {@linkcode disposed} state. - */ -export class TrackedDisposable extends Disposable { - - /** - * Tracks disposed state of this object. - */ - private _disposed = false; - - /** - * Check if the current object was already disposed. - */ - public get disposed(): boolean { - return this._disposed; - } - - /** - * Dispose current object if not already disposed. - * @returns - */ - public override dispose(): void { - if (this.disposed) { - return; - } - - this._disposed = true; - super.dispose(); - } -} diff --git a/src/vs/base/test/common/assert.test.ts b/src/vs/base/test/common/assert.test.ts index 11d99e8f2b36..bc5b7781510f 100644 --- a/src/vs/base/test/common/assert.test.ts +++ b/src/vs/base/test/common/assert.test.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { ok } from '../../common/assert.js'; +import { ok, assert as commonAssert } from '../../common/assert.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; +import { CancellationError, ReadonlyError } from '../../common/errors.js'; suite('Assert', () => { test('ok', () => { @@ -33,5 +34,70 @@ suite('Assert', () => { ok(5); }); + suite('throws a provided error object', () => { + test('generic error', () => { + const originalError = new Error('Oh no!'); + + try { + commonAssert( + false, + originalError, + ); + } catch (thrownError) { + assert.strictEqual( + thrownError, + originalError, + 'Must throw the provided error instance.', + ); + + assert.strictEqual( + thrownError.message, + 'Oh no!', + 'Must throw the provided error instance.', + ); + } + }); + + test('cancellation error', () => { + const originalError = new CancellationError(); + + try { + commonAssert( + false, + originalError, + ); + } catch (thrownError) { + assert.strictEqual( + thrownError, + originalError, + 'Must throw the provided error instance.', + ); + } + }); + + test('readonly error', () => { + const originalError = new ReadonlyError('World'); + + try { + commonAssert( + false, + originalError, + ); + } catch (thrownError) { + assert.strictEqual( + thrownError, + originalError, + 'Must throw the provided error instance.', + ); + + assert.strictEqual( + thrownError.message, + 'World is read-only and cannot be changed', + 'Must throw the provided error instance.', + ); + } + }); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/base/test/common/objectCache.test.ts b/src/vs/base/test/common/objectCache.test.ts new file mode 100644 index 000000000000..b47368902486 --- /dev/null +++ b/src/vs/base/test/common/objectCache.test.ts @@ -0,0 +1,332 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { spy } from 'sinon'; +import { ObjectCache } from '../../common/objectCache.js'; +import { wait } from '../../../base/test/common/testUtils.js'; +import { ObservableDisposable } from '../../common/observableDisposable.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js'; + +/** + * Test object class. + */ +class TestObject = string> extends ObservableDisposable { + constructor( + public readonly ID: TKey, + ) { + super(); + } + + /** + * Check if this object is equal to another one. + */ + public equal(other: TestObject>): boolean { + return this.ID === other.ID; + } +} + +suite('ObjectCache', function () { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + suite('get', () => { + /** + * Common test funtion to test core logic of the cache + * with provider test ID keys of some specific type. + * + * @param key1 Test key1. + * @param key2 Test key2. + */ + const testCoreLogic = async >(key1: TKey, key2: TKey) => { + const factory = spy(( + key: TKey, + ) => { + const result: TestObject = new TestObject(key); + + result.assertNotDisposed( + 'Object must not be disposed.', + ); + + return result; + }); + + const cache = disposables.add(new ObjectCache(factory)); + + /** + * Test the core logic of the cache using 2 objects. + */ + + const obj1 = cache.get(key1); + assert( + factory.calledOnceWithExactly(key1), + '[obj1] Must be called once with the correct arguments.', + ); + + assert( + obj1.ID === key1, + '[obj1] Returned object must have the correct ID.', + ); + + const obj2 = cache.get(key1); + assert( + factory.calledOnceWithExactly(key1), + '[obj2] Must be called once with the correct arguments.', + ); + + assert( + obj2.ID === key1, + '[obj2] Returned object must have the correct ID.', + ); + + assert( + obj1 === obj2 && obj1.equal(obj2), + '[obj2] Returned object must be the same instance.', + ); + + factory.resetHistory(); + + const obj3 = cache.get(key2); + assert( + factory.calledOnceWithExactly(key2), + '[obj3] Must be called once with the correct arguments.', + ); + + assert( + obj3.ID === key2, + '[obj3] Returned object must have the correct ID.', + ); + + factory.resetHistory(); + + const obj4 = cache.get(key1); + assert( + factory.notCalled, + '[obj4] Factory must not be called.', + ); + + assert( + obj4.ID === key1, + '[obj4] Returned object must have the correct ID.', + ); + + assert( + obj1 === obj4 && obj1.equal(obj4), + '[obj4] Returned object must be the same instance.', + ); + + factory.resetHistory(); + + /** + * Now test that the object is removed automatically from + * the cache when it is disposed. + */ + + obj3.dispose(); + // the object is removed from the cache asynchronously + // so add a small delay to ensure the object is removed + await wait(5); + + const obj5 = cache.get(key1); + assert( + factory.notCalled, + '[obj5] Factory must not be called.', + ); + + assert( + obj5.ID === key1, + '[obj5] Returned object must have the correct ID.', + ); + + assert( + obj1 === obj5 && obj1.equal(obj5), + '[obj5] Returned object must be the same instance.', + ); + + factory.resetHistory(); + + /** + * Test that the previously disposed object is recreated + * on the new retrieval call. + */ + + const obj6 = cache.get(key2); + assert( + factory.calledOnceWithExactly(key2), + '[obj6] Must be called once with the correct arguments.', + ); + + assert( + obj6.ID === key2, + '[obj6] Returned object must have the correct ID.', + ); + }; + + test('strings as keys', async function () { + await testCoreLogic('key1', 'key2'); + }); + + test('numbers as keys', async function () { + await testCoreLogic(10, 17065); + }); + + test('objects as keys', async function () { + await testCoreLogic( + disposables.add(new TestObject({})), + disposables.add(new TestObject({})), + ); + }); + }); + + suite('remove', () => { + /** + * Common test funtion to test remove logic of the cache + * with provider test ID keys of some specific type. + * + * @param key1 Test key1. + * @param key2 Test key2. + */ + const testRemoveLogic = async >( + key1: TKey, + key2: TKey, + disposeOnRemove: boolean, + ) => { + const factory = spy(( + key: TKey, + ) => { + const result: TestObject = new TestObject(key); + + result.assertNotDisposed( + 'Object must not be disposed.', + ); + + return result; + }); + + // ObjectCache, TKey> + const cache = disposables.add(new ObjectCache(factory)); + + /** + * Test the core logic of the cache. + */ + + const obj1 = cache.get(key1); + assert( + factory.calledOnceWithExactly(key1), + '[obj1] Must be called once with the correct arguments.', + ); + + assert( + obj1.ID === key1, + '[obj1] Returned object must have the correct ID.', + ); + + factory.resetHistory(); + + const obj2 = cache.get(key2); + assert( + factory.calledOnceWithExactly(key2), + '[obj2] Must be called once with the correct arguments.', + ); + + assert( + obj2.ID === key2, + '[obj2] Returned object must have the correct ID.', + ); + + cache.remove(key2, disposeOnRemove); + + const object2Disposed = obj2.disposed; + + // ensure we don't leak undisposed object in the tests + if (!obj2.disposed) { + obj2.dispose(); + } + + assert( + object2Disposed === disposeOnRemove, + `[obj2] Removed object must be disposed: ${disposeOnRemove}.`, + ); + + factory.resetHistory(); + + /** + * Validate that another object is not disposed. + */ + + assert( + !obj1.disposed, + '[obj1] Object must not be disposed.', + ); + + const obj3 = cache.get(key1); + assert( + factory.notCalled, + '[obj3] Factory must not be called.', + ); + + assert( + obj3.ID === key1, + '[obj3] Returned object must have the correct ID.', + ); + + assert( + obj1 === obj3 && obj1.equal(obj3), + '[obj3] Returned object must be the same instance.', + ); + + factory.resetHistory(); + }; + + test('strings as keys', async function () { + await testRemoveLogic('key1', 'key2', false); + await testRemoveLogic('some-key', 'another-key', true); + }); + + test('numbers as keys', async function () { + await testRemoveLogic(7, 2400700, false); + await testRemoveLogic(1090, 2654, true); + }); + + test('objects as keys', async function () { + await testRemoveLogic( + disposables.add(new TestObject(1)), + disposables.add(new TestObject(1)), + false, + ); + + await testRemoveLogic( + disposables.add(new TestObject(2)), + disposables.add(new TestObject(2)), + true, + ); + }); + }); + + test('throws if factory returns a disposed object', async function () { + const factory = ( + key: string, + ) => { + const result = new TestObject(key); + + if (key === 'key2') { + result.dispose(); + } + + // caution! explicit type casting below! + return result as TestObject & { disposed: false }; + }; + + // ObjectCache + const cache = disposables.add(new ObjectCache(factory)); + + assert.doesNotThrow(() => { + cache.get('key1'); + }); + + assert.throws(() => { + cache.get('key2'); + }); + }); +}); diff --git a/src/vs/base/test/common/observableDisposable.test.ts b/src/vs/base/test/common/observableDisposable.test.ts new file mode 100644 index 000000000000..24de6cc3ecc0 --- /dev/null +++ b/src/vs/base/test/common/observableDisposable.test.ts @@ -0,0 +1,222 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import assert from 'assert'; +import { spy } from 'sinon'; +import { wait, waitRandom } from './testUtils.js'; +import { Disposable } from '../../common/lifecycle.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; +import { assertNotDisposed, ObservableDisposable } from '../../common/observableDisposable.js'; + +suite('ObservableDisposable', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + test('tracks `disposed` state', () => { + // this is an abstract class, so we have to create + // an anonymous class that extends it + const object = new class extends ObservableDisposable { }(); + disposables.add(object); + + assert( + object instanceof ObservableDisposable, + 'Object must be instance of ObservableDisposable.', + ); + + assert( + object instanceof Disposable, + 'Object must be instance of Disposable.', + ); + + assert( + !object.disposed, + 'Object must not be disposed yet.', + ); + + object.dispose(); + + assert( + object.disposed, + 'Object must be disposed.', + ); + }); + + suite('onDispose', () => { + test('fires the event on dispose', async () => { + // this is an abstract class, so we have to create + // an anonymous class that extends it + const object = new class extends ObservableDisposable { }(); + disposables.add(object); + + assert( + !object.disposed, + 'Object must not be disposed yet.', + ); + + const onDisposeSpy = spy(() => { }); + object.onDispose(onDisposeSpy); + + assert( + onDisposeSpy.notCalled, + '`onDispose` callback must not be called yet.', + ); + + await waitRandom(10); + + assert( + onDisposeSpy.notCalled, + '`onDispose` callback must not be called yet.', + ); + + // dispose object and wait for the event to be fired/received + object.dispose(); + await wait(1); + + /** + * Validate that the callback was called. + */ + + assert( + object.disposed, + 'Object must be disposed.', + ); + + assert( + onDisposeSpy.calledOnce, + '`onDispose` callback must be called.', + ); + + /** + * Validate that the callback is not called again. + */ + + object.dispose(); + object.dispose(); + await waitRandom(10); + object.dispose(); + + assert( + onDisposeSpy.calledOnce, + '`onDispose` callback must not be called again.', + ); + + assert( + object.disposed, + 'Object must be disposed.', + ); + }); + + test('executes callback immediately if already disposed', async () => { + // this is an abstract class, so we have to create + // an anonymous class that extends it + const object = new class extends ObservableDisposable { }(); + disposables.add(object); + + // dispose object and wait for the event to be fired/received + object.dispose(); + await wait(1); + + const onDisposeSpy = spy(() => { }); + object.onDispose(onDisposeSpy); + + assert( + onDisposeSpy.calledOnce, + '`onDispose` callback must be called immediately.', + ); + + await waitRandom(10); + + object.onDispose(onDisposeSpy); + + assert( + onDisposeSpy.calledTwice, + '`onDispose` callback must be called immediately the second time.', + ); + + // dispose object and wait for the event to be fired/received + object.dispose(); + await wait(1); + + assert( + onDisposeSpy.calledTwice, + '`onDispose` callback must not be called again on dispose.', + ); + }); + }); + + suite('asserts', () => { + test('not disposed (method)', async () => { + // this is an abstract class, so we have to create + // an anonymous class that extends it + const object: ObservableDisposable = new class extends ObservableDisposable { }(); + disposables.add(object); + + assert.doesNotThrow(() => { + object.assertNotDisposed('Object must not be disposed.'); + }); + + await waitRandom(10); + + assert.doesNotThrow(() => { + object.assertNotDisposed('Object must not be disposed.'); + }); + + // dispose object and wait for the event to be fired/received + object.dispose(); + await wait(1); + + assert.throws(() => { + object.assertNotDisposed('Object must not be disposed.'); + }); + + await waitRandom(10); + + assert.throws(() => { + object.assertNotDisposed('Object must not be disposed.'); + }); + }); + + test('not disposed (function)', async () => { + // this is an abstract class, so we have to create + // an anonymous class that extends it + const object: ObservableDisposable = new class extends ObservableDisposable { }(); + disposables.add(object); + + assert.doesNotThrow(() => { + assertNotDisposed( + object, + 'Object must not be disposed.', + ); + }); + + await waitRandom(10); + + assert.doesNotThrow(() => { + assertNotDisposed( + object, + 'Object must not be disposed.', + ); + }); + + // dispose object and wait for the event to be fired/received + object.dispose(); + await wait(1); + + assert.throws(() => { + assertNotDisposed( + object, + 'Object must not be disposed.', + ); + }); + + await waitRandom(10); + + assert.throws(() => { + assertNotDisposed( + object, + 'Object must not be disposed.', + ); + }); + }); + }); +}); diff --git a/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts b/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts index 4874ac7789f7..b4c8947a2131 100644 --- a/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts +++ b/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { BaseToken } from '../../baseToken.js'; -import { Range } from '../../../core/range.js'; import { MarkdownToken } from './markdownToken.js'; +import { IRange, Range } from '../../../core/range.js'; import { assert } from '../../../../../base/common/assert.js'; /** @@ -28,7 +28,7 @@ export class MarkdownLink extends MarkdownToken { */ columnNumber: number, /** - * The caprtion of the link, including the square brackets. + * The caption of the link, including the square brackets. */ private readonly caption: string, /** @@ -105,6 +105,28 @@ export class MarkdownLink extends MarkdownToken { return this.text === other.text; } + /** + * Get the range of the `link part` of the token. + */ + public get linkRange(): IRange | undefined { + if (this.path.length === 0) { + return undefined; + } + + const { range } = this; + + // note! '+1' for openning `(` of the link + const startColumn = range.startColumn + this.caption.length + 1; + const endColumn = startColumn + this.path.length; + + return new Range( + range.startLineNumber, + startColumn, + range.endLineNumber, + endColumn, + ); + } + /** * Returns a string representation of the token. */ diff --git a/src/vs/editor/test/common/codecs/linesDecoder.test.ts b/src/vs/editor/test/common/codecs/linesDecoder.test.ts index cc8f391176d9..019344128bce 100644 --- a/src/vs/editor/test/common/codecs/linesDecoder.test.ts +++ b/src/vs/editor/test/common/codecs/linesDecoder.test.ts @@ -3,16 +3,98 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TestDecoder } from '../utils/testDecoder.js'; +import assert from 'assert'; import { Range } from '../../../common/core/range.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; -import { newWriteableStream } from '../../../../base/common/stream.js'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { Line } from '../../../common/codecs/linesCodec/tokens/line.js'; +import { TestDecoder, TTokensConsumeMethod } from '../utils/testDecoder.js'; import { NewLine } from '../../../common/codecs/linesCodec/tokens/newLine.js'; +import { newWriteableStream, WriteableStream } from '../../../../base/common/stream.js'; import { CarriageReturn } from '../../../common/codecs/linesCodec/tokens/carriageReturn.js'; import { LinesDecoder, TLineToken } from '../../../common/codecs/linesCodec/linesDecoder.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +/** + * Note! This decoder is also often used to test common logic of abstract {@linkcode BaseDecoder} + * class, because the {@linkcode LinesDecoder} is one of the simplest non-abstract decoders we have. + */ +suite('LinesDecoder', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + /** + * Test the core logic with specific method of consuming + * tokens that are produced by a lines decoder instance. + */ + suite('core logic', () => { + testLinesDecoder('async-generator', disposables); + testLinesDecoder('consume-all-method', disposables); + testLinesDecoder('on-data-event', disposables); + }); + + suite('settled promise', () => { + test('throws if accessed on not-yet-started decoder instance', () => { + const test = disposables.add(new TestLinesDecoder()); + + assert.throws( + () => { + // testing the field access that throws here, so + // its OK to not use the returned value afterwards + // eslint-disable-next-line local/code-no-unused-expressions + test.decoder.settled; + }, + [ + 'Cannot get `settled` promise of a stream that has not been started.', + 'Please call `start()` first.', + ].join(' '), + ); + }); + }); + + suite('start', () => { + test('throws if the decoder object is already `disposed`', () => { + const test = disposables.add(new TestLinesDecoder()); + const { decoder } = test; + decoder.dispose(); + + assert.throws( + decoder.start.bind(decoder), + 'Cannot start stream that has already disposed.', + ); + }); + + test('throws if the decoder object is already `ended`', async () => { + const inputStream = newWriteableStream(null); + const test = disposables.add(new TestLinesDecoder(inputStream)); + const { decoder } = test; + + setTimeout(() => { + test.sendData([ + 'hello', + 'world :wave:', + ]); + }, 5); + + const receivedTokens = await decoder.start() + .consumeAll(); + + // a basic sanity check for received tokens + assert.strictEqual( + receivedTokens.length, + 3, + 'Must produce the correct number of tokens.', + ); + + // validate that calling `start()` after stream has ended throws + assert.throws( + decoder.start.bind(decoder), + 'Cannot start stream that has already ended.', + ); + }); + }); +}); + + /** * A reusable test utility that asserts that a `LinesDecoder` instance * correctly decodes `inputData` into a stream of `TLineToken` tokens. @@ -21,7 +103,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c * * ```typescript * // create a new test utility instance - * const test = testDisposables.add(new TestLinesDecoder()); + * const test = disposables.add(new TestLinesDecoder()); * * // run the test * await test.run( @@ -33,108 +115,124 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c * ); */ export class TestLinesDecoder extends TestDecoder { - constructor() { - const stream = newWriteableStream(null); + constructor( + inputStream?: WriteableStream, + ) { + const stream = (inputStream) + ? inputStream + : newWriteableStream(null); + const decoder = new LinesDecoder(stream); super(stream, decoder); } } -suite('LinesDecoder', () => { - const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); +/** + * Common reusable test utility to validate {@linkcode LinesDecoder} logic with + * the provided {@linkcode tokensConsumeMethod} way of consuming decoder-produced tokens. + * + * @throws if a test fails, please see thrown error for failure details. + * @param tokensConsumeMethod The way to consume tokens produced by the decoder. + * @param disposables Test disposables store. + */ +function testLinesDecoder( + tokensConsumeMethod: TTokensConsumeMethod, + disposables: Pick, +) { + suite(tokensConsumeMethod, () => { + suite('produces expected tokens', () => { + test('input starts with line data', async () => { + const test = disposables.add(new TestLinesDecoder()); - suite('produces expected tokens', () => { - test('input starts with line data', async () => { - const test = testDisposables.add(new TestLinesDecoder()); + await test.run( + ' hello world\nhow are you doing?\n\n 😊 \r ', + [ + new Line(1, ' hello world'), + new NewLine(new Range(1, 13, 1, 14)), + new Line(2, 'how are you doing?'), + new NewLine(new Range(2, 19, 2, 20)), + new Line(3, ''), + new NewLine(new Range(3, 1, 3, 2)), + new Line(4, ' 😊 '), + new CarriageReturn(new Range(4, 5, 4, 6)), + new Line(5, ' '), + ], + ); + }); - await test.run( - ' hello world\nhow are you doing?\n\n 😊 \r ', - [ - new Line(1, ' hello world'), - new NewLine(new Range(1, 13, 1, 14)), - new Line(2, 'how are you doing?'), - new NewLine(new Range(2, 19, 2, 20)), - new Line(3, ''), - new NewLine(new Range(3, 1, 3, 2)), - new Line(4, ' 😊 '), - new CarriageReturn(new Range(4, 5, 4, 6)), - new Line(5, ' '), - ], - ); - }); + test('input starts with a new line', async () => { + const test = disposables.add(new TestLinesDecoder()); - test('input starts with a new line', async () => { - const test = testDisposables.add(new TestLinesDecoder()); + await test.run( + '\nsome text on this line\n\n\nanother 💬 on this line\r\n🤫\n', + [ + new Line(1, ''), + new NewLine(new Range(1, 1, 1, 2)), + new Line(2, 'some text on this line'), + new NewLine(new Range(2, 23, 2, 24)), + new Line(3, ''), + new NewLine(new Range(3, 1, 3, 2)), + new Line(4, ''), + new NewLine(new Range(4, 1, 4, 2)), + new Line(5, 'another 💬 on this line'), + new CarriageReturn(new Range(5, 24, 5, 25)), + new NewLine(new Range(5, 25, 5, 26)), + new Line(6, '🤫'), + new NewLine(new Range(6, 3, 6, 4)), + ], + ); + }); - await test.run( - '\nsome text on this line\n\n\nanother 💬 on this line\r\n🤫\n', - [ - new Line(1, ''), - new NewLine(new Range(1, 1, 1, 2)), - new Line(2, 'some text on this line'), - new NewLine(new Range(2, 23, 2, 24)), - new Line(3, ''), - new NewLine(new Range(3, 1, 3, 2)), - new Line(4, ''), - new NewLine(new Range(4, 1, 4, 2)), - new Line(5, 'another 💬 on this line'), - new CarriageReturn(new Range(5, 24, 5, 25)), - new NewLine(new Range(5, 25, 5, 26)), - new Line(6, '🤫'), - new NewLine(new Range(6, 3, 6, 4)), - ], - ); - }); + test('input starts and ends with multiple new lines', async () => { + const test = disposables.add(new TestLinesDecoder()); - test('input starts and ends with multiple new lines', async () => { - const test = testDisposables.add(new TestLinesDecoder()); + await test.run( + '\n\n\r\nciao! 🗯️\t💭 💥 come\tva?\n\n\n\n\n', + [ + new Line(1, ''), + new NewLine(new Range(1, 1, 1, 2)), + new Line(2, ''), + new NewLine(new Range(2, 1, 2, 2)), + new Line(3, ''), + new CarriageReturn(new Range(3, 1, 3, 2)), + new NewLine(new Range(3, 2, 3, 3)), + new Line(4, 'ciao! 🗯️\t💭 💥 come\tva?'), + new NewLine(new Range(4, 25, 4, 26)), + new Line(5, ''), + new NewLine(new Range(5, 1, 5, 2)), + new Line(6, ''), + new NewLine(new Range(6, 1, 6, 2)), + new Line(7, ''), + new NewLine(new Range(7, 1, 7, 2)), + new Line(8, ''), + new NewLine(new Range(8, 1, 8, 2)), + ], + ); + }); - await test.run( - '\n\n\r\nciao! 🗯️\t💭 💥 come\tva?\n\n\n\n\n', - [ - new Line(1, ''), - new NewLine(new Range(1, 1, 1, 2)), - new Line(2, ''), - new NewLine(new Range(2, 1, 2, 2)), - new Line(3, ''), - new CarriageReturn(new Range(3, 1, 3, 2)), - new NewLine(new Range(3, 2, 3, 3)), - new Line(4, 'ciao! 🗯️\t💭 💥 come\tva?'), - new NewLine(new Range(4, 25, 4, 26)), - new Line(5, ''), - new NewLine(new Range(5, 1, 5, 2)), - new Line(6, ''), - new NewLine(new Range(6, 1, 6, 2)), - new Line(7, ''), - new NewLine(new Range(7, 1, 7, 2)), - new Line(8, ''), - new NewLine(new Range(8, 1, 8, 2)), - ], - ); - }); + test('single carriage return is treated as new line', async () => { + const test = disposables.add(new TestLinesDecoder()); - test('single carriage return is treated as new line', async () => { - const test = testDisposables.add(new TestLinesDecoder()); - - await test.run( - '\r\rhaalo! 💥💥 how\'re you?\r ?!\r\n\r\n ', - [ - new Line(1, ''), - new CarriageReturn(new Range(1, 1, 1, 2)), - new Line(2, ''), - new CarriageReturn(new Range(2, 1, 2, 2)), - new Line(3, 'haalo! 💥💥 how\'re you?'), - new CarriageReturn(new Range(3, 24, 3, 25)), - new Line(4, ' ?!'), - new CarriageReturn(new Range(4, 4, 4, 5)), - new NewLine(new Range(4, 5, 4, 6)), - new Line(5, ''), - new CarriageReturn(new Range(5, 1, 5, 2)), - new NewLine(new Range(5, 2, 5, 3)), - new Line(6, ' '), - ], - ); + await test.run( + '\r\rhaalo! 💥💥 how\'re you?\r ?!\r\n\r\n ', + [ + new Line(1, ''), + new CarriageReturn(new Range(1, 1, 1, 2)), + new Line(2, ''), + new CarriageReturn(new Range(2, 1, 2, 2)), + new Line(3, 'haalo! 💥💥 how\'re you?'), + new CarriageReturn(new Range(3, 24, 3, 25)), + new Line(4, ' ?!'), + new CarriageReturn(new Range(4, 4, 4, 5)), + new NewLine(new Range(4, 5, 4, 6)), + new Line(5, ''), + new CarriageReturn(new Range(5, 1, 5, 2)), + new NewLine(new Range(5, 2, 5, 3)), + new Line(6, ' '), + ], + ); + }); }); }); -}); +} diff --git a/src/vs/editor/test/common/utils/testDecoder.ts b/src/vs/editor/test/common/utils/testDecoder.ts index d78dc0124d9f..a998a64e2cc1 100644 --- a/src/vs/editor/test/common/utils/testDecoder.ts +++ b/src/vs/editor/test/common/utils/testDecoder.ts @@ -10,9 +10,14 @@ import { BaseToken } from '../../../common/codecs/baseToken.js'; import { assertDefined } from '../../../../base/common/types.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { WriteableStream } from '../../../../base/common/stream.js'; -import { randomBoolean } from '../../../../base/test/common/testUtils.js'; import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; +/** + * Kind of decoder tokens consume methods are different ways + * consume tokens that a decoder produces out of a byte stream. + */ +export type TTokensConsumeMethod = 'async-generator' | 'consume-all-method' | 'on-data-event'; + /** * A reusable test utility that asserts that the given decoder * produces the expected `expectedTokens` sequence of tokens. @@ -38,7 +43,7 @@ import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; export class TestDecoder> extends Disposable { constructor( private readonly stream: WriteableStream, - private readonly decoder: D, + public readonly decoder: D, ) { super(); @@ -46,51 +51,149 @@ export class TestDecoder> extends } /** - * Run the test sending the `inputData` data to the stream and asserting - * that the decoder produces the `expectedTokens` sequence of tokens. + * Write provided {@linkcode inputData} data to the input byte stream + * asynchronously in the background in small random-length chunks. + * + * @param inputData Input data to send. */ - public async run( + public sendData( inputData: string | string[], - expectedTokens: readonly T[], - ): Promise { + ): this { // if input data was passed as an array of lines, // join them into a single string with newlines if (Array.isArray(inputData)) { inputData = inputData.join('\n'); } - // write the data to the stream after a short delay to ensure - // that the the data is sent after the reading loop below - setTimeout(() => { - let inputDataBytes = VSBuffer.fromString(inputData); + // write the input data to the stream in multiple random-length + // chunks to simulate real input stream data flows + let inputDataBytes = VSBuffer.fromString(inputData); + const interval = setInterval(() => { + if (inputDataBytes.byteLength <= 0) { + clearInterval(interval); + this.stream.end(); - // write the input data to the stream in multiple random-length chunks - while (inputDataBytes.byteLength > 0) { - const dataToSend = inputDataBytes.slice(0, randomInt(inputDataBytes.byteLength)); - this.stream.write(dataToSend); - inputDataBytes = inputDataBytes.slice(dataToSend.byteLength); + return; } - this.stream.end(); - }, 25); + const dataToSend = inputDataBytes.slice(0, randomInt(inputDataBytes.byteLength)); + this.stream.write(dataToSend); + inputDataBytes = inputDataBytes.slice(dataToSend.byteLength); + }, randomInt(5)); - // randomly use either the `async iterator` or the `.consume()` - // variants of getting tokens, they both must yield equal results - const receivedTokens: T[] = []; - if (randomBoolean()) { - // test the `async iterator` code path - for await (const token of this.decoder) { - if (token === null) { + return this; + } + + /** + * Run the test sending the `inputData` data to the stream and asserting + * that the decoder produces the `expectedTokens` sequence of tokens. + * + * @param inputData Input data of the input byte stream. + * @param expectedTokens List of expected tokens the test token must produce. + * @param tokensConsumeMethod *Optional* method of consuming the decoder stream. + * Defaults to a random method (see {@linkcode randomTokensConsumeMethod}). + */ + public async run( + inputData: string | string[], + expectedTokens: readonly T[], + tokensConsumeMethod: TTokensConsumeMethod = this.randomTokensConsumeMethod(), + ): Promise { + try { + // initiate the data sending flow + this.sendData(inputData); + + // consume the decoder tokens based on specified + // (or randomly generated) tokens consume method + const receivedTokens: T[] = []; + switch (tokensConsumeMethod) { + // test the `async iterator` code path + case 'async-generator': { + for await (const token of this.decoder) { + if (token === null) { + break; + } + + receivedTokens.push(token); + } + + break; + } + // test the `.consumeAll()` method code path + case 'consume-all-method': { + receivedTokens.push(...(await this.decoder.consumeAll())); break; } + // test the `.onData()` event consume flow + case 'on-data-event': { + this.decoder.onData((token) => { + receivedTokens.push(token); + }); + + // in this case we also test the `settled` promise of the decoder + await this.decoder.settled; - receivedTokens.push(token); + break; + } + // ensure that the switch block is exhaustive + default: { + throw new Error(`Unknown consume method '${tokensConsumeMethod}'.`); + } } - } else { - // test the `.consume()` code path - receivedTokens.push(...(await this.decoder.consumeAll())); + + // validate the received tokens + this.validateReceivedTokens( + receivedTokens, + expectedTokens, + ); + } catch (error) { + assertDefined( + error, + `An non-nullable error must be thrown.`, + ); + assert( + error instanceof Error, + `An error error instance must be thrown.`, + ); + + // add the tokens consume method to the error message so we + // would know which method of consuming the tokens failed exactly + error.message = `[${tokensConsumeMethod}] ${error.message}`; } + } + + /** + * Randomly generate a tokens consume method type for the test. + */ + private randomTokensConsumeMethod(): TTokensConsumeMethod { + const testConsumeMethodIndex = randomInt(2); + switch (testConsumeMethodIndex) { + // test the `async iterator` code path + case 0: { + return 'async-generator'; + } + // test the `.consumeAll()` method code path + case 1: { + return 'consume-all-method'; + } + // test the `.onData()` event consume flow + case 2: { + return 'on-data-event'; + } + // ensure that the switch block is exhaustive + default: { + throw new Error(`Unknown consume method index '${testConsumeMethodIndex}'.`); + } + } + } + + /** + * Validate that received tokens list is equal to the expected one. + */ + private validateReceivedTokens( + receivedTokens: readonly T[], + expectedTokens: readonly T[], + ) { for (let i = 0; i < expectedTokens.length; i++) { const expectedToken = expectedTokens[i]; const receivedtoken = receivedTokens[i]; diff --git a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts index fd0484ceb5e7..1f327ac3ec99 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts @@ -97,7 +97,7 @@ export class InstructionsAttachmentWidget extends Disposable { this.renderDisposables.clear(); this.domNode.classList.remove('warning', 'error', 'disabled'); - const { enabled, resolveIssue: errorCondition } = this.model; + const { enabled, topError } = this.model; if (!enabled) { this.domNode.classList.add('disabled'); } @@ -120,11 +120,15 @@ export class InstructionsAttachmentWidget extends Disposable { // if there are some errors/warning during the process of resolving // attachment references (including all the nested child references), // add the issue details in the hover title for the attachment - if (errorCondition) { - const { type, message: details } = errorCondition; - this.domNode.classList.add(type); + if (topError) { + const { isRootError, message: details } = topError; + const isWarning = !isRootError; - const errorCaption = type === 'warning' + this.domNode.classList.add( + (isWarning) ? 'error' : 'warning', + ); + + const errorCaption = (isWarning) ? localize('warning', "Warning") : localize('error', "Error"); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 37c4dbb7403a..965a2fe353da 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -81,6 +81,7 @@ import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesCon import { ChatQuotasService, ChatQuotasStatusBarEntry, IChatQuotasService } from './chatQuotasService.js'; import { BuiltinToolsContribution } from './tools/tools.js'; import { ChatSetupContribution } from './chatSetup.js'; +import '../common/promptSyntax/languageFeatures/promptLinkProvider.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts index e531ad4ebeed..6627b52ec423 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts @@ -3,43 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from '../../../../../nls.js'; import { URI } from '../../../../../base/common/uri.js'; import { Emitter } from '../../../../../base/common/event.js'; -import { basename } from '../../../../../base/common/resources.js'; -import { assertDefined } from '../../../../../base/common/types.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { FilePromptParser } from '../../common/promptSyntax/parsers/filePromptParser.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { FailedToResolveContentsStream, FileOpenFailed, NonPromptSnippetFile, ParseError, RecursiveReference } from '../../common/promptFileReferenceErrors.js'; - -/** - * Well-known localized error messages. - */ -const errorMessages = { - recursion: localize('chatPromptInstructionsRecursiveReference', 'Recursive reference found'), - fileOpenFailed: localize('chatPromptInstructionsFileOpenFailed', 'Failed to open file'), - brokenChild: localize('chatPromptInstructionsBrokenReference', 'Contains a broken reference that will be ignored'), -}; - -/** - * Object that represents an error that may occur during - * the process of resolving prompt instructions reference. - */ -interface IIssue { - /** - * Type of the failure. Currently all errors that occur on - * the "main" root reference directly attached to the chat - * are considered to be `error`s, while all failures on nested - * child references are considered to be `warning`s. - */ - type: 'error' | 'warning'; - - /** - * Error or warning message. - */ - message: string; -} /** * Model for a single chat prompt instructions attachment. @@ -62,15 +30,12 @@ export class ChatInstructionsAttachmentModel extends Disposable { * child references it may contain. */ public get references(): readonly URI[] { - const { reference, enabled, resolveIssue } = this; + const { reference, enabled } = this; + const { errorCondition } = this.reference; // return no references if the attachment is disabled - if (!enabled) { - return []; - } - - // if the model has an error, return no references - if (resolveIssue && !(resolveIssue instanceof NonPromptSnippetFile)) { + // or if this object itself has an error + if (!enabled || errorCondition) { return []; } @@ -82,120 +47,12 @@ export class ChatInstructionsAttachmentModel extends Disposable { ]; } - /** - * If the prompt instructions reference (or any of its child references) has - * failed to resolve, this field contains the failure details, otherwise `undefined`. - * - * See {@linkcode IIssue}. + * Get the top-level error of the prompt instructions + * reference, if any. */ - public get resolveIssue(): IIssue | undefined { - const { errorCondition } = this._reference; - - const errorConditions = this.collectErrorConditions(); - if (errorConditions.length === 0) { - return undefined; - } - - const [firstError, ...restErrors] = errorConditions; - - // if the first error is the error of the root reference, - // then return it as an `error` otherwise use `warning` - const isRootError = (firstError === errorCondition); - const type = (isRootError) - ? 'error' - : 'warning'; - - const moreSuffix = restErrors.length > 0 - ? `\n-\n +${restErrors.length} more error${restErrors.length > 1 ? 's' : ''}` - : ''; - - const errorMessage = this.getErrorMessage(firstError, isRootError); - return { - type, - message: `${errorMessage}${moreSuffix}`, - }; - } - - /** - * Get message for the provided error condition object. - * - * @param error Error object. - * @param isRootError If the error happened on the the "main" root reference. - * @returns Error message. - */ - private getErrorMessage( - error: ParseError, - isRootError: boolean, - ): string { - // if a child error - the error is somewhere in the nested references tree, - // then use message prefix to highlight that this is not a root error - const prefix = (!isRootError) - ? `${errorMessages.brokenChild}: ` - : ''; - - // if failed to open a file, return approprivate message and the file path - if (error instanceof FileOpenFailed || error instanceof FailedToResolveContentsStream) { - return `${prefix}${errorMessages.fileOpenFailed} '${error.uri.path}'.`; - } - - // if a recursion, provide the entire recursion path so users can use - // it for the debugging purposes - if (error instanceof RecursiveReference) { - const { recursivePath } = error; - - const recursivePathString = recursivePath - .map((path) => { - return basename(URI.file(path)); - }) - .join(' -> '); - - return `${prefix}${errorMessages.recursion}:\n${recursivePathString}`; - } - - return `${prefix}${error.message}`; - } - - /** - * Collect all failures that may have occurred during the process of resolving - * references in the entire references tree, including the current root reference. - * - * @returns List of errors in the references tree. - */ - private collectErrorConditions(): ParseError[] { - const result: ParseError[] = []; - - // add error conditions of this object - if (this._reference.errorCondition) { - result.push(this._reference.errorCondition); - } - - // collect error conditions of all child references - const childErrorConditions = this.reference - // get entire reference tree - .allReferences - // filter out children without error conditions or - // the ones that are non-prompt snippet files - .filter((childReference) => { - const { errorCondition } = childReference; - - return errorCondition && !(errorCondition instanceof NonPromptSnippetFile); - }) - // map to error condition objects - .map((childReference): ParseError => { - const { errorCondition } = childReference; - - // `must` always be `true` because of the `filter` call above - assertDefined( - errorCondition, - `Error condition must be present for '${childReference.uri.path}'.`, - ); - - return errorCondition; - }); - result.push(...childErrorConditions); - - return result; + public get topError() { + return this.reference.topError; } /** diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 1be9de5c8681..d315ab07737c 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -960,7 +960,7 @@ have to be updated for changes to the rules above, or to support more deeply nes opacity: 0.75; } /* - * This overly specific CSS selector is needed to beat priority of some + * This overly-specific CSS selector is needed to beat priority of some * styles applied on the the `.chat-attached-context-attachment` element. */ .chat-attached-context .chat-prompt-instructions-attachments .chat-prompt-instructions-attachment.error.implicit, diff --git a/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts b/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts index 0b56fcef0e87..1538f9e23164 100644 --- a/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts +++ b/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts @@ -49,10 +49,9 @@ export class FailedToResolveContentsStream extends ParseError { constructor( public readonly uri: URI, public readonly originalError: unknown, + message: string = `Failed to resolve prompt contents stream for '${uri.toString()}': ${originalError}.`, ) { - super( - `Failed to resolve prompt contents stream for '${uri.toString()}': ${originalError}.`, - ); + super(message); } } @@ -75,15 +74,16 @@ export abstract class ResolveError extends ParseError { /** * Error that reflects the case when attempt to open target file fails. */ -export class FileOpenFailed extends ResolveError { +export class FileOpenFailed extends FailedToResolveContentsStream { public override errorType = 'FileOpenError'; constructor( uri: URI, - public readonly originalError: unknown, + originalError: unknown, ) { super( uri, + originalError, `Failed to open file '${uri.toString()}': ${originalError}.`, ); } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/fileReference.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/fileReference.ts index 224e0086851c..5efc84d9a6cc 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/fileReference.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/fileReference.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { assert } from '../../../../../../../base/common/assert.js'; -import { Range } from '../../../../../../../editor/common/core/range.js'; +import { IRange, Range } from '../../../../../../../editor/common/core/range.js'; import { BaseToken } from '../../../../../../../editor/common/codecs/baseToken.js'; import { Word } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/word.js'; @@ -93,6 +93,24 @@ export class FileReference extends BaseToken { return this.text === other.text; } + /** + * Get the range of the `link part` of the token (e.g., + * the `/path/to/file.md` part of `#file:/path/to/file.md`). + */ + public get linkRange(): IRange | undefined { + if (this.path.length === 0) { + return undefined; + } + + const { range } = this; + return new Range( + range.startLineNumber, + range.startColumn + TOKEN_START.length, + range.endLineNumber, + range.endColumn, + ); + } + /** * Return a string representation of the token. */ diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts index 48e5252c05ee..9bda1b1acf33 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts @@ -5,6 +5,7 @@ import { IPromptContentsProvider } from './types.js'; import { URI } from '../../../../../../base/common/uri.js'; +import { assert } from '../../../../../../base/common/assert.js'; import { assertDefined } from '../../../../../../base/common/types.js'; import { CancellationError } from '../../../../../../base/common/errors.js'; import { PromptContentsProviderBase } from './promptContentsProviderBase.js'; @@ -55,9 +56,10 @@ export class FilePromptContentProvider extends PromptContentsProviderBase { - if (cancellationToken?.isCancellationRequested) { - throw new CancellationError(); - } + assert( + !cancellationToken?.isCancellationRequested, + new CancellationError(), + ); // get the binary stream of the file contents let fileStream; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts index bc1c47b8bd7b..c6983cfb71b8 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts @@ -10,7 +10,7 @@ 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 { TrackedDisposable } from '../../../../../../base/common/trackedDisposable.js'; +import { ObservableDisposable } from '../../../../../../base/common/observableDisposable.js'; import { FailedToResolveContentsStream, ParseError } from '../../promptFileReferenceErrors.js'; import { cancelPreviousCalls } from '../../../../../../base/common/decorators/cancelPreviousCalls.js'; @@ -35,7 +35,7 @@ export const PROMP_SNIPPET_FILE_EXTENSION: string = '.prompt.md'; */ export abstract class PromptContentsProviderBase< TChangeEvent extends NonNullable, -> extends TrackedDisposable implements IPromptContentsProvider { +> extends ObservableDisposable implements IPromptContentsProvider { /** * Internal event emitter for the prompt contents change event. Classes that extend * this abstract class are responsible to use this emitter to fire the contents change diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts index b76f374819d3..b980da871fb5 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts @@ -49,29 +49,44 @@ export class TextModelContentsProvider extends PromptContentsProviderBase { + // if we have written all lines or lines count is zero, + // end the stream and stop the interval timer + if (i >= linesCount) { + clearInterval(interval); + stream.end(); + stream.destroy(); + } + + // if model was disposed or cancellation was requested, + // end the stream with an error and stop the interval timer if (this.model.isDisposed() || cancellationToken?.isCancellationRequested) { clearInterval(interval); stream.error(new CancellationError()); - stream.end(); + stream.destroy(); return; } - // write the current line to the stream - stream.write( - VSBuffer.fromString(this.model.getLineContent(i)), - ); + try { + // write the current line to the stream + stream.write( + VSBuffer.fromString(this.model.getLineContent(i)), + ); + + // for all lines exept the last one, write the EOL character + // to separate the lines in the stream + if (i !== linesCount) { + stream.write( + VSBuffer.fromString(this.model.getEOL()), + ); + } + } catch (error) { + console.log(this.uri, i, error); + } // use the next line in the next iteration i++; - - // if we have written all lines, end the stream and stop - // the interval timer - if (i >= linesCount) { - clearInterval(interval); - stream.end(); - } }, 1); return stream; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts new file mode 100644 index 000000000000..51849561e95a --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assert } from '../../../../../../base/common/assert.js'; +import { 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 { ObjectCache } from '../../../../../../base/common/objectCache.js'; +import { CancellationError } from '../../../../../../base/common/errors.js'; +import { TextModelPromptParser } from '../parsers/textModelPromptParser.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 { PROMP_SNIPPET_FILE_EXTENSION } from '../contentProviders/promptContentsProviderBase.js'; +import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../../common/contributions.js'; + +/** + * Prompt files language selector. + */ +const languageSelector = { + pattern: `**/*${PROMP_SNIPPET_FILE_EXTENSION}`, +}; + +/** + * Provides link references for prompt files. + */ +export class PromptLinkProvider extends Disposable implements LinkProvider { + /** + * Cache of text model content prompt parsers. + */ + private readonly parserProvider: ObjectCache; + + constructor( + @IInstantiationService private readonly initService: IInstantiationService, + @ILanguageFeaturesService private readonly languageService: ILanguageFeaturesService, + ) { + super(); + + this.languageService.linkProvider.register(languageSelector, this); + this.parserProvider = this._register(new ObjectCache(this.createParser.bind(this))); + } + + /** + * Create new prompt parser instance for the provided text model. + * + * @param model - text model to create the parser for + * @param initService - the instantiation service + */ + private createParser( + model: ITextModel, + ): TextModelPromptParser & { disposed: false } { + const parser: TextModelPromptParser = this.initService.createInstance( + TextModelPromptParser, + model, + [], + ); + + parser.assertNotDisposed( + 'Created prompt parser must not be disposed.', + ); + + return parser; + } + + /** + * Provide list of links for the provided text model. + */ + public async provideLinks( + model: ITextModel, + token: CancellationToken, + ): Promise { + assert( + !token.isCancellationRequested, + new CancellationError(), + ); + + const parser = this.parserProvider.get(model); + assert( + !parser.disposed, + 'Prompt parser must not be disposed.', + ); + + // start the parser in case it was not started yet, + // and wait for it to settle to a final result + const { references } = await parser + .start() + .settled(); + + // validate that the cancellation was not yet requested + assert( + !token.isCancellationRequested, + new CancellationError(), + ); + + // filter out references that are not valid links + const links: ILink[] = references + .filter((reference) => { + const { errorCondition, linkRange } = reference; + if (!errorCondition && linkRange) { + return true; + } + + return errorCondition instanceof NonPromptSnippetFile; + }) + .map((reference) => { + const { linkRange } = reference; + + // must always be true because of the filter above + assertDefined( + linkRange, + 'Link range must be defined.', + ); + + + return { + range: linkRange, + url: reference.uri, + }; + }); + + return { + links, + }; + } +} + +// register the text model prompt decorators provider as a workbench contribution +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(PromptLinkProvider, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/types.d.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/types.d.ts new file mode 100644 index 000000000000..85e7f645b756 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/types.d.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IRange } from '../../../../../../editor/common/core/range.js'; +import { ModelDecorationOptions } from '../../../../../../editor/common/model/textModel.js'; + +/** + * Decoration object. + */ +export interface ITextModelDecoration { + /** + * Range of the decoration. + */ + range: IRange; + + /** + * Associated decoration options. + */ + options: ModelDecorationOptions; +} + +/** + * Decoration CSS class names. + */ +export enum DecorationClassNames { + /** + * CSS class name for `default` prompt syntax decoration. + */ + default = 'prompt-decoration', + + /** + * CSS class name for `file reference` prompt syntax decoration. + */ + fileReference = DecorationClassNames.default, +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index fb3badc49641..2e531924c2d3 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -3,31 +3,43 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IPromptFileReference } from './types.js'; +import { localize } from '../../../../../../nls.js'; import { URI } from '../../../../../../base/common/uri.js'; import { ChatPromptCodec } from '../codecs/chatPromptCodec.js'; import { Emitter } from '../../../../../../base/common/event.js'; +import { assert } from '../../../../../../base/common/assert.js'; +import { IPromptFileReference, IResolveError } from './types.js'; import { FileReference } from '../codecs/tokens/fileReference.js'; +import { ChatPromptDecoder } from '../codecs/chatPromptDecoder.js'; +import { IRange } from '../../../../../../editor/common/core/range.js'; +import { assertDefined } from '../../../../../../base/common/types.js'; import { IPromptContentsProvider } from '../contentProviders/types.js'; +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 { TrackedDisposable } from '../../../../../../base/common/trackedDisposable.js'; +import { ObservableDisposable } from '../../../../../../base/common/observableDisposable.js'; import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; +import { PROMP_SNIPPET_FILE_EXTENSION } from '../contentProviders/promptContentsProviderBase.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; -import { FileOpenFailed, NonPromptSnippetFile, RecursiveReference, ParseError } from '../../promptFileReferenceErrors.js'; +import { FileOpenFailed, NonPromptSnippetFile, RecursiveReference, ParseError, FailedToResolveContentsStream } from '../../promptFileReferenceErrors.js'; /** - * Error conditions that may happen during the file reference resolution. + * Well-known localized error messages. */ -export type TErrorCondition = FileOpenFailed | RecursiveReference | NonPromptSnippetFile; +const errorMessages = { + recursion: localize('chatPromptInstructionsRecursiveReference', 'Recursive reference found'), + fileOpenFailed: localize('chatPromptInstructionsFileOpenFailed', 'Failed to open file'), + streamOpenFailed: localize('chatPromptInstructionsStreamOpenFailed', 'Failed to open contents stream'), + brokenChild: localize('chatPromptInstructionsBrokenReference', 'Contains a broken reference that will be ignored'), +}; /** - * File extension for the prompt snippets. + * Error conditions that may happen during the file reference resolution. */ -export const PROMP_SNIPPET_FILE_EXTENSION: string = '.prompt.md'; +export type TErrorCondition = FileOpenFailed | RecursiveReference | NonPromptSnippetFile; /** * Configuration key for the prompt snippets feature. @@ -38,8 +50,7 @@ const PROMPT_SNIPPETS_CONFIG_KEY: string = 'chat.experimental.prompt-snippets'; * Base prompt parser class that provides a common interface for all * prompt parsers that are responsible for parsing chat prompt syntax. */ -export abstract class BasePromptParser extends TrackedDisposable { - +export abstract class BasePromptParser extends ObservableDisposable { /** * List of file references in the current branch of the file reference tree. */ @@ -70,23 +81,71 @@ export abstract class BasePromptParser extend return this._errorCondition; } - /** - * Whether file reference resolution was attempted at least once. - */ - private _resolveAttempted: boolean = false; - /** * Whether file references resolution failed. * Set to `undefined` if the `resolve` method hasn't been ever called yet. */ public get resolveFailed(): boolean | undefined { - if (!this._resolveAttempted) { + if (!this.firstParseResult.gotFirstResult) { return undefined; } return !!this._errorCondition; } + /** + * The promise is resolved when at least one parse result (a stream or + * an error) has been received from the prompt contents provider. + */ + private firstParseResult = new FirstParseResult(); + + /** + * Returned promise is resolved when the parser process is settled. + * The settled state means that the prompt parser stream exists and + * has ended, or an error condition has been set in case of failure. + * + * Furthermore, this function can be called multiple times and will + * block until the latest prompt contents parsing logic is settled + * (e.g., for every `onContentChanged` event of the prompt source). + */ + public async settled(): Promise { + assert( + this.started, + 'Cannot wait on the parser that did not start yet.', + ); + + await this.firstParseResult.promise; + + if (this.errorCondition) { + return this; + } + + assertDefined( + this.stream, + 'No stream reference found.', + ); + + await this.stream.settled; + + return this; + } + + /** + * Same as {@linkcode settled} but also waits for all possible + * nested child prompt references and their children to be settled. + */ + public async settledAll(): Promise { + await this.settled(); + + await Promise.allSettled( + this.references.map((reference) => { + return reference.settledAll(); + }), + ); + + return this; + } + constructor( private readonly promptContentsProvider: T, seenReferences: string[] = [], @@ -106,8 +165,8 @@ export abstract class BasePromptParser extend seenReferences.push(this.uri.path); this._errorCondition = new RecursiveReference(this.uri, seenReferences); - this._resolveAttempted = true; this._onUpdate.fire(); + this.firstParseResult.complete(); return this; } @@ -117,24 +176,22 @@ export abstract class BasePromptParser extend // even if the file doesn't exist, we would never end up in the recursion seenReferences.push(this.uri.path); - let currentStream: VSBufferReadableStream | undefined; this._register( this.promptContentsProvider.onContentChanged((streamOrError) => { - // destroy previously received stream - currentStream?.destroy(); - - if (!(streamOrError instanceof ParseError)) { - // save the current stream object so it can be destroyed when/if - // a new stream is received - currentStream = streamOrError; - } - // process the the received message this.onContentsChanged(streamOrError, seenReferences); + + // indicate that we've received at least one `onContentChanged` event + this.firstParseResult.complete(); }), ); } + /** + * The latest received stream of prompt tokens, if any. + */ + private stream: ChatPromptDecoder | undefined; + /** * Handler the event event that is triggered when prompt contents change. * @@ -149,11 +206,11 @@ export abstract class BasePromptParser extend streamOrError: VSBufferReadableStream | ParseError, seenReferences: string[], ): void { - // set the flag indicating that reference resolution was attempted - this._resolveAttempted = true; - - // prefix for all log messages produced by this callback - const logPrefix = `[prompt parser][${basename(this.uri)}]`; + // dispose and cleanup the previously received stream + // object or an error condition, if any received yet + this.stream?.dispose(); + delete this.stream; + delete this._errorCondition; // dispose all currently existing references this.disposeReferences(); @@ -166,24 +223,15 @@ export abstract class BasePromptParser extend return; } - // cleanup existing error condition (if any) - delete this._errorCondition; - // decode the byte stream to a stream of prompt tokens - const stream = ChatPromptCodec.decode(streamOrError); - - // on error or stream end, dispose the stream - stream.on('error', (error) => { - stream.dispose(); + this.stream = ChatPromptCodec.decode(streamOrError); - this.logService.warn( - `${logPrefix} received an error on the chat prompt decoder stream: ${error}`, - ); - }); - stream.on('end', stream.dispose.bind(stream)); + // on error or stream end, dispose the stream and fire the update event + this.stream.on('error', this.onStreamEnd.bind(this, this.stream)); + this.stream.on('end', this.onStreamEnd.bind(this, this.stream)); // when some tokens received, process and store the references - stream.on('data', (token) => { + this.stream.on('data', (token) => { if (token instanceof FileReference) { this.onReference(token, [...seenReferences]); } @@ -196,16 +244,16 @@ export abstract class BasePromptParser extend }); // calling `start` on a disposed stream throws, so we warn and return instead - if (stream.disposed) { + if (this.stream.disposed) { this.logService.warn( - `${logPrefix} cannot start stream that has been already disposed, aborting`, + `[prompt parser][${basename(this.uri)}] cannot start stream that has been already disposed, aborting`, ); return; } // start receiving data on the stream - stream.start(); + this.stream.start(); } /** @@ -228,6 +276,25 @@ export abstract class BasePromptParser extend return this; } + /** + * Handle the `stream` end event. + * + * @param stream The stream that has ended. + * @param error Optional error object if stream ended with an error. + */ + private onStreamEnd( + _stream: ChatPromptDecoder, + error?: Error, + ): this { + this.logService.warn( + `[prompt parser][${basename(this.uri)}]} received an error on the chat prompt decoder stream: ${error}`, + ); + + this._onUpdate.fire(); + + return this; + } + /** * Dispose all currently held references. */ @@ -239,17 +306,30 @@ export abstract class BasePromptParser extend this._references.length = 0; } + /** + * Private attribute to track if the {@linkcode start} + * method has been already called at least once. + */ + private started: boolean = false; + /** * Start the prompt parser. */ public start(): this { - // if already in error state, nothing to do + // if already started, nothing to do + if (this.started) { + return this; + } + this.started = true; + + + // if already in the error state that could be set + // in the constructor, then nothing to do if (this.errorCondition) { return this; } this.promptContentsProvider.start(); - return this; } @@ -296,7 +376,7 @@ export abstract class BasePromptParser extend /** * Get a list of all references of the prompt, including - * all possible nested references its children may contain. + * all possible nested references its children may have. */ public get allReferences(): readonly IPromptFileReference[] { const result: IPromptFileReference[] = []; @@ -331,6 +411,111 @@ export abstract class BasePromptParser extend .map(child => child.uri); } + /** + * List of all errors that occurred while resolving the current + * reference including all possible errors of nested children. + */ + public get allErrors(): ParseError[] { + const result: ParseError[] = []; + + // collect error conditions of all child references + const childErrorConditions = this + // get entire reference tree + .allReferences + // filter out children without error conditions or + // the ones that are non-prompt snippet files + .filter((childReference) => { + const { errorCondition } = childReference; + + return errorCondition && !(errorCondition instanceof NonPromptSnippetFile); + }) + // map to error condition objects + .map((childReference): ParseError => { + const { errorCondition } = childReference; + + // `must` always be `true` because of the `filter` call above + assertDefined( + errorCondition, + `Error condition must be present for '${childReference.uri.path}'.`, + ); + + return errorCondition; + }); + result.push(...childErrorConditions); + + return result; + } + + /** + * The top most error of the current reference or any of its + * possible child reference errors. + */ + public get topError(): IResolveError | undefined { + // get all errors, including error of this object + const errors = []; + if (this.errorCondition) { + errors.push(this.errorCondition); + } + errors.push(...this.allErrors); + + // if no errors, nothing to do + if (errors.length === 0) { + return undefined; + } + + + // if the first error is the error of the root reference, + // then return it as an `error` otherwise use `warning` + const [firstError, ...restErrors] = errors; + const isRootError = (firstError === this.errorCondition); + + // if a child error - the error is somewhere in the nested references tree, + // then use message prefix to highlight that this is not a root error + const prefix = (!isRootError) + ? `${errorMessages.brokenChild}: ` + : ''; + + const moreSuffix = restErrors.length > 0 + ? `\n-\n +${restErrors.length} more error${restErrors.length > 1 ? 's' : ''}` + : ''; + + const errorMessage = this.getErrorMessage(firstError); + return { + isRootError, + message: `${prefix}${errorMessage}${moreSuffix}`, + }; + } + + /** + * Get message for the provided error condition object. + * + * @param error Error object. + * @returns Error message. + */ + protected getErrorMessage(error: ParseError): string { + // if failed to resolve prompt contents stream, return + // the approprivate message and the prompt path + if (error instanceof FailedToResolveContentsStream) { + return `${errorMessages.streamOpenFailed} '${error.uri.path}'.`; + } + + // if a recursion, provide the entire recursion path so users + // can use it for the debugging purposes + if (error instanceof RecursiveReference) { + const { recursivePath } = error; + + const recursivePathString = recursivePath + .map((path) => { + return basename(URI.file(path)); + }) + .join(' -> '); + + return `${errorMessages.recursion}:\n${recursivePathString}`; + } + + return error.message; + } + /** * Check if the current reference points to a given resource. */ @@ -368,6 +553,7 @@ export abstract class BasePromptParser extend } this.disposeReferences(); + this.stream?.dispose(); this._onUpdate.fire(); super.dispose(); @@ -400,6 +586,23 @@ export class PromptFileReference extends BasePromptParser { + /** + * Private attribute to track if we have + * received at least one result. + */ + private _gotResult = false; + + /** + * Whether we've received at least one result. + */ + public get gotFirstResult(): boolean { + return this._gotResult; + } + + /** + * Get underlying promise reference. + */ + public get promise(): Promise { + return this.p; + } + + /** + * Complete the underlying promise. + */ + public override complete() { + this._gotResult = true; + return super.complete(void 0); + } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts index c00847b447b5..75139c716857 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts @@ -11,8 +11,8 @@ import { IConfigurationService } from '../../../../../../platform/configuration/ import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; /** - * Class capable of parsing prompt syntax out of a provided text - * model, including all the nested child file references it may have. + * Class capable of parsing prompt syntax out of a provided text model, + * including all the nested child file references it may have. */ export class TextModelPromptParser extends BasePromptParser { constructor( diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts index 37876a79e575..a2671e9e7aa5 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts @@ -6,6 +6,23 @@ import { URI } from '../../../../../../base/common/uri.js'; import { ParseError } from '../../promptFileReferenceErrors.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { IRange, Range } from '../../../../../../editor/common/core/range.js'; + +/** + * Interface for a resolve error. + */ +export interface IResolveError { + /** + * Localized error message. + */ + message: string; + + /** + * Whether this error is for the root reference + * object, or for one of its possible children. + */ + isRootError: boolean; +} /** * List of all available prompt reference types. @@ -20,11 +37,24 @@ export interface IPromptReference extends IDisposable { * Type of the prompt reference. */ readonly type: PromptReferenceTypes; + /** * URI component of the associated with this reference. */ readonly uri: URI; + /** + * The full range of the prompt reference in the source text, + * including the {@linkcode linkRange} and any additional + * parts the reference may contain (e.g., the `#file:` prefix). + */ + readonly range: Range; + + /** + * Range of the link part that the reference points to. + */ + readonly linkRange: IRange | undefined; + /** * Flag that indicates if resolving this reference failed. * The `undefined` means that no attempt to resolve the reference @@ -35,18 +65,30 @@ export interface IPromptReference extends IDisposable { readonly resolveFailed: boolean | undefined; /** - * If failed to resolve the reference this property contains an error - * object that describes the failure reason. + * If failed to resolve the reference this property contains + * an error object that describes the failure reason. * * See also {@linkcode resolveFailed}. */ readonly errorCondition: ParseError | undefined; + /** + * List of all errors that occurred while resolving the current + * reference including all possible errors of nested children. + */ + readonly allErrors: readonly ParseError[]; + + /** + * The top most error of the current reference or any of its + * possible child reference errors. + */ + readonly topError: IResolveError | undefined; + /** * All references that the current reference may have, * including the all possible nested child references. */ - allReferences: readonly IPromptFileReference[]; + allReferences: readonly IPromptReference[]; /** * All *valid* references that the current reference may have, @@ -56,7 +98,23 @@ export interface IPromptReference extends IDisposable { * without creating a circular reference loop or having any other * issues that would make the reference resolve logic to fail. */ - allValidReferences: readonly IPromptFileReference[]; + allValidReferences: readonly IPromptReference[]; + + /** + * Returns a promise that resolves when the reference contents + * are completely parsed and all existing tokens are returned. + */ + settled(): Promise; + + /** + * Returns a promise that resolves when the reference contents, + * and contents for all possible nested child references are + * completely parsed and entire tree of references is built. + * + * The same as {@linkcode settled} but for all prompts in + * the reference tree. + */ + settledAll(): Promise; } /** diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/fileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/fileReference.test.ts new file mode 100644 index 000000000000..4a48b3a850d4 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/fileReference.test.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { randomInt } from '../../../../../../../../base/common/numbers.js'; +import { Range } from '../../../../../../../../editor/common/core/range.js'; +import { assertDefined } from '../../../../../../../../base/common/types.js'; +import { FileReference } from '../../../../../common/promptSyntax/codecs/tokens/fileReference.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../../base/test/common/utils.js'; +import { BaseToken } from '../../../../../../../../editor/common/codecs/baseToken.js'; + +suite('FileReference', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('`linkRange`', () => { + const lineNumber = randomInt(100, 1); + const columnStartNumber = randomInt(100, 1); + const path = `/temp/test/file-${randomInt(Number.MAX_SAFE_INTEGER)}.txt`; + const columnEndNumber = columnStartNumber + path.length; + + const range = new Range( + lineNumber, + columnStartNumber, + lineNumber, + columnEndNumber, + ); + const fileReference = new FileReference(range, path); + const { linkRange } = fileReference; + + assertDefined( + linkRange, + 'The link range must be defined.', + ); + + const expectedLinkRange = new Range( + lineNumber, + columnStartNumber + '#file:'.length, + lineNumber, + columnStartNumber + path.length, + ); + assert( + expectedLinkRange.equalsRange(linkRange), + `Expected link range to be ${expectedLinkRange}, got ${linkRange}.`, + ); + }); + + test('`path`', () => { + const lineNumber = randomInt(100, 1); + const columnStartNumber = randomInt(100, 1); + const link = `/temp/test/file-${randomInt(Number.MAX_SAFE_INTEGER)}.txt`; + const columnEndNumber = columnStartNumber + link.length; + + const range = new Range( + lineNumber, + columnStartNumber, + lineNumber, + columnEndNumber, + ); + const fileReference = new FileReference(range, link); + + assert.strictEqual( + fileReference.path, + link, + 'Must return the correct link path.', + ); + }); + + test('extends `BaseToken`', () => { + const lineNumber = randomInt(100, 1); + const columnStartNumber = randomInt(100, 1); + const link = `/temp/test/file-${randomInt(Number.MAX_SAFE_INTEGER)}.txt`; + const columnEndNumber = columnStartNumber + link.length; + + const range = new Range( + lineNumber, + columnStartNumber, + lineNumber, + columnEndNumber, + ); + const fileReference = new FileReference(range, link); + + assert( + fileReference instanceof BaseToken, + 'Must extend `BaseToken`.', + ); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/markdownLink.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/markdownLink.test.ts new file mode 100644 index 000000000000..bfb7e3672d7d --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/markdownLink.test.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { randomInt } from '../../../../../../../../base/common/numbers.js'; +import { Range } from '../../../../../../../../editor/common/core/range.js'; +import { assertDefined } from '../../../../../../../../base/common/types.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../../base/test/common/utils.js'; +import { MarkdownLink } from '../../../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; +import { BaseToken } from '../../../../../../../../editor/common/codecs/baseToken.js'; +import { MarkdownToken } from '../../../../../../../../editor/common/codecs/markdownCodec/tokens/markdownToken.js'; + +suite('FileReference', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('`linkRange`', () => { + const lineNumber = randomInt(100, 1); + const columnStartNumber = randomInt(100, 1); + const caption = `[link-caption-${randomInt(Number.MAX_SAFE_INTEGER)}]`; + const link = `(/temp/test/file-${randomInt(Number.MAX_SAFE_INTEGER)}.md)`; + + const markdownLink = new MarkdownLink( + lineNumber, + columnStartNumber, + caption, + link, + ); + const { linkRange } = markdownLink; + + assertDefined( + linkRange, + 'The link range must be defined.', + ); + + const expectedLinkRange = new Range( + lineNumber, + // `+1` for the openning `(` character of the link + columnStartNumber + caption.length + 1, + lineNumber, + // `+1` for the openning `(` character of the link, and + // `-2` for the enclosing `()` part of the link + columnStartNumber + caption.length + 1 + link.length - 2, + ); + assert( + expectedLinkRange.equalsRange(linkRange), + `Expected link range to be ${expectedLinkRange}, got ${linkRange}.`, + ); + }); + + test('`path`', () => { + const lineNumber = randomInt(100, 1); + const columnStartNumber = randomInt(100, 1); + const caption = `[link-caption-${randomInt(Number.MAX_SAFE_INTEGER)}]`; + const rawLink = `/temp/test/file-${randomInt(Number.MAX_SAFE_INTEGER)}.md`; + const link = `(${rawLink})`; + + const markdownLink = new MarkdownLink( + lineNumber, + columnStartNumber, + caption, + link, + ); + const { path } = markdownLink; + + assert.strictEqual( + path, + rawLink, + 'Must return the correct link value.', + ); + }); + + test('extends `MarkdownToken`', () => { + const lineNumber = randomInt(100, 1); + const columnStartNumber = randomInt(100, 1); + const caption = `[link-caption-${randomInt(Number.MAX_SAFE_INTEGER)}]`; + const rawLink = `/temp/test/file-${randomInt(Number.MAX_SAFE_INTEGER)}.md`; + const link = `(${rawLink})`; + + const markdownLink = new MarkdownLink( + lineNumber, + columnStartNumber, + caption, + link, + ); + + assert( + markdownLink instanceof MarkdownToken, + 'Must extend `MarkdownToken`.', + ); + + assert( + markdownLink instanceof BaseToken, + 'Must extend `BaseToken`.', + ); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index 589fb8dc40e1..fe10fd243589 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -19,7 +19,7 @@ import { ILogService, NullLogService } from '../../../../../../platform/log/comm import { TErrorCondition } from '../../../common/promptSyntax/parsers/basePromptParser.js'; import { FileReference } from '../../../common/promptSyntax/codecs/tokens/fileReference.js'; import { FilePromptParser } from '../../../common/promptSyntax/parsers/filePromptParser.js'; -import { wait, waitRandom, randomBoolean } from '../../../../../../base/test/common/testUtils.js'; +import { waitRandom, randomBoolean } from '../../../../../../base/test/common/testUtils.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; @@ -118,9 +118,8 @@ class TestPromptFileReference extends Disposable { ), ).start(); - // nested child references are resolved asynchronously in - // the background and the process can take some time to complete - await wait(50); + // wait until entire prompts tree is resolved + await rootReference.settledAll(); // resolve the root file reference including all nested references const resolvedReferences: readonly (IPromptFileReference | undefined)[] = rootReference.allReferences; From e01c544575010702b1fa758428bafbf4d37c484f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:28:23 -0800 Subject: [PATCH 0776/3587] Move rasterizer ownership up, share between strats --- .../gpu/renderStrategy/baseRenderStrategy.ts | 9 +---- .../renderStrategy/fullFileRenderStrategy.ts | 20 ++-------- .../renderStrategy/viewportRenderStrategy.ts | 19 ++-------- .../viewParts/viewLinesGpu/viewLinesGpu.ts | 38 +++++++++++++++++-- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts index a5a925977ea6..b5d9fc895ccb 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MandatoryMutableDisposable } from '../../../../base/common/lifecycle.js'; -import { EditorOption } from '../../../common/config/editorOptions.js'; import { ViewEventHandler } from '../../../common/viewEventHandler.js'; import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; import type { ViewContext } from '../../../common/viewModel/viewContext.js'; @@ -15,7 +13,6 @@ import type { ViewGpuContext } from '../viewGpuContext.js'; export abstract class BaseRenderStrategy extends ViewEventHandler implements IGpuRenderStrategy { - protected readonly _glyphRasterizer: MandatoryMutableDisposable; get glyphRasterizer() { return this._glyphRasterizer.value; } abstract type: string; @@ -26,15 +23,11 @@ export abstract class BaseRenderStrategy extends ViewEventHandler implements IGp protected readonly _context: ViewContext, protected readonly _viewGpuContext: ViewGpuContext, protected readonly _device: GPUDevice, + protected readonly _glyphRasterizer: { value: GlyphRasterizer }, ) { super(); this._context.addEventHandler(this); - - const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); - const fontSize = this._context.configuration.options.get(EditorOption.fontSize); - - this._glyphRasterizer = this._register(new MandatoryMutableDisposable(new GlyphRasterizer(fontSize, fontFamily, this._viewGpuContext.devicePixelRatio.get()))); } abstract reset(): void; diff --git a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index 1dae90695566..dfe7244b64b6 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -6,7 +6,6 @@ import { getActiveWindow } from '../../../../base/browser/dom.js'; import { Color } from '../../../../base/common/color.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; -import { EditorOption } from '../../../common/config/editorOptions.js'; import { CursorColumns } from '../../../common/core/cursorColumns.js'; import type { IViewLineTokens } from '../../../common/tokens/lineTokens.js'; import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLineMappingChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../../common/viewEvents.js'; @@ -96,8 +95,9 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { context: ViewContext, viewGpuContext: ViewGpuContext, device: GPUDevice, + glyphRasterizer: { value: GlyphRasterizer }, ) { - super(context, viewGpuContext, device); + super(context, viewGpuContext, device, glyphRasterizer); const bufferSize = FullFileRenderStrategy.maxSupportedLines * FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { @@ -132,18 +132,6 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { public override onConfigurationChanged(e: ViewConfigurationChangedEvent): boolean { this._invalidateAllLines(); this._queueBufferUpdate(e); - - const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); - const fontSize = this._context.configuration.options.get(EditorOption.fontSize); - const devicePixelRatio = this._viewGpuContext.devicePixelRatio.get(); - if ( - this._glyphRasterizer.value.fontFamily !== fontFamily || - this._glyphRasterizer.value.fontSize !== fontSize || - this._glyphRasterizer.value.devicePixelRatio !== devicePixelRatio - ) { - this._glyphRasterizer.value = new GlyphRasterizer(fontSize, fontFamily, devicePixelRatio); - } - return true; } @@ -380,7 +368,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { chars = segment; if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) { - charWidth = this._glyphRasterizer.value.getTextMetrics(chars).width; + charWidth = this.glyphRasterizer.getTextMetrics(chars).width; } decorationStyleSetColor = undefined; @@ -455,7 +443,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { } const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity); - glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer.value, chars, tokenMetadata, decorationStyleSetId); + glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId); absoluteOffsetY = Math.round( // Top of layout box (includes line height) diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts index 640651b54db8..596510cc8d57 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -6,7 +6,6 @@ import { getActiveWindow } from '../../../../base/browser/dom.js'; import { Color } from '../../../../base/common/color.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; -import { EditorOption } from '../../../common/config/editorOptions.js'; import { CursorColumns } from '../../../common/core/cursorColumns.js'; import type { IViewLineTokens } from '../../../common/tokens/lineTokens.js'; import { type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLineMappingChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../../common/viewEvents.js'; @@ -80,8 +79,9 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { context: ViewContext, viewGpuContext: ViewGpuContext, device: GPUDevice, + glyphRasterizer: { value: GlyphRasterizer }, ) { - super(context, viewGpuContext, device); + super(context, viewGpuContext, device, glyphRasterizer); const bufferSize = viewportHeight * ViewportRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { @@ -114,17 +114,6 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { // cleared and uploaded to the GPU. public override onConfigurationChanged(e: ViewConfigurationChangedEvent): boolean { - const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); - const fontSize = this._context.configuration.options.get(EditorOption.fontSize); - const devicePixelRatio = this._viewGpuContext.devicePixelRatio.get(); - if ( - this._glyphRasterizer.value.fontFamily !== fontFamily || - this._glyphRasterizer.value.fontSize !== fontSize || - this._glyphRasterizer.value.devicePixelRatio !== devicePixelRatio - ) { - this._glyphRasterizer.value = new GlyphRasterizer(fontSize, fontFamily, devicePixelRatio); - } - return true; } @@ -263,7 +252,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { chars = segment; if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) { - charWidth = this._glyphRasterizer.value.getTextMetrics(chars).width; + charWidth = this.glyphRasterizer.getTextMetrics(chars).width; } decorationStyleSetColor = undefined; @@ -338,7 +327,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { } const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity); - glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer.value, chars, tokenMetadata, decorationStyleSetId); + glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId); absoluteOffsetY = Math.round( // Top of layout box (includes line height) diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index d92240e40258..3ae3ff00a2ce 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -29,6 +29,7 @@ import { ViewportRenderStrategy } from '../../gpu/renderStrategy/viewportRenderS import { FullFileRenderStrategy } from '../../gpu/renderStrategy/fullFileRenderStrategy.js'; import { MutableDisposable } from '../../../../base/common/lifecycle.js'; import type { ViewLineRenderingData } from '../../../common/viewModel.js'; +import { GlyphRasterizer } from '../../gpu/raster/glyphRasterizer.js'; const enum GlyphStorageBufferInfo { FloatsPerEntry = 2 + 2 + 2, @@ -63,7 +64,8 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { private _initialized = false; - private readonly _renderStrategy: MutableDisposable = new MutableDisposable(); + private readonly _glyphRasterizer: MutableDisposable = this._register(new MutableDisposable()); + private readonly _renderStrategy: MutableDisposable = this._register(new MutableDisposable()); private _rebuildBindGroup?: () => void; constructor( @@ -192,7 +194,15 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { // #region Storage buffers - this._renderStrategy.value = this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._viewGpuContext, this._device); + const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); + const fontSize = this._context.configuration.options.get(EditorOption.fontSize); + this._glyphRasterizer.value = this._register(new GlyphRasterizer(fontSize, fontFamily, this._viewGpuContext.devicePixelRatio.get())); + this._register(runOnChange(this._viewGpuContext.devicePixelRatio, () => { + this._refreshGlyphRasterizer(); + })); + + + this._renderStrategy.value = this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._viewGpuContext, this._device, this._glyphRasterizer as { value: GlyphRasterizer }); // this._renderStrategy.value = this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device); this._glyphStorageBuffer = this._register(GPULifecycle.createBuffer(this._device, { @@ -321,7 +331,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { return; } this._logService.trace(`File is larger than ${FullFileRenderStrategy.maxSupportedLines} lines or ${FullFileRenderStrategy.maxSupportedColumns} columns, switching to viewport render strategy`); - this._renderStrategy.value = this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device); + this._renderStrategy.value = this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device, this._glyphRasterizer as { value: GlyphRasterizer }); this._rebuildBindGroup?.(); } @@ -410,7 +420,10 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { // from that side. Luckily rendering is cheap, it's only when uploaded data changes does it // start to cost. - override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { return true; } + override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { + this._refreshGlyphRasterizer(); + return true; + } override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { return true; } override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { return true; } override onFlushed(e: viewEvents.ViewFlushedEvent): boolean { return true; } @@ -426,6 +439,23 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { // #endregion + private _refreshGlyphRasterizer() { + const glyphRasterizer = this._glyphRasterizer.value; + if (!glyphRasterizer) { + return; + } + const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); + const fontSize = this._context.configuration.options.get(EditorOption.fontSize); + const devicePixelRatio = this._viewGpuContext.devicePixelRatio.get(); + if ( + glyphRasterizer.fontFamily !== fontFamily || + glyphRasterizer.fontSize !== fontSize || + glyphRasterizer.devicePixelRatio !== devicePixelRatio + ) { + this._glyphRasterizer.value = new GlyphRasterizer(fontSize, fontFamily, devicePixelRatio); + } + } + public renderText(viewportData: ViewportData): void { if (this._initialized) { this._refreshRenderStrategy(viewportData); From e76c50f20e3201af2aa98bfcd4496e885204dfd2 Mon Sep 17 00:00:00 2001 From: Aman Karmani Date: Wed, 22 Jan 2025 11:12:39 -0800 Subject: [PATCH 0777/3587] build: switch `build/tsconfig.json` to `module: nodenext` (#238426) * build/tsconfig: switch to module: nodenext for bun compat * build: rewrite imports for nodenext compat * build: re-generate --- .../common/computeBuiltInDepsCacheKey.js | 13 +- .../common/computeBuiltInDepsCacheKey.ts | 6 +- .../common/computeNodeModulesCacheKey.js | 29 +-- .../common/computeNodeModulesCacheKey.ts | 6 +- .../azure-pipelines/common/listNodeModules.js | 15 +- .../azure-pipelines/common/listNodeModules.ts | 4 +- build/azure-pipelines/common/publish.js | 124 ++++++---- build/azure-pipelines/common/publish.ts | 12 +- build/azure-pipelines/common/sign-win32.js | 9 +- build/azure-pipelines/common/sign-win32.ts | 2 +- build/azure-pipelines/common/sign.js | 39 ++-- build/azure-pipelines/common/sign.ts | 10 +- build/azure-pipelines/distro/mixin-npm.js | 17 +- build/azure-pipelines/distro/mixin-npm.ts | 4 +- build/azure-pipelines/distro/mixin-quality.js | 21 +- build/azure-pipelines/distro/mixin-quality.ts | 4 +- .../publish-types/check-version.js | 7 +- .../publish-types/check-version.ts | 2 +- .../publish-types/update-types.js | 19 +- .../publish-types/update-types.ts | 6 +- build/azure-pipelines/upload-cdn.js | 60 ++++- build/azure-pipelines/upload-cdn.ts | 6 +- build/azure-pipelines/upload-nlsmetadata.js | 50 +++- build/azure-pipelines/upload-nlsmetadata.ts | 4 +- build/azure-pipelines/upload-sourcemaps.js | 66 ++++-- build/azure-pipelines/upload-sourcemaps.ts | 8 +- build/darwin/create-universal-app.js | 29 +-- build/darwin/create-universal-app.ts | 6 +- build/darwin/sign.js | 57 ++--- build/darwin/sign.ts | 6 +- build/darwin/verify-macho.js | 46 +++- build/darwin/verify-macho.ts | 2 +- build/lib/asar.js | 31 +-- build/lib/asar.ts | 8 +- build/lib/builtInExtensions.js | 126 ++++++---- build/lib/builtInExtensions.ts | 18 +- build/lib/builtInExtensionsCG.js | 35 +-- build/lib/builtInExtensionsCG.ts | 8 +- build/lib/bundle.js | 17 +- build/lib/bundle.ts | 6 +- build/lib/compilation.js | 134 +++++++---- build/lib/compilation.ts | 16 +- build/lib/date.js | 19 +- build/lib/date.ts | 4 +- build/lib/dependencies.js | 21 +- build/lib/dependencies.ts | 6 +- build/lib/electron.js | 66 ++++-- build/lib/electron.ts | 8 +- build/lib/extensions.js | 220 ++++++++++-------- build/lib/extensions.ts | 26 +-- build/lib/fetch.js | 39 ++-- build/lib/fetch.ts | 12 +- build/lib/formatter.js | 21 +- build/lib/formatter.ts | 6 +- build/lib/getVersion.js | 35 ++- build/lib/git.js | 21 +- build/lib/git.ts | 4 +- build/lib/i18n.js | 97 ++++---- build/lib/i18n.ts | 18 +- build/lib/inlineMeta.js | 7 +- build/lib/inlineMeta.ts | 4 +- build/lib/layersChecker.js | 19 +- build/lib/layersChecker.ts | 2 +- build/lib/mangle/index.js | 101 ++++---- build/lib/mangle/index.ts | 10 +- build/lib/mangle/renameWorker.js | 11 +- build/lib/mangle/renameWorker.ts | 4 +- build/lib/mangle/staticLanguageServiceHost.js | 31 +-- build/lib/mangle/staticLanguageServiceHost.ts | 4 +- build/lib/monaco-api.js | 33 +-- build/lib/monaco-api.ts | 8 +- build/lib/nls.js | 72 ++++-- build/lib/nls.ts | 6 +- build/lib/node.js | 15 +- build/lib/node.ts | 4 +- build/lib/optimize.js | 72 ++++-- build/lib/optimize.ts | 8 +- build/lib/policies.js | 27 ++- build/lib/policies.ts | 6 +- build/lib/postcss.js | 11 +- build/lib/postcss.ts | 6 +- build/lib/preLaunch.js | 9 +- build/lib/preLaunch.ts | 2 +- build/lib/reporter.js | 27 ++- build/lib/reporter.ts | 10 +- build/lib/snapshotLoader.js | 4 +- build/lib/snapshotLoader.ts | 2 +- build/lib/standalone.js | 110 ++++++--- build/lib/standalone.ts | 4 +- build/lib/stats.js | 27 ++- build/lib/stats.ts | 8 +- build/lib/stylelint/validateVariableNames.js | 7 +- build/lib/stylelint/validateVariableNames.ts | 2 +- build/lib/task.js | 11 +- build/lib/task.ts | 4 +- build/lib/test/i18n.test.js | 62 +++-- build/lib/test/i18n.test.ts | 4 +- build/lib/treeshaking.js | 37 +-- build/lib/treeshaking.ts | 4 +- build/lib/tsb/builder.js | 98 +++++--- build/lib/tsb/builder.ts | 12 +- build/lib/tsb/index.js | 62 +++-- build/lib/tsb/index.ts | 8 +- build/lib/tsb/transpiler.js | 37 +-- build/lib/tsb/transpiler.ts | 8 +- build/lib/typings/event-stream.d.ts | 2 +- build/lib/util.js | 121 +++++----- build/lib/util.ts | 20 +- build/lib/watch/index.js | 1 + build/lib/watch/watch-win32.js | 39 ++-- build/lib/watch/watch-win32.ts | 12 +- build/linux/debian/calculate-deps.js | 11 +- build/linux/debian/calculate-deps.ts | 4 +- build/linux/debian/install-sysroot.js | 67 +++--- build/linux/debian/install-sysroot.ts | 8 +- build/linux/dependencies-generator.js | 15 +- build/linux/dependencies-generator.ts | 2 +- build/linux/libcxx-fetcher.js | 33 +-- build/linux/libcxx-fetcher.ts | 8 +- build/tsconfig.json | 2 +- build/win32/explorer-appx-fetcher.js | 25 +- build/win32/explorer-appx-fetcher.ts | 8 +- 122 files changed, 1907 insertions(+), 1202 deletions(-) diff --git a/build/azure-pipelines/common/computeBuiltInDepsCacheKey.js b/build/azure-pipelines/common/computeBuiltInDepsCacheKey.js index 2d747f56cc73..10fa9087454f 100644 --- a/build/azure-pipelines/common/computeBuiltInDepsCacheKey.js +++ b/build/azure-pipelines/common/computeBuiltInDepsCacheKey.js @@ -3,12 +3,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); -const crypto = require("crypto"); -const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../../product.json'), 'utf8')); -const shasum = crypto.createHash('sha256'); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const crypto_1 = __importDefault(require("crypto")); +const productjson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../../product.json'), 'utf8')); +const shasum = crypto_1.default.createHash('sha256'); for (const ext of productjson.builtInExtensions) { shasum.update(`${ext.name}@${ext.version}`); } diff --git a/build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts b/build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts index 53d6c501ea9a..8abaaccb6543 100644 --- a/build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts +++ b/build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as crypto from 'crypto'; +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../../product.json'), 'utf8')); const shasum = crypto.createHash('sha256'); diff --git a/build/azure-pipelines/common/computeNodeModulesCacheKey.js b/build/azure-pipelines/common/computeNodeModulesCacheKey.js index 976e096fad26..c09c13be9d42 100644 --- a/build/azure-pipelines/common/computeNodeModulesCacheKey.js +++ b/build/azure-pipelines/common/computeNodeModulesCacheKey.js @@ -3,21 +3,24 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); -const crypto = require("crypto"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const crypto_1 = __importDefault(require("crypto")); const { dirs } = require('../../npm/dirs'); -const ROOT = path.join(__dirname, '../../../'); -const shasum = crypto.createHash('sha256'); -shasum.update(fs.readFileSync(path.join(ROOT, 'build/.cachesalt'))); -shasum.update(fs.readFileSync(path.join(ROOT, '.npmrc'))); -shasum.update(fs.readFileSync(path.join(ROOT, 'build', '.npmrc'))); -shasum.update(fs.readFileSync(path.join(ROOT, 'remote', '.npmrc'))); +const ROOT = path_1.default.join(__dirname, '../../../'); +const shasum = crypto_1.default.createHash('sha256'); +shasum.update(fs_1.default.readFileSync(path_1.default.join(ROOT, 'build/.cachesalt'))); +shasum.update(fs_1.default.readFileSync(path_1.default.join(ROOT, '.npmrc'))); +shasum.update(fs_1.default.readFileSync(path_1.default.join(ROOT, 'build', '.npmrc'))); +shasum.update(fs_1.default.readFileSync(path_1.default.join(ROOT, 'remote', '.npmrc'))); // Add `package.json` and `package-lock.json` files for (const dir of dirs) { - const packageJsonPath = path.join(ROOT, dir, 'package.json'); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString()); + const packageJsonPath = path_1.default.join(ROOT, dir, 'package.json'); + const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()); const relevantPackageJsonSections = { dependencies: packageJson.dependencies, devDependencies: packageJson.devDependencies, @@ -26,8 +29,8 @@ for (const dir of dirs) { distro: packageJson.distro }; shasum.update(JSON.stringify(relevantPackageJsonSections)); - const packageLockPath = path.join(ROOT, dir, 'package-lock.json'); - shasum.update(fs.readFileSync(packageLockPath)); + const packageLockPath = path_1.default.join(ROOT, dir, 'package-lock.json'); + shasum.update(fs_1.default.readFileSync(packageLockPath)); } // Add any other command line arguments for (let i = 2; i < process.argv.length; i++) { diff --git a/build/azure-pipelines/common/computeNodeModulesCacheKey.ts b/build/azure-pipelines/common/computeNodeModulesCacheKey.ts index 0940c929b540..57b35dc78de5 100644 --- a/build/azure-pipelines/common/computeNodeModulesCacheKey.ts +++ b/build/azure-pipelines/common/computeNodeModulesCacheKey.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as crypto from 'crypto'; +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; const { dirs } = require('../../npm/dirs'); const ROOT = path.join(__dirname, '../../../'); diff --git a/build/azure-pipelines/common/listNodeModules.js b/build/azure-pipelines/common/listNodeModules.js index aaa44c51a12d..301b5f930b61 100644 --- a/build/azure-pipelines/common/listNodeModules.js +++ b/build/azure-pipelines/common/listNodeModules.js @@ -3,16 +3,19 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); if (process.argv.length !== 3) { console.error('Usage: node listNodeModules.js OUTPUT_FILE'); process.exit(-1); } -const ROOT = path.join(__dirname, '../../../'); +const ROOT = path_1.default.join(__dirname, '../../../'); function findNodeModulesFiles(location, inNodeModules, result) { - const entries = fs.readdirSync(path.join(ROOT, location)); + const entries = fs_1.default.readdirSync(path_1.default.join(ROOT, location)); for (const entry of entries) { const entryPath = `${location}/${entry}`; if (/(^\/out)|(^\/src$)|(^\/.git$)|(^\/.build$)/.test(entryPath)) { @@ -20,7 +23,7 @@ function findNodeModulesFiles(location, inNodeModules, result) { } let stat; try { - stat = fs.statSync(path.join(ROOT, entryPath)); + stat = fs_1.default.statSync(path_1.default.join(ROOT, entryPath)); } catch (err) { continue; @@ -37,5 +40,5 @@ function findNodeModulesFiles(location, inNodeModules, result) { } const result = []; findNodeModulesFiles('', false, result); -fs.writeFileSync(process.argv[2], result.join('\n') + '\n'); +fs_1.default.writeFileSync(process.argv[2], result.join('\n') + '\n'); //# sourceMappingURL=listNodeModules.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/listNodeModules.ts b/build/azure-pipelines/common/listNodeModules.ts index aca461f8b5f2..fb85b25cfd1b 100644 --- a/build/azure-pipelines/common/listNodeModules.ts +++ b/build/azure-pipelines/common/listNodeModules.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; +import fs from 'fs'; +import path from 'path'; if (process.argv.length !== 3) { console.error('Usage: node listNodeModules.js OUTPUT_FILE'); diff --git a/build/azure-pipelines/common/publish.js b/build/azure-pipelines/common/publish.js index bcebd076c284..48093086c348 100644 --- a/build/azure-pipelines/common/publish.js +++ b/build/azure-pipelines/common/publish.js @@ -3,21 +3,57 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); const stream_1 = require("stream"); const promises_1 = require("node:stream/promises"); -const yauzl = require("yauzl"); -const crypto = require("crypto"); +const yauzl_1 = __importDefault(require("yauzl")); +const crypto_1 = __importDefault(require("crypto")); const retry_1 = require("./retry"); const cosmos_1 = require("@azure/cosmos"); -const cp = require("child_process"); -const os = require("os"); +const child_process_1 = __importDefault(require("child_process")); +const os_1 = __importDefault(require("os")); const node_worker_threads_1 = require("node:worker_threads"); const msal_node_1 = require("@azure/msal-node"); const storage_blob_1 = require("@azure/storage-blob"); -const jws = require("jws"); +const jws = __importStar(require("jws")); const node_timers_1 = require("node:timers"); function e(name) { const result = process.env[name]; @@ -28,7 +64,7 @@ function e(name) { } function hashStream(hashName, stream) { return new Promise((c, e) => { - const shasum = crypto.createHash(hashName); + const shasum = crypto_1.default.createHash(hashName); stream .on('data', shasum.update.bind(shasum)) .on('error', e) @@ -50,38 +86,38 @@ function getCertificateBuffer(input) { } function getThumbprint(input, algorithm) { const buffer = getCertificateBuffer(input); - return crypto.createHash(algorithm).update(buffer).digest(); + return crypto_1.default.createHash(algorithm).update(buffer).digest(); } function getKeyFromPFX(pfx) { - const pfxCertificatePath = path.join(os.tmpdir(), 'cert.pfx'); - const pemKeyPath = path.join(os.tmpdir(), 'key.pem'); + const pfxCertificatePath = path_1.default.join(os_1.default.tmpdir(), 'cert.pfx'); + const pemKeyPath = path_1.default.join(os_1.default.tmpdir(), 'key.pem'); try { const pfxCertificate = Buffer.from(pfx, 'base64'); - fs.writeFileSync(pfxCertificatePath, pfxCertificate); - cp.execSync(`openssl pkcs12 -in "${pfxCertificatePath}" -nocerts -nodes -out "${pemKeyPath}" -passin pass:`); - const raw = fs.readFileSync(pemKeyPath, 'utf-8'); + fs_1.default.writeFileSync(pfxCertificatePath, pfxCertificate); + child_process_1.default.execSync(`openssl pkcs12 -in "${pfxCertificatePath}" -nocerts -nodes -out "${pemKeyPath}" -passin pass:`); + const raw = fs_1.default.readFileSync(pemKeyPath, 'utf-8'); const result = raw.match(/-----BEGIN PRIVATE KEY-----[\s\S]+?-----END PRIVATE KEY-----/g)[0]; return result; } finally { - fs.rmSync(pfxCertificatePath, { force: true }); - fs.rmSync(pemKeyPath, { force: true }); + fs_1.default.rmSync(pfxCertificatePath, { force: true }); + fs_1.default.rmSync(pemKeyPath, { force: true }); } } function getCertificatesFromPFX(pfx) { - const pfxCertificatePath = path.join(os.tmpdir(), 'cert.pfx'); - const pemCertificatePath = path.join(os.tmpdir(), 'cert.pem'); + const pfxCertificatePath = path_1.default.join(os_1.default.tmpdir(), 'cert.pfx'); + const pemCertificatePath = path_1.default.join(os_1.default.tmpdir(), 'cert.pem'); try { const pfxCertificate = Buffer.from(pfx, 'base64'); - fs.writeFileSync(pfxCertificatePath, pfxCertificate); - cp.execSync(`openssl pkcs12 -in "${pfxCertificatePath}" -nokeys -out "${pemCertificatePath}" -passin pass:`); - const raw = fs.readFileSync(pemCertificatePath, 'utf-8'); + fs_1.default.writeFileSync(pfxCertificatePath, pfxCertificate); + child_process_1.default.execSync(`openssl pkcs12 -in "${pfxCertificatePath}" -nokeys -out "${pemCertificatePath}" -passin pass:`); + const raw = fs_1.default.readFileSync(pemCertificatePath, 'utf-8'); const matches = raw.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g); return matches ? matches.reverse() : []; } finally { - fs.rmSync(pfxCertificatePath, { force: true }); - fs.rmSync(pemCertificatePath, { force: true }); + fs_1.default.rmSync(pfxCertificatePath, { force: true }); + fs_1.default.rmSync(pemCertificatePath, { force: true }); } } class ESRPReleaseService { @@ -122,7 +158,7 @@ class ESRPReleaseService { this.containerClient = containerClient; } async createRelease(version, filePath, friendlyFileName) { - const correlationId = crypto.randomUUID(); + const correlationId = crypto_1.default.randomUUID(); const blobClient = this.containerClient.getBlockBlobClient(correlationId); this.log(`Uploading ${filePath} to ${blobClient.url}`); await blobClient.uploadFile(filePath); @@ -161,8 +197,8 @@ class ESRPReleaseService { } } async submitRelease(version, filePath, friendlyFileName, correlationId, blobClient) { - const size = fs.statSync(filePath).size; - const hash = await hashStream('sha256', fs.createReadStream(filePath)); + const size = fs_1.default.statSync(filePath).size; + const hash = await hashStream('sha256', fs_1.default.createReadStream(filePath)); const message = { customerCorrelationId: correlationId, esrpCorrelationId: correlationId, @@ -192,7 +228,7 @@ class ESRPReleaseService { intent: 'filedownloadlinkgeneration' }, files: [{ - name: path.basename(filePath), + name: path_1.default.basename(filePath), friendlyFileName, tenantFileLocation: blobClient.url, tenantFileLocationType: 'AzureBlob', @@ -268,19 +304,19 @@ class State { set = new Set(); constructor() { const pipelineWorkspacePath = e('PIPELINE_WORKSPACE'); - const previousState = fs.readdirSync(pipelineWorkspacePath) + const previousState = fs_1.default.readdirSync(pipelineWorkspacePath) .map(name => /^artifacts_processed_(\d+)$/.exec(name)) .filter((match) => !!match) .map(match => ({ name: match[0], attempt: Number(match[1]) })) .sort((a, b) => b.attempt - a.attempt)[0]; if (previousState) { - const previousStatePath = path.join(pipelineWorkspacePath, previousState.name, previousState.name + '.txt'); - fs.readFileSync(previousStatePath, 'utf8').split(/\n/).filter(name => !!name).forEach(name => this.set.add(name)); + const previousStatePath = path_1.default.join(pipelineWorkspacePath, previousState.name, previousState.name + '.txt'); + fs_1.default.readFileSync(previousStatePath, 'utf8').split(/\n/).filter(name => !!name).forEach(name => this.set.add(name)); } const stageAttempt = e('SYSTEM_STAGEATTEMPT'); - this.statePath = path.join(pipelineWorkspacePath, `artifacts_processed_${stageAttempt}`, `artifacts_processed_${stageAttempt}.txt`); - fs.mkdirSync(path.dirname(this.statePath), { recursive: true }); - fs.writeFileSync(this.statePath, [...this.set.values()].map(name => `${name}\n`).join('')); + this.statePath = path_1.default.join(pipelineWorkspacePath, `artifacts_processed_${stageAttempt}`, `artifacts_processed_${stageAttempt}.txt`); + fs_1.default.mkdirSync(path_1.default.dirname(this.statePath), { recursive: true }); + fs_1.default.writeFileSync(this.statePath, [...this.set.values()].map(name => `${name}\n`).join('')); } get size() { return this.set.size; @@ -290,7 +326,7 @@ class State { } add(name) { this.set.add(name); - fs.appendFileSync(this.statePath, `${name}\n`); + fs_1.default.appendFileSync(this.statePath, `${name}\n`); } [Symbol.iterator]() { return this.set[Symbol.iterator](); @@ -336,7 +372,7 @@ async function downloadArtifact(artifact, downloadPath) { if (!res.ok) { throw new Error(`Unexpected status code: ${res.status}`); } - await (0, promises_1.pipeline)(stream_1.Readable.fromWeb(res.body), fs.createWriteStream(downloadPath)); + await (0, promises_1.pipeline)(stream_1.Readable.fromWeb(res.body), fs_1.default.createWriteStream(downloadPath)); } finally { clearTimeout(timeout); @@ -344,7 +380,7 @@ async function downloadArtifact(artifact, downloadPath) { } async function unzip(packagePath, outputPath) { return new Promise((resolve, reject) => { - yauzl.open(packagePath, { lazyEntries: true, autoClose: true }, (err, zipfile) => { + yauzl_1.default.open(packagePath, { lazyEntries: true, autoClose: true }, (err, zipfile) => { if (err) { return reject(err); } @@ -358,9 +394,9 @@ async function unzip(packagePath, outputPath) { if (err) { return reject(err); } - const filePath = path.join(outputPath, entry.fileName); - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - const ostream = fs.createWriteStream(filePath); + const filePath = path_1.default.join(outputPath, entry.fileName); + fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true }); + const ostream = fs_1.default.createWriteStream(filePath); ostream.on('finish', () => { result.push(filePath); zipfile.readEntry(); @@ -523,7 +559,7 @@ async function processArtifact(artifact, filePath) { const { cosmosDBAccessToken, blobServiceAccessToken } = JSON.parse(e('PUBLISH_AUTH_TOKENS')); const quality = e('VSCODE_QUALITY'); const version = e('BUILD_SOURCEVERSION'); - const friendlyFileName = `${quality}/${version}/${path.basename(filePath)}`; + const friendlyFileName = `${quality}/${version}/${path_1.default.basename(filePath)}`; const blobServiceClient = new storage_blob_1.BlobServiceClient(`https://${e('VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME')}.blob.core.windows.net/`, { getToken: async () => blobServiceAccessToken }); const leasesContainerClient = blobServiceClient.getContainerClient('leases'); await leasesContainerClient.createIfNotExists(); @@ -546,8 +582,8 @@ async function processArtifact(artifact, filePath) { const isLegacy = artifact.name.includes('_legacy'); const platform = getPlatform(product, os, arch, unprocessedType, isLegacy); const type = getRealType(unprocessedType); - const size = fs.statSync(filePath).size; - const stream = fs.createReadStream(filePath); + const size = fs_1.default.statSync(filePath).size; + const stream = fs_1.default.createReadStream(filePath); const [hash, sha256hash] = await Promise.all([hashStream('sha1', stream), hashStream('sha256', stream)]); // CodeQL [SM04514] Using SHA1 only for legacy reasons, we are actually only respecting SHA256 const asset = { platform, type, url, hash: hash.toString('hex'), sha256hash: sha256hash.toString('hex'), size, supportsFastUpdate: true }; log('Creating asset...'); @@ -627,12 +663,12 @@ async function main() { continue; } console.log(`[${artifact.name}] Found new artifact`); - const artifactZipPath = path.join(e('AGENT_TEMPDIRECTORY'), `${artifact.name}.zip`); + const artifactZipPath = path_1.default.join(e('AGENT_TEMPDIRECTORY'), `${artifact.name}.zip`); await (0, retry_1.retry)(async (attempt) => { const start = Date.now(); console.log(`[${artifact.name}] Downloading (attempt ${attempt})...`); await downloadArtifact(artifact, artifactZipPath); - const archiveSize = fs.statSync(artifactZipPath).size; + const archiveSize = fs_1.default.statSync(artifactZipPath).size; const downloadDurationS = (Date.now() - start) / 1000; const downloadSpeedKBS = Math.round((archiveSize / 1024) / downloadDurationS); console.log(`[${artifact.name}] Successfully downloaded after ${Math.floor(downloadDurationS)} seconds(${downloadSpeedKBS} KB/s).`); diff --git a/build/azure-pipelines/common/publish.ts b/build/azure-pipelines/common/publish.ts index b8b99c3855bf..79444bebf13c 100644 --- a/build/azure-pipelines/common/publish.ts +++ b/build/azure-pipelines/common/publish.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; +import fs from 'fs'; +import path from 'path'; import { Readable } from 'stream'; import type { ReadableStream } from 'stream/web'; import { pipeline } from 'node:stream/promises'; -import * as yauzl from 'yauzl'; -import * as crypto from 'crypto'; +import yauzl from 'yauzl'; +import crypto from 'crypto'; import { retry } from './retry'; import { CosmosClient } from '@azure/cosmos'; -import * as cp from 'child_process'; -import * as os from 'os'; +import cp from 'child_process'; +import os from 'os'; import { Worker, isMainThread, workerData } from 'node:worker_threads'; import { ConfidentialClientApplication } from '@azure/msal-node'; import { BlobClient, BlobServiceClient, BlockBlobClient, ContainerClient } from '@azure/storage-blob'; diff --git a/build/azure-pipelines/common/sign-win32.js b/build/azure-pipelines/common/sign-win32.js index aa197bb1198b..f4e3f27c1f25 100644 --- a/build/azure-pipelines/common/sign-win32.js +++ b/build/azure-pipelines/common/sign-win32.js @@ -3,13 +3,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); const sign_1 = require("./sign"); -const path = require("path"); +const path_1 = __importDefault(require("path")); (0, sign_1.main)([ process.env['EsrpCliDllPath'], 'sign-windows', - path.dirname(process.argv[2]), - path.basename(process.argv[2]) + path_1.default.dirname(process.argv[2]), + path_1.default.basename(process.argv[2]) ]); //# sourceMappingURL=sign-win32.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/sign-win32.ts b/build/azure-pipelines/common/sign-win32.ts index c2f3dbda1516..ad88435b5a38 100644 --- a/build/azure-pipelines/common/sign-win32.ts +++ b/build/azure-pipelines/common/sign-win32.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { main } from './sign'; -import * as path from 'path'; +import path from 'path'; main([ process.env['EsrpCliDllPath']!, diff --git a/build/azure-pipelines/common/sign.js b/build/azure-pipelines/common/sign.js index df25de293996..fd87772b3b87 100644 --- a/build/azure-pipelines/common/sign.js +++ b/build/azure-pipelines/common/sign.js @@ -3,25 +3,28 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.Temp = void 0; exports.main = main; -const cp = require("child_process"); -const fs = require("fs"); -const crypto = require("crypto"); -const path = require("path"); -const os = require("os"); +const child_process_1 = __importDefault(require("child_process")); +const fs_1 = __importDefault(require("fs")); +const crypto_1 = __importDefault(require("crypto")); +const path_1 = __importDefault(require("path")); +const os_1 = __importDefault(require("os")); class Temp { _files = []; tmpNameSync() { - const file = path.join(os.tmpdir(), crypto.randomBytes(20).toString('hex')); + const file = path_1.default.join(os_1.default.tmpdir(), crypto_1.default.randomBytes(20).toString('hex')); this._files.push(file); return file; } dispose() { for (const file of this._files) { try { - fs.unlinkSync(file); + fs_1.default.unlinkSync(file); } catch (err) { // noop @@ -126,20 +129,20 @@ function getParams(type) { function main([esrpCliPath, type, folderPath, pattern]) { const tmp = new Temp(); process.on('exit', () => tmp.dispose()); - const key = crypto.randomBytes(32); - const iv = crypto.randomBytes(16); - const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); + const key = crypto_1.default.randomBytes(32); + const iv = crypto_1.default.randomBytes(16); + const cipher = crypto_1.default.createCipheriv('aes-256-cbc', key, iv); const encryptedToken = cipher.update(process.env['SYSTEM_ACCESSTOKEN'].trim(), 'utf8', 'hex') + cipher.final('hex'); const encryptionDetailsPath = tmp.tmpNameSync(); - fs.writeFileSync(encryptionDetailsPath, JSON.stringify({ key: key.toString('hex'), iv: iv.toString('hex') })); + fs_1.default.writeFileSync(encryptionDetailsPath, JSON.stringify({ key: key.toString('hex'), iv: iv.toString('hex') })); const encryptedTokenPath = tmp.tmpNameSync(); - fs.writeFileSync(encryptedTokenPath, encryptedToken); + fs_1.default.writeFileSync(encryptedTokenPath, encryptedToken); const patternPath = tmp.tmpNameSync(); - fs.writeFileSync(patternPath, pattern); + fs_1.default.writeFileSync(patternPath, pattern); const paramsPath = tmp.tmpNameSync(); - fs.writeFileSync(paramsPath, JSON.stringify(getParams(type))); - const dotnetVersion = cp.execSync('dotnet --version', { encoding: 'utf8' }).trim(); - const adoTaskVersion = path.basename(path.dirname(path.dirname(esrpCliPath))); + fs_1.default.writeFileSync(paramsPath, JSON.stringify(getParams(type))); + const dotnetVersion = child_process_1.default.execSync('dotnet --version', { encoding: 'utf8' }).trim(); + const adoTaskVersion = path_1.default.basename(path_1.default.dirname(path_1.default.dirname(esrpCliPath))); const federatedTokenData = { jobId: process.env['SYSTEM_JOBID'], planId: process.env['SYSTEM_PLANID'], @@ -149,7 +152,7 @@ function main([esrpCliPath, type, folderPath, pattern]) { managedIdentityId: process.env['VSCODE_ESRP_CLIENT_ID'], managedIdentityTenantId: process.env['VSCODE_ESRP_TENANT_ID'], serviceConnectionId: process.env['VSCODE_ESRP_SERVICE_CONNECTION_ID'], - tempDirectory: os.tmpdir(), + tempDirectory: os_1.default.tmpdir(), systemAccessToken: encryptedTokenPath, encryptionKey: encryptionDetailsPath }; @@ -188,7 +191,7 @@ function main([esrpCliPath, type, folderPath, pattern]) { '-federatedTokenData', JSON.stringify(federatedTokenData) ]; try { - cp.execFileSync('dotnet', args, { stdio: 'inherit' }); + child_process_1.default.execFileSync('dotnet', args, { stdio: 'inherit' }); } catch (err) { console.error('ESRP failed'); diff --git a/build/azure-pipelines/common/sign.ts b/build/azure-pipelines/common/sign.ts index e5f42e87da2e..19a288483c85 100644 --- a/build/azure-pipelines/common/sign.ts +++ b/build/azure-pipelines/common/sign.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as cp from 'child_process'; -import * as fs from 'fs'; -import * as crypto from 'crypto'; -import * as path from 'path'; -import * as os from 'os'; +import cp from 'child_process'; +import fs from 'fs'; +import crypto from 'crypto'; +import path from 'path'; +import os from 'os'; export class Temp { private _files: string[] = []; diff --git a/build/azure-pipelines/distro/mixin-npm.js b/build/azure-pipelines/distro/mixin-npm.js index 0c61bb3dcf41..87958a5d4490 100644 --- a/build/azure-pipelines/distro/mixin-npm.js +++ b/build/azure-pipelines/distro/mixin-npm.js @@ -3,24 +3,27 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); const { dirs } = require('../../npm/dirs'); function log(...args) { console.log(`[${new Date().toLocaleTimeString('en', { hour12: false })}]`, '[distro]', ...args); } function mixin(mixinPath) { - if (!fs.existsSync(`${mixinPath}/node_modules`)) { + if (!fs_1.default.existsSync(`${mixinPath}/node_modules`)) { log(`Skipping distro npm dependencies: ${mixinPath} (no node_modules)`); return; } log(`Mixing in distro npm dependencies: ${mixinPath}`); - const distroPackageJson = JSON.parse(fs.readFileSync(`${mixinPath}/package.json`, 'utf8')); - const targetPath = path.relative('.build/distro/npm', mixinPath); + const distroPackageJson = JSON.parse(fs_1.default.readFileSync(`${mixinPath}/package.json`, 'utf8')); + const targetPath = path_1.default.relative('.build/distro/npm', mixinPath); for (const dependency of Object.keys(distroPackageJson.dependencies)) { - fs.rmSync(`./${targetPath}/node_modules/${dependency}`, { recursive: true, force: true }); - fs.cpSync(`${mixinPath}/node_modules/${dependency}`, `./${targetPath}/node_modules/${dependency}`, { recursive: true, force: true, dereference: true }); + fs_1.default.rmSync(`./${targetPath}/node_modules/${dependency}`, { recursive: true, force: true }); + fs_1.default.cpSync(`${mixinPath}/node_modules/${dependency}`, `./${targetPath}/node_modules/${dependency}`, { recursive: true, force: true, dereference: true }); } log(`Mixed in distro npm dependencies: ${mixinPath} ✔︎`); } diff --git a/build/azure-pipelines/distro/mixin-npm.ts b/build/azure-pipelines/distro/mixin-npm.ts index da5eb24ca28b..6e32f10db508 100644 --- a/build/azure-pipelines/distro/mixin-npm.ts +++ b/build/azure-pipelines/distro/mixin-npm.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; +import fs from 'fs'; +import path from 'path'; const { dirs } = require('../../npm/dirs') as { dirs: string[] }; function log(...args: any[]): void { diff --git a/build/azure-pipelines/distro/mixin-quality.js b/build/azure-pipelines/distro/mixin-quality.js index 6e011b5a1e94..335f63ca1fc3 100644 --- a/build/azure-pipelines/distro/mixin-quality.js +++ b/build/azure-pipelines/distro/mixin-quality.js @@ -3,9 +3,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); function log(...args) { console.log(`[${new Date().toLocaleTimeString('en', { hour12: false })}]`, '[distro]', ...args); } @@ -16,12 +19,12 @@ function main() { } log(`Mixing in distro quality...`); const basePath = `.build/distro/mixin/${quality}`; - for (const name of fs.readdirSync(basePath)) { - const distroPath = path.join(basePath, name); - const ossPath = path.relative(basePath, distroPath); + for (const name of fs_1.default.readdirSync(basePath)) { + const distroPath = path_1.default.join(basePath, name); + const ossPath = path_1.default.relative(basePath, distroPath); if (ossPath === 'product.json') { - const distro = JSON.parse(fs.readFileSync(distroPath, 'utf8')); - const oss = JSON.parse(fs.readFileSync(ossPath, 'utf8')); + const distro = JSON.parse(fs_1.default.readFileSync(distroPath, 'utf8')); + const oss = JSON.parse(fs_1.default.readFileSync(ossPath, 'utf8')); let builtInExtensions = oss.builtInExtensions; if (Array.isArray(distro.builtInExtensions)) { log('Overwriting built-in extensions:', distro.builtInExtensions.map(e => e.name)); @@ -41,10 +44,10 @@ function main() { log('Inheriting OSS built-in extensions', builtInExtensions.map(e => e.name)); } const result = { webBuiltInExtensions: oss.webBuiltInExtensions, ...distro, builtInExtensions }; - fs.writeFileSync(ossPath, JSON.stringify(result, null, '\t'), 'utf8'); + fs_1.default.writeFileSync(ossPath, JSON.stringify(result, null, '\t'), 'utf8'); } else { - fs.cpSync(distroPath, ossPath, { force: true, recursive: true }); + fs_1.default.cpSync(distroPath, ossPath, { force: true, recursive: true }); } log(distroPath, '✔︎'); } diff --git a/build/azure-pipelines/distro/mixin-quality.ts b/build/azure-pipelines/distro/mixin-quality.ts index b9b3c4f6c42b..29c90f00a65d 100644 --- a/build/azure-pipelines/distro/mixin-quality.ts +++ b/build/azure-pipelines/distro/mixin-quality.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; +import fs from 'fs'; +import path from 'path'; interface IBuiltInExtension { readonly name: string; diff --git a/build/azure-pipelines/publish-types/check-version.js b/build/azure-pipelines/publish-types/check-version.js index 9e93a7fa4c97..5bd80a69bbfc 100644 --- a/build/azure-pipelines/publish-types/check-version.js +++ b/build/azure-pipelines/publish-types/check-version.js @@ -3,11 +3,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const cp = require("child_process"); +const child_process_1 = __importDefault(require("child_process")); let tag = ''; try { - tag = cp + tag = child_process_1.default .execSync('git describe --tags `git rev-list --tags --max-count=1`') .toString() .trim(); diff --git a/build/azure-pipelines/publish-types/check-version.ts b/build/azure-pipelines/publish-types/check-version.ts index 35c5a5115939..4496ed93af11 100644 --- a/build/azure-pipelines/publish-types/check-version.ts +++ b/build/azure-pipelines/publish-types/check-version.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as cp from 'child_process'; +import cp from 'child_process'; let tag = ''; try { diff --git a/build/azure-pipelines/publish-types/update-types.js b/build/azure-pipelines/publish-types/update-types.js index ed2deded3fce..29f9bfcf66eb 100644 --- a/build/azure-pipelines/publish-types/update-types.js +++ b/build/azure-pipelines/publish-types/update-types.js @@ -3,19 +3,22 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const cp = require("child_process"); -const path = require("path"); +const fs_1 = __importDefault(require("fs")); +const child_process_1 = __importDefault(require("child_process")); +const path_1 = __importDefault(require("path")); let tag = ''; try { - tag = cp + tag = child_process_1.default .execSync('git describe --tags `git rev-list --tags --max-count=1`') .toString() .trim(); const dtsUri = `https://raw.githubusercontent.com/microsoft/vscode/${tag}/src/vscode-dts/vscode.d.ts`; - const outPath = path.resolve(process.cwd(), 'DefinitelyTyped/types/vscode/index.d.ts'); - cp.execSync(`curl ${dtsUri} --output ${outPath}`); + const outPath = path_1.default.resolve(process.cwd(), 'DefinitelyTyped/types/vscode/index.d.ts'); + child_process_1.default.execSync(`curl ${dtsUri} --output ${outPath}`); updateDTSFile(outPath, tag); console.log(`Done updating vscode.d.ts at ${outPath}`); } @@ -25,9 +28,9 @@ catch (err) { process.exit(1); } function updateDTSFile(outPath, tag) { - const oldContent = fs.readFileSync(outPath, 'utf-8'); + const oldContent = fs_1.default.readFileSync(outPath, 'utf-8'); const newContent = getNewFileContent(oldContent, tag); - fs.writeFileSync(outPath, newContent); + fs_1.default.writeFileSync(outPath, newContent); } function repeat(str, times) { const result = new Array(times); diff --git a/build/azure-pipelines/publish-types/update-types.ts b/build/azure-pipelines/publish-types/update-types.ts index a727647e64a2..0f99b07cf9a3 100644 --- a/build/azure-pipelines/publish-types/update-types.ts +++ b/build/azure-pipelines/publish-types/update-types.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as cp from 'child_process'; -import * as path from 'path'; +import fs from 'fs'; +import cp from 'child_process'; +import path from 'path'; let tag = ''; try { diff --git a/build/azure-pipelines/upload-cdn.js b/build/azure-pipelines/upload-cdn.js index 8ec40a0108e1..a0ec9d93516a 100644 --- a/build/azure-pipelines/upload-cdn.js +++ b/build/azure-pipelines/upload-cdn.js @@ -3,13 +3,49 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const es = require("event-stream"); -const Vinyl = require("vinyl"); -const vfs = require("vinyl-fs"); -const filter = require("gulp-filter"); -const gzip = require("gulp-gzip"); -const mime = require("mime"); +const es = __importStar(require("event-stream")); +const vinyl_1 = __importDefault(require("vinyl")); +const vfs = __importStar(require("vinyl-fs")); +const gulp_filter_1 = __importDefault(require("gulp-filter")); +const gulp_gzip_1 = __importDefault(require("gulp-gzip")); +const mime = __importStar(require("mime")); const identity_1 = require("@azure/identity"); const azure = require('gulp-azure-storage'); const commit = process.env['BUILD_SOURCEVERSION']; @@ -83,13 +119,13 @@ async function main() { } }); const all = vfs.src('**', { cwd: '../vscode-web', base: '../vscode-web', dot: true }) - .pipe(filter(f => !f.isDirectory())); + .pipe((0, gulp_filter_1.default)(f => !f.isDirectory())); const compressed = all - .pipe(filter(f => MimeTypesToCompress.has(mime.lookup(f.path)))) - .pipe(gzip({ append: false })) + .pipe((0, gulp_filter_1.default)(f => MimeTypesToCompress.has(mime.lookup(f.path)))) + .pipe((0, gulp_gzip_1.default)({ append: false })) .pipe(azure.upload(options(true))); const uncompressed = all - .pipe(filter(f => !MimeTypesToCompress.has(mime.lookup(f.path)))) + .pipe((0, gulp_filter_1.default)(f => !MimeTypesToCompress.has(mime.lookup(f.path)))) .pipe(azure.upload(options(false))); const out = es.merge(compressed, uncompressed) .pipe(es.through(function (f) { @@ -99,13 +135,13 @@ async function main() { })); console.log(`Uploading files to CDN...`); // debug await wait(out); - const listing = new Vinyl({ + const listing = new vinyl_1.default({ path: 'files.txt', contents: Buffer.from(files.join('\n')), stat: { mode: 0o666 } }); const filesOut = es.readArray([listing]) - .pipe(gzip({ append: false })) + .pipe((0, gulp_gzip_1.default)({ append: false })) .pipe(azure.upload(options(true))); console.log(`Uploading: files.txt (${files.length} files)`); // debug await wait(filesOut); diff --git a/build/azure-pipelines/upload-cdn.ts b/build/azure-pipelines/upload-cdn.ts index a4a5857afe5c..719ecd09c36a 100644 --- a/build/azure-pipelines/upload-cdn.ts +++ b/build/azure-pipelines/upload-cdn.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as es from 'event-stream'; -import * as Vinyl from 'vinyl'; +import Vinyl from 'vinyl'; import * as vfs from 'vinyl-fs'; -import * as filter from 'gulp-filter'; -import * as gzip from 'gulp-gzip'; +import filter from 'gulp-filter'; +import gzip from 'gulp-gzip'; import * as mime from 'mime'; import { ClientAssertionCredential } from '@azure/identity'; const azure = require('gulp-azure-storage'); diff --git a/build/azure-pipelines/upload-nlsmetadata.js b/build/azure-pipelines/upload-nlsmetadata.js index de75dcb8b3ab..aac93a057325 100644 --- a/build/azure-pipelines/upload-nlsmetadata.js +++ b/build/azure-pipelines/upload-nlsmetadata.js @@ -3,11 +3,47 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const es = require("event-stream"); -const vfs = require("vinyl-fs"); -const merge = require("gulp-merge-json"); -const gzip = require("gulp-gzip"); +const es = __importStar(require("event-stream")); +const vfs = __importStar(require("vinyl-fs")); +const gulp_merge_json_1 = __importDefault(require("gulp-merge-json")); +const gulp_gzip_1 = __importDefault(require("gulp-gzip")); const identity_1 = require("@azure/identity"); const path = require("path"); const fs_1 = require("fs"); @@ -21,7 +57,7 @@ function main() { // it includes metadata for translators for `keys`. but for our purpose // we want only the `keys` and `messages` as `string`. es.merge(vfs.src('out-build/nls.keys.json', { base: 'out-build' }), vfs.src('out-build/nls.messages.json', { base: 'out-build' })) - .pipe(merge({ + .pipe((0, gulp_merge_json_1.default)({ fileName: 'vscode.json', jsonSpace: '', concatArrays: true, @@ -37,7 +73,7 @@ function main() { } })), // extensions - vfs.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' })).pipe(merge({ + vfs.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' })).pipe((0, gulp_merge_json_1.default)({ fileName: 'combined.nls.metadata.json', jsonSpace: '', concatArrays: true, @@ -95,7 +131,7 @@ function main() { })); const nlsMessagesJs = vfs.src('out-build/nls.messages.js', { base: 'out-build' }); es.merge(combinedMetadataJson, nlsMessagesJs) - .pipe(gzip({ append: false })) + .pipe((0, gulp_gzip_1.default)({ append: false })) .pipe(vfs.dest('./nlsMetadata')) .pipe(es.through(function (data) { console.log(`Uploading ${data.path}`); diff --git a/build/azure-pipelines/upload-nlsmetadata.ts b/build/azure-pipelines/upload-nlsmetadata.ts index 89a9eb6c536d..5c13f73a006a 100644 --- a/build/azure-pipelines/upload-nlsmetadata.ts +++ b/build/azure-pipelines/upload-nlsmetadata.ts @@ -6,8 +6,8 @@ import * as es from 'event-stream'; import * as Vinyl from 'vinyl'; import * as vfs from 'vinyl-fs'; -import * as merge from 'gulp-merge-json'; -import * as gzip from 'gulp-gzip'; +import merge from 'gulp-merge-json'; +import gzip from 'gulp-gzip'; import { ClientAssertionCredential } from '@azure/identity'; import path = require('path'); import { readFileSync } from 'fs'; diff --git a/build/azure-pipelines/upload-sourcemaps.js b/build/azure-pipelines/upload-sourcemaps.js index 6f5f73fb8b0e..68ee13dcf2d9 100644 --- a/build/azure-pipelines/upload-sourcemaps.js +++ b/build/azure-pipelines/upload-sourcemaps.js @@ -3,23 +3,59 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); -const es = require("event-stream"); -const vfs = require("vinyl-fs"); -const util = require("../lib/util"); +const path_1 = __importDefault(require("path")); +const event_stream_1 = __importDefault(require("event-stream")); +const vinyl_fs_1 = __importDefault(require("vinyl-fs")); +const util = __importStar(require("../lib/util")); // @ts-ignore -const deps = require("../lib/dependencies"); +const deps = __importStar(require("../lib/dependencies")); const identity_1 = require("@azure/identity"); const azure = require('gulp-azure-storage'); -const root = path.dirname(path.dirname(__dirname)); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); const commit = process.env['BUILD_SOURCEVERSION']; const credential = new identity_1.ClientAssertionCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], () => Promise.resolve(process.env['AZURE_ID_TOKEN'])); // optionally allow to pass in explicit base/maps to upload const [, , base, maps] = process.argv; function src(base, maps = `${base}/**/*.map`) { - return vfs.src(maps, { base }) - .pipe(es.mapSync((f) => { + return vinyl_fs_1.default.src(maps, { base }) + .pipe(event_stream_1.default.mapSync((f) => { f.path = `${f.base}/core/${f.relative}`; return f; })); @@ -31,12 +67,12 @@ function main() { const vs = src('out-vscode-min'); // client source-maps only sources.push(vs); const productionDependencies = deps.getProductionDependencies(root); - const productionDependenciesSrc = productionDependencies.map(d => path.relative(root, d)).map(d => `./${d}/**/*.map`); - const nodeModules = vfs.src(productionDependenciesSrc, { base: '.' }) - .pipe(util.cleanNodeModules(path.join(root, 'build', '.moduleignore'))) - .pipe(util.cleanNodeModules(path.join(root, 'build', `.moduleignore.${process.platform}`))); + const productionDependenciesSrc = productionDependencies.map(d => path_1.default.relative(root, d)).map(d => `./${d}/**/*.map`); + const nodeModules = vinyl_fs_1.default.src(productionDependenciesSrc, { base: '.' }) + .pipe(util.cleanNodeModules(path_1.default.join(root, 'build', '.moduleignore'))) + .pipe(util.cleanNodeModules(path_1.default.join(root, 'build', `.moduleignore.${process.platform}`))); sources.push(nodeModules); - const extensionsOut = vfs.src(['.build/extensions/**/*.js.map', '!**/node_modules/**'], { base: '.build' }); + const extensionsOut = vinyl_fs_1.default.src(['.build/extensions/**/*.js.map', '!**/node_modules/**'], { base: '.build' }); sources.push(extensionsOut); } // specific client base/maps @@ -44,8 +80,8 @@ function main() { sources.push(src(base, maps)); } return new Promise((c, e) => { - es.merge(...sources) - .pipe(es.through(function (data) { + event_stream_1.default.merge(...sources) + .pipe(event_stream_1.default.through(function (data) { console.log('Uploading Sourcemap', data.relative); // debug this.emit('data', data); })) diff --git a/build/azure-pipelines/upload-sourcemaps.ts b/build/azure-pipelines/upload-sourcemaps.ts index 2eb5e6969830..b4a9f38e1297 100644 --- a/build/azure-pipelines/upload-sourcemaps.ts +++ b/build/azure-pipelines/upload-sourcemaps.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as es from 'event-stream'; -import * as Vinyl from 'vinyl'; -import * as vfs from 'vinyl-fs'; +import path from 'path'; +import es from 'event-stream'; +import Vinyl from 'vinyl'; +import vfs from 'vinyl-fs'; import * as util from '../lib/util'; // @ts-ignore import * as deps from '../lib/dependencies'; diff --git a/build/darwin/create-universal-app.js b/build/darwin/create-universal-app.js index bced5a7166f3..535d46eb1745 100644 --- a/build/darwin/create-universal-app.js +++ b/build/darwin/create-universal-app.js @@ -3,24 +3,27 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); -const fs = require("fs"); -const minimatch = require("minimatch"); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); +const minimatch_1 = __importDefault(require("minimatch")); const vscode_universal_bundler_1 = require("vscode-universal-bundler"); -const root = path.dirname(path.dirname(__dirname)); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); async function main(buildDir) { const arch = process.env['VSCODE_ARCH']; if (!buildDir) { throw new Error('Build dir not provided'); } - const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); + const product = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'product.json'), 'utf8')); const appName = product.nameLong + '.app'; - const x64AppPath = path.join(buildDir, 'VSCode-darwin-x64', appName); - const arm64AppPath = path.join(buildDir, 'VSCode-darwin-arm64', appName); - const asarRelativePath = path.join('Contents', 'Resources', 'app', 'node_modules.asar'); - const outAppPath = path.join(buildDir, `VSCode-darwin-${arch}`, appName); - const productJsonPath = path.resolve(outAppPath, 'Contents', 'Resources', 'app', 'product.json'); + const x64AppPath = path_1.default.join(buildDir, 'VSCode-darwin-x64', appName); + const arm64AppPath = path_1.default.join(buildDir, 'VSCode-darwin-arm64', appName); + const asarRelativePath = path_1.default.join('Contents', 'Resources', 'app', 'node_modules.asar'); + const outAppPath = path_1.default.join(buildDir, `VSCode-darwin-${arch}`, appName); + const productJsonPath = path_1.default.resolve(outAppPath, 'Contents', 'Resources', 'app', 'product.json'); const filesToSkip = [ '**/CodeResources', '**/Credits.rtf', @@ -37,18 +40,18 @@ async function main(buildDir) { x64ArchFiles: '*/kerberos.node', filesToSkipComparison: (file) => { for (const expected of filesToSkip) { - if (minimatch(file, expected)) { + if ((0, minimatch_1.default)(file, expected)) { return true; } } return false; } }); - const productJson = JSON.parse(fs.readFileSync(productJsonPath, 'utf8')); + const productJson = JSON.parse(fs_1.default.readFileSync(productJsonPath, 'utf8')); Object.assign(productJson, { darwinUniversalAssetId: 'darwin-universal' }); - fs.writeFileSync(productJsonPath, JSON.stringify(productJson, null, '\t')); + fs_1.default.writeFileSync(productJsonPath, JSON.stringify(productJson, null, '\t')); } if (require.main === module) { main(process.argv[2]).catch(err => { diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index e05f780b38d0..9e013cdb10ca 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as fs from 'fs'; -import * as minimatch from 'minimatch'; +import path from 'path'; +import fs from 'fs'; +import minimatch from 'minimatch'; import { makeUniversalApp } from 'vscode-universal-bundler'; const root = path.dirname(path.dirname(__dirname)); diff --git a/build/darwin/sign.js b/build/darwin/sign.js index feb5834ff85e..dff30fd0e18a 100644 --- a/build/darwin/sign.js +++ b/build/darwin/sign.js @@ -3,14 +3,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); -const codesign = require("electron-osx-sign"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const electron_osx_sign_1 = __importDefault(require("electron-osx-sign")); const cross_spawn_promise_1 = require("@malept/cross-spawn-promise"); -const root = path.dirname(path.dirname(__dirname)); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); function getElectronVersion() { - const npmrc = fs.readFileSync(path.join(root, '.npmrc'), 'utf8'); + const npmrc = fs_1.default.readFileSync(path_1.default.join(root, '.npmrc'), 'utf8'); const target = /^target="(.*)"$/m.exec(npmrc)[1]; return target; } @@ -24,25 +27,25 @@ async function main(buildDir) { if (!tempDir) { throw new Error('$AGENT_TEMPDIRECTORY not set'); } - const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); - const baseDir = path.dirname(__dirname); - const appRoot = path.join(buildDir, `VSCode-darwin-${arch}`); + const product = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'product.json'), 'utf8')); + const baseDir = path_1.default.dirname(__dirname); + const appRoot = path_1.default.join(buildDir, `VSCode-darwin-${arch}`); const appName = product.nameLong + '.app'; - const appFrameworkPath = path.join(appRoot, appName, 'Contents', 'Frameworks'); + const appFrameworkPath = path_1.default.join(appRoot, appName, 'Contents', 'Frameworks'); const helperAppBaseName = product.nameShort; const gpuHelperAppName = helperAppBaseName + ' Helper (GPU).app'; const rendererHelperAppName = helperAppBaseName + ' Helper (Renderer).app'; const pluginHelperAppName = helperAppBaseName + ' Helper (Plugin).app'; - const infoPlistPath = path.resolve(appRoot, appName, 'Contents', 'Info.plist'); + const infoPlistPath = path_1.default.resolve(appRoot, appName, 'Contents', 'Info.plist'); const defaultOpts = { - app: path.join(appRoot, appName), + app: path_1.default.join(appRoot, appName), platform: 'darwin', - entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist'), - 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist'), + entitlements: path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist'), + 'entitlements-inherit': path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist'), hardenedRuntime: true, 'pre-auto-entitlements': false, 'pre-embed-provisioning-profile': false, - keychain: path.join(tempDir, 'buildagent.keychain'), + keychain: path_1.default.join(tempDir, 'buildagent.keychain'), version: getElectronVersion(), identity, 'gatekeeper-assess': false @@ -58,21 +61,21 @@ async function main(buildDir) { }; const gpuHelperOpts = { ...defaultOpts, - app: path.join(appFrameworkPath, gpuHelperAppName), - entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), - 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), + app: path_1.default.join(appFrameworkPath, gpuHelperAppName), + entitlements: path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), + 'entitlements-inherit': path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), }; const rendererHelperOpts = { ...defaultOpts, - app: path.join(appFrameworkPath, rendererHelperAppName), - entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), - 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), + app: path_1.default.join(appFrameworkPath, rendererHelperAppName), + entitlements: path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), + 'entitlements-inherit': path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), }; const pluginHelperOpts = { ...defaultOpts, - app: path.join(appFrameworkPath, pluginHelperAppName), - entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), - 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), + app: path_1.default.join(appFrameworkPath, pluginHelperAppName), + entitlements: path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), + 'entitlements-inherit': path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), }; // Only overwrite plist entries for x64 and arm64 builds, // universal will get its copy from the x64 build. @@ -99,10 +102,10 @@ async function main(buildDir) { `${infoPlistPath}` ]); } - await codesign.signAsync(gpuHelperOpts); - await codesign.signAsync(rendererHelperOpts); - await codesign.signAsync(pluginHelperOpts); - await codesign.signAsync(appOpts); + await electron_osx_sign_1.default.signAsync(gpuHelperOpts); + await electron_osx_sign_1.default.signAsync(rendererHelperOpts); + await electron_osx_sign_1.default.signAsync(pluginHelperOpts); + await electron_osx_sign_1.default.signAsync(appOpts); } if (require.main === module) { main(process.argv[2]).catch(err => { diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index 5b3413b79e12..ecf162743ef2 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as codesign from 'electron-osx-sign'; +import fs from 'fs'; +import path from 'path'; +import codesign from 'electron-osx-sign'; import { spawn } from '@malept/cross-spawn-promise'; const root = path.dirname(path.dirname(__dirname)); diff --git a/build/darwin/verify-macho.js b/build/darwin/verify-macho.js index 947184324e2c..2df99a351420 100644 --- a/build/darwin/verify-macho.js +++ b/build/darwin/verify-macho.js @@ -3,9 +3,45 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const assert = require("assert"); -const path = require("path"); +const assert_1 = __importDefault(require("assert")); +const path = __importStar(require("path")); const promises_1 = require("fs/promises"); const cross_spawn_promise_1 = require("@malept/cross-spawn-promise"); const MACHO_PREFIX = 'Mach-O '; @@ -78,7 +114,7 @@ async function checkMachOFiles(appPath, arch) { } else if (header_magic === MACHO_UNIVERSAL_MAGIC_LE) { const num_binaries = header.readUInt32BE(4); - assert.equal(num_binaries, 2); + assert_1.default.equal(num_binaries, 2); const file_entries_size = file_header_entry_size * num_binaries; const file_entries = Buffer.alloc(file_entries_size); read(p, file_entries, 0, file_entries_size, 8).then(_ => { @@ -103,8 +139,8 @@ async function checkMachOFiles(appPath, arch) { return invalidFiles; } const archToCheck = process.argv[2]; -assert(process.env['APP_PATH'], 'APP_PATH not set'); -assert(archToCheck === 'x64' || archToCheck === 'arm64' || archToCheck === 'universal', `Invalid architecture ${archToCheck} to check`); +(0, assert_1.default)(process.env['APP_PATH'], 'APP_PATH not set'); +(0, assert_1.default)(archToCheck === 'x64' || archToCheck === 'arm64' || archToCheck === 'universal', `Invalid architecture ${archToCheck} to check`); checkMachOFiles(process.env['APP_PATH'], archToCheck).then(invalidFiles => { if (invalidFiles.length > 0) { console.error('\x1b[31mThe following files are built for the wrong architecture:\x1b[0m'); diff --git a/build/darwin/verify-macho.ts b/build/darwin/verify-macho.ts index f418c44a2301..9e2f4b518f28 100644 --- a/build/darwin/verify-macho.ts +++ b/build/darwin/verify-macho.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as path from 'path'; import { open, stat, readdir, realpath } from 'fs/promises'; import { spawn, ExitCodeError } from '@malept/cross-spawn-promise'; diff --git a/build/lib/asar.js b/build/lib/asar.js index 19285ef71002..20c982a66217 100644 --- a/build/lib/asar.js +++ b/build/lib/asar.js @@ -3,18 +3,21 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.createAsar = createAsar; -const path = require("path"); -const es = require("event-stream"); +const path_1 = __importDefault(require("path")); +const event_stream_1 = __importDefault(require("event-stream")); const pickle = require('chromium-pickle-js'); const Filesystem = require('asar/lib/filesystem'); -const VinylFile = require("vinyl"); -const minimatch = require("minimatch"); +const vinyl_1 = __importDefault(require("vinyl")); +const minimatch_1 = __importDefault(require("minimatch")); function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFilename) { const shouldUnpackFile = (file) => { for (let i = 0; i < unpackGlobs.length; i++) { - if (minimatch(file.relative, unpackGlobs[i])) { + if ((0, minimatch_1.default)(file.relative, unpackGlobs[i])) { return true; } } @@ -22,7 +25,7 @@ function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFile }; const shouldSkipFile = (file) => { for (const skipGlob of skipGlobs) { - if (minimatch(file.relative, skipGlob)) { + if ((0, minimatch_1.default)(file.relative, skipGlob)) { return true; } } @@ -32,7 +35,7 @@ function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFile // node_modules.asar and node_modules const shouldDuplicateFile = (file) => { for (const duplicateGlob of duplicateGlobs) { - if (minimatch(file.relative, duplicateGlob)) { + if ((0, minimatch_1.default)(file.relative, duplicateGlob)) { return true; } } @@ -75,7 +78,7 @@ function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFile // Create a closure capturing `onFileInserted`. filesystem.insertFile(relativePath, shouldUnpack, { stat: stat }, {}).then(() => onFileInserted(), () => onFileInserted()); }; - return es.through(function (file) { + return event_stream_1.default.through(function (file) { if (file.stat.isDirectory()) { return; } @@ -83,7 +86,7 @@ function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFile throw new Error(`unknown item in stream!`); } if (shouldSkipFile(file)) { - this.queue(new VinylFile({ + this.queue(new vinyl_1.default({ base: '.', path: file.path, stat: file.stat, @@ -92,7 +95,7 @@ function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFile return; } if (shouldDuplicateFile(file)) { - this.queue(new VinylFile({ + this.queue(new vinyl_1.default({ base: '.', path: file.path, stat: file.stat, @@ -103,10 +106,10 @@ function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFile insertFile(file.relative, { size: file.contents.length, mode: file.stat.mode }, shouldUnpack); if (shouldUnpack) { // The file goes outside of xx.asar, in a folder xx.asar.unpacked - const relative = path.relative(folderPath, file.path); - this.queue(new VinylFile({ + const relative = path_1.default.relative(folderPath, file.path); + this.queue(new vinyl_1.default({ base: '.', - path: path.join(destFilename + '.unpacked', relative), + path: path_1.default.join(destFilename + '.unpacked', relative), stat: file.stat, contents: file.contents })); @@ -129,7 +132,7 @@ function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFile } const contents = Buffer.concat(out); out.length = 0; - this.queue(new VinylFile({ + this.queue(new vinyl_1.default({ base: '.', path: destFilename, contents: contents diff --git a/build/lib/asar.ts b/build/lib/asar.ts index 0b225ab1624f..5f2df925bde9 100644 --- a/build/lib/asar.ts +++ b/build/lib/asar.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as es from 'event-stream'; +import path from 'path'; +import es from 'event-stream'; const pickle = require('chromium-pickle-js'); const Filesystem = require('asar/lib/filesystem'); -import * as VinylFile from 'vinyl'; -import * as minimatch from 'minimatch'; +import VinylFile from 'vinyl'; +import minimatch from 'minimatch'; declare class AsarFilesystem { readonly header: unknown; diff --git a/build/lib/builtInExtensions.js b/build/lib/builtInExtensions.js index ac784c035060..400ca6885a85 100644 --- a/build/lib/builtInExtensions.js +++ b/build/lib/builtInExtensions.js @@ -3,39 +3,75 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.getExtensionStream = getExtensionStream; exports.getBuiltInExtensions = getBuiltInExtensions; -const fs = require("fs"); -const path = require("path"); -const os = require("os"); -const rimraf = require("rimraf"); -const es = require("event-stream"); -const rename = require("gulp-rename"); -const vfs = require("vinyl-fs"); -const ext = require("./extensions"); -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); -const root = path.dirname(path.dirname(__dirname)); -const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const os_1 = __importDefault(require("os")); +const rimraf_1 = __importDefault(require("rimraf")); +const event_stream_1 = __importDefault(require("event-stream")); +const gulp_rename_1 = __importDefault(require("gulp-rename")); +const vinyl_fs_1 = __importDefault(require("vinyl-fs")); +const ext = __importStar(require("./extensions")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); +const productjson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../product.json'), 'utf8')); const builtInExtensions = productjson.builtInExtensions || []; const webBuiltInExtensions = productjson.webBuiltInExtensions || []; -const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json'); +const controlFilePath = path_1.default.join(os_1.default.homedir(), '.vscode-oss-dev', 'extensions', 'control.json'); const ENABLE_LOGGING = !process.env['VSCODE_BUILD_BUILTIN_EXTENSIONS_SILENCE_PLEASE']; function log(...messages) { if (ENABLE_LOGGING) { - fancyLog(...messages); + (0, fancy_log_1.default)(...messages); } } function getExtensionPath(extension) { - return path.join(root, '.build', 'builtInExtensions', extension.name); + return path_1.default.join(root, '.build', 'builtInExtensions', extension.name); } function isUpToDate(extension) { - const packagePath = path.join(getExtensionPath(extension), 'package.json'); - if (!fs.existsSync(packagePath)) { + const packagePath = path_1.default.join(getExtensionPath(extension), 'package.json'); + if (!fs_1.default.existsSync(packagePath)) { return false; } - const packageContents = fs.readFileSync(packagePath, { encoding: 'utf8' }); + const packageContents = fs_1.default.readFileSync(packagePath, { encoding: 'utf8' }); try { const diskVersion = JSON.parse(packageContents).version; return (diskVersion === extension.version); @@ -47,71 +83,71 @@ function isUpToDate(extension) { function getExtensionDownloadStream(extension) { const galleryServiceUrl = productjson.extensionsGallery?.serviceUrl; return (galleryServiceUrl ? ext.fromMarketplace(galleryServiceUrl, extension) : ext.fromGithub(extension)) - .pipe(rename(p => p.dirname = `${extension.name}/${p.dirname}`)); + .pipe((0, gulp_rename_1.default)(p => p.dirname = `${extension.name}/${p.dirname}`)); } function getExtensionStream(extension) { // if the extension exists on disk, use those files instead of downloading anew if (isUpToDate(extension)) { - log('[extensions]', `${extension.name}@${extension.version} up to date`, ansiColors.green('✔︎')); - return vfs.src(['**'], { cwd: getExtensionPath(extension), dot: true }) - .pipe(rename(p => p.dirname = `${extension.name}/${p.dirname}`)); + log('[extensions]', `${extension.name}@${extension.version} up to date`, ansi_colors_1.default.green('✔︎')); + return vinyl_fs_1.default.src(['**'], { cwd: getExtensionPath(extension), dot: true }) + .pipe((0, gulp_rename_1.default)(p => p.dirname = `${extension.name}/${p.dirname}`)); } return getExtensionDownloadStream(extension); } function syncMarketplaceExtension(extension) { const galleryServiceUrl = productjson.extensionsGallery?.serviceUrl; - const source = ansiColors.blue(galleryServiceUrl ? '[marketplace]' : '[github]'); + const source = ansi_colors_1.default.blue(galleryServiceUrl ? '[marketplace]' : '[github]'); if (isUpToDate(extension)) { - log(source, `${extension.name}@${extension.version}`, ansiColors.green('✔︎')); - return es.readArray([]); + log(source, `${extension.name}@${extension.version}`, ansi_colors_1.default.green('✔︎')); + return event_stream_1.default.readArray([]); } - rimraf.sync(getExtensionPath(extension)); + rimraf_1.default.sync(getExtensionPath(extension)); return getExtensionDownloadStream(extension) - .pipe(vfs.dest('.build/builtInExtensions')) - .on('end', () => log(source, extension.name, ansiColors.green('✔︎'))); + .pipe(vinyl_fs_1.default.dest('.build/builtInExtensions')) + .on('end', () => log(source, extension.name, ansi_colors_1.default.green('✔︎'))); } function syncExtension(extension, controlState) { if (extension.platforms) { const platforms = new Set(extension.platforms); if (!platforms.has(process.platform)) { - log(ansiColors.gray('[skip]'), `${extension.name}@${extension.version}: Platform '${process.platform}' not supported: [${extension.platforms}]`, ansiColors.green('✔︎')); - return es.readArray([]); + log(ansi_colors_1.default.gray('[skip]'), `${extension.name}@${extension.version}: Platform '${process.platform}' not supported: [${extension.platforms}]`, ansi_colors_1.default.green('✔︎')); + return event_stream_1.default.readArray([]); } } switch (controlState) { case 'disabled': - log(ansiColors.blue('[disabled]'), ansiColors.gray(extension.name)); - return es.readArray([]); + log(ansi_colors_1.default.blue('[disabled]'), ansi_colors_1.default.gray(extension.name)); + return event_stream_1.default.readArray([]); case 'marketplace': return syncMarketplaceExtension(extension); default: - if (!fs.existsSync(controlState)) { - log(ansiColors.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but that path does not exist.`)); - return es.readArray([]); + if (!fs_1.default.existsSync(controlState)) { + log(ansi_colors_1.default.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but that path does not exist.`)); + return event_stream_1.default.readArray([]); } - else if (!fs.existsSync(path.join(controlState, 'package.json'))) { - log(ansiColors.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but there is no 'package.json' file in that directory.`)); - return es.readArray([]); + else if (!fs_1.default.existsSync(path_1.default.join(controlState, 'package.json'))) { + log(ansi_colors_1.default.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but there is no 'package.json' file in that directory.`)); + return event_stream_1.default.readArray([]); } - log(ansiColors.blue('[local]'), `${extension.name}: ${ansiColors.cyan(controlState)}`, ansiColors.green('✔︎')); - return es.readArray([]); + log(ansi_colors_1.default.blue('[local]'), `${extension.name}: ${ansi_colors_1.default.cyan(controlState)}`, ansi_colors_1.default.green('✔︎')); + return event_stream_1.default.readArray([]); } } function readControlFile() { try { - return JSON.parse(fs.readFileSync(controlFilePath, 'utf8')); + return JSON.parse(fs_1.default.readFileSync(controlFilePath, 'utf8')); } catch (err) { return {}; } } function writeControlFile(control) { - fs.mkdirSync(path.dirname(controlFilePath), { recursive: true }); - fs.writeFileSync(controlFilePath, JSON.stringify(control, null, 2)); + fs_1.default.mkdirSync(path_1.default.dirname(controlFilePath), { recursive: true }); + fs_1.default.writeFileSync(controlFilePath, JSON.stringify(control, null, 2)); } function getBuiltInExtensions() { log('Synchronizing built-in extensions...'); - log(`You can manage built-in extensions with the ${ansiColors.cyan('--builtin')} flag`); + log(`You can manage built-in extensions with the ${ansi_colors_1.default.cyan('--builtin')} flag`); const control = readControlFile(); const streams = []; for (const extension of [...builtInExtensions, ...webBuiltInExtensions]) { @@ -121,7 +157,7 @@ function getBuiltInExtensions() { } writeControlFile(control); return new Promise((resolve, reject) => { - es.merge(streams) + event_stream_1.default.merge(streams) .on('error', reject) .on('end', resolve); }); diff --git a/build/lib/builtInExtensions.ts b/build/lib/builtInExtensions.ts index 8b831d42d44f..9b1ec7356ef3 100644 --- a/build/lib/builtInExtensions.ts +++ b/build/lib/builtInExtensions.ts @@ -3,16 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; -import * as rimraf from 'rimraf'; -import * as es from 'event-stream'; -import * as rename from 'gulp-rename'; -import * as vfs from 'vinyl-fs'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import rimraf from 'rimraf'; +import es from 'event-stream'; +import rename from 'gulp-rename'; +import vfs from 'vinyl-fs'; import * as ext from './extensions'; -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; import { Stream } from 'stream'; export interface IExtensionDefinition { diff --git a/build/lib/builtInExtensionsCG.js b/build/lib/builtInExtensionsCG.js index 6a1e5ea539ec..3dc0ae27f0a8 100644 --- a/build/lib/builtInExtensionsCG.js +++ b/build/lib/builtInExtensionsCG.js @@ -3,14 +3,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); -const url = require("url"); -const ansiColors = require("ansi-colors"); -const root = path.dirname(path.dirname(__dirname)); -const rootCG = path.join(root, 'extensionsCG'); -const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const url_1 = __importDefault(require("url")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); +const rootCG = path_1.default.join(root, 'extensionsCG'); +const productjson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../product.json'), 'utf8')); const builtInExtensions = productjson.builtInExtensions || []; const webBuiltInExtensions = productjson.webBuiltInExtensions || []; const token = process.env['GITHUB_TOKEN']; @@ -18,7 +21,7 @@ const contentBasePath = 'raw.githubusercontent.com'; const contentFileNames = ['package.json', 'package-lock.json']; async function downloadExtensionDetails(extension) { const extensionLabel = `${extension.name}@${extension.version}`; - const repository = url.parse(extension.repo).path.substr(1); + const repository = url_1.default.parse(extension.repo).path.substr(1); const repositoryContentBaseUrl = `https://${token ? `${token}@` : ''}${contentBasePath}/${repository}/v${extension.version}`; async function getContent(fileName) { try { @@ -42,16 +45,16 @@ async function downloadExtensionDetails(extension) { const results = await Promise.all(promises); for (const result of results) { if (result.body) { - const extensionFolder = path.join(rootCG, extension.name); - fs.mkdirSync(extensionFolder, { recursive: true }); - fs.writeFileSync(path.join(extensionFolder, result.fileName), result.body); - console.log(` - ${result.fileName} ${ansiColors.green('✔︎')}`); + const extensionFolder = path_1.default.join(rootCG, extension.name); + fs_1.default.mkdirSync(extensionFolder, { recursive: true }); + fs_1.default.writeFileSync(path_1.default.join(extensionFolder, result.fileName), result.body); + console.log(` - ${result.fileName} ${ansi_colors_1.default.green('✔︎')}`); } else if (result.body === undefined) { - console.log(` - ${result.fileName} ${ansiColors.yellow('⚠️')}`); + console.log(` - ${result.fileName} ${ansi_colors_1.default.yellow('⚠️')}`); } else { - console.log(` - ${result.fileName} ${ansiColors.red('🛑')}`); + console.log(` - ${result.fileName} ${ansi_colors_1.default.red('🛑')}`); } } // Validation @@ -68,10 +71,10 @@ async function main() { } } main().then(() => { - console.log(`Built-in extensions component data downloaded ${ansiColors.green('✔︎')}`); + console.log(`Built-in extensions component data downloaded ${ansi_colors_1.default.green('✔︎')}`); process.exit(0); }, err => { - console.log(`Built-in extensions component data could not be downloaded ${ansiColors.red('🛑')}`); + console.log(`Built-in extensions component data could not be downloaded ${ansi_colors_1.default.red('🛑')}`); console.error(err); process.exit(1); }); diff --git a/build/lib/builtInExtensionsCG.ts b/build/lib/builtInExtensionsCG.ts index 9d11dea3dcae..4628b365a2e1 100644 --- a/build/lib/builtInExtensionsCG.ts +++ b/build/lib/builtInExtensionsCG.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as url from 'url'; -import ansiColors = require('ansi-colors'); +import fs from 'fs'; +import path from 'path'; +import url from 'url'; +import ansiColors from 'ansi-colors'; import { IExtensionDefinition } from './builtInExtensions'; const root = path.dirname(path.dirname(__dirname)); diff --git a/build/lib/bundle.js b/build/lib/bundle.js index 7f7e55963acb..f1490f4ad4b7 100644 --- a/build/lib/bundle.js +++ b/build/lib/bundle.js @@ -3,12 +3,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.bundle = bundle; exports.removeAllTSBoilerplate = removeAllTSBoilerplate; -const fs = require("fs"); -const path = require("path"); -const vm = require("vm"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const vm_1 = __importDefault(require("vm")); /** * Bundle `entryPoints` given config `config`. */ @@ -30,8 +33,8 @@ function bundle(entryPoints, config, callback) { allMentionedModulesMap[excludedModule] = true; }); }); - const code = require('fs').readFileSync(path.join(__dirname, '../../src/vs/loader.js')); - const r = vm.runInThisContext('(function(require, module, exports) { ' + code + '\n});'); + const code = require('fs').readFileSync(path_1.default.join(__dirname, '../../src/vs/loader.js')); + const r = vm_1.default.runInThisContext('(function(require, module, exports) { ' + code + '\n});'); const loaderModule = { exports: {} }; r.call({}, require, loaderModule, loaderModule.exports); const loader = loaderModule.exports; @@ -149,7 +152,7 @@ function extractStrings(destFiles) { _path = pieces[0]; } if (/^\.\//.test(_path) || /^\.\.\//.test(_path)) { - const res = path.join(path.dirname(module), _path).replace(/\\/g, '/'); + const res = path_1.default.join(path_1.default.dirname(module), _path).replace(/\\/g, '/'); return prefix + res; } return prefix + _path; @@ -359,7 +362,7 @@ function emitEntryPoint(modulesMap, deps, entryPoint, includedModules, prepend, } function readFileAndRemoveBOM(path) { const BOM_CHAR_CODE = 65279; - let contents = fs.readFileSync(path, 'utf8'); + let contents = fs_1.default.readFileSync(path, 'utf8'); // Remove BOM if (contents.charCodeAt(0) === BOM_CHAR_CODE) { contents = contents.substring(1); diff --git a/build/lib/bundle.ts b/build/lib/bundle.ts index 47a686d00477..68182e6b85de 100644 --- a/build/lib/bundle.ts +++ b/build/lib/bundle.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as vm from 'vm'; +import fs from 'fs'; +import path from 'path'; +import vm from 'vm'; interface IPosition { line: number; diff --git a/build/lib/compilation.js b/build/lib/compilation.js index 7b9d73facbbd..841dbe13ecf5 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -3,24 +3,60 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.watchApiProposalNamesTask = exports.compileApiProposalNamesTask = void 0; exports.transpileTask = transpileTask; exports.compileTask = compileTask; exports.watchTask = watchTask; -const es = require("event-stream"); -const fs = require("fs"); -const gulp = require("gulp"); -const path = require("path"); -const monacodts = require("./monaco-api"); -const nls = require("./nls"); +const event_stream_1 = __importDefault(require("event-stream")); +const fs_1 = __importDefault(require("fs")); +const gulp_1 = __importDefault(require("gulp")); +const path_1 = __importDefault(require("path")); +const monacodts = __importStar(require("./monaco-api")); +const nls = __importStar(require("./nls")); const reporter_1 = require("./reporter"); -const util = require("./util"); -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); -const os = require("os"); -const File = require("vinyl"); -const task = require("./task"); +const util = __importStar(require("./util")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const os_1 = __importDefault(require("os")); +const vinyl_1 = __importDefault(require("vinyl")); +const task = __importStar(require("./task")); const index_1 = require("./mangle/index"); const postcss_1 = require("./postcss"); const ts = require("typescript"); @@ -28,7 +64,7 @@ const watch = require('./watch'); // --- gulp-tsb: compile and transpile -------------------------------- const reporter = (0, reporter_1.createReporter)(); function getTypeScriptCompilerOptions(src) { - const rootDir = path.join(__dirname, `../../${src}`); + const rootDir = path_1.default.join(__dirname, `../../${src}`); const options = {}; options.verbose = false; options.sourceMap = true; @@ -38,13 +74,13 @@ function getTypeScriptCompilerOptions(src) { options.rootDir = rootDir; options.baseUrl = rootDir; options.sourceRoot = util.toFileUri(rootDir); - options.newLine = /\r\n/.test(fs.readFileSync(__filename, 'utf8')) ? 0 : 1; + options.newLine = /\r\n/.test(fs_1.default.readFileSync(__filename, 'utf8')) ? 0 : 1; return options; } function createCompile(src, { build, emitError, transpileOnly, preserveEnglish }) { const tsb = require('./tsb'); const sourcemaps = require('gulp-sourcemaps'); - const projectPath = path.join(__dirname, '../../', src, 'tsconfig.json'); + const projectPath = path_1.default.join(__dirname, '../../', src, 'tsconfig.json'); const overrideOptions = { ...getTypeScriptCompilerOptions(src), inlineSources: Boolean(build) }; if (!build) { overrideOptions.inlineSourceMap = true; @@ -62,7 +98,7 @@ function createCompile(src, { build, emitError, transpileOnly, preserveEnglish } const isCSS = (f) => f.path.endsWith('.css') && !f.path.includes('fixtures'); const noDeclarationsFilter = util.filter(data => !(/\.d\.ts$/.test(data.path))); const postcssNesting = require('postcss-nesting'); - const input = es.through(); + const input = event_stream_1.default.through(); const output = input .pipe(util.$if(isUtf8Test, bom())) // this is required to preserve BOM in test files that loose it otherwise .pipe(util.$if(!build && isRuntimeJs, util.appendOwnPathSourceURL())) @@ -80,7 +116,7 @@ function createCompile(src, { build, emitError, transpileOnly, preserveEnglish } }))) .pipe(tsFilter.restore) .pipe(reporter.end(!!emitError)); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } pipeline.tsProjectSrc = () => { return compilation.src({ base: src }); @@ -91,31 +127,31 @@ function createCompile(src, { build, emitError, transpileOnly, preserveEnglish } function transpileTask(src, out, esbuild) { const task = () => { const transpile = createCompile(src, { build: false, emitError: true, transpileOnly: { esbuild }, preserveEnglish: false }); - const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); + const srcPipe = gulp_1.default.src(`${src}/**`, { base: `${src}` }); return srcPipe .pipe(transpile()) - .pipe(gulp.dest(out)); + .pipe(gulp_1.default.dest(out)); }; - task.taskName = `transpile-${path.basename(src)}`; + task.taskName = `transpile-${path_1.default.basename(src)}`; return task; } function compileTask(src, out, build, options = {}) { const task = () => { - if (os.totalmem() < 4_000_000_000) { + if (os_1.default.totalmem() < 4_000_000_000) { throw new Error('compilation requires 4GB of RAM'); } const compile = createCompile(src, { build, emitError: true, transpileOnly: false, preserveEnglish: !!options.preserveEnglish }); - const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); + const srcPipe = gulp_1.default.src(`${src}/**`, { base: `${src}` }); const generator = new MonacoGenerator(false); if (src === 'src') { generator.execute(); } // mangle: TypeScript to TypeScript - let mangleStream = es.through(); + let mangleStream = event_stream_1.default.through(); if (build && !options.disableMangle) { - let ts2tsMangler = new index_1.Mangler(compile.projectPath, (...data) => fancyLog(ansiColors.blue('[mangler]'), ...data), { mangleExports: true, manglePrivateFields: true }); + let ts2tsMangler = new index_1.Mangler(compile.projectPath, (...data) => (0, fancy_log_1.default)(ansi_colors_1.default.blue('[mangler]'), ...data), { mangleExports: true, manglePrivateFields: true }); const newContentsByFileName = ts2tsMangler.computeNewFileContents(new Set(['saveState'])); - mangleStream = es.through(async function write(data) { + mangleStream = event_stream_1.default.through(async function write(data) { const tsNormalPath = ts.normalizePath(data.path); const newContents = (await newContentsByFileName).get(tsNormalPath); if (newContents !== undefined) { @@ -134,27 +170,27 @@ function compileTask(src, out, build, options = {}) { .pipe(mangleStream) .pipe(generator.stream) .pipe(compile()) - .pipe(gulp.dest(out)); + .pipe(gulp_1.default.dest(out)); }; - task.taskName = `compile-${path.basename(src)}`; + task.taskName = `compile-${path_1.default.basename(src)}`; return task; } function watchTask(out, build, srcPath = 'src') { const task = () => { const compile = createCompile(srcPath, { build, emitError: false, transpileOnly: false, preserveEnglish: false }); - const src = gulp.src(`${srcPath}/**`, { base: srcPath }); + const src = gulp_1.default.src(`${srcPath}/**`, { base: srcPath }); const watchSrc = watch(`${srcPath}/**`, { base: srcPath, readDelay: 200 }); const generator = new MonacoGenerator(true); generator.execute(); return watchSrc .pipe(generator.stream) .pipe(util.incremental(compile, src, true)) - .pipe(gulp.dest(out)); + .pipe(gulp_1.default.dest(out)); }; - task.taskName = `watch-${path.basename(out)}`; + task.taskName = `watch-${path_1.default.basename(out)}`; return task; } -const REPO_SRC_FOLDER = path.join(__dirname, '../../src'); +const REPO_SRC_FOLDER = path_1.default.join(__dirname, '../../src'); class MonacoGenerator { _isWatch; stream; @@ -163,7 +199,7 @@ class MonacoGenerator { _declarationResolver; constructor(isWatch) { this._isWatch = isWatch; - this.stream = es.through(); + this.stream = event_stream_1.default.through(); this._watchedFiles = {}; const onWillReadFile = (moduleId, filePath) => { if (!this._isWatch) { @@ -173,7 +209,7 @@ class MonacoGenerator { return; } this._watchedFiles[filePath] = true; - fs.watchFile(filePath, () => { + fs_1.default.watchFile(filePath, () => { this._declarationResolver.invalidateCache(moduleId); this._executeSoon(); }); @@ -186,7 +222,7 @@ class MonacoGenerator { }; this._declarationResolver = new monacodts.DeclarationResolver(this._fsProvider); if (this._isWatch) { - fs.watchFile(monacodts.RECIPE_PATH, () => { + fs_1.default.watchFile(monacodts.RECIPE_PATH, () => { this._executeSoon(); }); } @@ -211,7 +247,7 @@ class MonacoGenerator { return r; } _log(message, ...rest) { - fancyLog(ansiColors.cyan('[monaco.d.ts]'), message, ...rest); + (0, fancy_log_1.default)(ansi_colors_1.default.cyan('[monaco.d.ts]'), message, ...rest); } execute() { const startTime = Date.now(); @@ -223,8 +259,8 @@ class MonacoGenerator { if (result.isTheSame) { return; } - fs.writeFileSync(result.filePath, result.content); - fs.writeFileSync(path.join(REPO_SRC_FOLDER, 'vs/editor/common/standalone/standaloneEnums.ts'), result.enums); + fs_1.default.writeFileSync(result.filePath, result.content); + fs_1.default.writeFileSync(path_1.default.join(REPO_SRC_FOLDER, 'vs/editor/common/standalone/standaloneEnums.ts'), result.enums); this._log(`monaco.d.ts is changed - total time took ${Date.now() - startTime} ms`); if (!this._isWatch) { this.stream.emit('error', 'monaco.d.ts is no longer up to date. Please run gulp watch and commit the new file.'); @@ -234,21 +270,21 @@ class MonacoGenerator { function generateApiProposalNames() { let eol; try { - const src = fs.readFileSync('src/vs/platform/extensions/common/extensionsApiProposals.ts', 'utf-8'); + const src = fs_1.default.readFileSync('src/vs/platform/extensions/common/extensionsApiProposals.ts', 'utf-8'); const match = /\r?\n/m.exec(src); - eol = match ? match[0] : os.EOL; + eol = match ? match[0] : os_1.default.EOL; } catch { - eol = os.EOL; + eol = os_1.default.EOL; } const pattern = /vscode\.proposed\.([a-zA-Z\d]+)\.d\.ts$/; const versionPattern = /^\s*\/\/\s*version\s*:\s*(\d+)\s*$/mi; const proposals = new Map(); - const input = es.through(); + const input = event_stream_1.default.through(); const output = input .pipe(util.filter((f) => pattern.test(f.path))) - .pipe(es.through((f) => { - const name = path.basename(f.path); + .pipe(event_stream_1.default.through((f) => { + const name = path_1.default.basename(f.path); const match = pattern.exec(name); if (!match) { return; @@ -281,27 +317,27 @@ function generateApiProposalNames() { 'export type ApiProposalName = keyof typeof _allApiProposals;', '', ].join(eol); - this.emit('data', new File({ + this.emit('data', new vinyl_1.default({ path: 'vs/platform/extensions/common/extensionsApiProposals.ts', contents: Buffer.from(contents) })); this.emit('end'); })); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } const apiProposalNamesReporter = (0, reporter_1.createReporter)('api-proposal-names'); exports.compileApiProposalNamesTask = task.define('compile-api-proposal-names', () => { - return gulp.src('src/vscode-dts/**') + return gulp_1.default.src('src/vscode-dts/**') .pipe(generateApiProposalNames()) - .pipe(gulp.dest('src')) + .pipe(gulp_1.default.dest('src')) .pipe(apiProposalNamesReporter.end(true)); }); exports.watchApiProposalNamesTask = task.define('watch-api-proposal-names', () => { - const task = () => gulp.src('src/vscode-dts/**') + const task = () => gulp_1.default.src('src/vscode-dts/**') .pipe(generateApiProposalNames()) .pipe(apiProposalNamesReporter.end(true)); return watch('src/vscode-dts/**', { readDelay: 200 }) .pipe(util.debounce(task)) - .pipe(gulp.dest('src')); + .pipe(gulp_1.default.dest('src')); }); //# sourceMappingURL=compilation.js.map \ No newline at end of file diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index 124bcc17c17c..6e1fcab5186f 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; -import * as fs from 'fs'; -import * as gulp from 'gulp'; -import * as path from 'path'; +import es from 'event-stream'; +import fs from 'fs'; +import gulp from 'gulp'; +import path from 'path'; import * as monacodts from './monaco-api'; import * as nls from './nls'; import { createReporter } from './reporter'; import * as util from './util'; -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; -import * as os from 'os'; -import * as File from 'vinyl'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; +import os from 'os'; +import File from 'vinyl'; import * as task from './task'; import { Mangler } from './mangle/index'; import { RawSourceMap } from 'source-map'; diff --git a/build/lib/date.js b/build/lib/date.js index 77fff0e5073e..1ed884fb7ee7 100644 --- a/build/lib/date.js +++ b/build/lib/date.js @@ -3,12 +3,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.writeISODate = writeISODate; exports.readISODate = readISODate; -const path = require("path"); -const fs = require("fs"); -const root = path.join(__dirname, '..', '..'); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); +const root = path_1.default.join(__dirname, '..', '..'); /** * Writes a `outDir/date` file with the contents of the build * so that other tasks during the build process can use it and @@ -16,17 +19,17 @@ const root = path.join(__dirname, '..', '..'); */ function writeISODate(outDir) { const result = () => new Promise((resolve, _) => { - const outDirectory = path.join(root, outDir); - fs.mkdirSync(outDirectory, { recursive: true }); + const outDirectory = path_1.default.join(root, outDir); + fs_1.default.mkdirSync(outDirectory, { recursive: true }); const date = new Date().toISOString(); - fs.writeFileSync(path.join(outDirectory, 'date'), date, 'utf8'); + fs_1.default.writeFileSync(path_1.default.join(outDirectory, 'date'), date, 'utf8'); resolve(); }); result.taskName = 'build-date-file'; return result; } function readISODate(outDir) { - const outDirectory = path.join(root, outDir); - return fs.readFileSync(path.join(outDirectory, 'date'), 'utf8'); + const outDirectory = path_1.default.join(root, outDir); + return fs_1.default.readFileSync(path_1.default.join(outDirectory, 'date'), 'utf8'); } //# sourceMappingURL=date.js.map \ No newline at end of file diff --git a/build/lib/date.ts b/build/lib/date.ts index 998e89f8e6ab..8a9331789520 100644 --- a/build/lib/date.ts +++ b/build/lib/date.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as fs from 'fs'; +import path from 'path'; +import fs from 'fs'; const root = path.join(__dirname, '..', '..'); diff --git a/build/lib/dependencies.js b/build/lib/dependencies.js index 9bcd1204eab8..04a09f98708a 100644 --- a/build/lib/dependencies.js +++ b/build/lib/dependencies.js @@ -3,16 +3,19 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.getProductionDependencies = getProductionDependencies; -const fs = require("fs"); -const path = require("path"); -const cp = require("child_process"); -const root = fs.realpathSync(path.dirname(path.dirname(__dirname))); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const child_process_1 = __importDefault(require("child_process")); +const root = fs_1.default.realpathSync(path_1.default.dirname(path_1.default.dirname(__dirname))); function getNpmProductionDependencies(folder) { let raw; try { - raw = cp.execSync('npm ls --all --omit=dev --parseable', { cwd: folder, encoding: 'utf8', env: { ...process.env, NODE_ENV: 'production' }, stdio: [null, null, null] }); + raw = child_process_1.default.execSync('npm ls --all --omit=dev --parseable', { cwd: folder, encoding: 'utf8', env: { ...process.env, NODE_ENV: 'production' }, stdio: [null, null, null] }); } catch (err) { const regex = /^npm ERR! .*$/gm; @@ -34,16 +37,16 @@ function getNpmProductionDependencies(folder) { raw = err.stdout; } return raw.split(/\r?\n/).filter(line => { - return !!line.trim() && path.relative(root, line) !== path.relative(root, folder); + return !!line.trim() && path_1.default.relative(root, line) !== path_1.default.relative(root, folder); }); } function getProductionDependencies(folderPath) { const result = getNpmProductionDependencies(folderPath); // Account for distro npm dependencies - const realFolderPath = fs.realpathSync(folderPath); - const relativeFolderPath = path.relative(root, realFolderPath); + const realFolderPath = fs_1.default.realpathSync(folderPath); + const relativeFolderPath = path_1.default.relative(root, realFolderPath); const distroFolderPath = `${root}/.build/distro/npm/${relativeFolderPath}`; - if (fs.existsSync(distroFolderPath)) { + if (fs_1.default.existsSync(distroFolderPath)) { result.push(...getNpmProductionDependencies(distroFolderPath)); } return [...new Set(result)]; diff --git a/build/lib/dependencies.ts b/build/lib/dependencies.ts index 45368ffd26db..a5bc70088a7f 100644 --- a/build/lib/dependencies.ts +++ b/build/lib/dependencies.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as cp from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import cp from 'child_process'; const root = fs.realpathSync(path.dirname(path.dirname(__dirname))); function getNpmProductionDependencies(folder: string): string[] { diff --git a/build/lib/electron.js b/build/lib/electron.js index 99252e4e64a2..f0eb583f2cba 100644 --- a/build/lib/electron.js +++ b/build/lib/electron.js @@ -3,19 +3,55 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.config = void 0; -const fs = require("fs"); -const path = require("path"); -const vfs = require("vinyl-fs"); -const filter = require("gulp-filter"); -const util = require("./util"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const vinyl_fs_1 = __importDefault(require("vinyl-fs")); +const gulp_filter_1 = __importDefault(require("gulp-filter")); +const util = __importStar(require("./util")); const getVersion_1 = require("./getVersion"); function isDocumentSuffix(str) { return str === 'document' || str === 'script' || str === 'file' || str === 'source code'; } -const root = path.dirname(path.dirname(__dirname)); -const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); +const product = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'product.json'), 'utf8')); const commit = (0, getVersion_1.getVersion)(root); function createTemplate(input) { return (params) => { @@ -24,7 +60,7 @@ function createTemplate(input) { }); }; } -const darwinCreditsTemplate = product.darwinCredits && createTemplate(fs.readFileSync(path.join(root, product.darwinCredits), 'utf8')); +const darwinCreditsTemplate = product.darwinCredits && createTemplate(fs_1.default.readFileSync(path_1.default.join(root, product.darwinCredits), 'utf8')); /** * Generate a `DarwinDocumentType` given a list of file extensions, an icon name, and an optional suffix or file type name. * @param extensions A list of file extensions, such as `['bat', 'cmd']` @@ -183,7 +219,7 @@ exports.config = { token: process.env['GITHUB_TOKEN'], repo: product.electronRepository || undefined, validateChecksum: true, - checksumFile: path.join(root, 'build', 'checksums', 'electron.txt'), + checksumFile: path_1.default.join(root, 'build', 'checksums', 'electron.txt'), }; function getElectron(arch) { return () => { @@ -196,18 +232,18 @@ function getElectron(arch) { ffmpegChromium: false, keepDefaultApp: true }; - return vfs.src('package.json') + return vinyl_fs_1.default.src('package.json') .pipe(json({ name: product.nameShort })) .pipe(electron(electronOpts)) - .pipe(filter(['**', '!**/app/package.json'])) - .pipe(vfs.dest('.build/electron')); + .pipe((0, gulp_filter_1.default)(['**', '!**/app/package.json'])) + .pipe(vinyl_fs_1.default.dest('.build/electron')); }; } async function main(arch = process.arch) { const version = electronVersion; - const electronPath = path.join(root, '.build', 'electron'); - const versionFile = path.join(electronPath, 'version'); - const isUpToDate = fs.existsSync(versionFile) && fs.readFileSync(versionFile, 'utf8') === `${version}`; + const electronPath = path_1.default.join(root, '.build', 'electron'); + const versionFile = path_1.default.join(electronPath, 'version'); + const isUpToDate = fs_1.default.existsSync(versionFile) && fs_1.default.readFileSync(versionFile, 'utf8') === `${version}`; if (!isUpToDate) { await util.rimraf(electronPath)(); await util.streamToPromise(getElectron(arch)()); diff --git a/build/lib/electron.ts b/build/lib/electron.ts index da2387f68f63..57b27022df86 100644 --- a/build/lib/electron.ts +++ b/build/lib/electron.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as vfs from 'vinyl-fs'; -import * as filter from 'gulp-filter'; +import fs from 'fs'; +import path from 'path'; +import vfs from 'vinyl-fs'; +import filter from 'gulp-filter'; import * as util from './util'; import { getVersion } from './getVersion'; diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 8630c8fa061f..6afa72e5bfa9 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -3,6 +3,42 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.fromMarketplace = fromMarketplace; exports.fromGithub = fromGithub; @@ -14,35 +50,35 @@ exports.scanBuiltinExtensions = scanBuiltinExtensions; exports.translatePackageJSON = translatePackageJSON; exports.webpackExtensions = webpackExtensions; exports.buildExtensionMedia = buildExtensionMedia; -const es = require("event-stream"); -const fs = require("fs"); -const cp = require("child_process"); -const glob = require("glob"); -const gulp = require("gulp"); -const path = require("path"); -const File = require("vinyl"); +const event_stream_1 = __importDefault(require("event-stream")); +const fs_1 = __importDefault(require("fs")); +const child_process_1 = __importDefault(require("child_process")); +const glob_1 = __importDefault(require("glob")); +const gulp_1 = __importDefault(require("gulp")); +const path_1 = __importDefault(require("path")); +const vinyl_1 = __importDefault(require("vinyl")); const stats_1 = require("./stats"); -const util2 = require("./util"); +const util2 = __importStar(require("./util")); const vzip = require('gulp-vinyl-zip'); -const filter = require("gulp-filter"); -const rename = require("gulp-rename"); -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); -const buffer = require('gulp-buffer'); -const jsoncParser = require("jsonc-parser"); +const gulp_filter_1 = __importDefault(require("gulp-filter")); +const gulp_rename_1 = __importDefault(require("gulp-rename")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const gulp_buffer_1 = __importDefault(require("gulp-buffer")); +const jsoncParser = __importStar(require("jsonc-parser")); const dependencies_1 = require("./dependencies"); const builtInExtensions_1 = require("./builtInExtensions"); const getVersion_1 = require("./getVersion"); const fetch_1 = require("./fetch"); -const root = path.dirname(path.dirname(__dirname)); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); const commit = (0, getVersion_1.getVersion)(root); const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; function minifyExtensionResources(input) { - const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); + const jsonFilter = (0, gulp_filter_1.default)(['**/*.json', '**/*.code-snippets'], { restore: true }); return input .pipe(jsonFilter) - .pipe(buffer()) - .pipe(es.mapSync((f) => { + .pipe((0, gulp_buffer_1.default)()) + .pipe(event_stream_1.default.mapSync((f) => { const errors = []; const value = jsoncParser.parse(f.contents.toString('utf8'), errors, { allowTrailingComma: true }); if (errors.length === 0) { @@ -54,11 +90,11 @@ function minifyExtensionResources(input) { .pipe(jsonFilter.restore); } function updateExtensionPackageJSON(input, update) { - const packageJsonFilter = filter('extensions/*/package.json', { restore: true }); + const packageJsonFilter = (0, gulp_filter_1.default)('extensions/*/package.json', { restore: true }); return input .pipe(packageJsonFilter) - .pipe(buffer()) - .pipe(es.mapSync((f) => { + .pipe((0, gulp_buffer_1.default)()) + .pipe(event_stream_1.default.mapSync((f) => { const data = JSON.parse(f.contents.toString('utf8')); f.contents = Buffer.from(JSON.stringify(update(data))); return f; @@ -67,7 +103,7 @@ function updateExtensionPackageJSON(input, update) { } function fromLocal(extensionPath, forWeb, disableMangle) { const webpackConfigFileName = forWeb ? 'extension-browser.webpack.config.js' : 'extension.webpack.config.js'; - const isWebPacked = fs.existsSync(path.join(extensionPath, webpackConfigFileName)); + const isWebPacked = fs_1.default.existsSync(path_1.default.join(extensionPath, webpackConfigFileName)); let input = isWebPacked ? fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle) : fromLocalNormal(extensionPath); @@ -88,11 +124,11 @@ function fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle) { const vsce = require('@vscode/vsce'); const webpack = require('webpack'); const webpackGulp = require('webpack-stream'); - const result = es.through(); + const result = event_stream_1.default.through(); const packagedDependencies = []; - const packageJsonConfig = require(path.join(extensionPath, 'package.json')); + const packageJsonConfig = require(path_1.default.join(extensionPath, 'package.json')); if (packageJsonConfig.dependencies) { - const webpackRootConfig = require(path.join(extensionPath, webpackConfigFileName)); + const webpackRootConfig = require(path_1.default.join(extensionPath, webpackConfigFileName)); for (const key in webpackRootConfig.externals) { if (key in packageJsonConfig.dependencies) { packagedDependencies.push(key); @@ -106,19 +142,19 @@ function fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle) { // as a temporary workaround. vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.None, packagedDependencies }).then(fileNames => { const files = fileNames - .map(fileName => path.join(extensionPath, fileName)) - .map(filePath => new File({ + .map(fileName => path_1.default.join(extensionPath, fileName)) + .map(filePath => new vinyl_1.default({ path: filePath, - stat: fs.statSync(filePath), + stat: fs_1.default.statSync(filePath), base: extensionPath, - contents: fs.createReadStream(filePath) + contents: fs_1.default.createReadStream(filePath) })); // check for a webpack configuration files, then invoke webpack // and merge its output with the files stream. - const webpackConfigLocations = glob.sync(path.join(extensionPath, '**', webpackConfigFileName), { ignore: ['**/node_modules'] }); + const webpackConfigLocations = glob_1.default.sync(path_1.default.join(extensionPath, '**', webpackConfigFileName), { ignore: ['**/node_modules'] }); const webpackStreams = webpackConfigLocations.flatMap(webpackConfigPath => { const webpackDone = (err, stats) => { - fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath)))}...`); + (0, fancy_log_1.default)(`Bundled extension: ${ansi_colors_1.default.yellow(path_1.default.join(path_1.default.basename(extensionPath), path_1.default.relative(extensionPath, webpackConfigPath)))}...`); if (err) { result.emit('error', err); } @@ -149,28 +185,28 @@ function fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle) { } } } - const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); + const relativeOutputPath = path_1.default.relative(extensionPath, webpackConfig.output.path); return webpackGulp(webpackConfig, webpack, webpackDone) - .pipe(es.through(function (data) { + .pipe(event_stream_1.default.through(function (data) { data.stat = data.stat || {}; data.base = extensionPath; this.emit('data', data); })) - .pipe(es.through(function (data) { + .pipe(event_stream_1.default.through(function (data) { // source map handling: // * rewrite sourceMappingURL // * save to disk so that upload-task picks this up - if (path.extname(data.basename) === '.js') { + if (path_1.default.extname(data.basename) === '.js') { const contents = data.contents.toString('utf8'); data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { - return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; + return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path_1.default.basename(extensionPath)}/${relativeOutputPath}/${g1}`; }), 'utf8'); } this.emit('data', data); })); }); }); - es.merge(...webpackStreams, es.readArray(files)) + event_stream_1.default.merge(...webpackStreams, event_stream_1.default.readArray(files)) // .pipe(es.through(function (data) { // // debug // console.log('out', data.path, data.contents.length); @@ -182,25 +218,25 @@ function fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle) { console.error(packagedDependencies); result.emit('error', err); }); - return result.pipe((0, stats_1.createStatsStream)(path.basename(extensionPath))); + return result.pipe((0, stats_1.createStatsStream)(path_1.default.basename(extensionPath))); } function fromLocalNormal(extensionPath) { const vsce = require('@vscode/vsce'); - const result = es.through(); + const result = event_stream_1.default.through(); vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.Npm }) .then(fileNames => { const files = fileNames - .map(fileName => path.join(extensionPath, fileName)) - .map(filePath => new File({ + .map(fileName => path_1.default.join(extensionPath, fileName)) + .map(filePath => new vinyl_1.default({ path: filePath, - stat: fs.statSync(filePath), + stat: fs_1.default.statSync(filePath), base: extensionPath, - contents: fs.createReadStream(filePath) + contents: fs_1.default.createReadStream(filePath) })); - es.readArray(files).pipe(result); + event_stream_1.default.readArray(files).pipe(result); }) .catch(err => result.emit('error', err)); - return result.pipe((0, stats_1.createStatsStream)(path.basename(extensionPath))); + return result.pipe((0, stats_1.createStatsStream)(path_1.default.basename(extensionPath))); } const userAgent = 'VSCode Build'; const baseHeaders = { @@ -212,8 +248,8 @@ function fromMarketplace(serviceUrl, { name: extensionName, version, sha256, met const json = require('gulp-json-editor'); const [publisher, name] = extensionName.split('.'); const url = `${serviceUrl}/publishers/${publisher}/vsextensions/${name}/${version}/vspackage`; - fancyLog('Downloading extension:', ansiColors.yellow(`${extensionName}@${version}`), '...'); - const packageJsonFilter = filter('package.json', { restore: true }); + (0, fancy_log_1.default)('Downloading extension:', ansi_colors_1.default.yellow(`${extensionName}@${version}`), '...'); + const packageJsonFilter = (0, gulp_filter_1.default)('package.json', { restore: true }); return (0, fetch_1.fetchUrls)('', { base: url, nodeFetchOptions: { @@ -222,28 +258,28 @@ function fromMarketplace(serviceUrl, { name: extensionName, version, sha256, met checksumSha256: sha256 }) .pipe(vzip.src()) - .pipe(filter('extension/**')) - .pipe(rename(p => p.dirname = p.dirname.replace(/^extension\/?/, ''))) + .pipe((0, gulp_filter_1.default)('extension/**')) + .pipe((0, gulp_rename_1.default)(p => p.dirname = p.dirname.replace(/^extension\/?/, ''))) .pipe(packageJsonFilter) - .pipe(buffer()) + .pipe((0, gulp_buffer_1.default)()) .pipe(json({ __metadata: metadata })) .pipe(packageJsonFilter.restore); } function fromGithub({ name, version, repo, sha256, metadata }) { const json = require('gulp-json-editor'); - fancyLog('Downloading extension from GH:', ansiColors.yellow(`${name}@${version}`), '...'); - const packageJsonFilter = filter('package.json', { restore: true }); + (0, fancy_log_1.default)('Downloading extension from GH:', ansi_colors_1.default.yellow(`${name}@${version}`), '...'); + const packageJsonFilter = (0, gulp_filter_1.default)('package.json', { restore: true }); return (0, fetch_1.fetchGithub)(new URL(repo).pathname, { version, name: name => name.endsWith('.vsix'), checksumSha256: sha256 }) - .pipe(buffer()) + .pipe((0, gulp_buffer_1.default)()) .pipe(vzip.src()) - .pipe(filter('extension/**')) - .pipe(rename(p => p.dirname = p.dirname.replace(/^extension\/?/, ''))) + .pipe((0, gulp_filter_1.default)('extension/**')) + .pipe((0, gulp_rename_1.default)(p => p.dirname = p.dirname.replace(/^extension\/?/, ''))) .pipe(packageJsonFilter) - .pipe(buffer()) + .pipe((0, gulp_buffer_1.default)()) .pipe(json({ __metadata: metadata })) .pipe(packageJsonFilter.restore); } @@ -269,7 +305,7 @@ const marketplaceWebExtensionsExclude = new Set([ 'ms-vscode.js-debug', 'ms-vscode.vscode-js-profile-table' ]); -const productJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); +const productJson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../product.json'), 'utf8')); const builtInExtensions = productJson.builtInExtensions || []; const webBuiltInExtensions = productJson.webBuiltInExtensions || []; /** @@ -326,7 +362,7 @@ function packageNativeLocalExtensionsStream(forWeb, disableMangle) { * @returns a stream */ function packageAllLocalExtensionsStream(forWeb, disableMangle) { - return es.merge([ + return event_stream_1.default.merge([ packageNonNativeLocalExtensionsStream(forWeb, disableMangle), packageNativeLocalExtensionsStream(forWeb, disableMangle) ]); @@ -338,20 +374,20 @@ function packageAllLocalExtensionsStream(forWeb, disableMangle) { */ function doPackageLocalExtensionsStream(forWeb, disableMangle, native) { const nativeExtensionsSet = new Set(nativeExtensions); - const localExtensionsDescriptions = (glob.sync('extensions/*/package.json') + const localExtensionsDescriptions = (glob_1.default.sync('extensions/*/package.json') .map(manifestPath => { - const absoluteManifestPath = path.join(root, manifestPath); - const extensionPath = path.dirname(path.join(root, manifestPath)); - const extensionName = path.basename(extensionPath); + const absoluteManifestPath = path_1.default.join(root, manifestPath); + const extensionPath = path_1.default.dirname(path_1.default.join(root, manifestPath)); + const extensionName = path_1.default.basename(extensionPath); return { name: extensionName, path: extensionPath, manifestPath: absoluteManifestPath }; }) .filter(({ name }) => native ? nativeExtensionsSet.has(name) : !nativeExtensionsSet.has(name)) .filter(({ name }) => excludedExtensions.indexOf(name) === -1) .filter(({ name }) => builtInExtensions.every(b => b.name !== name)) .filter(({ manifestPath }) => (forWeb ? isWebExtension(require(manifestPath)) : true))); - const localExtensionsStream = minifyExtensionResources(es.merge(...localExtensionsDescriptions.map(extension => { + const localExtensionsStream = minifyExtensionResources(event_stream_1.default.merge(...localExtensionsDescriptions.map(extension => { return fromLocal(extension.path, forWeb, disableMangle) - .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); + .pipe((0, gulp_rename_1.default)(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); }))); let result; if (forWeb) { @@ -360,10 +396,10 @@ function doPackageLocalExtensionsStream(forWeb, disableMangle, native) { else { // also include shared production node modules const productionDependencies = (0, dependencies_1.getProductionDependencies)('extensions/'); - const dependenciesSrc = productionDependencies.map(d => path.relative(root, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`]).flat(); - result = es.merge(localExtensionsStream, gulp.src(dependenciesSrc, { base: '.' }) - .pipe(util2.cleanNodeModules(path.join(root, 'build', '.moduleignore'))) - .pipe(util2.cleanNodeModules(path.join(root, 'build', `.moduleignore.${process.platform}`)))); + const dependenciesSrc = productionDependencies.map(d => path_1.default.relative(root, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`]).flat(); + result = event_stream_1.default.merge(localExtensionsStream, gulp_1.default.src(dependenciesSrc, { base: '.' }) + .pipe(util2.cleanNodeModules(path_1.default.join(root, 'build', '.moduleignore'))) + .pipe(util2.cleanNodeModules(path_1.default.join(root, 'build', `.moduleignore.${process.platform}`)))); } return (result .pipe(util2.setExecutableBit(['**/*.sh']))); @@ -373,9 +409,9 @@ function packageMarketplaceExtensionsStream(forWeb) { ...builtInExtensions.filter(({ name }) => (forWeb ? !marketplaceWebExtensionsExclude.has(name) : true)), ...(forWeb ? webBuiltInExtensions : []) ]; - const marketplaceExtensionsStream = minifyExtensionResources(es.merge(...marketplaceExtensionsDescriptions + const marketplaceExtensionsStream = minifyExtensionResources(event_stream_1.default.merge(...marketplaceExtensionsDescriptions .map(extension => { - const src = (0, builtInExtensions_1.getExtensionStream)(extension).pipe(rename(p => p.dirname = `extensions/${p.dirname}`)); + const src = (0, builtInExtensions_1.getExtensionStream)(extension).pipe((0, gulp_rename_1.default)(p => p.dirname = `extensions/${p.dirname}`)); return updateExtensionPackageJSON(src, (data) => { delete data.scripts; delete data.dependencies; @@ -389,30 +425,30 @@ function packageMarketplaceExtensionsStream(forWeb) { function scanBuiltinExtensions(extensionsRoot, exclude = []) { const scannedExtensions = []; try { - const extensionsFolders = fs.readdirSync(extensionsRoot); + const extensionsFolders = fs_1.default.readdirSync(extensionsRoot); for (const extensionFolder of extensionsFolders) { if (exclude.indexOf(extensionFolder) >= 0) { continue; } - const packageJSONPath = path.join(extensionsRoot, extensionFolder, 'package.json'); - if (!fs.existsSync(packageJSONPath)) { + const packageJSONPath = path_1.default.join(extensionsRoot, extensionFolder, 'package.json'); + if (!fs_1.default.existsSync(packageJSONPath)) { continue; } - const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath).toString('utf8')); + const packageJSON = JSON.parse(fs_1.default.readFileSync(packageJSONPath).toString('utf8')); if (!isWebExtension(packageJSON)) { continue; } - const children = fs.readdirSync(path.join(extensionsRoot, extensionFolder)); + const children = fs_1.default.readdirSync(path_1.default.join(extensionsRoot, extensionFolder)); const packageNLSPath = children.filter(child => child === 'package.nls.json')[0]; - const packageNLS = packageNLSPath ? JSON.parse(fs.readFileSync(path.join(extensionsRoot, extensionFolder, packageNLSPath)).toString()) : undefined; + const packageNLS = packageNLSPath ? JSON.parse(fs_1.default.readFileSync(path_1.default.join(extensionsRoot, extensionFolder, packageNLSPath)).toString()) : undefined; const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0]; const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0]; scannedExtensions.push({ extensionPath: extensionFolder, packageJSON, packageNLS, - readmePath: readme ? path.join(extensionFolder, readme) : undefined, - changelogPath: changelog ? path.join(extensionFolder, changelog) : undefined, + readmePath: readme ? path_1.default.join(extensionFolder, readme) : undefined, + changelogPath: changelog ? path_1.default.join(extensionFolder, changelog) : undefined, }); } return scannedExtensions; @@ -423,7 +459,7 @@ function scanBuiltinExtensions(extensionsRoot, exclude = []) { } function translatePackageJSON(packageJSON, packageNLSPath) { const CharCode_PC = '%'.charCodeAt(0); - const packageNls = JSON.parse(fs.readFileSync(packageNLSPath).toString()); + const packageNls = JSON.parse(fs_1.default.readFileSync(packageNLSPath).toString()); const translate = (obj) => { for (const key in obj) { const val = obj[key]; @@ -444,7 +480,7 @@ function translatePackageJSON(packageJSON, packageNLSPath) { translate(packageJSON); return packageJSON; } -const extensionsPath = path.join(root, 'extensions'); +const extensionsPath = path_1.default.join(root, 'extensions'); // Additional projects to run esbuild on. These typically build code for webviews const esbuildMediaScripts = [ 'markdown-language-features/esbuild-notebook.js', @@ -463,7 +499,7 @@ async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { for (const configOrFn of Array.isArray(configOrFnOrArray) ? configOrFnOrArray : [configOrFnOrArray]) { const config = typeof configOrFn === 'function' ? configOrFn({}, {}) : configOrFn; if (outputRoot) { - config.output.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output.path)); + config.output.path = path_1.default.join(outputRoot, path_1.default.relative(path_1.default.dirname(configPath), config.output.path)); } webpackConfigs.push(config); } @@ -475,18 +511,18 @@ async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { for (const stats of fullStats.children) { const outputPath = stats.outputPath; if (outputPath) { - const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/'); + const relativePath = path_1.default.relative(extensionsPath, outputPath).replace(/\\/g, '/'); const match = relativePath.match(/[^\/]+(\/server|\/client)?/); - fancyLog(`Finished ${ansiColors.green(taskName)} ${ansiColors.cyan(match[0])} with ${stats.errors.length} errors.`); + (0, fancy_log_1.default)(`Finished ${ansi_colors_1.default.green(taskName)} ${ansi_colors_1.default.cyan(match[0])} with ${stats.errors.length} errors.`); } if (Array.isArray(stats.errors)) { stats.errors.forEach((error) => { - fancyLog.error(error); + fancy_log_1.default.error(error); }); } if (Array.isArray(stats.warnings)) { stats.warnings.forEach((warning) => { - fancyLog.warn(warning); + fancy_log_1.default.warn(warning); }); } } @@ -506,7 +542,7 @@ async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { else { webpack(webpackConfigs).run((err, stats) => { if (err) { - fancyLog.error(err); + fancy_log_1.default.error(err); reject(); } else { @@ -520,9 +556,9 @@ async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { async function esbuildExtensions(taskName, isWatch, scripts) { function reporter(stdError, script) { const matches = (stdError || '').match(/\> (.+): error: (.+)?/g); - fancyLog(`Finished ${ansiColors.green(taskName)} ${script} with ${matches ? matches.length : 0} errors.`); + (0, fancy_log_1.default)(`Finished ${ansi_colors_1.default.green(taskName)} ${script} with ${matches ? matches.length : 0} errors.`); for (const match of matches || []) { - fancyLog.error(match); + fancy_log_1.default.error(match); } } const tasks = scripts.map(({ script, outputRoot }) => { @@ -534,7 +570,7 @@ async function esbuildExtensions(taskName, isWatch, scripts) { if (outputRoot) { args.push('--outputRoot', outputRoot); } - const proc = cp.execFile(process.argv[0], args, {}, (error, _stdout, stderr) => { + const proc = child_process_1.default.execFile(process.argv[0], args, {}, (error, _stdout, stderr) => { if (error) { return reject(error); } @@ -542,7 +578,7 @@ async function esbuildExtensions(taskName, isWatch, scripts) { return resolve(); }); proc.stdout.on('data', (data) => { - fancyLog(`${ansiColors.green(taskName)}: ${data.toString('utf8')}`); + (0, fancy_log_1.default)(`${ansi_colors_1.default.green(taskName)}: ${data.toString('utf8')}`); }); }); }); @@ -550,8 +586,8 @@ async function esbuildExtensions(taskName, isWatch, scripts) { } async function buildExtensionMedia(isWatch, outputRoot) { return esbuildExtensions('esbuilding extension media', isWatch, esbuildMediaScripts.map(p => ({ - script: path.join(extensionsPath, p), - outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined + script: path_1.default.join(extensionsPath, p), + outputRoot: outputRoot ? path_1.default.join(root, outputRoot, path_1.default.dirname(p)) : undefined }))); } //# sourceMappingURL=extensions.js.map \ No newline at end of file diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index a881d3153dab..7ddfbb035877 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -3,24 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; -import * as fs from 'fs'; -import * as cp from 'child_process'; -import * as glob from 'glob'; -import * as gulp from 'gulp'; -import * as path from 'path'; +import es from 'event-stream'; +import fs from 'fs'; +import cp from 'child_process'; +import glob from 'glob'; +import gulp from 'gulp'; +import path from 'path'; import { Stream } from 'stream'; -import * as File from 'vinyl'; +import File from 'vinyl'; import { createStatsStream } from './stats'; import * as util2 from './util'; const vzip = require('gulp-vinyl-zip'); -import filter = require('gulp-filter'); -import rename = require('gulp-rename'); -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; -const buffer = require('gulp-buffer'); +import filter from 'gulp-filter'; +import rename from 'gulp-rename'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; +import buffer from 'gulp-buffer'; import * as jsoncParser from 'jsonc-parser'; -import webpack = require('webpack'); +import webpack from 'webpack'; import { getProductionDependencies } from './dependencies'; import { IExtensionDefinition, getExtensionStream } from './builtInExtensions'; import { getVersion } from './getVersion'; diff --git a/build/lib/fetch.js b/build/lib/fetch.js index b7da65f4af24..078706cdd000 100644 --- a/build/lib/fetch.js +++ b/build/lib/fetch.js @@ -3,16 +3,19 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.fetchUrls = fetchUrls; exports.fetchUrl = fetchUrl; exports.fetchGithub = fetchGithub; -const es = require("event-stream"); -const VinylFile = require("vinyl"); -const log = require("fancy-log"); -const ansiColors = require("ansi-colors"); -const crypto = require("crypto"); -const through2 = require("through2"); +const event_stream_1 = __importDefault(require("event-stream")); +const vinyl_1 = __importDefault(require("vinyl")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const crypto_1 = __importDefault(require("crypto")); +const through2_1 = __importDefault(require("through2")); function fetchUrls(urls, options) { if (options === undefined) { options = {}; @@ -23,7 +26,7 @@ function fetchUrls(urls, options) { if (!Array.isArray(urls)) { urls = [urls]; } - return es.readArray(urls).pipe(es.map((data, cb) => { + return event_stream_1.default.readArray(urls).pipe(event_stream_1.default.map((data, cb) => { const url = [options.base, data].join(''); fetchUrl(url, options).then(file => { cb(undefined, file); @@ -37,7 +40,7 @@ async function fetchUrl(url, options, retries = 10, retryDelay = 1000) { try { let startTime = 0; if (verbose) { - log(`Start fetching ${ansiColors.magenta(url)}${retries !== 10 ? ` (${10 - retries} retry)` : ''}`); + (0, fancy_log_1.default)(`Start fetching ${ansi_colors_1.default.magenta(url)}${retries !== 10 ? ` (${10 - retries} retry)` : ''}`); startTime = new Date().getTime(); } const controller = new AbortController(); @@ -48,33 +51,33 @@ async function fetchUrl(url, options, retries = 10, retryDelay = 1000) { signal: controller.signal /* Typings issue with lib.dom.d.ts */ }); if (verbose) { - log(`Fetch completed: Status ${response.status}. Took ${ansiColors.magenta(`${new Date().getTime() - startTime} ms`)}`); + (0, fancy_log_1.default)(`Fetch completed: Status ${response.status}. Took ${ansi_colors_1.default.magenta(`${new Date().getTime() - startTime} ms`)}`); } if (response.ok && (response.status >= 200 && response.status < 300)) { const contents = Buffer.from(await response.arrayBuffer()); if (options.checksumSha256) { - const actualSHA256Checksum = crypto.createHash('sha256').update(contents).digest('hex'); + const actualSHA256Checksum = crypto_1.default.createHash('sha256').update(contents).digest('hex'); if (actualSHA256Checksum !== options.checksumSha256) { - throw new Error(`Checksum mismatch for ${ansiColors.cyan(url)} (expected ${options.checksumSha256}, actual ${actualSHA256Checksum}))`); + throw new Error(`Checksum mismatch for ${ansi_colors_1.default.cyan(url)} (expected ${options.checksumSha256}, actual ${actualSHA256Checksum}))`); } else if (verbose) { - log(`Verified SHA256 checksums match for ${ansiColors.cyan(url)}`); + (0, fancy_log_1.default)(`Verified SHA256 checksums match for ${ansi_colors_1.default.cyan(url)}`); } } else if (verbose) { - log(`Skipping checksum verification for ${ansiColors.cyan(url)} because no expected checksum was provided`); + (0, fancy_log_1.default)(`Skipping checksum verification for ${ansi_colors_1.default.cyan(url)} because no expected checksum was provided`); } if (verbose) { - log(`Fetched response body buffer: ${ansiColors.magenta(`${contents.byteLength} bytes`)}`); + (0, fancy_log_1.default)(`Fetched response body buffer: ${ansi_colors_1.default.magenta(`${contents.byteLength} bytes`)}`); } - return new VinylFile({ + return new vinyl_1.default({ cwd: '/', base: options.base, path: url, contents }); } - let err = `Request ${ansiColors.magenta(url)} failed with status code: ${response.status}`; + let err = `Request ${ansi_colors_1.default.magenta(url)} failed with status code: ${response.status}`; if (response.status === 403) { err += ' (you may be rate limited)'; } @@ -86,7 +89,7 @@ async function fetchUrl(url, options, retries = 10, retryDelay = 1000) { } catch (e) { if (verbose) { - log(`Fetching ${ansiColors.cyan(url)} failed: ${e}`); + (0, fancy_log_1.default)(`Fetching ${ansi_colors_1.default.cyan(url)} failed: ${e}`); } if (retries > 0) { await new Promise(resolve => setTimeout(resolve, retryDelay)); @@ -117,7 +120,7 @@ function fetchGithub(repo, options) { base: 'https://api.github.com', verbose: options.verbose, nodeFetchOptions: { headers: ghApiHeaders } - }).pipe(through2.obj(async function (file, _enc, callback) { + }).pipe(through2_1.default.obj(async function (file, _enc, callback) { const assetFilter = typeof options.name === 'string' ? (name) => name === options.name : options.name; const asset = JSON.parse(file.contents.toString()).assets.find((a) => assetFilter(a.name)); if (!asset) { diff --git a/build/lib/fetch.ts b/build/lib/fetch.ts index 0c44b8e567f1..47a65b88fb53 100644 --- a/build/lib/fetch.ts +++ b/build/lib/fetch.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; -import * as VinylFile from 'vinyl'; -import * as log from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; -import * as crypto from 'crypto'; -import * as through2 from 'through2'; +import es from 'event-stream'; +import VinylFile from 'vinyl'; +import log from 'fancy-log'; +import ansiColors from 'ansi-colors'; +import crypto from 'crypto'; +import through2 from 'through2'; import { Stream } from 'stream'; export interface IFetchOptions { diff --git a/build/lib/formatter.js b/build/lib/formatter.js index 29f265c8289d..1085ea8f4889 100644 --- a/build/lib/formatter.js +++ b/build/lib/formatter.js @@ -1,17 +1,20 @@ "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.format = format; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const fs = require("fs"); -const path = require("path"); -const ts = require("typescript"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const typescript_1 = __importDefault(require("typescript")); class LanguageServiceHost { files = {}; addFile(fileName, text) { - this.files[fileName] = ts.ScriptSnapshot.fromString(text); + this.files[fileName] = typescript_1.default.ScriptSnapshot.fromString(text); } fileExists(path) { return !!this.files[path]; @@ -20,18 +23,18 @@ class LanguageServiceHost { return this.files[path]?.getText(0, this.files[path].getLength()); } // for ts.LanguageServiceHost - getCompilationSettings = () => ts.getDefaultCompilerOptions(); + getCompilationSettings = () => typescript_1.default.getDefaultCompilerOptions(); getScriptFileNames = () => Object.keys(this.files); getScriptVersion = (_fileName) => '0'; getScriptSnapshot = (fileName) => this.files[fileName]; getCurrentDirectory = () => process.cwd(); - getDefaultLibFileName = (options) => ts.getDefaultLibFilePath(options); + getDefaultLibFileName = (options) => typescript_1.default.getDefaultLibFilePath(options); } const defaults = { baseIndentSize: 0, indentSize: 4, tabSize: 4, - indentStyle: ts.IndentStyle.Smart, + indentStyle: typescript_1.default.IndentStyle.Smart, newLineCharacter: '\r\n', convertTabsToSpaces: false, insertSpaceAfterCommaDelimiter: true, @@ -54,14 +57,14 @@ const defaults = { const getOverrides = (() => { let value; return () => { - value ??= JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'tsfmt.json'), 'utf8')); + value ??= JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', '..', 'tsfmt.json'), 'utf8')); return value; }; })(); function format(fileName, text) { const host = new LanguageServiceHost(); host.addFile(fileName, text); - const languageService = ts.createLanguageService(host); + const languageService = typescript_1.default.createLanguageService(host); const edits = languageService.getFormattingEditsForDocument(fileName, { ...defaults, ...getOverrides() }); edits .sort((a, b) => a.span.start - b.span.start) diff --git a/build/lib/formatter.ts b/build/lib/formatter.ts index 0d9035b3d87c..993722e5f924 100644 --- a/build/lib/formatter.ts +++ b/build/lib/formatter.ts @@ -2,9 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as ts from 'typescript'; +import fs from 'fs'; +import path from 'path'; +import ts from 'typescript'; class LanguageServiceHost implements ts.LanguageServiceHost { diff --git a/build/lib/getVersion.js b/build/lib/getVersion.js index b50ead538a25..7606c17ab14f 100644 --- a/build/lib/getVersion.js +++ b/build/lib/getVersion.js @@ -3,9 +3,42 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); Object.defineProperty(exports, "__esModule", { value: true }); exports.getVersion = getVersion; -const git = require("./git"); +const git = __importStar(require("./git")); function getVersion(root) { let version = process.env['BUILD_SOURCEVERSION']; if (!version || !/^[0-9a-f]{40}$/i.test(version.trim())) { diff --git a/build/lib/git.js b/build/lib/git.js index 798a408bdb91..30de97ed6e36 100644 --- a/build/lib/git.js +++ b/build/lib/git.js @@ -1,21 +1,24 @@ "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.getVersion = getVersion; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const path = require("path"); -const fs = require("fs"); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); /** * Returns the sha1 commit version of a repository or undefined in case of failure. */ function getVersion(repo) { - const git = path.join(repo, '.git'); - const headPath = path.join(git, 'HEAD'); + const git = path_1.default.join(repo, '.git'); + const headPath = path_1.default.join(git, 'HEAD'); let head; try { - head = fs.readFileSync(headPath, 'utf8').trim(); + head = fs_1.default.readFileSync(headPath, 'utf8').trim(); } catch (e) { return undefined; @@ -28,17 +31,17 @@ function getVersion(repo) { return undefined; } const ref = refMatch[1]; - const refPath = path.join(git, ref); + const refPath = path_1.default.join(git, ref); try { - return fs.readFileSync(refPath, 'utf8').trim(); + return fs_1.default.readFileSync(refPath, 'utf8').trim(); } catch (e) { // noop } - const packedRefsPath = path.join(git, 'packed-refs'); + const packedRefsPath = path_1.default.join(git, 'packed-refs'); let refsRaw; try { - refsRaw = fs.readFileSync(packedRefsPath, 'utf8').trim(); + refsRaw = fs_1.default.readFileSync(packedRefsPath, 'utf8').trim(); } catch (e) { return undefined; diff --git a/build/lib/git.ts b/build/lib/git.ts index dbb424f21df7..a3c23d8c29b3 100644 --- a/build/lib/git.ts +++ b/build/lib/git.ts @@ -2,8 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as fs from 'fs'; +import path from 'path'; +import fs from 'fs'; /** * Returns the sha1 commit version of a repository or undefined in case of failure. diff --git a/build/lib/i18n.js b/build/lib/i18n.js index 6964616291b3..9483d319a50b 100644 --- a/build/lib/i18n.js +++ b/build/lib/i18n.js @@ -3,6 +3,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.EXTERNAL_EXTENSIONS = exports.XLF = exports.Line = exports.extraLanguages = exports.defaultLanguages = void 0; exports.processNlsFiles = processNlsFiles; @@ -12,20 +15,20 @@ exports.createXlfFilesForExtensions = createXlfFilesForExtensions; exports.createXlfFilesForIsl = createXlfFilesForIsl; exports.prepareI18nPackFiles = prepareI18nPackFiles; exports.prepareIslFiles = prepareIslFiles; -const path = require("path"); -const fs = require("fs"); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); const event_stream_1 = require("event-stream"); -const jsonMerge = require("gulp-merge-json"); -const File = require("vinyl"); -const xml2js = require("xml2js"); -const gulp = require("gulp"); -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); -const iconv = require("@vscode/iconv-lite-umd"); +const gulp_merge_json_1 = __importDefault(require("gulp-merge-json")); +const vinyl_1 = __importDefault(require("vinyl")); +const xml2js_1 = __importDefault(require("xml2js")); +const gulp_1 = __importDefault(require("gulp")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const iconv_lite_umd_1 = __importDefault(require("@vscode/iconv-lite-umd")); const l10n_dev_1 = require("@vscode/l10n-dev"); -const REPO_ROOT_PATH = path.join(__dirname, '../..'); +const REPO_ROOT_PATH = path_1.default.join(__dirname, '../..'); function log(message, ...rest) { - fancyLog(ansiColors.green('[i18n]'), message, ...rest); + (0, fancy_log_1.default)(ansi_colors_1.default.green('[i18n]'), message, ...rest); } exports.defaultLanguages = [ { id: 'zh-tw', folderName: 'cht', translationId: 'zh-hant' }, @@ -188,7 +191,7 @@ class XLF { } static parse = function (xlfString) { return new Promise((resolve, reject) => { - const parser = new xml2js.Parser(); + const parser = new xml2js_1.default.Parser(); const files = []; parser.parseString(xlfString, function (err, result) { if (err) { @@ -278,8 +281,8 @@ function stripComments(content) { return result; } function processCoreBundleFormat(base, fileHeader, languages, json, emitter) { - const languageDirectory = path.join(REPO_ROOT_PATH, '..', 'vscode-loc', 'i18n'); - if (!fs.existsSync(languageDirectory)) { + const languageDirectory = path_1.default.join(REPO_ROOT_PATH, '..', 'vscode-loc', 'i18n'); + if (!fs_1.default.existsSync(languageDirectory)) { log(`No VS Code localization repository found. Looking at ${languageDirectory}`); log(`To bundle translations please check out the vscode-loc repository as a sibling of the vscode repository.`); } @@ -289,10 +292,10 @@ function processCoreBundleFormat(base, fileHeader, languages, json, emitter) { log(`Generating nls bundles for: ${language.id}`); } const languageFolderName = language.translationId || language.id; - const i18nFile = path.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json'); + const i18nFile = path_1.default.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json'); let allMessages; - if (fs.existsSync(i18nFile)) { - const content = stripComments(fs.readFileSync(i18nFile, 'utf8')); + if (fs_1.default.existsSync(i18nFile)) { + const content = stripComments(fs_1.default.readFileSync(i18nFile, 'utf8')); allMessages = JSON.parse(content); } let nlsIndex = 0; @@ -304,7 +307,7 @@ function processCoreBundleFormat(base, fileHeader, languages, json, emitter) { nlsIndex++; } } - emitter.queue(new File({ + emitter.queue(new vinyl_1.default({ contents: Buffer.from(`${fileHeader} globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(nlsResult)}; globalThis._VSCODE_NLS_LANGUAGE=${JSON.stringify(language.id)};`), @@ -315,10 +318,10 @@ globalThis._VSCODE_NLS_LANGUAGE=${JSON.stringify(language.id)};`), } function processNlsFiles(opts) { return (0, event_stream_1.through)(function (file) { - const fileName = path.basename(file.path); + const fileName = path_1.default.basename(file.path); if (fileName === 'bundleInfo.json') { // pick a root level file to put the core bundles (TODO@esm this file is not created anymore, pick another) try { - const json = JSON.parse(fs.readFileSync(path.join(REPO_ROOT_PATH, opts.out, 'nls.keys.json')).toString()); + const json = JSON.parse(fs_1.default.readFileSync(path_1.default.join(REPO_ROOT_PATH, opts.out, 'nls.keys.json')).toString()); if (NLSKeysFormat.is(json)) { processCoreBundleFormat(file.base, opts.fileHeader, opts.languages, json, this); } @@ -366,7 +369,7 @@ function getResource(sourceFile) { } function createXlfFilesForCoreBundle() { return (0, event_stream_1.through)(function (file) { - const basename = path.basename(file.path); + const basename = path_1.default.basename(file.path); if (basename === 'nls.metadata.json') { if (file.isBuffer()) { const xlfs = Object.create(null); @@ -393,7 +396,7 @@ function createXlfFilesForCoreBundle() { for (const resource in xlfs) { const xlf = xlfs[resource]; const filePath = `${xlf.project}/${resource.replace(/\//g, '_')}.xlf`; - const xlfFile = new File({ + const xlfFile = new vinyl_1.default({ path: filePath, contents: Buffer.from(xlf.toString(), 'utf8') }); @@ -413,7 +416,7 @@ function createXlfFilesForCoreBundle() { } function createL10nBundleForExtension(extensionFolderName, prefixWithBuildFolder) { const prefix = prefixWithBuildFolder ? '.build/' : ''; - return gulp + return gulp_1.default .src([ // For source code of extensions `${prefix}extensions/${extensionFolderName}/{src,client,server}/**/*.{ts,tsx}`, @@ -429,12 +432,12 @@ function createL10nBundleForExtension(extensionFolderName, prefixWithBuildFolder callback(); return; } - const extension = path.extname(file.relative); + const extension = path_1.default.extname(file.relative); if (extension !== '.json') { const contents = file.contents.toString('utf8'); (0, l10n_dev_1.getL10nJson)([{ contents, extension }]) .then((json) => { - callback(undefined, new File({ + callback(undefined, new vinyl_1.default({ path: `extensions/${extensionFolderName}/bundle.l10n.json`, contents: Buffer.from(JSON.stringify(json), 'utf8') })); @@ -464,7 +467,7 @@ function createL10nBundleForExtension(extensionFolderName, prefixWithBuildFolder } callback(undefined, file); })) - .pipe(jsonMerge({ + .pipe((0, gulp_merge_json_1.default)({ fileName: `extensions/${extensionFolderName}/bundle.l10n.json`, jsonSpace: '', concatArrays: true @@ -481,16 +484,16 @@ function createXlfFilesForExtensions() { let folderStreamEndEmitted = false; return (0, event_stream_1.through)(function (extensionFolder) { const folderStream = this; - const stat = fs.statSync(extensionFolder.path); + const stat = fs_1.default.statSync(extensionFolder.path); if (!stat.isDirectory()) { return; } - const extensionFolderName = path.basename(extensionFolder.path); + const extensionFolderName = path_1.default.basename(extensionFolder.path); if (extensionFolderName === 'node_modules') { return; } // Get extension id and use that as the id - const manifest = fs.readFileSync(path.join(extensionFolder.path, 'package.json'), 'utf-8'); + const manifest = fs_1.default.readFileSync(path_1.default.join(extensionFolder.path, 'package.json'), 'utf-8'); const manifestJson = JSON.parse(manifest); const extensionId = manifestJson.publisher + '.' + manifestJson.name; counter++; @@ -501,17 +504,17 @@ function createXlfFilesForExtensions() { } return _l10nMap; } - (0, event_stream_1.merge)(gulp.src([`.build/extensions/${extensionFolderName}/package.nls.json`, `.build/extensions/${extensionFolderName}/**/nls.metadata.json`], { allowEmpty: true }), createL10nBundleForExtension(extensionFolderName, exports.EXTERNAL_EXTENSIONS.includes(extensionId))).pipe((0, event_stream_1.through)(function (file) { + (0, event_stream_1.merge)(gulp_1.default.src([`.build/extensions/${extensionFolderName}/package.nls.json`, `.build/extensions/${extensionFolderName}/**/nls.metadata.json`], { allowEmpty: true }), createL10nBundleForExtension(extensionFolderName, exports.EXTERNAL_EXTENSIONS.includes(extensionId))).pipe((0, event_stream_1.through)(function (file) { if (file.isBuffer()) { const buffer = file.contents; - const basename = path.basename(file.path); + const basename = path_1.default.basename(file.path); if (basename === 'package.nls.json') { const json = JSON.parse(buffer.toString('utf8')); getL10nMap().set(`extensions/${extensionId}/package`, json); } else if (basename === 'nls.metadata.json') { const json = JSON.parse(buffer.toString('utf8')); - const relPath = path.relative(`.build/extensions/${extensionFolderName}`, path.dirname(file.path)); + const relPath = path_1.default.relative(`.build/extensions/${extensionFolderName}`, path_1.default.dirname(file.path)); for (const file in json) { const fileContent = json[file]; const info = Object.create(null); @@ -536,8 +539,8 @@ function createXlfFilesForExtensions() { } }, function () { if (_l10nMap?.size > 0) { - const xlfFile = new File({ - path: path.join(extensionsProject, extensionId + '.xlf'), + const xlfFile = new vinyl_1.default({ + path: path_1.default.join(extensionsProject, extensionId + '.xlf'), contents: Buffer.from((0, l10n_dev_1.getL10nXlf)(_l10nMap), 'utf8') }); folderStream.queue(xlfFile); @@ -560,7 +563,7 @@ function createXlfFilesForExtensions() { function createXlfFilesForIsl() { return (0, event_stream_1.through)(function (file) { let projectName, resourceFile; - if (path.basename(file.path) === 'messages.en.isl') { + if (path_1.default.basename(file.path) === 'messages.en.isl') { projectName = setupProject; resourceFile = 'messages.xlf'; } @@ -602,8 +605,8 @@ function createXlfFilesForIsl() { const originalPath = file.path.substring(file.cwd.length + 1, file.path.split('.')[0].length).replace(/\\/g, '/'); xlf.addFile(originalPath, keys, messages); // Emit only upon all ISL files combined into single XLF instance - const newFilePath = path.join(projectName, resourceFile); - const xlfFile = new File({ path: newFilePath, contents: Buffer.from(xlf.toString(), 'utf-8') }); + const newFilePath = path_1.default.join(projectName, resourceFile); + const xlfFile = new vinyl_1.default({ path: newFilePath, contents: Buffer.from(xlf.toString(), 'utf-8') }); this.queue(xlfFile); }); } @@ -623,8 +626,8 @@ function createI18nFile(name, messages) { if (process.platform === 'win32') { content = content.replace(/\n/g, '\r\n'); } - return new File({ - path: path.join(name + '.i18n.json'), + return new vinyl_1.default({ + path: path_1.default.join(name + '.i18n.json'), contents: Buffer.from(content, 'utf8') }); } @@ -643,9 +646,9 @@ function prepareI18nPackFiles(resultingTranslationPaths) { const extensionsPacks = {}; const errors = []; return (0, event_stream_1.through)(function (xlf) { - let project = path.basename(path.dirname(path.dirname(xlf.relative))); + let project = path_1.default.basename(path_1.default.dirname(path_1.default.dirname(xlf.relative))); // strip `-new` since vscode-extensions-loc uses the `-new` suffix to indicate that it's from the new loc pipeline - const resource = path.basename(path.basename(xlf.relative, '.xlf'), '-new'); + const resource = path_1.default.basename(path_1.default.basename(xlf.relative, '.xlf'), '-new'); if (exports.EXTERNAL_EXTENSIONS.find(e => e === resource)) { project = extensionsProject; } @@ -720,11 +723,11 @@ function prepareIslFiles(language, innoSetupConfig) { function createIslFile(name, messages, language, innoSetup) { const content = []; let originalContent; - if (path.basename(name) === 'Default') { - originalContent = new TextModel(fs.readFileSync(name + '.isl', 'utf8')); + if (path_1.default.basename(name) === 'Default') { + originalContent = new TextModel(fs_1.default.readFileSync(name + '.isl', 'utf8')); } else { - originalContent = new TextModel(fs.readFileSync(name + '.en.isl', 'utf8')); + originalContent = new TextModel(fs_1.default.readFileSync(name + '.en.isl', 'utf8')); } originalContent.lines.forEach(line => { if (line.length > 0) { @@ -746,10 +749,10 @@ function createIslFile(name, messages, language, innoSetup) { } } }); - const basename = path.basename(name); + const basename = path_1.default.basename(name); const filePath = `${basename}.${language.id}.isl`; - const encoded = iconv.encode(Buffer.from(content.join('\r\n'), 'utf8').toString(), innoSetup.codePage); - return new File({ + const encoded = iconv_lite_umd_1.default.encode(Buffer.from(content.join('\r\n'), 'utf8').toString(), innoSetup.codePage); + return new vinyl_1.default({ path: filePath, contents: Buffer.from(encoded), }); diff --git a/build/lib/i18n.ts b/build/lib/i18n.ts index cd7e522ad361..d2904ccf0fb5 100644 --- a/build/lib/i18n.ts +++ b/build/lib/i18n.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as fs from 'fs'; +import path from 'path'; +import fs from 'fs'; import { map, merge, through, ThroughStream } from 'event-stream'; -import * as jsonMerge from 'gulp-merge-json'; -import * as File from 'vinyl'; -import * as xml2js from 'xml2js'; -import * as gulp from 'gulp'; -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; -import * as iconv from '@vscode/iconv-lite-umd'; +import jsonMerge from 'gulp-merge-json'; +import File from 'vinyl'; +import xml2js from 'xml2js'; +import gulp from 'gulp'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; +import iconv from '@vscode/iconv-lite-umd'; import { l10nJsonFormat, getL10nXlf, l10nJsonDetails, getL10nFilesFromXlf, getL10nJson } from '@vscode/l10n-dev'; const REPO_ROOT_PATH = path.join(__dirname, '../..'); diff --git a/build/lib/inlineMeta.js b/build/lib/inlineMeta.js index 5ec7e9e9c078..3b473ae091e1 100644 --- a/build/lib/inlineMeta.js +++ b/build/lib/inlineMeta.js @@ -3,9 +3,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.inlineMeta = inlineMeta; -const es = require("event-stream"); +const event_stream_1 = __importDefault(require("event-stream")); const path_1 = require("path"); const packageJsonMarkerId = 'BUILD_INSERT_PACKAGE_CONFIGURATION'; // TODO in order to inline `product.json`, more work is @@ -16,7 +19,7 @@ const packageJsonMarkerId = 'BUILD_INSERT_PACKAGE_CONFIGURATION'; // - a `target` is added in `gulpfile.vscode.win32.js` // const productJsonMarkerId = 'BUILD_INSERT_PRODUCT_CONFIGURATION'; function inlineMeta(result, ctx) { - return result.pipe(es.through(function (file) { + return result.pipe(event_stream_1.default.through(function (file) { if (matchesFile(file, ctx)) { let content = file.contents.toString(); let markerFound = false; diff --git a/build/lib/inlineMeta.ts b/build/lib/inlineMeta.ts index dc061aca8d17..2a0db13d06e2 100644 --- a/build/lib/inlineMeta.ts +++ b/build/lib/inlineMeta.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; +import es from 'event-stream'; import { basename } from 'path'; -import * as File from 'vinyl'; +import File from 'vinyl'; export interface IInlineMetaContext { readonly targetPaths: string[]; diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index e52525bf61d1..5cf5c58402c7 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -3,8 +3,11 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const ts = require("typescript"); +const typescript_1 = __importDefault(require("typescript")); const fs_1 = require("fs"); const path_1 = require("path"); const minimatch_1 = require("minimatch"); @@ -295,8 +298,8 @@ let hasErrors = false; function checkFile(program, sourceFile, rule) { checkNode(sourceFile); function checkNode(node) { - if (node.kind !== ts.SyntaxKind.Identifier) { - return ts.forEachChild(node, checkNode); // recurse down + if (node.kind !== typescript_1.default.SyntaxKind.Identifier) { + return typescript_1.default.forEachChild(node, checkNode); // recurse down } const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(node); @@ -352,11 +355,11 @@ function checkFile(program, sourceFile, rule) { } } function createProgram(tsconfigPath) { - const tsConfig = ts.readConfigFile(tsconfigPath, ts.sys.readFile); - const configHostParser = { fileExists: fs_1.existsSync, readDirectory: ts.sys.readDirectory, readFile: file => (0, fs_1.readFileSync)(file, 'utf8'), useCaseSensitiveFileNames: process.platform === 'linux' }; - const tsConfigParsed = ts.parseJsonConfigFileContent(tsConfig.config, configHostParser, (0, path_1.resolve)((0, path_1.dirname)(tsconfigPath)), { noEmit: true }); - const compilerHost = ts.createCompilerHost(tsConfigParsed.options, true); - return ts.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost); + const tsConfig = typescript_1.default.readConfigFile(tsconfigPath, typescript_1.default.sys.readFile); + const configHostParser = { fileExists: fs_1.existsSync, readDirectory: typescript_1.default.sys.readDirectory, readFile: file => (0, fs_1.readFileSync)(file, 'utf8'), useCaseSensitiveFileNames: process.platform === 'linux' }; + const tsConfigParsed = typescript_1.default.parseJsonConfigFileContent(tsConfig.config, configHostParser, (0, path_1.resolve)((0, path_1.dirname)(tsconfigPath)), { noEmit: true }); + const compilerHost = typescript_1.default.createCompilerHost(tsConfigParsed.options, true); + return typescript_1.default.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost); } // // Create program and start checking diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index e16a775b23e9..633773289282 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as ts from 'typescript'; +import ts from 'typescript'; import { readFileSync, existsSync } from 'fs'; import { resolve, dirname, join } from 'path'; import { match } from 'minimatch'; diff --git a/build/lib/mangle/index.js b/build/lib/mangle/index.js index 1c2c8cc3dd39..b93003221a4f 100644 --- a/build/lib/mangle/index.js +++ b/build/lib/mangle/index.js @@ -3,16 +3,19 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.Mangler = void 0; -const v8 = require("node:v8"); -const fs = require("fs"); -const path = require("path"); +const node_v8_1 = __importDefault(require("node:v8")); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); const process_1 = require("process"); const source_map_1 = require("source-map"); -const ts = require("typescript"); +const typescript_1 = __importDefault(require("typescript")); const url_1 = require("url"); -const workerpool = require("workerpool"); +const workerpool_1 = __importDefault(require("workerpool")); const staticLanguageServiceHost_1 = require("./staticLanguageServiceHost"); const buildfile = require('../../buildfile'); class ShortIdent { @@ -66,29 +69,29 @@ class ClassData { this.node = node; const candidates = []; for (const member of node.members) { - if (ts.isMethodDeclaration(member)) { + if (typescript_1.default.isMethodDeclaration(member)) { // method `foo() {}` candidates.push(member); } - else if (ts.isPropertyDeclaration(member)) { + else if (typescript_1.default.isPropertyDeclaration(member)) { // property `foo = 234` candidates.push(member); } - else if (ts.isGetAccessor(member)) { + else if (typescript_1.default.isGetAccessor(member)) { // getter: `get foo() { ... }` candidates.push(member); } - else if (ts.isSetAccessor(member)) { + else if (typescript_1.default.isSetAccessor(member)) { // setter: `set foo() { ... }` candidates.push(member); } - else if (ts.isConstructorDeclaration(member)) { + else if (typescript_1.default.isConstructorDeclaration(member)) { // constructor-prop:`constructor(private foo) {}` for (const param of member.parameters) { - if (hasModifier(param, ts.SyntaxKind.PrivateKeyword) - || hasModifier(param, ts.SyntaxKind.ProtectedKeyword) - || hasModifier(param, ts.SyntaxKind.PublicKeyword) - || hasModifier(param, ts.SyntaxKind.ReadonlyKeyword)) { + if (hasModifier(param, typescript_1.default.SyntaxKind.PrivateKeyword) + || hasModifier(param, typescript_1.default.SyntaxKind.ProtectedKeyword) + || hasModifier(param, typescript_1.default.SyntaxKind.PublicKeyword) + || hasModifier(param, typescript_1.default.SyntaxKind.ReadonlyKeyword)) { candidates.push(param); } } @@ -109,8 +112,8 @@ class ClassData { } const { name } = node; let ident = name.getText(); - if (name.kind === ts.SyntaxKind.ComputedPropertyName) { - if (name.expression.kind !== ts.SyntaxKind.StringLiteral) { + if (name.kind === typescript_1.default.SyntaxKind.ComputedPropertyName) { + if (name.expression.kind !== typescript_1.default.SyntaxKind.StringLiteral) { // unsupported: [Symbol.foo] or [abc + 'field'] return; } @@ -120,10 +123,10 @@ class ClassData { return ident; } static _getFieldType(node) { - if (hasModifier(node, ts.SyntaxKind.PrivateKeyword)) { + if (hasModifier(node, typescript_1.default.SyntaxKind.PrivateKeyword)) { return 2 /* FieldType.Private */; } - else if (hasModifier(node, ts.SyntaxKind.ProtectedKeyword)) { + else if (hasModifier(node, typescript_1.default.SyntaxKind.ProtectedKeyword)) { return 1 /* FieldType.Protected */; } else { @@ -302,7 +305,7 @@ class DeclarationData { this.replacementName = fileIdents.next(); } getLocations(service) { - if (ts.isVariableDeclaration(this.node)) { + if (typescript_1.default.isVariableDeclaration(this.node)) { // If the const aliases any types, we need to rename those too const definitionResult = service.getDefinitionAndBoundSpan(this.fileName, this.node.name.getStart()); if (definitionResult?.definitions && definitionResult.definitions.length > 1) { @@ -350,20 +353,20 @@ class Mangler { this.projectPath = projectPath; this.log = log; this.config = config; - this.renameWorkerPool = workerpool.pool(path.join(__dirname, 'renameWorker.js'), { + this.renameWorkerPool = workerpool_1.default.pool(path_1.default.join(__dirname, 'renameWorker.js'), { maxWorkers: 1, minWorkers: 'max' }); } async computeNewFileContents(strictImplicitPublicHandling) { - const service = ts.createLanguageService(new staticLanguageServiceHost_1.StaticLanguageServiceHost(this.projectPath)); + const service = typescript_1.default.createLanguageService(new staticLanguageServiceHost_1.StaticLanguageServiceHost(this.projectPath)); // STEP: // - Find all classes and their field info. // - Find exported symbols. const fileIdents = new ShortIdent('$'); const visit = (node) => { if (this.config.manglePrivateFields) { - if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { + if (typescript_1.default.isClassDeclaration(node) || typescript_1.default.isClassExpression(node)) { const anchor = node.name ?? node; const key = `${node.getSourceFile().fileName}|${anchor.getStart()}`; if (this.allClassDataByKey.has(key)) { @@ -376,19 +379,19 @@ class Mangler { // Find exported classes, functions, and vars if (( // Exported class - ts.isClassDeclaration(node) - && hasModifier(node, ts.SyntaxKind.ExportKeyword) + typescript_1.default.isClassDeclaration(node) + && hasModifier(node, typescript_1.default.SyntaxKind.ExportKeyword) && node.name) || ( // Exported function - ts.isFunctionDeclaration(node) - && ts.isSourceFile(node.parent) - && hasModifier(node, ts.SyntaxKind.ExportKeyword) + typescript_1.default.isFunctionDeclaration(node) + && typescript_1.default.isSourceFile(node.parent) + && hasModifier(node, typescript_1.default.SyntaxKind.ExportKeyword) && node.name && node.body // On named function and not on the overload ) || ( // Exported variable - ts.isVariableDeclaration(node) - && hasModifier(node.parent.parent, ts.SyntaxKind.ExportKeyword) // Variable statement is exported - && ts.isSourceFile(node.parent.parent.parent)) + typescript_1.default.isVariableDeclaration(node) + && hasModifier(node.parent.parent, typescript_1.default.SyntaxKind.ExportKeyword) // Variable statement is exported + && typescript_1.default.isSourceFile(node.parent.parent.parent)) // Disabled for now because we need to figure out how to handle // enums that are used in monaco or extHost interfaces. /* || ( @@ -406,17 +409,17 @@ class Mangler { this.allExportedSymbols.add(new DeclarationData(node.getSourceFile().fileName, node, fileIdents)); } } - ts.forEachChild(node, visit); + typescript_1.default.forEachChild(node, visit); }; for (const file of service.getProgram().getSourceFiles()) { if (!file.isDeclarationFile) { - ts.forEachChild(file, visit); + typescript_1.default.forEachChild(file, visit); } } this.log(`Done collecting. Classes: ${this.allClassDataByKey.size}. Exported symbols: ${this.allExportedSymbols.size}`); // STEP: connect sub and super-types const setupParents = (data) => { - const extendsClause = data.node.heritageClauses?.find(h => h.token === ts.SyntaxKind.ExtendsKeyword); + const extendsClause = data.node.heritageClauses?.find(h => h.token === typescript_1.default.SyntaxKind.ExtendsKeyword); if (!extendsClause) { // no EXTENDS-clause return; @@ -497,7 +500,7 @@ class Mangler { .then((locations) => ({ newName, locations }))); }; for (const data of this.allClassDataByKey.values()) { - if (hasModifier(data.node, ts.SyntaxKind.DeclareKeyword)) { + if (hasModifier(data.node, typescript_1.default.SyntaxKind.DeclareKeyword)) { continue; } fields: for (const [name, info] of data.fields) { @@ -545,7 +548,7 @@ class Mangler { let savedBytes = 0; for (const item of service.getProgram().getSourceFiles()) { const { mapRoot, sourceRoot } = service.getProgram().getCompilerOptions(); - const projectDir = path.dirname(this.projectPath); + const projectDir = path_1.default.dirname(this.projectPath); const sourceMapRoot = mapRoot ?? (0, url_1.pathToFileURL)(sourceRoot ?? projectDir).toString(); // source maps let generator; @@ -557,7 +560,7 @@ class Mangler { } else { // source map generator - const relativeFileName = normalize(path.relative(projectDir, item.fileName)); + const relativeFileName = normalize(path_1.default.relative(projectDir, item.fileName)); const mappingsByLine = new Map(); // apply renames edits.sort((a, b) => b.offset - a.offset); @@ -596,7 +599,7 @@ class Mangler { }); } // source map generation, make sure to get mappings per line correct - generator = new source_map_1.SourceMapGenerator({ file: path.basename(item.fileName), sourceRoot: sourceMapRoot }); + generator = new source_map_1.SourceMapGenerator({ file: path_1.default.basename(item.fileName), sourceRoot: sourceMapRoot }); generator.setSourceContent(relativeFileName, item.getFullText()); for (const [, mappings] of mappingsByLine) { let lineDelta = 0; @@ -614,19 +617,19 @@ class Mangler { } service.dispose(); this.renameWorkerPool.terminate(); - this.log(`Done: ${savedBytes / 1000}kb saved, memory-usage: ${JSON.stringify(v8.getHeapStatistics())}`); + this.log(`Done: ${savedBytes / 1000}kb saved, memory-usage: ${JSON.stringify(node_v8_1.default.getHeapStatistics())}`); return result; } } exports.Mangler = Mangler; // --- ast utils function hasModifier(node, kind) { - const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined; + const modifiers = typescript_1.default.canHaveModifiers(node) ? typescript_1.default.getModifiers(node) : undefined; return Boolean(modifiers?.find(mode => mode.kind === kind)); } function isInAmbientContext(node) { for (let p = node.parent; p; p = p.parent) { - if (ts.isModuleDeclaration(p)) { + if (typescript_1.default.isModuleDeclaration(p)) { return true; } } @@ -636,21 +639,21 @@ function normalize(path) { return path.replace(/\\/g, '/'); } async function _run() { - const root = path.join(__dirname, '..', '..', '..'); - const projectBase = path.join(root, 'src'); - const projectPath = path.join(projectBase, 'tsconfig.json'); - const newProjectBase = path.join(path.dirname(projectBase), path.basename(projectBase) + '2'); - fs.cpSync(projectBase, newProjectBase, { recursive: true }); + const root = path_1.default.join(__dirname, '..', '..', '..'); + const projectBase = path_1.default.join(root, 'src'); + const projectPath = path_1.default.join(projectBase, 'tsconfig.json'); + const newProjectBase = path_1.default.join(path_1.default.dirname(projectBase), path_1.default.basename(projectBase) + '2'); + fs_1.default.cpSync(projectBase, newProjectBase, { recursive: true }); const mangler = new Mangler(projectPath, console.log, { mangleExports: true, manglePrivateFields: true, }); for (const [fileName, contents] of await mangler.computeNewFileContents(new Set(['saveState']))) { - const newFilePath = path.join(newProjectBase, path.relative(projectBase, fileName)); - await fs.promises.mkdir(path.dirname(newFilePath), { recursive: true }); - await fs.promises.writeFile(newFilePath, contents.out); + const newFilePath = path_1.default.join(newProjectBase, path_1.default.relative(projectBase, fileName)); + await fs_1.default.promises.mkdir(path_1.default.dirname(newFilePath), { recursive: true }); + await fs_1.default.promises.writeFile(newFilePath, contents.out); if (contents.sourceMap) { - await fs.promises.writeFile(newFilePath + '.map', contents.sourceMap); + await fs_1.default.promises.writeFile(newFilePath + '.map', contents.sourceMap); } } } diff --git a/build/lib/mangle/index.ts b/build/lib/mangle/index.ts index f291bd63f6b9..a6f066e2d2a7 100644 --- a/build/lib/mangle/index.ts +++ b/build/lib/mangle/index.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as v8 from 'node:v8'; -import * as fs from 'fs'; -import * as path from 'path'; +import v8 from 'node:v8'; +import fs from 'fs'; +import path from 'path'; import { argv } from 'process'; import { Mapping, SourceMapGenerator } from 'source-map'; -import * as ts from 'typescript'; +import ts from 'typescript'; import { pathToFileURL } from 'url'; -import * as workerpool from 'workerpool'; +import workerpool from 'workerpool'; import { StaticLanguageServiceHost } from './staticLanguageServiceHost'; const buildfile = require('../../buildfile'); diff --git a/build/lib/mangle/renameWorker.js b/build/lib/mangle/renameWorker.js index 6cd429b8c9ae..8bd59a4e2d55 100644 --- a/build/lib/mangle/renameWorker.js +++ b/build/lib/mangle/renameWorker.js @@ -3,20 +3,23 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const ts = require("typescript"); -const workerpool = require("workerpool"); +const typescript_1 = __importDefault(require("typescript")); +const workerpool_1 = __importDefault(require("workerpool")); const staticLanguageServiceHost_1 = require("./staticLanguageServiceHost"); let service; function findRenameLocations(projectPath, fileName, position) { if (!service) { - service = ts.createLanguageService(new staticLanguageServiceHost_1.StaticLanguageServiceHost(projectPath)); + service = typescript_1.default.createLanguageService(new staticLanguageServiceHost_1.StaticLanguageServiceHost(projectPath)); } return service.findRenameLocations(fileName, position, false, false, { providePrefixAndSuffixTextForRename: true, }) ?? []; } -workerpool.worker({ +workerpool_1.default.worker({ findRenameLocations }); //# sourceMappingURL=renameWorker.js.map \ No newline at end of file diff --git a/build/lib/mangle/renameWorker.ts b/build/lib/mangle/renameWorker.ts index 29b34e8c5147..0cce5677593c 100644 --- a/build/lib/mangle/renameWorker.ts +++ b/build/lib/mangle/renameWorker.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as ts from 'typescript'; -import * as workerpool from 'workerpool'; +import ts from 'typescript'; +import workerpool from 'workerpool'; import { StaticLanguageServiceHost } from './staticLanguageServiceHost'; let service: ts.LanguageService | undefined; diff --git a/build/lib/mangle/staticLanguageServiceHost.js b/build/lib/mangle/staticLanguageServiceHost.js index 1f338f0e61c5..7777888dd06a 100644 --- a/build/lib/mangle/staticLanguageServiceHost.js +++ b/build/lib/mangle/staticLanguageServiceHost.js @@ -3,10 +3,13 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.StaticLanguageServiceHost = void 0; -const ts = require("typescript"); -const path = require("path"); +const typescript_1 = __importDefault(require("typescript")); +const path_1 = __importDefault(require("path")); class StaticLanguageServiceHost { projectPath; _cmdLine; @@ -14,11 +17,11 @@ class StaticLanguageServiceHost { constructor(projectPath) { this.projectPath = projectPath; const existingOptions = {}; - const parsed = ts.readConfigFile(projectPath, ts.sys.readFile); + const parsed = typescript_1.default.readConfigFile(projectPath, typescript_1.default.sys.readFile); if (parsed.error) { throw parsed.error; } - this._cmdLine = ts.parseJsonConfigFileContent(parsed.config, ts.sys, path.dirname(projectPath), existingOptions); + this._cmdLine = typescript_1.default.parseJsonConfigFileContent(parsed.config, typescript_1.default.sys, path_1.default.dirname(projectPath), existingOptions); if (this._cmdLine.errors.length > 0) { throw parsed.error; } @@ -38,28 +41,28 @@ class StaticLanguageServiceHost { getScriptSnapshot(fileName) { let result = this._scriptSnapshots.get(fileName); if (result === undefined) { - const content = ts.sys.readFile(fileName); + const content = typescript_1.default.sys.readFile(fileName); if (content === undefined) { return undefined; } - result = ts.ScriptSnapshot.fromString(content); + result = typescript_1.default.ScriptSnapshot.fromString(content); this._scriptSnapshots.set(fileName, result); } return result; } getCurrentDirectory() { - return path.dirname(this.projectPath); + return path_1.default.dirname(this.projectPath); } getDefaultLibFileName(options) { - return ts.getDefaultLibFilePath(options); + return typescript_1.default.getDefaultLibFilePath(options); } - directoryExists = ts.sys.directoryExists; - getDirectories = ts.sys.getDirectories; - fileExists = ts.sys.fileExists; - readFile = ts.sys.readFile; - readDirectory = ts.sys.readDirectory; + directoryExists = typescript_1.default.sys.directoryExists; + getDirectories = typescript_1.default.sys.getDirectories; + fileExists = typescript_1.default.sys.fileExists; + readFile = typescript_1.default.sys.readFile; + readDirectory = typescript_1.default.sys.readDirectory; // this is necessary to make source references work. - realpath = ts.sys.realpath; + realpath = typescript_1.default.sys.realpath; } exports.StaticLanguageServiceHost = StaticLanguageServiceHost; //# sourceMappingURL=staticLanguageServiceHost.js.map \ No newline at end of file diff --git a/build/lib/mangle/staticLanguageServiceHost.ts b/build/lib/mangle/staticLanguageServiceHost.ts index c2793342ce34..b41b4e521336 100644 --- a/build/lib/mangle/staticLanguageServiceHost.ts +++ b/build/lib/mangle/staticLanguageServiceHost.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as ts from 'typescript'; -import * as path from 'path'; +import ts from 'typescript'; +import path from 'path'; export class StaticLanguageServiceHost implements ts.LanguageServiceHost { diff --git a/build/lib/monaco-api.js b/build/lib/monaco-api.js index 2052806c46bc..84cc556cb625 100644 --- a/build/lib/monaco-api.js +++ b/build/lib/monaco-api.js @@ -3,21 +3,24 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.DeclarationResolver = exports.FSProvider = exports.RECIPE_PATH = void 0; exports.run3 = run3; exports.execute = execute; -const fs = require("fs"); -const path = require("path"); -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); const dtsv = '3'; const tsfmt = require('../../tsfmt.json'); -const SRC = path.join(__dirname, '../../src'); -exports.RECIPE_PATH = path.join(__dirname, '../monaco/monaco.d.ts.recipe'); -const DECLARATION_PATH = path.join(__dirname, '../../src/vs/monaco.d.ts'); +const SRC = path_1.default.join(__dirname, '../../src'); +exports.RECIPE_PATH = path_1.default.join(__dirname, '../monaco/monaco.d.ts.recipe'); +const DECLARATION_PATH = path_1.default.join(__dirname, '../../src/vs/monaco.d.ts'); function logErr(message, ...rest) { - fancyLog(ansiColors.yellow(`[monaco.d.ts]`), message, ...rest); + (0, fancy_log_1.default)(ansi_colors_1.default.yellow(`[monaco.d.ts]`), message, ...rest); } function isDeclaration(ts, a) { return (a.kind === ts.SyntaxKind.InterfaceDeclaration @@ -464,7 +467,7 @@ function generateDeclarationFile(ts, recipe, sourceFileGetter) { }; } function _run(ts, sourceFileGetter) { - const recipe = fs.readFileSync(exports.RECIPE_PATH).toString(); + const recipe = fs_1.default.readFileSync(exports.RECIPE_PATH).toString(); const t = generateDeclarationFile(ts, recipe, sourceFileGetter); if (!t) { return null; @@ -472,7 +475,7 @@ function _run(ts, sourceFileGetter) { const result = t.result; const usageContent = t.usageContent; const enums = t.enums; - const currentContent = fs.readFileSync(DECLARATION_PATH).toString(); + const currentContent = fs_1.default.readFileSync(DECLARATION_PATH).toString(); const one = currentContent.replace(/\r\n/gm, '\n'); const other = result.replace(/\r\n/gm, '\n'); const isTheSame = (one === other); @@ -486,13 +489,13 @@ function _run(ts, sourceFileGetter) { } class FSProvider { existsSync(filePath) { - return fs.existsSync(filePath); + return fs_1.default.existsSync(filePath); } statSync(filePath) { - return fs.statSync(filePath); + return fs_1.default.statSync(filePath); } readFileSync(_moduleId, filePath) { - return fs.readFileSync(filePath); + return fs_1.default.readFileSync(filePath); } } exports.FSProvider = FSProvider; @@ -532,9 +535,9 @@ class DeclarationResolver { } _getFileName(moduleId) { if (/\.d\.ts$/.test(moduleId)) { - return path.join(SRC, moduleId); + return path_1.default.join(SRC, moduleId); } - return path.join(SRC, `${moduleId}.ts`); + return path_1.default.join(SRC, `${moduleId}.ts`); } _getDeclarationSourceFile(moduleId) { const fileName = this._getFileName(moduleId); diff --git a/build/lib/monaco-api.ts b/build/lib/monaco-api.ts index 288bec0f858f..5dc9a04266c5 100644 --- a/build/lib/monaco-api.ts +++ b/build/lib/monaco-api.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; +import fs from 'fs'; import type * as ts from 'typescript'; -import * as path from 'path'; -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; +import path from 'path'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; const dtsv = '3'; diff --git a/build/lib/nls.js b/build/lib/nls.js index 6ddcd46167a9..af648b40ed8a 100644 --- a/build/lib/nls.js +++ b/build/lib/nls.js @@ -3,14 +3,50 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.nls = nls; -const lazy = require("lazy.js"); +const lazy_js_1 = __importDefault(require("lazy.js")); const event_stream_1 = require("event-stream"); -const File = require("vinyl"); -const sm = require("source-map"); -const path = require("path"); -const sort = require("gulp-sort"); +const vinyl_1 = __importDefault(require("vinyl")); +const sm = __importStar(require("source-map")); +const path = __importStar(require("path")); +const gulp_sort_1 = __importDefault(require("gulp-sort")); var CollectStepResult; (function (CollectStepResult) { CollectStepResult[CollectStepResult["Yes"] = 0] = "Yes"; @@ -46,7 +82,7 @@ function nls(options) { let base; const input = (0, event_stream_1.through)(); const output = input - .pipe(sort()) // IMPORTANT: to ensure stable NLS metadata generation, we must sort the files because NLS messages are globally extracted and indexed across all files + .pipe((0, gulp_sort_1.default)()) // IMPORTANT: to ensure stable NLS metadata generation, we must sort the files because NLS messages are globally extracted and indexed across all files .pipe((0, event_stream_1.through)(function (f) { if (!f.sourceMap) { return this.emit('error', new Error(`File ${f.relative} does not have sourcemaps.`)); @@ -67,7 +103,7 @@ function nls(options) { this.emit('data', _nls.patchFile(f, typescript, options)); }, function () { for (const file of [ - new File({ + new vinyl_1.default({ contents: Buffer.from(JSON.stringify({ keys: _nls.moduleToNLSKeys, messages: _nls.moduleToNLSMessages, @@ -75,17 +111,17 @@ function nls(options) { base, path: `${base}/nls.metadata.json` }), - new File({ + new vinyl_1.default({ contents: Buffer.from(JSON.stringify(_nls.allNLSMessages)), base, path: `${base}/nls.messages.json` }), - new File({ + new vinyl_1.default({ contents: Buffer.from(JSON.stringify(_nls.allNLSModulesAndKeys)), base, path: `${base}/nls.keys.json` }), - new File({ + new vinyl_1.default({ contents: Buffer.from(`/*--------------------------------------------------------- * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ @@ -111,7 +147,7 @@ var _nls; _nls.allNLSModulesAndKeys = []; let allNLSMessagesIndex = 0; function fileFrom(file, contents, path = file.path) { - return new File({ + return new vinyl_1.default({ contents: Buffer.from(contents), base: file.base, cwd: file.cwd, @@ -163,7 +199,7 @@ var _nls; const service = ts.createLanguageService(serviceHost); const sourceFile = ts.createSourceFile(filename, contents, ts.ScriptTarget.ES5, true); // all imports - const imports = lazy(collect(ts, sourceFile, n => isImportNode(ts, n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse)); + const imports = (0, lazy_js_1.default)(collect(ts, sourceFile, n => isImportNode(ts, n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse)); // import nls = require('vs/nls'); const importEqualsDeclarations = imports .filter(n => n.kind === ts.SyntaxKind.ImportEqualsDeclaration) @@ -188,7 +224,7 @@ var _nls; .filter(r => !r.isWriteAccess) // find the deepest call expressions AST nodes that contain those references .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) - .map(a => lazy(a).last()) + .map(a => (0, lazy_js_1.default)(a).last()) .filter(n => !!n) .map(n => n) // only `localize` calls @@ -214,7 +250,7 @@ var _nls; const localizeCallExpressions = localizeReferences .concat(namedLocalizeReferences) .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) - .map(a => lazy(a).last()) + .map(a => (0, lazy_js_1.default)(a).last()) .filter(n => !!n) .map(n => n); // collect everything @@ -281,14 +317,14 @@ var _nls; } } toString() { - return lazy(this.lines).zip(this.lineEndings) + return (0, lazy_js_1.default)(this.lines).zip(this.lineEndings) .flatten().toArray().join(''); } } function patchJavascript(patches, contents) { const model = new TextModel(contents); // patch the localize calls - lazy(patches).reverse().each(p => model.apply(p)); + (0, lazy_js_1.default)(patches).reverse().each(p => model.apply(p)); return model.toString(); } function patchSourcemap(patches, rsm, smc) { @@ -349,7 +385,7 @@ var _nls; const end = lcFrom(smc.generatedPositionFor(positionFrom(c.range.end))); return { span: { start, end }, content: c.content }; }; - const localizePatches = lazy(localizeCalls) + const localizePatches = (0, lazy_js_1.default)(localizeCalls) .map(lc => (options.preserveEnglish ? [ { range: lc.keySpan, content: `${allNLSMessagesIndex++}` } // localize('key', "message") => localize(, "message") ] : [ @@ -358,7 +394,7 @@ var _nls; ])) .flatten() .map(toPatch); - const localize2Patches = lazy(localize2Calls) + const localize2Patches = (0, lazy_js_1.default)(localize2Calls) .map(lc => ({ range: lc.keySpan, content: `${allNLSMessagesIndex++}` } // localize2('key', "message") => localize(, "message") )) .map(toPatch); diff --git a/build/lib/nls.ts b/build/lib/nls.ts index cac832903a3b..4194eb3c489b 100644 --- a/build/lib/nls.ts +++ b/build/lib/nls.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import type * as ts from 'typescript'; -import * as lazy from 'lazy.js'; +import lazy from 'lazy.js'; import { duplex, through } from 'event-stream'; -import * as File from 'vinyl'; +import File from 'vinyl'; import * as sm from 'source-map'; import * as path from 'path'; -import * as sort from 'gulp-sort'; +import sort from 'gulp-sort'; declare class FileSourceMap extends File { public sourceMap: sm.RawSourceMap; diff --git a/build/lib/node.js b/build/lib/node.js index 74a54a3c1708..01a381183ff5 100644 --- a/build/lib/node.js +++ b/build/lib/node.js @@ -3,16 +3,19 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); -const fs = require("fs"); -const root = path.dirname(path.dirname(__dirname)); -const npmrcPath = path.join(root, 'remote', '.npmrc'); -const npmrc = fs.readFileSync(npmrcPath, 'utf8'); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); +const npmrcPath = path_1.default.join(root, 'remote', '.npmrc'); +const npmrc = fs_1.default.readFileSync(npmrcPath, 'utf8'); const version = /^target="(.*)"$/m.exec(npmrc)[1]; const platform = process.platform; const arch = process.arch; const node = platform === 'win32' ? 'node.exe' : 'node'; -const nodePath = path.join(root, '.build', 'node', `v${version}`, `${platform}-${arch}`, node); +const nodePath = path_1.default.join(root, '.build', 'node', `v${version}`, `${platform}-${arch}`, node); console.log(nodePath); //# sourceMappingURL=node.js.map \ No newline at end of file diff --git a/build/lib/node.ts b/build/lib/node.ts index 4beb13ae91bf..a2fdc361aa15 100644 --- a/build/lib/node.ts +++ b/build/lib/node.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as fs from 'fs'; +import path from 'path'; +import fs from 'fs'; const root = path.dirname(path.dirname(__dirname)); const npmrcPath = path.join(root, 'remote', '.npmrc'); diff --git a/build/lib/optimize.js b/build/lib/optimize.js index 83f34dc0745c..d45ff0d67d3c 100644 --- a/build/lib/optimize.js +++ b/build/lib/optimize.js @@ -3,22 +3,58 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.bundleTask = bundleTask; exports.minifyTask = minifyTask; -const es = require("event-stream"); -const gulp = require("gulp"); -const filter = require("gulp-filter"); -const path = require("path"); -const fs = require("fs"); -const pump = require("pump"); -const VinylFile = require("vinyl"); -const bundle = require("./bundle"); +const es = __importStar(require("event-stream")); +const gulp = __importStar(require("gulp")); +const gulp_filter_1 = __importDefault(require("gulp-filter")); +const path = __importStar(require("path")); +const fs = __importStar(require("fs")); +const pump_1 = __importDefault(require("pump")); +const vinyl_1 = __importDefault(require("vinyl")); +const bundle = __importStar(require("./bundle")); const postcss_1 = require("./postcss"); -const esbuild = require("esbuild"); -const sourcemaps = require("gulp-sourcemaps"); -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); +const esbuild = __importStar(require("esbuild")); +const sourcemaps = __importStar(require("gulp-sourcemaps")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansiColors = __importStar(require("ansi-colors")); const REPO_ROOT_PATH = path.join(__dirname, '../..'); const DEFAULT_FILE_HEADER = [ '/*!--------------------------------------------------------', @@ -44,7 +80,7 @@ function bundleESMTask(opts) { const files = []; const tasks = []; for (const entryPoint of entryPoints) { - fancyLog(`Bundled entry point: ${ansiColors.yellow(entryPoint.name)}...`); + (0, fancy_log_1.default)(`Bundled entry point: ${ansiColors.yellow(entryPoint.name)}...`); // support for 'dest' via esbuild#in/out const dest = entryPoint.dest?.replace(/\.[^/.]+$/, '') ?? entryPoint.name; // banner contents @@ -128,7 +164,7 @@ function bundleESMTask(opts) { path: file.path, base: path.join(REPO_ROOT_PATH, opts.src) }; - files.push(new VinylFile(fileProps)); + files.push(new vinyl_1.default(fileProps)); } }); tasks.push(task); @@ -160,10 +196,10 @@ function minifyTask(src, sourceMapBaseUrl) { return cb => { const cssnano = require('cssnano'); const svgmin = require('gulp-svgmin'); - const jsFilter = filter('**/*.js', { restore: true }); - const cssFilter = filter('**/*.css', { restore: true }); - const svgFilter = filter('**/*.svg', { restore: true }); - pump(gulp.src([src + '/**', '!' + src + '/**/*.map']), jsFilter, sourcemaps.init({ loadMaps: true }), es.map((f, cb) => { + const jsFilter = (0, gulp_filter_1.default)('**/*.js', { restore: true }); + const cssFilter = (0, gulp_filter_1.default)('**/*.css', { restore: true }); + const svgFilter = (0, gulp_filter_1.default)('**/*.svg', { restore: true }); + (0, pump_1.default)(gulp.src([src + '/**', '!' + src + '/**/*.map']), jsFilter, sourcemaps.init({ loadMaps: true }), es.map((f, cb) => { esbuild.build({ entryPoints: [f.path], minify: true, diff --git a/build/lib/optimize.ts b/build/lib/optimize.ts index 8c49fa818882..55566d4f2418 100644 --- a/build/lib/optimize.ts +++ b/build/lib/optimize.ts @@ -5,16 +5,16 @@ import * as es from 'event-stream'; import * as gulp from 'gulp'; -import * as filter from 'gulp-filter'; +import filter from 'gulp-filter'; import * as path from 'path'; import * as fs from 'fs'; -import * as pump from 'pump'; -import * as VinylFile from 'vinyl'; +import pump from 'pump'; +import VinylFile from 'vinyl'; import * as bundle from './bundle'; import { gulpPostcss } from './postcss'; import * as esbuild from 'esbuild'; import * as sourcemaps from 'gulp-sourcemaps'; -import * as fancyLog from 'fancy-log'; +import fancyLog from 'fancy-log'; import * as ansiColors from 'ansi-colors'; const REPO_ROOT_PATH = path.join(__dirname, '../..'); diff --git a/build/lib/policies.js b/build/lib/policies.js index 1560dc7415d9..d52015c550bc 100644 --- a/build/lib/policies.js +++ b/build/lib/policies.js @@ -3,13 +3,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); const child_process_1 = require("child_process"); const fs_1 = require("fs"); -const path = require("path"); -const byline = require("byline"); +const path_1 = __importDefault(require("path")); +const byline_1 = __importDefault(require("byline")); const ripgrep_1 = require("@vscode/ripgrep"); -const Parser = require("tree-sitter"); +const tree_sitter_1 = __importDefault(require("tree-sitter")); const { typescript } = require('tree-sitter-typescript'); const product = require('../../product.json'); const packageJson = require('../../package.json'); @@ -258,7 +261,7 @@ const StringArrayQ = { } }; function getProperty(qtype, node, key) { - const query = new Parser.Query(typescript, `( + const query = new tree_sitter_1.default.Query(typescript, `( (pair key: [(property_identifier)(string)] @key value: ${qtype.Q} @@ -331,7 +334,7 @@ function getPolicy(moduleName, configurationNode, settingNode, policyNode, categ return result; } function getPolicies(moduleName, node) { - const query = new Parser.Query(typescript, ` + const query = new tree_sitter_1.default.Query(typescript, ` ( (call_expression function: (member_expression property: (property_identifier) @registerConfigurationFn) (#eq? @registerConfigurationFn registerConfiguration) @@ -360,7 +363,7 @@ async function getFiles(root) { return new Promise((c, e) => { const result = []; const rg = (0, child_process_1.spawn)(ripgrep_1.rgPath, ['-l', 'registerConfiguration\\(', '-g', 'src/**/*.ts', '-g', '!src/**/test/**', root]); - const stream = byline(rg.stdout.setEncoding('utf8')); + const stream = (0, byline_1.default)(rg.stdout.setEncoding('utf8')); stream.on('data', path => result.push(path)); stream.on('error', err => e(err)); stream.on('end', () => c(result)); @@ -494,13 +497,13 @@ async function getNLS(extensionGalleryServiceUrl, resourceUrlTemplate, languageI return await getSpecificNLS(resourceUrlTemplate, languageId, latestCompatibleVersion); } async function parsePolicies() { - const parser = new Parser(); + const parser = new tree_sitter_1.default(); parser.setLanguage(typescript); const files = await getFiles(process.cwd()); - const base = path.join(process.cwd(), 'src'); + const base = path_1.default.join(process.cwd(), 'src'); const policies = []; for (const file of files) { - const moduleName = path.relative(base, file).replace(/\.ts$/i, '').replace(/\\/g, '/'); + const moduleName = path_1.default.relative(base, file).replace(/\.ts$/i, '').replace(/\\/g, '/'); const contents = await fs_1.promises.readFile(file, { encoding: 'utf8' }); const tree = parser.parse(contents); policies.push(...getPolicies(moduleName, tree.rootNode)); @@ -529,11 +532,11 @@ async function main() { const root = '.build/policies/win32'; await fs_1.promises.rm(root, { recursive: true, force: true }); await fs_1.promises.mkdir(root, { recursive: true }); - await fs_1.promises.writeFile(path.join(root, `${product.win32RegValueName}.admx`), admx.replace(/\r?\n/g, '\n')); + await fs_1.promises.writeFile(path_1.default.join(root, `${product.win32RegValueName}.admx`), admx.replace(/\r?\n/g, '\n')); for (const { languageId, contents } of adml) { - const languagePath = path.join(root, languageId === 'en-us' ? 'en-us' : Languages[languageId]); + const languagePath = path_1.default.join(root, languageId === 'en-us' ? 'en-us' : Languages[languageId]); await fs_1.promises.mkdir(languagePath, { recursive: true }); - await fs_1.promises.writeFile(path.join(languagePath, `${product.win32RegValueName}.adml`), contents.replace(/\r?\n/g, '\n')); + await fs_1.promises.writeFile(path_1.default.join(languagePath, `${product.win32RegValueName}.adml`), contents.replace(/\r?\n/g, '\n')); } } if (require.main === module) { diff --git a/build/lib/policies.ts b/build/lib/policies.ts index f602c8a0d6e8..57941d8e9676 100644 --- a/build/lib/policies.ts +++ b/build/lib/policies.ts @@ -5,10 +5,10 @@ import { spawn } from 'child_process'; import { promises as fs } from 'fs'; -import * as path from 'path'; -import * as byline from 'byline'; +import path from 'path'; +import byline from 'byline'; import { rgPath } from '@vscode/ripgrep'; -import * as Parser from 'tree-sitter'; +import Parser from 'tree-sitter'; const { typescript } = require('tree-sitter-typescript'); const product = require('../../product.json'); const packageJson = require('../../package.json'); diff --git a/build/lib/postcss.js b/build/lib/postcss.js index 356015ab1596..210a184e5f54 100644 --- a/build/lib/postcss.js +++ b/build/lib/postcss.js @@ -1,15 +1,18 @@ "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.gulpPostcss = gulpPostcss; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const postcss = require("postcss"); -const es = require("event-stream"); +const postcss_1 = __importDefault(require("postcss")); +const event_stream_1 = __importDefault(require("event-stream")); function gulpPostcss(plugins, handleError) { - const instance = postcss(plugins); - return es.map((file, callback) => { + const instance = (0, postcss_1.default)(plugins); + return event_stream_1.default.map((file, callback) => { if (file.isNull()) { return callback(null, file); } diff --git a/build/lib/postcss.ts b/build/lib/postcss.ts index cf3121e221e7..9ec2188d13ac 100644 --- a/build/lib/postcss.ts +++ b/build/lib/postcss.ts @@ -2,9 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as postcss from 'postcss'; -import * as File from 'vinyl'; -import * as es from 'event-stream'; +import postcss from 'postcss'; +import File from 'vinyl'; +import es from 'event-stream'; export function gulpPostcss(plugins: postcss.AcceptedPlugin[], handleError?: (err: Error) => void) { const instance = postcss(plugins); diff --git a/build/lib/preLaunch.js b/build/lib/preLaunch.js index 4791514fdfe7..75207fe50c03 100644 --- a/build/lib/preLaunch.js +++ b/build/lib/preLaunch.js @@ -3,13 +3,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); // @ts-check -const path = require("path"); +const path_1 = __importDefault(require("path")); const child_process_1 = require("child_process"); const fs_1 = require("fs"); const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; -const rootDir = path.resolve(__dirname, '..', '..'); +const rootDir = path_1.default.resolve(__dirname, '..', '..'); function runProcess(command, args = []) { return new Promise((resolve, reject) => { const child = (0, child_process_1.spawn)(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env, shell: process.platform === 'win32' }); @@ -19,7 +22,7 @@ function runProcess(command, args = []) { } async function exists(subdir) { try { - await fs_1.promises.stat(path.join(rootDir, subdir)); + await fs_1.promises.stat(path_1.default.join(rootDir, subdir)); return true; } catch { diff --git a/build/lib/preLaunch.ts b/build/lib/preLaunch.ts index e0ea274458a4..0c178afcb598 100644 --- a/build/lib/preLaunch.ts +++ b/build/lib/preLaunch.ts @@ -5,7 +5,7 @@ // @ts-check -import * as path from 'path'; +import path from 'path'; import { spawn } from 'child_process'; import { promises as fs } from 'fs'; diff --git a/build/lib/reporter.js b/build/lib/reporter.js index 9d4a1b4fd796..16bb44ec539d 100644 --- a/build/lib/reporter.js +++ b/build/lib/reporter.js @@ -3,13 +3,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.createReporter = createReporter; -const es = require("event-stream"); -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); -const fs = require("fs"); -const path = require("path"); +const event_stream_1 = __importDefault(require("event-stream")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); class ErrorLog { id; constructor(id) { @@ -23,7 +26,7 @@ class ErrorLog { return; } this.startTime = new Date().getTime(); - fancyLog(`Starting ${ansiColors.green('compilation')}${this.id ? ansiColors.blue(` ${this.id}`) : ''}...`); + (0, fancy_log_1.default)(`Starting ${ansi_colors_1.default.green('compilation')}${this.id ? ansi_colors_1.default.blue(` ${this.id}`) : ''}...`); } onEnd() { if (--this.count > 0) { @@ -37,10 +40,10 @@ class ErrorLog { errors.map(err => { if (!seen.has(err)) { seen.add(err); - fancyLog(`${ansiColors.red('Error')}: ${err}`); + (0, fancy_log_1.default)(`${ansi_colors_1.default.red('Error')}: ${err}`); } }); - fancyLog(`Finished ${ansiColors.green('compilation')}${this.id ? ansiColors.blue(` ${this.id}`) : ''} with ${errors.length} errors after ${ansiColors.magenta((new Date().getTime() - this.startTime) + ' ms')}`); + (0, fancy_log_1.default)(`Finished ${ansi_colors_1.default.green('compilation')}${this.id ? ansi_colors_1.default.blue(` ${this.id}`) : ''} with ${errors.length} errors after ${ansi_colors_1.default.magenta((new Date().getTime() - this.startTime) + ' ms')}`); const regex = /^([^(]+)\((\d+),(\d+)\): (.*)$/s; const messages = errors .map(err => regex.exec(err)) @@ -49,7 +52,7 @@ class ErrorLog { .map(([, path, line, column, message]) => ({ path, line: parseInt(line), column: parseInt(column), message })); try { const logFileName = 'log' + (this.id ? `_${this.id}` : ''); - fs.writeFileSync(path.join(buildLogFolder, logFileName), JSON.stringify(messages)); + fs_1.default.writeFileSync(path_1.default.join(buildLogFolder, logFileName), JSON.stringify(messages)); } catch (err) { //noop @@ -65,9 +68,9 @@ function getErrorLog(id = '') { } return errorLog; } -const buildLogFolder = path.join(path.dirname(path.dirname(__dirname)), '.build'); +const buildLogFolder = path_1.default.join(path_1.default.dirname(path_1.default.dirname(__dirname)), '.build'); try { - fs.mkdirSync(buildLogFolder); + fs_1.default.mkdirSync(buildLogFolder); } catch (err) { // ignore @@ -81,7 +84,7 @@ function createReporter(id) { result.end = (emitError) => { errors.length = 0; errorLog.onStart(); - return es.through(undefined, function () { + return event_stream_1.default.through(undefined, function () { errorLog.onEnd(); if (emitError && errors.length > 0) { if (!errors.__logged__) { diff --git a/build/lib/reporter.ts b/build/lib/reporter.ts index 382e0c785464..c21fd841c0d1 100644 --- a/build/lib/reporter.ts +++ b/build/lib/reporter.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; -import * as fs from 'fs'; -import * as path from 'path'; +import es from 'event-stream'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; +import fs from 'fs'; +import path from 'path'; class ErrorLog { constructor(public id: string) { diff --git a/build/lib/snapshotLoader.js b/build/lib/snapshotLoader.js index 0e58ceedffaa..7d9b3f154f17 100644 --- a/build/lib/snapshotLoader.js +++ b/build/lib/snapshotLoader.js @@ -3,6 +3,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.snaps = void 0; var snaps; (function (snaps) { const fs = require('fs'); @@ -52,5 +54,5 @@ var snaps; fs.writeFileSync(wrappedInputFilepath, wrappedInputFile); cp.execFileSync(mksnapshot, [wrappedInputFilepath, `--startup_blob`, startupBlobFilepath]); } -})(snaps || (snaps = {})); +})(snaps || (exports.snaps = snaps = {})); //# sourceMappingURL=snapshotLoader.js.map \ No newline at end of file diff --git a/build/lib/snapshotLoader.ts b/build/lib/snapshotLoader.ts index c3d66dba7e12..3cb2191144d3 100644 --- a/build/lib/snapshotLoader.ts +++ b/build/lib/snapshotLoader.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -namespace snaps { +export namespace snaps { const fs = require('fs'); const path = require('path'); diff --git a/build/lib/standalone.js b/build/lib/standalone.js index 16ae1e2b2d8f..0e7a9ecc7824 100644 --- a/build/lib/standalone.js +++ b/build/lib/standalone.js @@ -3,14 +3,50 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.extractEditor = extractEditor; exports.createESMSourcesAndResources2 = createESMSourcesAndResources2; -const fs = require("fs"); -const path = require("path"); -const tss = require("./treeshaking"); -const REPO_ROOT = path.join(__dirname, '../../'); -const SRC_DIR = path.join(REPO_ROOT, 'src'); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const tss = __importStar(require("./treeshaking")); +const REPO_ROOT = path_1.default.join(__dirname, '../../'); +const SRC_DIR = path_1.default.join(REPO_ROOT, 'src'); const dirCache = {}; function writeFile(filePath, contents) { function ensureDirs(dirPath) { @@ -18,21 +54,21 @@ function writeFile(filePath, contents) { return; } dirCache[dirPath] = true; - ensureDirs(path.dirname(dirPath)); - if (fs.existsSync(dirPath)) { + ensureDirs(path_1.default.dirname(dirPath)); + if (fs_1.default.existsSync(dirPath)) { return; } - fs.mkdirSync(dirPath); + fs_1.default.mkdirSync(dirPath); } - ensureDirs(path.dirname(filePath)); - fs.writeFileSync(filePath, contents); + ensureDirs(path_1.default.dirname(filePath)); + fs_1.default.writeFileSync(filePath, contents); } function extractEditor(options) { const ts = require('typescript'); - const tsConfig = JSON.parse(fs.readFileSync(path.join(options.sourcesRoot, 'tsconfig.monaco.json')).toString()); + const tsConfig = JSON.parse(fs_1.default.readFileSync(path_1.default.join(options.sourcesRoot, 'tsconfig.monaco.json')).toString()); let compilerOptions; if (tsConfig.extends) { - compilerOptions = Object.assign({}, require(path.join(options.sourcesRoot, tsConfig.extends)).compilerOptions, tsConfig.compilerOptions); + compilerOptions = Object.assign({}, require(path_1.default.join(options.sourcesRoot, tsConfig.extends)).compilerOptions, tsConfig.compilerOptions); delete tsConfig.extends; } else { @@ -62,7 +98,7 @@ function extractEditor(options) { const result = tss.shake(options); for (const fileName in result) { if (result.hasOwnProperty(fileName)) { - writeFile(path.join(options.destRoot, fileName), result[fileName]); + writeFile(path_1.default.join(options.destRoot, fileName), result[fileName]); } } const copied = {}; @@ -71,12 +107,12 @@ function extractEditor(options) { return; } copied[fileName] = true; - const srcPath = path.join(options.sourcesRoot, fileName); - const dstPath = path.join(options.destRoot, fileName); - writeFile(dstPath, fs.readFileSync(srcPath)); + const srcPath = path_1.default.join(options.sourcesRoot, fileName); + const dstPath = path_1.default.join(options.destRoot, fileName); + writeFile(dstPath, fs_1.default.readFileSync(srcPath)); }; const writeOutputFile = (fileName, contents) => { - writeFile(path.join(options.destRoot, fileName), contents); + writeFile(path_1.default.join(options.destRoot, fileName), contents); }; for (const fileName in result) { if (result.hasOwnProperty(fileName)) { @@ -86,14 +122,14 @@ function extractEditor(options) { const importedFileName = info.importedFiles[i].fileName; let importedFilePath = importedFileName; if (/(^\.\/)|(^\.\.\/)/.test(importedFilePath)) { - importedFilePath = path.join(path.dirname(fileName), importedFilePath); + importedFilePath = path_1.default.join(path_1.default.dirname(fileName), importedFilePath); } if (/\.css$/.test(importedFilePath)) { transportCSS(importedFilePath, copyFile, writeOutputFile); } else { - const pathToCopy = path.join(options.sourcesRoot, importedFilePath); - if (fs.existsSync(pathToCopy) && !fs.statSync(pathToCopy).isDirectory()) { + const pathToCopy = path_1.default.join(options.sourcesRoot, importedFilePath); + if (fs_1.default.existsSync(pathToCopy) && !fs_1.default.statSync(pathToCopy).isDirectory()) { copyFile(importedFilePath); } } @@ -107,18 +143,18 @@ function extractEditor(options) { ].forEach(copyFile); } function createESMSourcesAndResources2(options) { - const SRC_FOLDER = path.join(REPO_ROOT, options.srcFolder); - const OUT_FOLDER = path.join(REPO_ROOT, options.outFolder); - const OUT_RESOURCES_FOLDER = path.join(REPO_ROOT, options.outResourcesFolder); + const SRC_FOLDER = path_1.default.join(REPO_ROOT, options.srcFolder); + const OUT_FOLDER = path_1.default.join(REPO_ROOT, options.outFolder); + const OUT_RESOURCES_FOLDER = path_1.default.join(REPO_ROOT, options.outResourcesFolder); const getDestAbsoluteFilePath = (file) => { const dest = options.renames[file.replace(/\\/g, '/')] || file; if (dest === 'tsconfig.json') { - return path.join(OUT_FOLDER, `tsconfig.json`); + return path_1.default.join(OUT_FOLDER, `tsconfig.json`); } if (/\.ts$/.test(dest)) { - return path.join(OUT_FOLDER, dest); + return path_1.default.join(OUT_FOLDER, dest); } - return path.join(OUT_RESOURCES_FOLDER, dest); + return path_1.default.join(OUT_RESOURCES_FOLDER, dest); }; const allFiles = walkDirRecursive(SRC_FOLDER); for (const file of allFiles) { @@ -126,15 +162,15 @@ function createESMSourcesAndResources2(options) { continue; } if (file === 'tsconfig.json') { - const tsConfig = JSON.parse(fs.readFileSync(path.join(SRC_FOLDER, file)).toString()); + const tsConfig = JSON.parse(fs_1.default.readFileSync(path_1.default.join(SRC_FOLDER, file)).toString()); tsConfig.compilerOptions.module = 'es2022'; - tsConfig.compilerOptions.outDir = path.join(path.relative(OUT_FOLDER, OUT_RESOURCES_FOLDER), 'vs').replace(/\\/g, '/'); + tsConfig.compilerOptions.outDir = path_1.default.join(path_1.default.relative(OUT_FOLDER, OUT_RESOURCES_FOLDER), 'vs').replace(/\\/g, '/'); write(getDestAbsoluteFilePath(file), JSON.stringify(tsConfig, null, '\t')); continue; } if (/\.ts$/.test(file) || /\.d\.ts$/.test(file) || /\.css$/.test(file) || /\.js$/.test(file) || /\.ttf$/.test(file)) { // Transport the files directly - write(getDestAbsoluteFilePath(file), fs.readFileSync(path.join(SRC_FOLDER, file))); + write(getDestAbsoluteFilePath(file), fs_1.default.readFileSync(path_1.default.join(SRC_FOLDER, file))); continue; } console.log(`UNKNOWN FILE: ${file}`); @@ -148,10 +184,10 @@ function createESMSourcesAndResources2(options) { return result; } function _walkDirRecursive(dir, result, trimPos) { - const files = fs.readdirSync(dir); + const files = fs_1.default.readdirSync(dir); for (let i = 0; i < files.length; i++) { - const file = path.join(dir, files[i]); - if (fs.statSync(file).isDirectory()) { + const file = path_1.default.join(dir, files[i]); + if (fs_1.default.statSync(file).isDirectory()) { _walkDirRecursive(file, result, trimPos); } else { @@ -206,8 +242,8 @@ function transportCSS(module, enqueue, write) { if (!/\.css/.test(module)) { return false; } - const filename = path.join(SRC_DIR, module); - const fileContents = fs.readFileSync(filename).toString(); + const filename = path_1.default.join(SRC_DIR, module); + const fileContents = fs_1.default.readFileSync(filename).toString(); const inlineResources = 'base64'; // see https://github.com/microsoft/monaco-editor/issues/148 const newContents = _rewriteOrInlineUrls(fileContents, inlineResources === 'base64'); write(module, newContents); @@ -217,12 +253,12 @@ function transportCSS(module, enqueue, write) { const fontMatch = url.match(/^(.*).ttf\?(.*)$/); if (fontMatch) { const relativeFontPath = `${fontMatch[1]}.ttf`; // trim the query parameter - const fontPath = path.join(path.dirname(module), relativeFontPath); + const fontPath = path_1.default.join(path_1.default.dirname(module), relativeFontPath); enqueue(fontPath); return relativeFontPath; } - const imagePath = path.join(path.dirname(module), url); - const fileContents = fs.readFileSync(path.join(SRC_DIR, imagePath)); + const imagePath = path_1.default.join(path_1.default.dirname(module), url); + const fileContents = fs_1.default.readFileSync(path_1.default.join(SRC_DIR, imagePath)); const MIME = /\.svg$/.test(url) ? 'image/svg+xml' : 'image/png'; let DATA = ';base64,' + fileContents.toString('base64'); if (!forceBase64 && /\.svg$/.test(url)) { diff --git a/build/lib/standalone.ts b/build/lib/standalone.ts index 8736583fb092..b2ae02f10073 100644 --- a/build/lib/standalone.ts +++ b/build/lib/standalone.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; +import fs from 'fs'; +import path from 'path'; import * as tss from './treeshaking'; const REPO_ROOT = path.join(__dirname, '../../'); diff --git a/build/lib/stats.js b/build/lib/stats.js index e089cb0c1b44..3f6d953ae407 100644 --- a/build/lib/stats.js +++ b/build/lib/stats.js @@ -3,11 +3,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.createStatsStream = createStatsStream; -const es = require("event-stream"); -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); +const event_stream_1 = __importDefault(require("event-stream")); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); class Entry { name; totalCount; @@ -28,13 +31,13 @@ class Entry { } else { if (this.totalCount === 1) { - return `Stats for '${ansiColors.grey(this.name)}': ${Math.round(this.totalSize / 1204)}KB`; + return `Stats for '${ansi_colors_1.default.grey(this.name)}': ${Math.round(this.totalSize / 1204)}KB`; } else { const count = this.totalCount < 100 - ? ansiColors.green(this.totalCount.toString()) - : ansiColors.red(this.totalCount.toString()); - return `Stats for '${ansiColors.grey(this.name)}': ${count} files, ${Math.round(this.totalSize / 1204)}KB`; + ? ansi_colors_1.default.green(this.totalCount.toString()) + : ansi_colors_1.default.red(this.totalCount.toString()); + return `Stats for '${ansi_colors_1.default.grey(this.name)}': ${count} files, ${Math.round(this.totalSize / 1204)}KB`; } } } @@ -43,7 +46,7 @@ const _entries = new Map(); function createStatsStream(group, log) { const entry = new Entry(group, 0, 0); _entries.set(entry.name, entry); - return es.through(function (data) { + return event_stream_1.default.through(function (data) { const file = data; if (typeof file.path === 'string') { entry.totalCount += 1; @@ -61,13 +64,13 @@ function createStatsStream(group, log) { }, function () { if (log) { if (entry.totalCount === 1) { - fancyLog(`Stats for '${ansiColors.grey(entry.name)}': ${Math.round(entry.totalSize / 1204)}KB`); + (0, fancy_log_1.default)(`Stats for '${ansi_colors_1.default.grey(entry.name)}': ${Math.round(entry.totalSize / 1204)}KB`); } else { const count = entry.totalCount < 100 - ? ansiColors.green(entry.totalCount.toString()) - : ansiColors.red(entry.totalCount.toString()); - fancyLog(`Stats for '${ansiColors.grey(entry.name)}': ${count} files, ${Math.round(entry.totalSize / 1204)}KB`); + ? ansi_colors_1.default.green(entry.totalCount.toString()) + : ansi_colors_1.default.red(entry.totalCount.toString()); + (0, fancy_log_1.default)(`Stats for '${ansi_colors_1.default.grey(entry.name)}': ${count} files, ${Math.round(entry.totalSize / 1204)}KB`); } } this.emit('end'); diff --git a/build/lib/stats.ts b/build/lib/stats.ts index fe4b22453b50..8db55d3e7774 100644 --- a/build/lib/stats.ts +++ b/build/lib/stats.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; -import * as File from 'vinyl'; +import es from 'event-stream'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; +import File from 'vinyl'; class Entry { constructor(readonly name: string, public totalCount: number, public totalSize: number) { } diff --git a/build/lib/stylelint/validateVariableNames.js b/build/lib/stylelint/validateVariableNames.js index 6a50d1d6894a..b0e064e7b561 100644 --- a/build/lib/stylelint/validateVariableNames.js +++ b/build/lib/stylelint/validateVariableNames.js @@ -3,15 +3,18 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.getVariableNameValidator = getVariableNameValidator; const fs_1 = require("fs"); -const path = require("path"); +const path_1 = __importDefault(require("path")); const RE_VAR_PROP = /var\(\s*(--([\w\-\.]+))/g; let knownVariables; function getKnownVariableNames() { if (!knownVariables) { - const knownVariablesFileContent = (0, fs_1.readFileSync)(path.join(__dirname, './vscode-known-variables.json'), 'utf8').toString(); + const knownVariablesFileContent = (0, fs_1.readFileSync)(path_1.default.join(__dirname, './vscode-known-variables.json'), 'utf8').toString(); const knownVariablesInfo = JSON.parse(knownVariablesFileContent); knownVariables = new Set([...knownVariablesInfo.colors, ...knownVariablesInfo.others]); } diff --git a/build/lib/stylelint/validateVariableNames.ts b/build/lib/stylelint/validateVariableNames.ts index 6d9fa8a7cef5..b28aed13f4b9 100644 --- a/build/lib/stylelint/validateVariableNames.ts +++ b/build/lib/stylelint/validateVariableNames.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { readFileSync } from 'fs'; -import path = require('path'); +import path from 'path'; const RE_VAR_PROP = /var\(\s*(--([\w\-\.]+))/g; diff --git a/build/lib/task.js b/build/lib/task.js index 597b2a0d3976..6887714681af 100644 --- a/build/lib/task.js +++ b/build/lib/task.js @@ -3,12 +3,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.series = series; exports.parallel = parallel; exports.define = define; -const fancyLog = require("fancy-log"); -const ansiColors = require("ansi-colors"); +const fancy_log_1 = __importDefault(require("fancy-log")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); function _isPromise(p) { if (typeof p.then === 'function') { return true; @@ -21,14 +24,14 @@ function _renderTime(time) { async function _execute(task) { const name = task.taskName || task.displayName || ``; if (!task._tasks) { - fancyLog('Starting', ansiColors.cyan(name), '...'); + (0, fancy_log_1.default)('Starting', ansi_colors_1.default.cyan(name), '...'); } const startTime = process.hrtime(); await _doExecute(task); const elapsedArr = process.hrtime(startTime); const elapsedNanoseconds = (elapsedArr[0] * 1e9 + elapsedArr[1]); if (!task._tasks) { - fancyLog(`Finished`, ansiColors.cyan(name), 'after', ansiColors.magenta(_renderTime(elapsedNanoseconds / 1e6))); + (0, fancy_log_1.default)(`Finished`, ansi_colors_1.default.cyan(name), 'after', ansi_colors_1.default.magenta(_renderTime(elapsedNanoseconds / 1e6))); } } async function _doExecute(task) { diff --git a/build/lib/task.ts b/build/lib/task.ts index 7d2a4dee0169..6af239831785 100644 --- a/build/lib/task.ts +++ b/build/lib/task.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; export interface BaseTask { displayName?: string; diff --git a/build/lib/test/i18n.test.js b/build/lib/test/i18n.test.js index b8f4a2bedef5..41aa8a7f668d 100644 --- a/build/lib/test/i18n.test.js +++ b/build/lib/test/i18n.test.js @@ -3,9 +3,45 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const assert = require("assert"); -const i18n = require("../i18n"); +const assert_1 = __importDefault(require("assert")); +const i18n = __importStar(require("../i18n")); suite('XLF Parser Tests', () => { const sampleXlf = 'Key #1Key #2 &'; const sampleTranslatedXlf = 'Key #1Кнопка #1Key #2 &Кнопка #2 &'; @@ -17,25 +53,25 @@ suite('XLF Parser Tests', () => { const xlf = new i18n.XLF('vscode-workbench'); xlf.addFile(name, keys, messages); const xlfString = xlf.toString(); - assert.strictEqual(xlfString.replace(/\s{2,}/g, ''), sampleXlf); + assert_1.default.strictEqual(xlfString.replace(/\s{2,}/g, ''), sampleXlf); }); test('XLF to keys & messages conversion', () => { i18n.XLF.parse(sampleTranslatedXlf).then(function (resolvedFiles) { - assert.deepStrictEqual(resolvedFiles[0].messages, translatedMessages); - assert.strictEqual(resolvedFiles[0].name, name); + assert_1.default.deepStrictEqual(resolvedFiles[0].messages, translatedMessages); + assert_1.default.strictEqual(resolvedFiles[0].name, name); }); }); test('JSON file source path to Transifex resource match', () => { const editorProject = 'vscode-editor', workbenchProject = 'vscode-workbench'; const platform = { name: 'vs/platform', project: editorProject }, editorContrib = { name: 'vs/editor/contrib', project: editorProject }, editor = { name: 'vs/editor', project: editorProject }, base = { name: 'vs/base', project: editorProject }, code = { name: 'vs/code', project: workbenchProject }, workbenchParts = { name: 'vs/workbench/contrib/html', project: workbenchProject }, workbenchServices = { name: 'vs/workbench/services/textfile', project: workbenchProject }, workbench = { name: 'vs/workbench', project: workbenchProject }; - assert.deepStrictEqual(i18n.getResource('vs/platform/actions/browser/menusExtensionPoint'), platform); - assert.deepStrictEqual(i18n.getResource('vs/editor/contrib/clipboard/browser/clipboard'), editorContrib); - assert.deepStrictEqual(i18n.getResource('vs/editor/common/modes/modesRegistry'), editor); - assert.deepStrictEqual(i18n.getResource('vs/base/common/errorMessage'), base); - assert.deepStrictEqual(i18n.getResource('vs/code/electron-main/window'), code); - assert.deepStrictEqual(i18n.getResource('vs/workbench/contrib/html/browser/webview'), workbenchParts); - assert.deepStrictEqual(i18n.getResource('vs/workbench/services/textfile/node/testFileService'), workbenchServices); - assert.deepStrictEqual(i18n.getResource('vs/workbench/browser/parts/panel/panelActions'), workbench); + assert_1.default.deepStrictEqual(i18n.getResource('vs/platform/actions/browser/menusExtensionPoint'), platform); + assert_1.default.deepStrictEqual(i18n.getResource('vs/editor/contrib/clipboard/browser/clipboard'), editorContrib); + assert_1.default.deepStrictEqual(i18n.getResource('vs/editor/common/modes/modesRegistry'), editor); + assert_1.default.deepStrictEqual(i18n.getResource('vs/base/common/errorMessage'), base); + assert_1.default.deepStrictEqual(i18n.getResource('vs/code/electron-main/window'), code); + assert_1.default.deepStrictEqual(i18n.getResource('vs/workbench/contrib/html/browser/webview'), workbenchParts); + assert_1.default.deepStrictEqual(i18n.getResource('vs/workbench/services/textfile/node/testFileService'), workbenchServices); + assert_1.default.deepStrictEqual(i18n.getResource('vs/workbench/browser/parts/panel/panelActions'), workbench); }); }); //# sourceMappingURL=i18n.test.js.map \ No newline at end of file diff --git a/build/lib/test/i18n.test.ts b/build/lib/test/i18n.test.ts index b8a68323dd71..4e4545548b86 100644 --- a/build/lib/test/i18n.test.ts +++ b/build/lib/test/i18n.test.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import assert = require('assert'); -import i18n = require('../i18n'); +import assert from 'assert'; +import * as i18n from '../i18n'; suite('XLF Parser Tests', () => { const sampleXlf = 'Key #1Key #2 &'; diff --git a/build/lib/treeshaking.js b/build/lib/treeshaking.js index af06f4e3ec5d..d51eee91f1e3 100644 --- a/build/lib/treeshaking.js +++ b/build/lib/treeshaking.js @@ -3,13 +3,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.ShakeLevel = void 0; exports.toStringShakeLevel = toStringShakeLevel; exports.shake = shake; -const fs = require("fs"); -const path = require("path"); -const TYPESCRIPT_LIB_FOLDER = path.dirname(require.resolve('typescript/lib/lib.d.ts')); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const TYPESCRIPT_LIB_FOLDER = path_1.default.dirname(require.resolve('typescript/lib/lib.d.ts')); var ShakeLevel; (function (ShakeLevel) { ShakeLevel[ShakeLevel["Files"] = 0] = "Files"; @@ -30,7 +33,7 @@ function printDiagnostics(options, diagnostics) { for (const diag of diagnostics) { let result = ''; if (diag.file) { - result += `${path.join(options.sourcesRoot, diag.file.fileName)}`; + result += `${path_1.default.join(options.sourcesRoot, diag.file.fileName)}`; } if (diag.file && diag.start) { const location = diag.file.getLineAndCharacterOfPosition(diag.start); @@ -72,8 +75,8 @@ function createTypeScriptLanguageService(ts, options) { }); // Add additional typings options.typings.forEach((typing) => { - const filePath = path.join(options.sourcesRoot, typing); - FILES[typing] = fs.readFileSync(filePath).toString(); + const filePath = path_1.default.join(options.sourcesRoot, typing); + FILES[typing] = fs_1.default.readFileSync(filePath).toString(); }); // Resolve libs const RESOLVED_LIBS = processLibFiles(ts, options); @@ -104,19 +107,19 @@ function discoverAndReadFiles(ts, options) { if (options.redirects[moduleId]) { redirectedModuleId = options.redirects[moduleId]; } - const dts_filename = path.join(options.sourcesRoot, redirectedModuleId + '.d.ts'); - if (fs.existsSync(dts_filename)) { - const dts_filecontents = fs.readFileSync(dts_filename).toString(); + const dts_filename = path_1.default.join(options.sourcesRoot, redirectedModuleId + '.d.ts'); + if (fs_1.default.existsSync(dts_filename)) { + const dts_filecontents = fs_1.default.readFileSync(dts_filename).toString(); FILES[`${moduleId}.d.ts`] = dts_filecontents; continue; } - const js_filename = path.join(options.sourcesRoot, redirectedModuleId + '.js'); - if (fs.existsSync(js_filename)) { + const js_filename = path_1.default.join(options.sourcesRoot, redirectedModuleId + '.js'); + if (fs_1.default.existsSync(js_filename)) { // This is an import for a .js file, so ignore it... continue; } - const ts_filename = path.join(options.sourcesRoot, redirectedModuleId + '.ts'); - const ts_filecontents = fs.readFileSync(ts_filename).toString(); + const ts_filename = path_1.default.join(options.sourcesRoot, redirectedModuleId + '.ts'); + const ts_filecontents = fs_1.default.readFileSync(ts_filename).toString(); const info = ts.preProcessFile(ts_filecontents); for (let i = info.importedFiles.length - 1; i >= 0; i--) { const importedFileName = info.importedFiles[i].fileName; @@ -126,7 +129,7 @@ function discoverAndReadFiles(ts, options) { } let importedModuleId = importedFileName; if (/(^\.\/)|(^\.\.\/)/.test(importedModuleId)) { - importedModuleId = path.join(path.dirname(moduleId), importedModuleId); + importedModuleId = path_1.default.join(path_1.default.dirname(moduleId), importedModuleId); if (importedModuleId.endsWith('.js')) { // ESM: code imports require to be relative and have a '.js' file extension importedModuleId = importedModuleId.substr(0, importedModuleId.length - 3); } @@ -148,8 +151,8 @@ function processLibFiles(ts, options) { const key = `defaultLib:${filename}`; if (!result[key]) { // add this file - const filepath = path.join(TYPESCRIPT_LIB_FOLDER, filename); - const sourceText = fs.readFileSync(filepath).toString(); + const filepath = path_1.default.join(TYPESCRIPT_LIB_FOLDER, filename); + const sourceText = fs_1.default.readFileSync(filepath).toString(); result[key] = sourceText; // precess dependencies and "recurse" const info = ts.preProcessFile(sourceText); @@ -459,7 +462,7 @@ function markNodes(ts, languageService, options) { if (importText.endsWith('.js')) { // ESM: code imports require to be relative and to have a '.js' file extension importText = importText.substr(0, importText.length - 3); } - fullPath = path.join(path.dirname(nodeSourceFile.fileName), importText) + '.ts'; + fullPath = path_1.default.join(path_1.default.dirname(nodeSourceFile.fileName), importText) + '.ts'; } else { fullPath = importText + '.ts'; diff --git a/build/lib/treeshaking.ts b/build/lib/treeshaking.ts index cd17c5f02784..ac71bb205da7 100644 --- a/build/lib/treeshaking.ts +++ b/build/lib/treeshaking.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; +import fs from 'fs'; +import path from 'path'; import type * as ts from 'typescript'; const TYPESCRIPT_LIB_FOLDER = path.dirname(require.resolve('typescript/lib/lib.d.ts')); diff --git a/build/lib/tsb/builder.js b/build/lib/tsb/builder.js index e7a2519d1c92..f720699680d8 100644 --- a/build/lib/tsb/builder.js +++ b/build/lib/tsb/builder.js @@ -3,16 +3,52 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.CancellationToken = void 0; exports.createTypeScriptBuilder = createTypeScriptBuilder; -const fs = require("fs"); -const path = require("path"); -const crypto = require("crypto"); -const utils = require("./utils"); -const colors = require("ansi-colors"); -const ts = require("typescript"); -const Vinyl = require("vinyl"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const crypto_1 = __importDefault(require("crypto")); +const utils = __importStar(require("./utils")); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const typescript_1 = __importDefault(require("typescript")); +const vinyl_1 = __importDefault(require("vinyl")); const source_map_1 = require("source-map"); var CancellationToken; (function (CancellationToken) { @@ -28,7 +64,7 @@ function createTypeScriptBuilder(config, projectFile, cmd) { const host = new LanguageServiceHost(cmd, projectFile, _log); const outHost = new LanguageServiceHost({ ...cmd, options: { ...cmd.options, sourceRoot: cmd.options.outDir } }, cmd.options.outDir ?? '', _log); let lastCycleCheckVersion; - const service = ts.createLanguageService(host, ts.createDocumentRegistry()); + const service = typescript_1.default.createLanguageService(host, typescript_1.default.createDocumentRegistry()); const lastBuildVersion = Object.create(null); const lastDtsHash = Object.create(null); const userWantsDeclarations = cmd.options.declaration; @@ -92,7 +128,7 @@ function createTypeScriptBuilder(config, projectFile, cmd) { if (/\.d\.ts$/.test(fileName)) { // if it's already a d.ts file just emit it signature const snapshot = host.getScriptSnapshot(fileName); - const signature = crypto.createHash('sha256') + const signature = crypto_1.default.createHash('sha256') .update(snapshot.getText(0, snapshot.getLength())) .digest('base64'); return resolve({ @@ -109,7 +145,7 @@ function createTypeScriptBuilder(config, projectFile, cmd) { continue; } if (/\.d\.ts$/.test(file.name)) { - signature = crypto.createHash('sha256') + signature = crypto_1.default.createHash('sha256') .update(file.text) .digest('base64'); if (!userWantsDeclarations) { @@ -117,7 +153,7 @@ function createTypeScriptBuilder(config, projectFile, cmd) { continue; } } - const vinyl = new Vinyl({ + const vinyl = new vinyl_1.default({ path: file.name, contents: Buffer.from(file.text), base: !config._emitWithoutBasePath && baseFor(host.getScriptSnapshot(fileName)) || undefined @@ -125,9 +161,9 @@ function createTypeScriptBuilder(config, projectFile, cmd) { if (!emitSourceMapsInStream && /\.js$/.test(file.name)) { const sourcemapFile = output.outputFiles.filter(f => /\.js\.map$/.test(f.name))[0]; if (sourcemapFile) { - const extname = path.extname(vinyl.relative); - const basename = path.basename(vinyl.relative, extname); - const dirname = path.dirname(vinyl.relative); + const extname = path_1.default.extname(vinyl.relative); + const basename = path_1.default.basename(vinyl.relative, extname); + const dirname = path_1.default.dirname(vinyl.relative); const tsname = (dirname === '.' ? '' : dirname + '/') + basename + '.ts'; let sourceMap = JSON.parse(sourcemapFile.text); sourceMap.sources[0] = tsname.replace(/\\/g, '/'); @@ -359,7 +395,7 @@ function createTypeScriptBuilder(config, projectFile, cmd) { delete oldErrors[projectFile]; if (oneCycle) { const cycleError = { - category: ts.DiagnosticCategory.Error, + category: typescript_1.default.DiagnosticCategory.Error, code: 1, file: undefined, start: undefined, @@ -383,7 +419,7 @@ function createTypeScriptBuilder(config, projectFile, cmd) { // print stats const headNow = process.memoryUsage().heapUsed; const MB = 1024 * 1024; - _log('[tsb]', `time: ${colors.yellow((Date.now() - t1) + 'ms')} + \nmem: ${colors.cyan(Math.ceil(headNow / MB) + 'MB')} ${colors.bgcyan('delta: ' + Math.ceil((headNow - headUsed) / MB))}`); + _log('[tsb]', `time: ${ansi_colors_1.default.yellow((Date.now() - t1) + 'ms')} + \nmem: ${ansi_colors_1.default.cyan(Math.ceil(headNow / MB) + 'MB')} ${ansi_colors_1.default.bgcyan('delta: ' + Math.ceil((headNow - headUsed) / MB))}`); headUsed = headNow; }); } @@ -480,11 +516,11 @@ class LanguageServiceHost { let result = this._snapshots[filename]; if (!result && resolve) { try { - result = new VinylScriptSnapshot(new Vinyl({ + result = new VinylScriptSnapshot(new vinyl_1.default({ path: filename, - contents: fs.readFileSync(filename), + contents: fs_1.default.readFileSync(filename), base: this.getCompilationSettings().outDir, - stat: fs.statSync(filename) + stat: fs_1.default.statSync(filename) })); this.addScriptSnapshot(filename, result); } @@ -529,16 +565,16 @@ class LanguageServiceHost { return delete this._snapshots[filename]; } getCurrentDirectory() { - return path.dirname(this._projectPath); + return path_1.default.dirname(this._projectPath); } getDefaultLibFileName(options) { - return ts.getDefaultLibFilePath(options); + return typescript_1.default.getDefaultLibFilePath(options); } - directoryExists = ts.sys.directoryExists; - getDirectories = ts.sys.getDirectories; - fileExists = ts.sys.fileExists; - readFile = ts.sys.readFile; - readDirectory = ts.sys.readDirectory; + directoryExists = typescript_1.default.sys.directoryExists; + getDirectories = typescript_1.default.sys.getDirectories; + fileExists = typescript_1.default.sys.fileExists; + readFile = typescript_1.default.sys.readFile; + readDirectory = typescript_1.default.sys.readDirectory; // ---- dependency management collectDependents(filename, target) { while (this._dependenciesRecomputeList.length) { @@ -570,18 +606,18 @@ class LanguageServiceHost { this._log('processFile', `Missing snapshot for: ${filename}`); return; } - const info = ts.preProcessFile(snapshot.getText(0, snapshot.getLength()), true); + const info = typescript_1.default.preProcessFile(snapshot.getText(0, snapshot.getLength()), true); // (0) clear out old dependencies this._dependencies.resetNode(filename); // (1) ///-references info.referencedFiles.forEach(ref => { - const resolvedPath = path.resolve(path.dirname(filename), ref.fileName); + const resolvedPath = path_1.default.resolve(path_1.default.dirname(filename), ref.fileName); const normalizedPath = normalize(resolvedPath); this._dependencies.inertEdge(filename, normalizedPath); }); // (2) import-require statements info.importedFiles.forEach(ref => { - if (!ref.fileName.startsWith('.') || path.extname(ref.fileName) === '') { + if (!ref.fileName.startsWith('.') || path_1.default.extname(ref.fileName) === '') { // node module? return; } @@ -589,8 +625,8 @@ class LanguageServiceHost { let dirname = filename; let found = false; while (!found && dirname.indexOf(stopDirname) === 0) { - dirname = path.dirname(dirname); - let resolvedPath = path.resolve(dirname, ref.fileName); + dirname = path_1.default.dirname(dirname); + let resolvedPath = path_1.default.resolve(dirname, ref.fileName); if (resolvedPath.endsWith('.js')) { resolvedPath = resolvedPath.slice(0, -3); } diff --git a/build/lib/tsb/builder.ts b/build/lib/tsb/builder.ts index 509284d0cdcb..403d2cec9327 100644 --- a/build/lib/tsb/builder.ts +++ b/build/lib/tsb/builder.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import * as crypto from 'crypto'; +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; import * as utils from './utils'; -import * as colors from 'ansi-colors'; -import * as ts from 'typescript'; -import * as Vinyl from 'vinyl'; +import colors from 'ansi-colors'; +import ts from 'typescript'; +import Vinyl from 'vinyl'; import { RawSourceMap, SourceMapConsumer, SourceMapGenerator } from 'source-map'; export interface IConfiguration { diff --git a/build/lib/tsb/index.js b/build/lib/tsb/index.js index 204c06e80ac9..843b76c823fc 100644 --- a/build/lib/tsb/index.js +++ b/build/lib/tsb/index.js @@ -3,17 +3,53 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.create = create; -const Vinyl = require("vinyl"); -const through = require("through"); -const builder = require("./builder"); -const ts = require("typescript"); +const vinyl_1 = __importDefault(require("vinyl")); +const through_1 = __importDefault(require("through")); +const builder = __importStar(require("./builder")); +const typescript_1 = __importDefault(require("typescript")); const stream_1 = require("stream"); const path_1 = require("path"); const utils_1 = require("./utils"); const fs_1 = require("fs"); -const log = require("fancy-log"); +const fancy_log_1 = __importDefault(require("fancy-log")); const transpiler_1 = require("./transpiler"); const colors = require("ansi-colors"); class EmptyDuplex extends stream_1.Duplex { @@ -32,31 +68,31 @@ function create(projectPath, existingOptions, config, onError = _defaultOnError) onError(diag.message); } else if (!diag.file || !diag.start) { - onError(ts.flattenDiagnosticMessageText(diag.messageText, '\n')); + onError(typescript_1.default.flattenDiagnosticMessageText(diag.messageText, '\n')); } else { const lineAndCh = diag.file.getLineAndCharacterOfPosition(diag.start); - onError(utils_1.strings.format('{0}({1},{2}): {3}', diag.file.fileName, lineAndCh.line + 1, lineAndCh.character + 1, ts.flattenDiagnosticMessageText(diag.messageText, '\n'))); + onError(utils_1.strings.format('{0}({1},{2}): {3}', diag.file.fileName, lineAndCh.line + 1, lineAndCh.character + 1, typescript_1.default.flattenDiagnosticMessageText(diag.messageText, '\n'))); } } - const parsed = ts.readConfigFile(projectPath, ts.sys.readFile); + const parsed = typescript_1.default.readConfigFile(projectPath, typescript_1.default.sys.readFile); if (parsed.error) { printDiagnostic(parsed.error); return createNullCompiler(); } - const cmdLine = ts.parseJsonConfigFileContent(parsed.config, ts.sys, (0, path_1.dirname)(projectPath), existingOptions); + const cmdLine = typescript_1.default.parseJsonConfigFileContent(parsed.config, typescript_1.default.sys, (0, path_1.dirname)(projectPath), existingOptions); if (cmdLine.errors.length > 0) { cmdLine.errors.forEach(printDiagnostic); return createNullCompiler(); } function logFn(topic, message) { if (config.verbose) { - log(colors.cyan(topic), message); + (0, fancy_log_1.default)(colors.cyan(topic), message); } } // FULL COMPILE stream doing transpile, syntax and semantic diagnostics function createCompileStream(builder, token) { - return through(function (file) { + return (0, through_1.default)(function (file) { // give the file to the compiler if (file.isStream()) { this.emit('error', 'no support for streams'); @@ -70,7 +106,7 @@ function create(projectPath, existingOptions, config, onError = _defaultOnError) } // TRANSPILE ONLY stream doing just TS to JS conversion function createTranspileStream(transpiler) { - return through(function (file) { + return (0, through_1.default)(function (file) { // give the file to the compiler if (file.isStream()) { this.emit('error', 'no support for streams'); @@ -116,7 +152,7 @@ function create(projectPath, existingOptions, config, onError = _defaultOnError) let path; for (; more && _pos < _fileNames.length; _pos++) { path = _fileNames[_pos]; - more = this.push(new Vinyl({ + more = this.push(new vinyl_1.default({ path, contents: (0, fs_1.readFileSync)(path), stat: (0, fs_1.statSync)(path), diff --git a/build/lib/tsb/index.ts b/build/lib/tsb/index.ts index 53c752d2655a..e577d386cd9d 100644 --- a/build/lib/tsb/index.ts +++ b/build/lib/tsb/index.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as Vinyl from 'vinyl'; -import * as through from 'through'; +import Vinyl from 'vinyl'; +import through from 'through'; import * as builder from './builder'; -import * as ts from 'typescript'; +import ts from 'typescript'; import { Readable, Writable, Duplex } from 'stream'; import { dirname } from 'path'; import { strings } from './utils'; import { readFileSync, statSync } from 'fs'; -import * as log from 'fancy-log'; +import log from 'fancy-log'; import { ESBuildTranspiler, ITranspiler, TscTranspiler } from './transpiler'; import colors = require('ansi-colors'); diff --git a/build/lib/tsb/transpiler.js b/build/lib/tsb/transpiler.js index a4439b8d7aeb..adccb1044166 100644 --- a/build/lib/tsb/transpiler.js +++ b/build/lib/tsb/transpiler.js @@ -3,28 +3,31 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.ESBuildTranspiler = exports.TscTranspiler = void 0; -const esbuild = require("esbuild"); -const ts = require("typescript"); -const threads = require("node:worker_threads"); -const Vinyl = require("vinyl"); +const esbuild_1 = __importDefault(require("esbuild")); +const typescript_1 = __importDefault(require("typescript")); +const node_worker_threads_1 = __importDefault(require("node:worker_threads")); +const vinyl_1 = __importDefault(require("vinyl")); const node_os_1 = require("node:os"); function transpile(tsSrc, options) { const isAmd = /\n(import|export)/m.test(tsSrc); - if (!isAmd && options.compilerOptions?.module === ts.ModuleKind.AMD) { + if (!isAmd && options.compilerOptions?.module === typescript_1.default.ModuleKind.AMD) { // enforce NONE module-system for not-amd cases - options = { ...options, ...{ compilerOptions: { ...options.compilerOptions, module: ts.ModuleKind.None } } }; + options = { ...options, ...{ compilerOptions: { ...options.compilerOptions, module: typescript_1.default.ModuleKind.None } } }; } - const out = ts.transpileModule(tsSrc, options); + const out = typescript_1.default.transpileModule(tsSrc, options); return { jsSrc: out.outputText, diag: out.diagnostics ?? [] }; } -if (!threads.isMainThread) { +if (!node_worker_threads_1.default.isMainThread) { // WORKER - threads.parentPort?.addListener('message', (req) => { + node_worker_threads_1.default.parentPort?.addListener('message', (req) => { const res = { jsSrcs: [], diagnostics: [] @@ -34,7 +37,7 @@ if (!threads.isMainThread) { res.jsSrcs.push(out.jsSrc); res.diagnostics.push(out.diag); } - threads.parentPort.postMessage(res); + node_worker_threads_1.default.parentPort.postMessage(res); }); } class OutputFileNameOracle { @@ -43,7 +46,7 @@ class OutputFileNameOracle { this.getOutputFileName = (file) => { try { // windows: path-sep normalizing - file = ts.normalizePath(file); + file = typescript_1.default.normalizePath(file); if (!cmdLine.options.configFilePath) { // this is needed for the INTERNAL getOutputFileNames-call below... cmdLine.options.configFilePath = configFilePath; @@ -53,7 +56,7 @@ class OutputFileNameOracle { file = file.slice(0, -5) + '.ts'; cmdLine.fileNames.push(file); } - const outfile = ts.getOutputFileNames(cmdLine, file, true)[0]; + const outfile = typescript_1.default.getOutputFileNames(cmdLine, file, true)[0]; if (isDts) { cmdLine.fileNames.pop(); } @@ -70,7 +73,7 @@ class OutputFileNameOracle { class TranspileWorker { static pool = 1; id = TranspileWorker.pool++; - _worker = new threads.Worker(__filename); + _worker = new node_worker_threads_1.default.Worker(__filename); _pending; _durations = []; constructor(outFileFn) { @@ -107,7 +110,7 @@ class TranspileWorker { } const outBase = options.compilerOptions?.outDir ?? file.base; const outPath = outFileFn(file.path); - outFiles.push(new Vinyl({ + outFiles.push(new vinyl_1.default({ path: outPath, base: outBase, contents: Buffer.from(jsSrc), @@ -249,7 +252,7 @@ class ESBuildTranspiler { compilerOptions: { ...this._cmdLine.options, ...{ - module: isExtension ? ts.ModuleKind.CommonJS : undefined + module: isExtension ? typescript_1.default.ModuleKind.CommonJS : undefined } } }), @@ -270,7 +273,7 @@ class ESBuildTranspiler { throw Error('file.contents must be a Buffer'); } const t1 = Date.now(); - this._jobs.push(esbuild.transform(file.contents, { + this._jobs.push(esbuild_1.default.transform(file.contents, { ...this._transformOpts, sourcefile: file.path, }).then(result => { @@ -281,7 +284,7 @@ class ESBuildTranspiler { } const outBase = this._cmdLine.options.outDir ?? file.base; const outPath = this._outputFileNames.getOutputFileName(file.path); - this.onOutfile(new Vinyl({ + this.onOutfile(new vinyl_1.default({ path: outPath, base: outBase, contents: Buffer.from(result.code), diff --git a/build/lib/tsb/transpiler.ts b/build/lib/tsb/transpiler.ts index ae841dcf88b7..16a3b3475388 100644 --- a/build/lib/tsb/transpiler.ts +++ b/build/lib/tsb/transpiler.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as esbuild from 'esbuild'; -import * as ts from 'typescript'; -import * as threads from 'node:worker_threads'; -import * as Vinyl from 'vinyl'; +import esbuild from 'esbuild'; +import ts from 'typescript'; +import threads from 'node:worker_threads'; +import Vinyl from 'vinyl'; import { cpus } from 'node:os'; interface TranspileReq { diff --git a/build/lib/typings/event-stream.d.ts b/build/lib/typings/event-stream.d.ts index 260051be52e5..2b021ef258ee 100644 --- a/build/lib/typings/event-stream.d.ts +++ b/build/lib/typings/event-stream.d.ts @@ -1,7 +1,7 @@ declare module "event-stream" { import { Stream } from 'stream'; import { ThroughStream as _ThroughStream } from 'through'; - import * as File from 'vinyl'; + import File from 'vinyl'; export interface ThroughStream extends _ThroughStream { queue(data: File | null): any; diff --git a/build/lib/util.js b/build/lib/util.js index 82e4189dd1a7..8b6f03962817 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -3,6 +3,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.incremental = incremental; exports.debounce = debounce; @@ -23,20 +26,20 @@ exports.rebase = rebase; exports.filter = filter; exports.streamToPromise = streamToPromise; exports.getElectronVersion = getElectronVersion; -const es = require("event-stream"); -const _debounce = require("debounce"); -const _filter = require("gulp-filter"); -const rename = require("gulp-rename"); -const path = require("path"); -const fs = require("fs"); -const _rimraf = require("rimraf"); +const event_stream_1 = __importDefault(require("event-stream")); +const debounce_1 = __importDefault(require("debounce")); +const gulp_filter_1 = __importDefault(require("gulp-filter")); +const gulp_rename_1 = __importDefault(require("gulp-rename")); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); +const rimraf_1 = __importDefault(require("rimraf")); const url_1 = require("url"); -const ternaryStream = require("ternary-stream"); -const root = path.dirname(path.dirname(__dirname)); +const ternary_stream_1 = __importDefault(require("ternary-stream")); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); const NoCancellationToken = { isCancellationRequested: () => false }; function incremental(streamProvider, initial, supportsCancellation) { - const input = es.through(); - const output = es.through(); + const input = event_stream_1.default.through(); + const output = event_stream_1.default.through(); let state = 'idle'; let buffer = Object.create(null); const token = !supportsCancellation ? undefined : { isCancellationRequested: () => Object.keys(buffer).length > 0 }; @@ -45,7 +48,7 @@ function incremental(streamProvider, initial, supportsCancellation) { const stream = !supportsCancellation ? streamProvider() : streamProvider(isCancellable ? token : NoCancellationToken); input .pipe(stream) - .pipe(es.through(undefined, () => { + .pipe(event_stream_1.default.through(undefined, () => { state = 'idle'; eventuallyRun(); })) @@ -54,14 +57,14 @@ function incremental(streamProvider, initial, supportsCancellation) { if (initial) { run(initial, false); } - const eventuallyRun = _debounce(() => { + const eventuallyRun = (0, debounce_1.default)(() => { const paths = Object.keys(buffer); if (paths.length === 0) { return; } const data = paths.map(path => buffer[path]); buffer = Object.create(null); - run(es.readArray(data), true); + run(event_stream_1.default.readArray(data), true); }, 500); input.on('data', (f) => { buffer[f.path] = f; @@ -69,16 +72,16 @@ function incremental(streamProvider, initial, supportsCancellation) { eventuallyRun(); } }); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } function debounce(task, duration = 500) { - const input = es.through(); - const output = es.through(); + const input = event_stream_1.default.through(); + const output = event_stream_1.default.through(); let state = 'idle'; const run = () => { state = 'running'; task() - .pipe(es.through(undefined, () => { + .pipe(event_stream_1.default.through(undefined, () => { const shouldRunAgain = state === 'stale'; state = 'idle'; if (shouldRunAgain) { @@ -88,7 +91,7 @@ function debounce(task, duration = 500) { .pipe(output); }; run(); - const eventuallyRun = _debounce(() => run(), duration); + const eventuallyRun = (0, debounce_1.default)(() => run(), duration); input.on('data', () => { if (state === 'idle') { eventuallyRun(); @@ -97,13 +100,13 @@ function debounce(task, duration = 500) { state = 'stale'; } }); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } function fixWin32DirectoryPermissions() { if (!/win32/.test(process.platform)) { - return es.through(); + return event_stream_1.default.through(); } - return es.mapSync(f => { + return event_stream_1.default.mapSync(f => { if (f.stat && f.stat.isDirectory && f.stat.isDirectory()) { f.stat.mode = 16877; } @@ -111,7 +114,7 @@ function fixWin32DirectoryPermissions() { }); } function setExecutableBit(pattern) { - const setBit = es.mapSync(f => { + const setBit = event_stream_1.default.mapSync(f => { if (!f.stat) { f.stat = { isFile() { return true; } }; } @@ -121,13 +124,13 @@ function setExecutableBit(pattern) { if (!pattern) { return setBit; } - const input = es.through(); - const filter = _filter(pattern, { restore: true }); + const input = event_stream_1.default.through(); + const filter = (0, gulp_filter_1.default)(pattern, { restore: true }); const output = input .pipe(filter) .pipe(setBit) .pipe(filter.restore); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } function toFileUri(filePath) { const match = filePath.match(/^([a-z])\:(.*)$/i); @@ -137,27 +140,27 @@ function toFileUri(filePath) { return 'file://' + filePath.replace(/\\/g, '/'); } function skipDirectories() { - return es.mapSync(f => { + return event_stream_1.default.mapSync(f => { if (!f.isDirectory()) { return f; } }); } function cleanNodeModules(rulePath) { - const rules = fs.readFileSync(rulePath, 'utf8') + const rules = fs_1.default.readFileSync(rulePath, 'utf8') .split(/\r?\n/g) .map(line => line.trim()) .filter(line => line && !/^#/.test(line)); const excludes = rules.filter(line => !/^!/.test(line)).map(line => `!**/node_modules/${line}`); const includes = rules.filter(line => /^!/.test(line)).map(line => `**/node_modules/${line.substr(1)}`); - const input = es.through(); - const output = es.merge(input.pipe(_filter(['**', ...excludes])), input.pipe(_filter(includes))); - return es.duplex(input, output); + const input = event_stream_1.default.through(); + const output = event_stream_1.default.merge(input.pipe((0, gulp_filter_1.default)(['**', ...excludes])), input.pipe((0, gulp_filter_1.default)(includes))); + return event_stream_1.default.duplex(input, output); } function loadSourcemaps() { - const input = es.through(); + const input = event_stream_1.default.through(); const output = input - .pipe(es.map((f, cb) => { + .pipe(event_stream_1.default.map((f, cb) => { if (f.sourceMap) { cb(undefined, f); return; @@ -185,7 +188,7 @@ function loadSourcemaps() { return; } f.contents = Buffer.from(contents.replace(/\/\/# sourceMappingURL=(.*)$/g, ''), 'utf8'); - fs.readFile(path.join(path.dirname(f.path), lastMatch[1]), 'utf8', (err, contents) => { + fs_1.default.readFile(path_1.default.join(path_1.default.dirname(f.path), lastMatch[1]), 'utf8', (err, contents) => { if (err) { return cb(err); } @@ -193,54 +196,54 @@ function loadSourcemaps() { cb(undefined, f); }); })); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } function stripSourceMappingURL() { - const input = es.through(); + const input = event_stream_1.default.through(); const output = input - .pipe(es.mapSync(f => { + .pipe(event_stream_1.default.mapSync(f => { const contents = f.contents.toString('utf8'); f.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, ''), 'utf8'); return f; })); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } /** Splits items in the stream based on the predicate, sending them to onTrue if true, or onFalse otherwise */ -function $if(test, onTrue, onFalse = es.through()) { +function $if(test, onTrue, onFalse = event_stream_1.default.through()) { if (typeof test === 'boolean') { return test ? onTrue : onFalse; } - return ternaryStream(test, onTrue, onFalse); + return (0, ternary_stream_1.default)(test, onTrue, onFalse); } /** Operator that appends the js files' original path a sourceURL, so debug locations map */ function appendOwnPathSourceURL() { - const input = es.through(); + const input = event_stream_1.default.through(); const output = input - .pipe(es.mapSync(f => { + .pipe(event_stream_1.default.mapSync(f => { if (!(f.contents instanceof Buffer)) { throw new Error(`contents of ${f.path} are not a buffer`); } f.contents = Buffer.concat([f.contents, Buffer.from(`\n//# sourceURL=${(0, url_1.pathToFileURL)(f.path)}`)]); return f; })); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } function rewriteSourceMappingURL(sourceMappingURLBase) { - const input = es.through(); + const input = event_stream_1.default.through(); const output = input - .pipe(es.mapSync(f => { + .pipe(event_stream_1.default.mapSync(f => { const contents = f.contents.toString('utf8'); - const str = `//# sourceMappingURL=${sourceMappingURLBase}/${path.dirname(f.relative).replace(/\\/g, '/')}/$1`; + const str = `//# sourceMappingURL=${sourceMappingURLBase}/${path_1.default.dirname(f.relative).replace(/\\/g, '/')}/$1`; f.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, str)); return f; })); - return es.duplex(input, output); + return event_stream_1.default.duplex(input, output); } function rimraf(dir) { const result = () => new Promise((c, e) => { let retries = 0; const retry = () => { - _rimraf(dir, { maxBusyTries: 1 }, (err) => { + (0, rimraf_1.default)(dir, { maxBusyTries: 1 }, (err) => { if (!err) { return c(); } @@ -252,14 +255,14 @@ function rimraf(dir) { }; retry(); }); - result.taskName = `clean-${path.basename(dir).toLowerCase()}`; + result.taskName = `clean-${path_1.default.basename(dir).toLowerCase()}`; return result; } function _rreaddir(dirPath, prepend, result) { - const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + const entries = fs_1.default.readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { - _rreaddir(path.join(dirPath, entry.name), `${prepend}/${entry.name}`, result); + _rreaddir(path_1.default.join(dirPath, entry.name), `${prepend}/${entry.name}`, result); } else { result.push(`${prepend}/${entry.name}`); @@ -272,20 +275,20 @@ function rreddir(dirPath) { return result; } function ensureDir(dirPath) { - if (fs.existsSync(dirPath)) { + if (fs_1.default.existsSync(dirPath)) { return; } - ensureDir(path.dirname(dirPath)); - fs.mkdirSync(dirPath); + ensureDir(path_1.default.dirname(dirPath)); + fs_1.default.mkdirSync(dirPath); } function rebase(count) { - return rename(f => { + return (0, gulp_rename_1.default)(f => { const parts = f.dirname ? f.dirname.split(/[\/\\]/) : []; - f.dirname = parts.slice(count).join(path.sep); + f.dirname = parts.slice(count).join(path_1.default.sep); }); } function filter(fn) { - const result = es.through(function (data) { + const result = event_stream_1.default.through(function (data) { if (fn(data)) { this.emit('data', data); } @@ -293,7 +296,7 @@ function filter(fn) { result.restore.push(data); } }); - result.restore = es.through(); + result.restore = event_stream_1.default.through(); return result; } function streamToPromise(stream) { @@ -303,7 +306,7 @@ function streamToPromise(stream) { }); } function getElectronVersion() { - const npmrc = fs.readFileSync(path.join(root, '.npmrc'), 'utf8'); + const npmrc = fs_1.default.readFileSync(path_1.default.join(root, '.npmrc'), 'utf8'); const electronVersion = /^target="(.*)"$/m.exec(npmrc)[1]; const msBuildId = /^ms_build_id="(.*)"$/m.exec(npmrc)[1]; return { electronVersion, msBuildId }; diff --git a/build/lib/util.ts b/build/lib/util.ts index 08921834676d..ad81730b3de3 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; -import _debounce = require('debounce'); -import * as _filter from 'gulp-filter'; -import * as rename from 'gulp-rename'; -import * as path from 'path'; -import * as fs from 'fs'; -import * as _rimraf from 'rimraf'; -import * as VinylFile from 'vinyl'; +import es from 'event-stream'; +import _debounce from 'debounce'; +import _filter from 'gulp-filter'; +import rename from 'gulp-rename'; +import path from 'path'; +import fs from 'fs'; +import _rimraf from 'rimraf'; +import VinylFile from 'vinyl'; import { ThroughStream } from 'through'; -import * as sm from 'source-map'; +import sm from 'source-map'; import { pathToFileURL } from 'url'; -import * as ternaryStream from 'ternary-stream'; +import ternaryStream from 'ternary-stream'; const root = path.dirname(path.dirname(__dirname)); diff --git a/build/lib/watch/index.js b/build/lib/watch/index.js index 86d2611febf9..69eca78fd704 100644 --- a/build/lib/watch/index.js +++ b/build/lib/watch/index.js @@ -3,6 +3,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); const watch = process.platform === 'win32' ? require('./watch-win32') : require('vscode-gulp-watch'); module.exports = function () { return watch.apply(null, arguments); diff --git a/build/lib/watch/watch-win32.js b/build/lib/watch/watch-win32.js index 934d8e8110f2..7b77981d620e 100644 --- a/build/lib/watch/watch-win32.js +++ b/build/lib/watch/watch-win32.js @@ -3,14 +3,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); -const cp = require("child_process"); -const fs = require("fs"); -const File = require("vinyl"); -const es = require("event-stream"); -const filter = require("gulp-filter"); -const watcherPath = path.join(__dirname, 'watcher.exe'); +const path_1 = __importDefault(require("path")); +const child_process_1 = __importDefault(require("child_process")); +const fs_1 = __importDefault(require("fs")); +const vinyl_1 = __importDefault(require("vinyl")); +const event_stream_1 = __importDefault(require("event-stream")); +const gulp_filter_1 = __importDefault(require("gulp-filter")); +const watcherPath = path_1.default.join(__dirname, 'watcher.exe'); function toChangeType(type) { switch (type) { case '0': return 'change'; @@ -19,8 +22,8 @@ function toChangeType(type) { } } function watch(root) { - const result = es.through(); - let child = cp.spawn(watcherPath, [root]); + const result = event_stream_1.default.through(); + let child = child_process_1.default.spawn(watcherPath, [root]); child.stdout.on('data', function (data) { const lines = data.toString('utf8').split('\n'); for (let i = 0; i < lines.length; i++) { @@ -34,8 +37,8 @@ function watch(root) { if (/^\.git/.test(changePath) || /(^|\\)out($|\\)/.test(changePath)) { continue; } - const changePathFull = path.join(root, changePath); - const file = new File({ + const changePathFull = path_1.default.join(root, changePath); + const file = new vinyl_1.default({ path: changePathFull, base: root }); @@ -60,20 +63,20 @@ function watch(root) { const cache = Object.create(null); module.exports = function (pattern, options) { options = options || {}; - const cwd = path.normalize(options.cwd || process.cwd()); + const cwd = path_1.default.normalize(options.cwd || process.cwd()); let watcher = cache[cwd]; if (!watcher) { watcher = cache[cwd] = watch(cwd); } - const rebase = !options.base ? es.through() : es.mapSync(function (f) { + const rebase = !options.base ? event_stream_1.default.through() : event_stream_1.default.mapSync(function (f) { f.base = options.base; return f; }); return watcher - .pipe(filter(['**', '!.git{,/**}'], { dot: options.dot })) // ignore all things git - .pipe(filter(pattern, { dot: options.dot })) - .pipe(es.map(function (file, cb) { - fs.stat(file.path, function (err, stat) { + .pipe((0, gulp_filter_1.default)(['**', '!.git{,/**}'], { dot: options.dot })) // ignore all things git + .pipe((0, gulp_filter_1.default)(pattern, { dot: options.dot })) + .pipe(event_stream_1.default.map(function (file, cb) { + fs_1.default.stat(file.path, function (err, stat) { if (err && err.code === 'ENOENT') { return cb(undefined, file); } @@ -83,7 +86,7 @@ module.exports = function (pattern, options) { if (!stat.isFile()) { return cb(); } - fs.readFile(file.path, function (err, contents) { + fs_1.default.readFile(file.path, function (err, contents) { if (err && err.code === 'ENOENT') { return cb(undefined, file); } diff --git a/build/lib/watch/watch-win32.ts b/build/lib/watch/watch-win32.ts index afde6a79f221..bbfde6afba98 100644 --- a/build/lib/watch/watch-win32.ts +++ b/build/lib/watch/watch-win32.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as cp from 'child_process'; -import * as fs from 'fs'; -import * as File from 'vinyl'; -import * as es from 'event-stream'; -import * as filter from 'gulp-filter'; +import path from 'path'; +import cp from 'child_process'; +import fs from 'fs'; +import File from 'vinyl'; +import es from 'event-stream'; +import filter from 'gulp-filter'; import { Stream } from 'stream'; const watcherPath = path.join(__dirname, 'watcher.exe'); diff --git a/build/linux/debian/calculate-deps.js b/build/linux/debian/calculate-deps.js index bbcb6bfc3de4..34276ce7705c 100644 --- a/build/linux/debian/calculate-deps.js +++ b/build/linux/debian/calculate-deps.js @@ -3,13 +3,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.generatePackageDeps = generatePackageDeps; const child_process_1 = require("child_process"); const fs_1 = require("fs"); const os_1 = require("os"); -const path = require("path"); -const manifests = require("../../../cgmanifest.json"); +const path_1 = __importDefault(require("path")); +const cgmanifest_json_1 = __importDefault(require("../../../cgmanifest.json")); const dep_lists_1 = require("./dep-lists"); function generatePackageDeps(files, arch, chromiumSysroot, vscodeSysroot) { const dependencies = files.map(file => calculatePackageDeps(file, arch, chromiumSysroot, vscodeSysroot)); @@ -29,7 +32,7 @@ function calculatePackageDeps(binaryPath, arch, chromiumSysroot, vscodeSysroot) console.error('Tried to stat ' + binaryPath + ' but failed.'); } // Get the Chromium dpkg-shlibdeps file. - const chromiumManifest = manifests.registrations.filter(registration => { + const chromiumManifest = cgmanifest_json_1.default.registrations.filter(registration => { return registration.component.type === 'git' && registration.component.git.name === 'chromium'; }); const dpkgShlibdepsUrl = `https://raw.githubusercontent.com/chromium/chromium/${chromiumManifest[0].version}/third_party/dpkg-shlibdeps/dpkg-shlibdeps.pl`; @@ -52,7 +55,7 @@ function calculatePackageDeps(binaryPath, arch, chromiumSysroot, vscodeSysroot) } cmd.push(`-l${chromiumSysroot}/usr/lib`); cmd.push(`-L${vscodeSysroot}/debian/libxkbfile1/DEBIAN/shlibs`); - cmd.push('-O', '-e', path.resolve(binaryPath)); + cmd.push('-O', '-e', path_1.default.resolve(binaryPath)); const dpkgShlibdepsResult = (0, child_process_1.spawnSync)('perl', cmd, { cwd: chromiumSysroot }); if (dpkgShlibdepsResult.status !== 0) { throw new Error(`dpkg-shlibdeps failed with exit code ${dpkgShlibdepsResult.status}. stderr:\n${dpkgShlibdepsResult.stderr} `); diff --git a/build/linux/debian/calculate-deps.ts b/build/linux/debian/calculate-deps.ts index 92f8065f2629..addc38696a88 100644 --- a/build/linux/debian/calculate-deps.ts +++ b/build/linux/debian/calculate-deps.ts @@ -6,8 +6,8 @@ import { spawnSync } from 'child_process'; import { constants, statSync } from 'fs'; import { tmpdir } from 'os'; -import path = require('path'); -import * as manifests from '../../../cgmanifest.json'; +import path from 'path'; +import manifests from '../../../cgmanifest.json'; import { additionalDeps } from './dep-lists'; import { DebianArchString } from './types'; diff --git a/build/linux/debian/install-sysroot.js b/build/linux/debian/install-sysroot.js index 354c67a2909e..16d8d01468f0 100644 --- a/build/linux/debian/install-sysroot.js +++ b/build/linux/debian/install-sysroot.js @@ -3,20 +3,23 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.getVSCodeSysroot = getVSCodeSysroot; exports.getChromiumSysroot = getChromiumSysroot; const child_process_1 = require("child_process"); const os_1 = require("os"); -const fs = require("fs"); -const https = require("https"); -const path = require("path"); +const fs_1 = __importDefault(require("fs")); +const https_1 = __importDefault(require("https")); +const path_1 = __importDefault(require("path")); const crypto_1 = require("crypto"); -const ansiColors = require("ansi-colors"); +const ansi_colors_1 = __importDefault(require("ansi-colors")); // Based on https://source.chromium.org/chromium/chromium/src/+/main:build/linux/sysroot_scripts/install-sysroot.py. const URL_PREFIX = 'https://msftelectronbuild.z5.web.core.windows.net'; const URL_PATH = 'sysroots/toolchain'; -const REPO_ROOT = path.dirname(path.dirname(path.dirname(__dirname))); +const REPO_ROOT = path_1.default.dirname(path_1.default.dirname(path_1.default.dirname(__dirname))); const ghApiHeaders = { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'VSCode Build', @@ -29,7 +32,7 @@ const ghDownloadHeaders = { Accept: 'application/octet-stream', }; function getElectronVersion() { - const npmrc = fs.readFileSync(path.join(REPO_ROOT, '.npmrc'), 'utf8'); + const npmrc = fs_1.default.readFileSync(path_1.default.join(REPO_ROOT, '.npmrc'), 'utf8'); const electronVersion = /^target="(.*)"$/m.exec(npmrc)[1]; const msBuildId = /^ms_build_id="(.*)"$/m.exec(npmrc)[1]; return { electronVersion, msBuildId }; @@ -37,11 +40,11 @@ function getElectronVersion() { function getSha(filename) { const hash = (0, crypto_1.createHash)('sha256'); // Read file 1 MB at a time - const fd = fs.openSync(filename, 'r'); + const fd = fs_1.default.openSync(filename, 'r'); const buffer = Buffer.alloc(1024 * 1024); let position = 0; let bytesRead = 0; - while ((bytesRead = fs.readSync(fd, buffer, 0, buffer.length, position)) === buffer.length) { + while ((bytesRead = fs_1.default.readSync(fd, buffer, 0, buffer.length, position)) === buffer.length) { hash.update(buffer); position += bytesRead; } @@ -49,7 +52,7 @@ function getSha(filename) { return hash.digest('hex'); } function getVSCodeSysrootChecksum(expectedName) { - const checksums = fs.readFileSync(path.join(REPO_ROOT, 'build', 'checksums', 'vscode-sysroot.txt'), 'utf8'); + const checksums = fs_1.default.readFileSync(path_1.default.join(REPO_ROOT, 'build', 'checksums', 'vscode-sysroot.txt'), 'utf8'); for (const line of checksums.split('\n')) { const [checksum, name] = line.split(/\s+/); if (name === expectedName) { @@ -86,22 +89,22 @@ async function fetchUrl(options, retries = 10, retryDelay = 1000) { }); if (assetResponse.ok && (assetResponse.status >= 200 && assetResponse.status < 300)) { const assetContents = Buffer.from(await assetResponse.arrayBuffer()); - console.log(`Fetched response body buffer: ${ansiColors.magenta(`${assetContents.byteLength} bytes`)}`); + console.log(`Fetched response body buffer: ${ansi_colors_1.default.magenta(`${assetContents.byteLength} bytes`)}`); if (options.checksumSha256) { const actualSHA256Checksum = (0, crypto_1.createHash)('sha256').update(assetContents).digest('hex'); if (actualSHA256Checksum !== options.checksumSha256) { - throw new Error(`Checksum mismatch for ${ansiColors.cyan(asset.url)} (expected ${options.checksumSha256}, actual ${actualSHA256Checksum}))`); + throw new Error(`Checksum mismatch for ${ansi_colors_1.default.cyan(asset.url)} (expected ${options.checksumSha256}, actual ${actualSHA256Checksum}))`); } } - console.log(`Verified SHA256 checksums match for ${ansiColors.cyan(asset.url)}`); + console.log(`Verified SHA256 checksums match for ${ansi_colors_1.default.cyan(asset.url)}`); const tarCommand = `tar -xz -C ${options.dest}`; (0, child_process_1.execSync)(tarCommand, { input: assetContents }); console.log(`Fetch complete!`); return; } - throw new Error(`Request ${ansiColors.magenta(asset.url)} failed with status code: ${assetResponse.status}`); + throw new Error(`Request ${ansi_colors_1.default.magenta(asset.url)} failed with status code: ${assetResponse.status}`); } - throw new Error(`Request ${ansiColors.magenta('https://api.github.com')} failed with status code: ${response.status}`); + throw new Error(`Request ${ansi_colors_1.default.magenta('https://api.github.com')} failed with status code: ${response.status}`); } finally { clearTimeout(timeout); @@ -139,21 +142,21 @@ async function getVSCodeSysroot(arch) { if (!checksumSha256) { throw new Error(`Could not find checksum for ${expectedName}`); } - const sysroot = process.env['VSCODE_SYSROOT_DIR'] ?? path.join((0, os_1.tmpdir)(), `vscode-${arch}-sysroot`); - const stamp = path.join(sysroot, '.stamp'); + const sysroot = process.env['VSCODE_SYSROOT_DIR'] ?? path_1.default.join((0, os_1.tmpdir)(), `vscode-${arch}-sysroot`); + const stamp = path_1.default.join(sysroot, '.stamp'); const result = `${sysroot}/${triple}/${triple}/sysroot`; - if (fs.existsSync(stamp) && fs.readFileSync(stamp).toString() === expectedName) { + if (fs_1.default.existsSync(stamp) && fs_1.default.readFileSync(stamp).toString() === expectedName) { return result; } console.log(`Installing ${arch} root image: ${sysroot}`); - fs.rmSync(sysroot, { recursive: true, force: true }); - fs.mkdirSync(sysroot); + fs_1.default.rmSync(sysroot, { recursive: true, force: true }); + fs_1.default.mkdirSync(sysroot); await fetchUrl({ checksumSha256, assetName: expectedName, dest: sysroot }); - fs.writeFileSync(stamp, expectedName); + fs_1.default.writeFileSync(stamp, expectedName); return result; } async function getChromiumSysroot(arch) { @@ -168,24 +171,24 @@ async function getChromiumSysroot(arch) { const sysrootDict = sysrootInfo[sysrootArch]; const tarballFilename = sysrootDict['Tarball']; const tarballSha = sysrootDict['Sha256Sum']; - const sysroot = path.join((0, os_1.tmpdir)(), sysrootDict['SysrootDir']); + const sysroot = path_1.default.join((0, os_1.tmpdir)(), sysrootDict['SysrootDir']); const url = [URL_PREFIX, URL_PATH, tarballSha].join('/'); - const stamp = path.join(sysroot, '.stamp'); - if (fs.existsSync(stamp) && fs.readFileSync(stamp).toString() === url) { + const stamp = path_1.default.join(sysroot, '.stamp'); + if (fs_1.default.existsSync(stamp) && fs_1.default.readFileSync(stamp).toString() === url) { return sysroot; } console.log(`Installing Debian ${arch} root image: ${sysroot}`); - fs.rmSync(sysroot, { recursive: true, force: true }); - fs.mkdirSync(sysroot); - const tarball = path.join(sysroot, tarballFilename); + fs_1.default.rmSync(sysroot, { recursive: true, force: true }); + fs_1.default.mkdirSync(sysroot); + const tarball = path_1.default.join(sysroot, tarballFilename); console.log(`Downloading ${url}`); let downloadSuccess = false; for (let i = 0; i < 3 && !downloadSuccess; i++) { - fs.writeFileSync(tarball, ''); + fs_1.default.writeFileSync(tarball, ''); await new Promise((c) => { - https.get(url, (res) => { + https_1.default.get(url, (res) => { res.on('data', (chunk) => { - fs.appendFileSync(tarball, chunk); + fs_1.default.appendFileSync(tarball, chunk); }); res.on('end', () => { downloadSuccess = true; @@ -198,7 +201,7 @@ async function getChromiumSysroot(arch) { }); } if (!downloadSuccess) { - fs.rmSync(tarball); + fs_1.default.rmSync(tarball); throw new Error('Failed to download ' + url); } const sha = getSha(tarball); @@ -209,8 +212,8 @@ async function getChromiumSysroot(arch) { if (proc.status) { throw new Error('Tarball extraction failed with code ' + proc.status); } - fs.rmSync(tarball); - fs.writeFileSync(stamp, url); + fs_1.default.rmSync(tarball); + fs_1.default.writeFileSync(stamp, url); return sysroot; } //# sourceMappingURL=install-sysroot.js.map \ No newline at end of file diff --git a/build/linux/debian/install-sysroot.ts b/build/linux/debian/install-sysroot.ts index 8ea43a523cf3..aa10e39f95f0 100644 --- a/build/linux/debian/install-sysroot.ts +++ b/build/linux/debian/install-sysroot.ts @@ -5,12 +5,12 @@ import { spawnSync, execSync } from 'child_process'; import { tmpdir } from 'os'; -import * as fs from 'fs'; -import * as https from 'https'; -import * as path from 'path'; +import fs from 'fs'; +import https from 'https'; +import path from 'path'; import { createHash } from 'crypto'; import { DebianArchString } from './types'; -import * as ansiColors from 'ansi-colors'; +import ansiColors from 'ansi-colors'; // Based on https://source.chromium.org/chromium/chromium/src/+/main:build/linux/sysroot_scripts/install-sysroot.py. const URL_PREFIX = 'https://msftelectronbuild.z5.web.core.windows.net'; diff --git a/build/linux/dependencies-generator.js b/build/linux/dependencies-generator.js index 80b11b3d5b72..386495598739 100644 --- a/build/linux/dependencies-generator.js +++ b/build/linux/dependencies-generator.js @@ -3,10 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.getDependencies = getDependencies; const child_process_1 = require("child_process"); -const path = require("path"); +const path_1 = __importDefault(require("path")); const install_sysroot_1 = require("./debian/install-sysroot"); const calculate_deps_1 = require("./debian/calculate-deps"); const calculate_deps_2 = require("./rpm/calculate-deps"); @@ -44,23 +47,23 @@ async function getDependencies(packageType, buildDir, applicationName, arch) { } // Get the files for which we want to find dependencies. const canAsar = false; // TODO@esm ASAR disabled in ESM - const nativeModulesPath = path.join(buildDir, 'resources', 'app', canAsar ? 'node_modules.asar.unpacked' : 'node_modules'); + const nativeModulesPath = path_1.default.join(buildDir, 'resources', 'app', canAsar ? 'node_modules.asar.unpacked' : 'node_modules'); const findResult = (0, child_process_1.spawnSync)('find', [nativeModulesPath, '-name', '*.node']); if (findResult.status) { console.error('Error finding files:'); console.error(findResult.stderr.toString()); return []; } - const appPath = path.join(buildDir, applicationName); + const appPath = path_1.default.join(buildDir, applicationName); // Add the native modules const files = findResult.stdout.toString().trimEnd().split('\n'); // Add the tunnel binary. - files.push(path.join(buildDir, 'bin', product.tunnelApplicationName)); + files.push(path_1.default.join(buildDir, 'bin', product.tunnelApplicationName)); // Add the main executable. files.push(appPath); // Add chrome sandbox and crashpad handler. - files.push(path.join(buildDir, 'chrome-sandbox')); - files.push(path.join(buildDir, 'chrome_crashpad_handler')); + files.push(path_1.default.join(buildDir, 'chrome-sandbox')); + files.push(path_1.default.join(buildDir, 'chrome_crashpad_handler')); // Generate the dependencies. let dependencies; if (packageType === 'deb') { diff --git a/build/linux/dependencies-generator.ts b/build/linux/dependencies-generator.ts index 3163aee54505..46be92eb8472 100644 --- a/build/linux/dependencies-generator.ts +++ b/build/linux/dependencies-generator.ts @@ -6,7 +6,7 @@ 'use strict'; import { spawnSync } from 'child_process'; -import path = require('path'); +import path from 'path'; import { getChromiumSysroot, getVSCodeSysroot } from './debian/install-sysroot'; import { generatePackageDeps as generatePackageDepsDebian } from './debian/calculate-deps'; import { generatePackageDeps as generatePackageDepsRpm } from './rpm/calculate-deps'; diff --git a/build/linux/libcxx-fetcher.js b/build/linux/libcxx-fetcher.js index cfdc9498502e..d6c998e5aea9 100644 --- a/build/linux/libcxx-fetcher.js +++ b/build/linux/libcxx-fetcher.js @@ -3,23 +3,26 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.downloadLibcxxHeaders = downloadLibcxxHeaders; exports.downloadLibcxxObjects = downloadLibcxxObjects; // Can be removed once https://github.com/electron/electron-rebuild/pull/703 is available. -const fs = require("fs"); -const path = require("path"); -const debug = require("debug"); -const extract = require("extract-zip"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const debug_1 = __importDefault(require("debug")); +const extract_zip_1 = __importDefault(require("extract-zip")); const get_1 = require("@electron/get"); -const root = path.dirname(path.dirname(__dirname)); -const d = debug('libcxx-fetcher'); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); +const d = (0, debug_1.default)('libcxx-fetcher'); async function downloadLibcxxHeaders(outDir, electronVersion, lib_name) { - if (await fs.existsSync(path.resolve(outDir, 'include'))) { + if (await fs_1.default.existsSync(path_1.default.resolve(outDir, 'include'))) { return; } - if (!await fs.existsSync(outDir)) { - await fs.mkdirSync(outDir, { recursive: true }); + if (!await fs_1.default.existsSync(outDir)) { + await fs_1.default.mkdirSync(outDir, { recursive: true }); } d(`downloading ${lib_name}_headers`); const headers = await (0, get_1.downloadArtifact)({ @@ -28,14 +31,14 @@ async function downloadLibcxxHeaders(outDir, electronVersion, lib_name) { artifactName: `${lib_name}_headers.zip`, }); d(`unpacking ${lib_name}_headers from ${headers}`); - await extract(headers, { dir: outDir }); + await (0, extract_zip_1.default)(headers, { dir: outDir }); } async function downloadLibcxxObjects(outDir, electronVersion, targetArch = 'x64') { - if (await fs.existsSync(path.resolve(outDir, 'libc++.a'))) { + if (await fs_1.default.existsSync(path_1.default.resolve(outDir, 'libc++.a'))) { return; } - if (!await fs.existsSync(outDir)) { - await fs.mkdirSync(outDir, { recursive: true }); + if (!await fs_1.default.existsSync(outDir)) { + await fs_1.default.mkdirSync(outDir, { recursive: true }); } d(`downloading libcxx-objects-linux-${targetArch}`); const objects = await (0, get_1.downloadArtifact)({ @@ -45,14 +48,14 @@ async function downloadLibcxxObjects(outDir, electronVersion, targetArch = 'x64' arch: targetArch, }); d(`unpacking libcxx-objects from ${objects}`); - await extract(objects, { dir: outDir }); + await (0, extract_zip_1.default)(objects, { dir: outDir }); } async function main() { const libcxxObjectsDirPath = process.env['VSCODE_LIBCXX_OBJECTS_DIR']; const libcxxHeadersDownloadDir = process.env['VSCODE_LIBCXX_HEADERS_DIR']; const libcxxabiHeadersDownloadDir = process.env['VSCODE_LIBCXXABI_HEADERS_DIR']; const arch = process.env['VSCODE_ARCH']; - const packageJSON = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); + const packageJSON = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'package.json'), 'utf8')); const electronVersion = packageJSON.devDependencies.electron; if (!libcxxObjectsDirPath || !libcxxHeadersDownloadDir || !libcxxabiHeadersDownloadDir) { throw new Error('Required build env not set'); diff --git a/build/linux/libcxx-fetcher.ts b/build/linux/libcxx-fetcher.ts index 6abb67faa762..6bdbd8a4f302 100644 --- a/build/linux/libcxx-fetcher.ts +++ b/build/linux/libcxx-fetcher.ts @@ -5,10 +5,10 @@ // Can be removed once https://github.com/electron/electron-rebuild/pull/703 is available. -import * as fs from 'fs'; -import * as path from 'path'; -import * as debug from 'debug'; -import * as extract from 'extract-zip'; +import fs from 'fs'; +import path from 'path'; +import debug from 'debug'; +import extract from 'extract-zip'; import { downloadArtifact } from '@electron/get'; const root = path.dirname(path.dirname(__dirname)); diff --git a/build/tsconfig.json b/build/tsconfig.json index ce7a493a7aac..f3ad981d62fe 100644 --- a/build/tsconfig.json +++ b/build/tsconfig.json @@ -4,7 +4,7 @@ "lib": [ "ES2020" ], - "module": "commonjs", + "module": "nodenext", "alwaysStrict": true, "removeComments": false, "preserveConstEnums": true, diff --git a/build/win32/explorer-appx-fetcher.js b/build/win32/explorer-appx-fetcher.js index 554b449d872c..78d2317147ec 100644 --- a/build/win32/explorer-appx-fetcher.js +++ b/build/win32/explorer-appx-fetcher.js @@ -3,23 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.downloadExplorerAppx = downloadExplorerAppx; -const fs = require("fs"); -const debug = require("debug"); -const extract = require("extract-zip"); -const path = require("path"); +const fs_1 = __importDefault(require("fs")); +const debug_1 = __importDefault(require("debug")); +const extract_zip_1 = __importDefault(require("extract-zip")); +const path_1 = __importDefault(require("path")); const get_1 = require("@electron/get"); -const root = path.dirname(path.dirname(__dirname)); -const d = debug('explorer-appx-fetcher'); +const root = path_1.default.dirname(path_1.default.dirname(__dirname)); +const d = (0, debug_1.default)('explorer-appx-fetcher'); async function downloadExplorerAppx(outDir, quality = 'stable', targetArch = 'x64') { const fileNamePrefix = quality === 'insider' ? 'code_insiders' : 'code'; const fileName = `${fileNamePrefix}_explorer_${targetArch}.zip`; - if (await fs.existsSync(path.resolve(outDir, 'resources.pri'))) { + if (await fs_1.default.existsSync(path_1.default.resolve(outDir, 'resources.pri'))) { return; } - if (!await fs.existsSync(outDir)) { - await fs.mkdirSync(outDir, { recursive: true }); + if (!await fs_1.default.existsSync(outDir)) { + await fs_1.default.mkdirSync(outDir, { recursive: true }); } d(`downloading ${fileName}`); const artifact = await (0, get_1.downloadArtifact)({ @@ -34,14 +37,14 @@ async function downloadExplorerAppx(outDir, quality = 'stable', targetArch = 'x6 } }); d(`unpacking from ${fileName}`); - await extract(artifact, { dir: fs.realpathSync(outDir) }); + await (0, extract_zip_1.default)(artifact, { dir: fs_1.default.realpathSync(outDir) }); } async function main(outputDir) { const arch = process.env['VSCODE_ARCH']; if (!outputDir) { throw new Error('Required build env not set'); } - const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); + const product = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'product.json'), 'utf8')); await downloadExplorerAppx(outputDir, product.quality, arch); } if (require.main === module) { diff --git a/build/win32/explorer-appx-fetcher.ts b/build/win32/explorer-appx-fetcher.ts index 89fbb57c064c..95121cd65031 100644 --- a/build/win32/explorer-appx-fetcher.ts +++ b/build/win32/explorer-appx-fetcher.ts @@ -5,10 +5,10 @@ 'use strict'; -import * as fs from 'fs'; -import * as debug from 'debug'; -import * as extract from 'extract-zip'; -import * as path from 'path'; +import fs from 'fs'; +import debug from 'debug'; +import extract from 'extract-zip'; +import path from 'path'; import { downloadArtifact } from '@electron/get'; const root = path.dirname(path.dirname(__dirname)); From 070b1d58d0ac2786b6ffdadb90dfd30cee0da1c4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:13:00 -0800 Subject: [PATCH 0778/3587] Rebuild bind buffer when viewport size changes --- .../renderStrategy/viewportRenderStrategy.ts | 43 ++++++++++++------- .../viewParts/viewLinesGpu/viewLinesGpu.ts | 4 +- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts index 596510cc8d57..73f269ad0395 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -6,6 +6,7 @@ import { getActiveWindow } from '../../../../base/browser/dom.js'; import { Color } from '../../../../base/common/color.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; +import { Emitter } from '../../../../base/common/event.js'; import { CursorColumns } from '../../../common/core/cursorColumns.js'; import type { IViewLineTokens } from '../../../common/tokens/lineTokens.js'; import { type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLineMappingChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../../common/viewEvents.js'; @@ -38,13 +39,10 @@ const enum CellBufferInfo { TextureIndex = 5, } -const viewportHeight = 100; - /** * A render strategy that uploads the content of the entire viewport every frame. */ export class ViewportRenderStrategy extends BaseRenderStrategy { - // TODO: Can this be removed? /** * The hard cap for line columns that can be rendered by the GPU renderer. */ @@ -53,6 +51,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { readonly type = 'viewport'; readonly wgsl: string = fullFileRenderStrategyWgsl; + private _cellBindBufferLineCapacity = 25; private _cellBindBuffer!: GPUBuffer; /** @@ -75,6 +74,9 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { ]; } + private readonly _onDidChangeBindGroupEntries = this._register(new Emitter()); + readonly onDidChangeBindGroupEntries = this._onDidChangeBindGroupEntries.event; + constructor( context: ViewContext, viewGpuContext: ViewGpuContext, @@ -83,7 +85,21 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { ) { super(context, viewGpuContext, device, glyphRasterizer); - const bufferSize = viewportHeight * ViewportRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; + this._rebuildCellBuffer(this._cellBindBufferLineCapacity); + + const scrollOffsetBufferSize = 2; + this._scrollOffsetBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco scroll offset buffer', + size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + })).object; + this._scrollOffsetValueBuffer = new Float32Array(scrollOffsetBufferSize); + } + + private _rebuildCellBuffer(lineCount: number) { + this._cellBindBuffer?.destroy(); + + const bufferSize = lineCount * ViewportRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco full file cell buffer', size: bufferSize, @@ -93,14 +109,9 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { new ArrayBuffer(bufferSize), new ArrayBuffer(bufferSize), ]; + this._cellBindBufferLineCapacity = lineCount; - const scrollOffsetBufferSize = 2; - this._scrollOffsetBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { - label: 'Monaco scroll offset buffer', - size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - })).object; - this._scrollOffsetValueBuffer = new Float32Array(scrollOffsetBufferSize); + this._onDidChangeBindGroupEntries.fire(); } // #region Event handlers @@ -208,7 +219,10 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { this._scrollInitialized = true; } - // Zero out cell buffer + // Zero out cell buffer or rebuild if needed + if (this._cellBindBufferLineCapacity < viewportData.endLineNumber - viewportData.startLineNumber + 1) { + this._rebuildCellBuffer(viewportData.endLineNumber - viewportData.startLineNumber + 1); + } const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]); cellBuffer.fill(0); @@ -382,10 +396,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { if (this._visibleObjectCount <= 0) { throw new BugIndicatingError('Attempt to draw 0 objects'); } - pass.draw( - quadVertices.length / 2, - this._visibleObjectCount, - ); + pass.draw(quadVertices.length / 2, this._visibleObjectCount); } } diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index 3ae3ff00a2ce..a6cabd559512 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -331,7 +331,9 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { return; } this._logService.trace(`File is larger than ${FullFileRenderStrategy.maxSupportedLines} lines or ${FullFileRenderStrategy.maxSupportedColumns} columns, switching to viewport render strategy`); - this._renderStrategy.value = this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device, this._glyphRasterizer as { value: GlyphRasterizer }); + const viewportRenderStrategy = this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device, this._glyphRasterizer as { value: GlyphRasterizer }); + this._renderStrategy.value = viewportRenderStrategy; + this._register(viewportRenderStrategy.onDidChangeBindGroupEntries(() => this._rebuildBindGroup?.())); this._rebuildBindGroup?.(); } From 8058ecf2e08c9f9c405e0b4ed424a6b59577326a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:17:35 -0800 Subject: [PATCH 0779/3587] Tweak initial and increment buffer values --- .../gpu/renderStrategy/viewportRenderStrategy.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts index 73f269ad0395..c75923532712 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -26,6 +26,8 @@ import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js'; const enum Constants { IndicesPerCell = 6, + CellBindBufferCapacityIncrement = 32, + CellBindBufferInitialCapacity = 63, // Will be rounded up to nearest increment } const enum CellBufferInfo { @@ -51,7 +53,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { readonly type = 'viewport'; readonly wgsl: string = fullFileRenderStrategyWgsl; - private _cellBindBufferLineCapacity = 25; + private _cellBindBufferLineCapacity = Constants.CellBindBufferInitialCapacity; private _cellBindBuffer!: GPUBuffer; /** @@ -99,7 +101,10 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { private _rebuildCellBuffer(lineCount: number) { this._cellBindBuffer?.destroy(); - const bufferSize = lineCount * ViewportRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; + // Increase in chunks so resizing a window by hand doesn't keep allocating and throwing away + const lineCountWithIncrement = (Math.floor(lineCount / Constants.CellBindBufferCapacityIncrement) + 1) * Constants.CellBindBufferCapacityIncrement; + + const bufferSize = lineCountWithIncrement * ViewportRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco full file cell buffer', size: bufferSize, @@ -109,7 +114,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { new ArrayBuffer(bufferSize), new ArrayBuffer(bufferSize), ]; - this._cellBindBufferLineCapacity = lineCount; + this._cellBindBufferLineCapacity = lineCountWithIncrement; this._onDidChangeBindGroupEntries.fire(); } From 5778f451f90145669b74aaac8a3acb3e0be16305 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:18:55 -0800 Subject: [PATCH 0780/3587] Clean up --- .../editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts | 2 +- .../editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index dfe7244b64b6..7706390f0eed 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -357,7 +357,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { tokenMetadata = tokens.getMetadata(tokenIndex); for (x = tokenStartIndex; x < tokenEndIndex; x++) { - // TODO: This needs to move to a dynamic long line rendering strategy + // Only render lines that do not exceed maximum columns if (x > FullFileRenderStrategy.maxSupportedColumns) { break; } diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts index c75923532712..71dea59e0f41 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -260,7 +260,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { tokenMetadata = tokens.getMetadata(tokenIndex); for (x = tokenStartIndex; x < tokenEndIndex; x++) { - // TODO: This needs to move to a dynamic long line rendering strategy + // Only render lines that do not exceed maximum columns if (x > ViewportRenderStrategy.maxSupportedColumns) { break; } From 45a47c2b0f0d62194b7763adb3e6d7434fd146de Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 22 Jan 2025 11:32:15 -0800 Subject: [PATCH 0781/3587] [instructions]: remove enable/disable eye button (#238490) --- .../instructionsAttachment.ts | 23 +++------------- .../chatInstructionsAttachment.ts | 26 ++----------------- 2 files changed, 5 insertions(+), 44 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts index 1f327ac3ec99..d924c65f1e6b 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts @@ -97,10 +97,7 @@ export class InstructionsAttachmentWidget extends Disposable { this.renderDisposables.clear(); this.domNode.classList.remove('warning', 'error', 'disabled'); - const { enabled, topError } = this.model; - if (!enabled) { - this.domNode.classList.add('disabled'); - } + const { topError } = this.model; const label = this.resourceLabels.create(this.domNode, { supportIcons: true }); const file = this.model.reference.uri; @@ -112,10 +109,8 @@ export class InstructionsAttachmentWidget extends Disposable { const uriLabel = this.labelService.getUriLabel(file, { relative: true }); const currentFile = localize('openEditor', "Prompt instructions"); - const inactive = localize('enableHint', "disabled"); - const currentFileHint = currentFile + (enabled ? '' : ` (${inactive})`); - let title = `${currentFileHint} ${uriLabel}`; + let title = `${currentFile} ${uriLabel}`; // if there are some errors/warning during the process of resolving // attachment references (including all the nested child references), @@ -149,18 +144,6 @@ export class InstructionsAttachmentWidget extends Disposable { const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, localize('instructions', 'Instructions'))); this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), hintElement, title)); - // create `toggle` enabled state button - const toggleButtonMsg = enabled - ? localize('disable', "Disable") - : localize('enable', "Enable"); - - const toggleButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: toggleButtonMsg })); - toggleButton.icon = enabled ? Codicon.eye : Codicon.eyeClosed; - this.renderDisposables.add(toggleButton.onDidClick((e) => { - e.stopPropagation(); - this.model.toggle(); - })); - // create the `remove` button const removeButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: localize('remove', "Remove") })); removeButton.icon = Codicon.x; @@ -169,7 +152,7 @@ export class InstructionsAttachmentWidget extends Disposable { this.model.dispose(); })); - // Context menu + // context menu const scopedContextKeyService = this.renderDisposables.add(this.contextKeyService.createScoped(this.domNode)); const resourceContextKey = this.renderDisposables.add( diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts index 6627b52ec423..a53dcb7b3436 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts @@ -30,12 +30,12 @@ export class ChatInstructionsAttachmentModel extends Disposable { * child references it may contain. */ public get references(): readonly URI[] { - const { reference, enabled } = this; + const { reference } = this; const { errorCondition } = this.reference; // return no references if the attachment is disabled // or if this object itself has an error - if (!enabled || errorCondition) { + if (errorCondition) { return []; } @@ -88,18 +88,6 @@ export class ChatInstructionsAttachmentModel extends Disposable { return this; } - /** - * Private property to track the `enabled` state of the prompt - * instructions attachment. - */ - private _enabled: boolean = true; - /** - * Get the `enabled` state of the prompt instructions attachment. - */ - public get enabled(): boolean { - return this._enabled; - } - constructor( uri: URI, @IInstantiationService private readonly initService: IInstantiationService, @@ -121,16 +109,6 @@ export class ChatInstructionsAttachmentModel extends Disposable { return this; } - /** - * Toggle the `enabled` state of the prompt instructions attachment. - */ - public toggle(): this { - this._enabled = !this._enabled; - this._onUpdate.fire(); - - return this; - } - public override dispose(): void { this._onDispose.fire(); From 15d619c0a99046465dec93809bae8939eedeb17f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 22 Jan 2025 20:42:13 +0100 Subject: [PATCH 0782/3587] Copilot setup: support for ghe.com accounts fix microsoft/vscode-copilot#11890 fix /issues/238013 --- package.json | 2 +- src/vs/base/common/product.ts | 6 + .../chat/browser/actions/chatActions.ts | 4 +- .../contrib/chat/browser/chatSetup.ts | 224 +++++++++++++----- .../chat/browser/media/chatViewSetup.css | 8 - 5 files changed, 178 insertions(+), 66 deletions(-) diff --git a/package.json b/package.json index b2a0de674fca..10e479fe6c49 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.97.0", - "distro": "03bcc2befd4c31b21f1b2fb56c56d767e3db462b", + "distro": "b9d91be3f17ff1f0fd2bfce91f40f2d6bfe9c87b", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index ac7c5529b27f..627eff87f5bc 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -306,6 +306,7 @@ export interface IAiGeneratedWorkspaceTrust { export interface IDefaultChatAgent { readonly extensionId: string; readonly chatExtensionId: string; + readonly documentationUrl: string; readonly termsStatementUrl: string; readonly privacyStatementUrl: string; @@ -314,9 +315,14 @@ export interface IDefaultChatAgent { readonly manageSettingsUrl: string; readonly managePlanUrl: string; readonly upgradePlanUrl: string; + readonly providerId: string; readonly providerName: string; + readonly enterpriseProviderId: string; + readonly providerSetting: string; + readonly providerUriSetting: string; readonly providerScopes: string[][]; + readonly entitlementUrl: string; readonly entitlementSignupLimitedUrl: string; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index bbe671bd992f..db8068585535 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -485,7 +485,7 @@ export function registerChatActions() { }); } - const nonEnterpriseCopilotUsers = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.notEquals('config.github.copilot.advanced.authProvider', 'github-enterprise')); + const nonEnterpriseCopilotUsers = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.notEquals(`config.${defaultChat.providerSetting}`, defaultChat.enterpriseProviderId)); registerOpenLinkAction('workbench.action.chat.managePlan', localize2('managePlan', "Manage Copilot Plan"), defaultChat.managePlanUrl, 1, nonEnterpriseCopilotUsers); registerOpenLinkAction('workbench.action.chat.manageSettings', localize2('manageSettings', "Manage Copilot Settings"), defaultChat.manageSettingsUrl, 2, nonEnterpriseCopilotUsers); registerOpenLinkAction('workbench.action.chat.learnMore', localize2('learnMore', "Learn More"), defaultChat.documentationUrl, 3); @@ -523,6 +523,8 @@ const defaultChat = { documentationUrl: product.defaultChatAgent?.documentationUrl ?? '', manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '', managePlanUrl: product.defaultChatAgent?.managePlanUrl ?? '', + enterpriseProviderId: product.defaultChatAgent?.enterpriseProviderId ?? '', + providerSetting: product.defaultChatAgent?.providerSetting ?? '', }; MenuRegistry.appendMenuItem(MenuId.CommandCenter, { diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 7f6fda724de0..101e7d3dba05 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -5,9 +5,9 @@ import './media/chatViewSetup.css'; import { $, getActiveElement, setVisibility } from '../../../../base/browser/dom.js'; -import { Button, ButtonWithDropdown } from '../../../../base/browser/ui/button/button.js'; +import { ButtonWithDropdown } from '../../../../base/browser/ui/button/button.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { IAction, toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; +import { toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; import { Barrier, timeout } from '../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; @@ -22,7 +22,7 @@ import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRend import { localize, localize2 } from '../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; @@ -45,7 +45,7 @@ import { AuthenticationSession, IAuthenticationExtensionsService, IAuthenticatio import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; -import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; +import { IExtension, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { CHAT_CATEGORY } from './actions/chatActions.js'; @@ -64,6 +64,8 @@ import { ExtensionUrlHandlerOverrideRegistry } from '../../../services/extension import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; const defaultChat = { @@ -75,8 +77,11 @@ const defaultChat = { skusDocumentationUrl: product.defaultChatAgent?.skusDocumentationUrl ?? '', publicCodeMatchesUrl: product.defaultChatAgent?.publicCodeMatchesUrl ?? '', upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '', - providerIds: [product.defaultChatAgent?.providerId ?? '', 'github-enterprise'], + providerId: product.defaultChatAgent?.providerId ?? '', providerName: product.defaultChatAgent?.providerName ?? '', + enterpriseProviderId: product.defaultChatAgent?.enterpriseProviderId ?? '', + providerSetting: product.defaultChatAgent?.providerSetting ?? '', + providerUriSetting: product.defaultChatAgent?.providerUriSetting ?? '', providerScopes: product.defaultChatAgent?.providerScopes ?? [[]], entitlementUrl: product.defaultChatAgent?.entitlementUrl ?? '', entitlementSignupLimitedUrl: product.defaultChatAgent?.entitlementSignupLimitedUrl ?? '', @@ -357,6 +362,14 @@ interface IChatEntitlements { class ChatSetupRequests extends Disposable { + static providerId(configurationService: IConfigurationService): string { + if (configurationService.getValue(defaultChat.providerSetting) === defaultChat.enterpriseProviderId) { + return defaultChat.enterpriseProviderId; + } + + return defaultChat.providerId; + } + private state: IChatEntitlements = { entitlement: this.context.state.entitlement }; private pendingResolveCts = new CancellationTokenSource(); @@ -384,19 +397,19 @@ class ChatSetupRequests extends Disposable { this._register(this.authenticationService.onDidChangeDeclaredProviders(() => this.resolve())); this._register(this.authenticationService.onDidChangeSessions(e => { - if (defaultChat.providerIds.includes(e.providerId)) { + if (e.providerId === ChatSetupRequests.providerId(this.configurationService)) { this.resolve(); } })); this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => { - if (defaultChat.providerIds.includes(e.id)) { + if (e.id === ChatSetupRequests.providerId(this.configurationService)) { this.resolve(); } })); this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => { - if (defaultChat.providerIds.includes(e.id)) { + if (e.id === ChatSetupRequests.providerId(this.configurationService)) { this.resolve(); } })); @@ -443,31 +456,15 @@ class ChatSetupRequests extends Disposable { } private async findMatchingProviderSession(token: CancellationToken): Promise { - const authProviders: string[] = []; - const configuredAuthProvider = this.configurationService.getValue('github.copilot.advanced.authProvider'); - if (configuredAuthProvider) { - authProviders.push(configuredAuthProvider); - } else { - authProviders.push(...defaultChat.providerIds); + const sessions = await this.doGetSessions(ChatSetupRequests.providerId(this.configurationService)); + if (token.isCancellationRequested) { + return undefined; } - let sessions: ReadonlyArray = []; - for (const authProvider of authProviders) { - if (token.isCancellationRequested) { - return undefined; - } - - sessions = await this.doGetSessions(authProvider); - - if (token.isCancellationRequested) { - return undefined; - } - - for (const session of sessions) { - for (const scopes of defaultChat.providerScopes) { - if (this.scopesMatch(session.scopes, scopes)) { - return session; - } + for (const session of sessions) { + for (const scopes of defaultChat.providerScopes) { + if (this.scopesMatch(session.scopes, scopes)) { + return session; } } } @@ -500,6 +497,11 @@ class ChatSetupRequests extends Disposable { } private async doResolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise { + if (ChatSetupRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId) { + this.logService.trace('[chat setup] entitlement: enterprise provider, assuming Pro'); + return { entitlement: ChatEntitlement.Pro }; + } + if (token.isCancellationRequested) { return undefined; } @@ -769,6 +771,7 @@ class ChatSetupController extends Disposable { @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, @IDialogService private readonly dialogService: IDialogService, + @IConfigurationService private readonly configurationService: IConfigurationService, @ILifecycleService private readonly lifecycleService: ILifecycleService, ) { super(); @@ -813,13 +816,14 @@ class ChatSetupController extends Disposable { let focusChatInput = false; try { + const providerId = ChatSetupRequests.providerId(this.configurationService); let session: AuthenticationSession | undefined; let entitlement: ChatEntitlement | undefined; // Entitlement Unknown: we need to sign-in user if (this.context.state.entitlement === ChatEntitlement.Unknown) { this.setStep(ChatSetupStep.SigningIn); - const result = await this.signIn(); + const result = await this.signIn(providerId); if (!result.session) { this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', installDuration: watch.elapsed(), signUpErrorCode: undefined }); return; @@ -841,7 +845,7 @@ class ChatSetupController extends Disposable { // Install this.setStep(ChatSetupStep.Installing); - await this.install(session, entitlement ?? this.context.state.entitlement, watch); + await this.install(session, entitlement ?? this.context.state.entitlement, providerId, watch); const currentActiveElement = getActiveElement(); focusChatInput = activeElement === currentActiveElement || currentActiveElement === mainWindow.document.body; @@ -855,16 +859,16 @@ class ChatSetupController extends Disposable { } } - private async signIn(): Promise<{ session: AuthenticationSession | undefined; entitlement: ChatEntitlement | undefined }> { + private async signIn(providerId: string): Promise<{ session: AuthenticationSession | undefined; entitlement: ChatEntitlement | undefined }> { let session: AuthenticationSession | undefined; let entitlement: ChatEntitlement | undefined; try { showCopilotView(this.viewsService, this.layoutService); - session = await this.authenticationService.createSession(defaultChat.providerIds[0], defaultChat.providerScopes[0]); + session = await this.authenticationService.createSession(providerId, defaultChat.providerScopes[0]); - this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, defaultChat.providerIds[0], session.account); - this.authenticationExtensionsService.updateAccountPreference(defaultChat.chatExtensionId, defaultChat.providerIds[0], session.account); + this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, providerId, session.account); + this.authenticationExtensionsService.updateAccountPreference(defaultChat.chatExtensionId, providerId, session.account); entitlement = await this.requests.forceResolveEntitlement(session); } catch (e) { @@ -874,29 +878,34 @@ class ChatSetupController extends Disposable { if (!session && !this.willShutdown) { const { confirmed } = await this.dialogService.confirm({ type: Severity.Error, - message: localize('unknownSignInError', "Unable to sign in to {0}. Would you like to try again?", defaultChat.providerName), + message: localize('unknownSignInError', "Failed to sign in to {0}. Would you like to try again?", defaultChat.providerName), + detail: localize('unknownSignInErrorDetail', "You must be signed in to use Copilot."), primaryButton: localize('retry', "Retry") }); if (confirmed) { - return this.signIn(); + return this.signIn(providerId); } } return { session, entitlement }; } - private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, watch: StopWatch,): Promise { + private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch,): Promise { const wasInstalled = this.context.state.installed; let signUpResult: boolean | { errorCode: number } | undefined = undefined; try { showCopilotView(this.viewsService, this.layoutService); - if (entitlement !== ChatEntitlement.Limited && entitlement !== ChatEntitlement.Pro && entitlement !== ChatEntitlement.Unavailable) { + if ( + entitlement !== ChatEntitlement.Limited && // User is not signed up to Copilot Free + entitlement !== ChatEntitlement.Pro && // User is not signed up to Copilot Pro + entitlement !== ChatEntitlement.Unavailable // User is eligible for Copilot Free + ) { if (!session) { try { - session = (await this.authenticationService.getSessions(defaultChat.providerIds[0])).at(0); + session = (await this.authenticationService.getSessions(providerId)).at(0); } catch (error) { // ignore - errors can throw if a provider is not registered } @@ -976,8 +985,10 @@ class ChatSetupWelcomeContent extends Disposable { private readonly context: ChatSetupContext, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextMenuService private readonly contextMenuService: IContextMenuService, - @ICommandService private readonly commandService: ICommandService, + @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IDialogService private readonly dialogService: IDialogService, ) { super(); @@ -1023,18 +1034,24 @@ class ChatSetupWelcomeContent extends Disposable { freeContainer.appendChild(this._register(markdown.render(new MarkdownString(free, { isTrusted: true, supportThemeIcons: true }))).element); // Setup Button - const actions: IAction[] = []; - if (this.context.state.installed) { - actions.push(toAction({ id: 'chatSetup.signInGh', label: localize('signInGh', "Sign in with a GitHub.com Account"), run: () => this.commandService.executeCommand('github.copilotChat.signIn') })); - actions.push(toAction({ id: 'chatSetup.signInGhe', label: localize('signInGhe', "Sign in with a GHE.com Account"), run: () => this.commandService.executeCommand('github.copilotChat.signInGHE') })); - } const buttonContainer = this.element.appendChild($('p')); buttonContainer.classList.add('button-container'); - const button = this._register(actions.length === 0 ? new Button(buttonContainer, { - supportIcons: true, - ...defaultButtonStyles - }) : new ButtonWithDropdown(buttonContainer, { - actions, + const button = this._register(new ButtonWithDropdown(buttonContainer, { + actions: { + getActions: () => { + if (this.context.state.entitlement === ChatEntitlement.Unknown) { + return [ + toAction({ id: 'chatSetup.signInGh', label: localize('signInGh', "Sign in with a GitHub.com Account"), run: () => this.updateProvider(false) }), + toAction({ id: 'chatSetup.signInGhe', label: localize('signInGhe', "Sign in with a GHE.com Account"), run: () => this.updateProvider(true) }), + ]; + } else { + return [ + toAction({ id: 'chatSetup.useGh', label: localize('useGh', "Use a GitHub.com Account"), run: () => this.updateProvider(false) }), + toAction({ id: 'chatSetup.useGhe', label: localize('useGhe', "Use a GHE.com Account"), run: () => this.updateProvider(true) }), + ]; + } + } + }, addPrimaryActionToDropdown: false, contextMenuProvider: this.contextMenuService, supportIcons: true, @@ -1055,7 +1072,7 @@ class ChatSetupWelcomeContent extends Disposable { this._register(Event.runAndSubscribe(this.controller.onDidChange, () => this.update(freeContainer, settingsContainer, button))); } - private update(freeContainer: HTMLElement, settingsContainer: HTMLElement, button: Button | ButtonWithDropdown): void { + private update(freeContainer: HTMLElement, settingsContainer: HTMLElement, button: ButtonWithDropdown): void { const showSettings = this.telemetryService.telemetryLevel !== TelemetryLevel.NONE; let showFree: boolean; let buttonLabel: string; @@ -1096,6 +1113,101 @@ class ChatSetupWelcomeContent extends Disposable { button.label = buttonLabel; button.enabled = this.controller.step === ChatSetupStep.Initial; } + + private async updateProvider(useGhe: boolean): Promise { + const registry = Registry.as(ConfigurationExtensions.Configuration); + registry.registerConfiguration({ + 'id': 'copilot.setup', + 'type': 'object', + 'properties': { + [defaultChat.providerSetting]: { + 'type': 'string' + }, + [defaultChat.providerUriSetting]: { + 'type': 'string' + } + } + }); + + if (useGhe) { + await this.configurationService.updateValue(defaultChat.providerSetting, defaultChat.enterpriseProviderId, ConfigurationTarget.USER); + const success = await this.handleGheUri(); + if (!success) { + return; // not properly configured, abort + } + } else { + await this.configurationService.updateValue(defaultChat.providerSetting, undefined, ConfigurationTarget.USER); + await this.configurationService.updateValue(defaultChat.providerUriSetting, undefined, ConfigurationTarget.USER); + } + + return this.controller.setup(); + } + + private async handleGheUri(): Promise { + const uri = this.configurationService.getValue(defaultChat.providerUriSetting); + if (uri) { + return true; // already setup + } + + let isSingleWord = false; + const result = await this.quickInputService.input({ + prompt: localize('gheInstance', "What is your GHE.com instance?"), + placeHolder: localize('gheInstancePlaceholder', 'i.e. "octocat" or "https://octocat.ghe.com"...'), + validateInput: async value => { + isSingleWord = false; + if (!value) { + return undefined; + } + + if (/^[a-zA-Z\-_]+$/.test(value)) { + isSingleWord = true; + return { + content: localize('willResolveTo', "Will resolve to {0}", `https://${value}.ghe.com`), + severity: Severity.Info + }; + } else { + const regex = /^(https:\/\/)?([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.ghe\.com\/?$/; + if (!regex.test(value)) { + return { + content: localize('invalidGheInstance', 'Please enter a valid GHE.com instance (i.e. "octocat" or "https://octocat.ghe.com")'), + severity: Severity.Error + }; + } + } + + return undefined; + } + }); + + if (!result) { + const { confirmed } = await this.dialogService.confirm({ + type: Severity.Error, + message: localize('gheSetupError', "The provided GHE.com instance is invalid. Would you like to enter it again?", defaultChat.providerName), + primaryButton: localize('retry', "Retry") + }); + + if (confirmed) { + return this.handleGheUri(); + } + + return false; + } + + let resolvedUri = result; + if (isSingleWord) { + resolvedUri = `https://${resolvedUri}.ghe.com`; + } else { + const normalizedUri = result.toLowerCase(); + const hasHttps = normalizedUri.startsWith('https://'); + if (!hasHttps) { + resolvedUri = `https://${result}`; + } + } + + await this.configurationService.updateValue(defaultChat.providerUriSetting, resolvedUri, ConfigurationTarget.USER); + + return true; + } } //#endregion @@ -1147,11 +1259,11 @@ class ChatSetupContext extends Disposable { private async checkExtensionInstallation(): Promise { - // Await extensions to be ready to be queries + // Await extensions to be ready to be queried await this.extensionsWorkbenchService.queryLocal(); // Listen to change and process extensions once - this._register(Event.runAndSubscribe(this.extensionsWorkbenchService.onChange, (e) => { + this._register(Event.runAndSubscribe(this.extensionsWorkbenchService.onChange, e => { if (e && !ExtensionIdentifier.equals(e.identifier.id, defaultChat.extensionId)) { return; // unrelated event } diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css b/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css index e2d82625ecdb..785aeaa2fa70 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css @@ -48,12 +48,4 @@ .monaco-button-dropdown .monaco-text-button { width: 100%; } - - /** Single Button */ - p > .monaco-button { - text-align: center; - display: inline-block; - width: 100%; - padding: 4px 7px; - } } From 438e5e0af6071021d7a536b4e4c997d21ab0a664 Mon Sep 17 00:00:00 2001 From: scott Date: Thu, 23 Jan 2025 03:56:59 +0800 Subject: [PATCH 0783/3587] fix: remove duplicate `!**/*.mk` entry in dependenciesSrc (#236683) --- build/gulpfile.vscode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 030c39a861e8..a63f693c95a6 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -299,7 +299,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op const jsFilter = util.filter(data => !data.isDirectory() && /\.js$/.test(data.path)); const root = path.resolve(path.join(__dirname, '..')); const productionDependencies = getProductionDependencies(root); - const dependenciesSrc = productionDependencies.map(d => path.relative(root, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`, `!**/*.mk`]).flat(); + const dependenciesSrc = productionDependencies.map(d => path.relative(root, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`]).flat().concat('!**/*.mk'); const deps = gulp.src(dependenciesSrc, { base: '.', dot: true }) .pipe(filter(['**', `!**/${config.version}/**`, '!**/bin/darwin-arm64-87/**', '!**/package-lock.json', '!**/yarn.lock', '!**/*.js.map'])) From 90f0ae2be2227b4be51f0ed242614d698457bede Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Wed, 22 Jan 2025 12:09:13 -0800 Subject: [PATCH 0784/3587] fix: expand chat/edits empty selection attachments to the whole line (#238499) --- .../contrib/chat/browser/actions/chatContextActions.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 2535ac2888dd..fd9bd39d5c7c 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -329,7 +329,8 @@ class AttachSelectionToChatAction extends Action2 { const selection = activeEditor?.getSelection(); if (selection) { (await showChatView(accessor.get(IViewsService)))?.focusInput(); - variablesService.attachContext('file', { uri: activeUri, range: selection }, ChatAgentLocation.Panel); + const range = selection.isEmpty() ? new Range(selection.startLineNumber, 1, selection.startLineNumber + 1, 1) : selection; + variablesService.attachContext('file', { uri: activeUri, range }, ChatAgentLocation.Panel); } } } @@ -401,7 +402,8 @@ class AttachSelectionToEditingSessionAction extends Action2 { const selection = activeEditor?.getSelection(); if (selection) { (await showEditsView(accessor.get(IViewsService)))?.focusInput(); - variablesService.attachContext('file', { uri: activeUri, range: selection }, ChatAgentLocation.EditingSession); + const range = selection.isEmpty() ? new Range(selection.startLineNumber, 1, selection.startLineNumber + 1, 1) : selection; + variablesService.attachContext('file', { uri: activeUri, range }, ChatAgentLocation.EditingSession); } } } From 485f68f364f3cdb5a8c844dc1df786c718f60fc9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 22 Jan 2025 21:33:07 +0100 Subject: [PATCH 0785/3587] chat setup - force sign-in when picked (#238502) --- package.json | 2 +- src/vs/base/common/product.ts | 1 + .../contrib/chat/browser/chatSetup.ts | 50 ++++++++----------- 3 files changed, 22 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 10e479fe6c49..499a2821d3bc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.97.0", - "distro": "b9d91be3f17ff1f0fd2bfce91f40f2d6bfe9c87b", + "distro": "3bc3cc2a38be414a418f2930e0aa7ba1aa39043a", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 627eff87f5bc..4567a7c8af64 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -319,6 +319,7 @@ export interface IDefaultChatAgent { readonly providerId: string; readonly providerName: string; readonly enterpriseProviderId: string; + readonly enterpriseProviderName: string; readonly providerSetting: string; readonly providerUriSetting: string; readonly providerScopes: string[][]; diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 101e7d3dba05..c94c9eacf308 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -80,6 +80,7 @@ const defaultChat = { providerId: product.defaultChatAgent?.providerId ?? '', providerName: product.defaultChatAgent?.providerName ?? '', enterpriseProviderId: product.defaultChatAgent?.enterpriseProviderId ?? '', + enterpriseProviderName: product.defaultChatAgent?.enterpriseProviderName ?? '', providerSetting: product.defaultChatAgent?.providerSetting ?? '', providerUriSetting: product.defaultChatAgent?.providerUriSetting ?? '', providerScopes: product.defaultChatAgent?.providerScopes ?? [[]], @@ -793,7 +794,7 @@ class ChatSetupController extends Disposable { this._onDidChange.fire(); } - async setup(): Promise { + async setup(options?: { forceSignIn: boolean }): Promise { const watch = new StopWatch(false); const title = localize('setupChatProgress', "Getting Copilot ready..."); const badge = this.activityService.showViewContainerActivity(preferCopilotEditsView(this.viewsService) ? CHAT_EDITING_SIDEBAR_PANEL_ID : CHAT_SIDEBAR_PANEL_ID, { @@ -805,13 +806,13 @@ class ChatSetupController extends Disposable { location: ProgressLocation.Window, command: TRIGGER_SETUP_COMMAND_ID, title, - }, () => this.doSetup(watch)); + }, () => this.doSetup(options?.forceSignIn ?? false, watch)); } finally { badge.dispose(); } } - private async doSetup(watch: StopWatch): Promise { + private async doSetup(forceSignIn: boolean, watch: StopWatch): Promise { this.context.suspend(); // reduces flicker let focusChatInput = false; @@ -820,8 +821,8 @@ class ChatSetupController extends Disposable { let session: AuthenticationSession | undefined; let entitlement: ChatEntitlement | undefined; - // Entitlement Unknown: we need to sign-in user - if (this.context.state.entitlement === ChatEntitlement.Unknown) { + // Entitlement Unknown or `forceSignIn`: we need to sign-in user + if (this.context.state.entitlement === ChatEntitlement.Unknown || forceSignIn) { this.setStep(ChatSetupStep.SigningIn); const result = await this.signIn(providerId); if (!result.session) { @@ -1037,21 +1038,10 @@ class ChatSetupWelcomeContent extends Disposable { const buttonContainer = this.element.appendChild($('p')); buttonContainer.classList.add('button-container'); const button = this._register(new ButtonWithDropdown(buttonContainer, { - actions: { - getActions: () => { - if (this.context.state.entitlement === ChatEntitlement.Unknown) { - return [ - toAction({ id: 'chatSetup.signInGh', label: localize('signInGh', "Sign in with a GitHub.com Account"), run: () => this.updateProvider(false) }), - toAction({ id: 'chatSetup.signInGhe', label: localize('signInGhe', "Sign in with a GHE.com Account"), run: () => this.updateProvider(true) }), - ]; - } else { - return [ - toAction({ id: 'chatSetup.useGh', label: localize('useGh', "Use a GitHub.com Account"), run: () => this.updateProvider(false) }), - toAction({ id: 'chatSetup.useGhe', label: localize('useGhe', "Use a GHE.com Account"), run: () => this.updateProvider(true) }), - ]; - } - } - }, + actions: [ + toAction({ id: 'chatSetup.setupWithProvider', label: localize('setupWithProvider', "Sign in with a {0} Account", defaultChat.providerName), run: () => this.setupWithProvider(false) }), + toAction({ id: 'chatSetup.setupWithEnterpriseProvider', label: localize('setupWithEnterpriseProvider', "Sign in with a {0} Account", defaultChat.enterpriseProviderName), run: () => this.setupWithProvider(true) }) + ], addPrimaryActionToDropdown: false, contextMenuProvider: this.contextMenuService, supportIcons: true, @@ -1114,7 +1104,7 @@ class ChatSetupWelcomeContent extends Disposable { button.enabled = this.controller.step === ChatSetupStep.Initial; } - private async updateProvider(useGhe: boolean): Promise { + private async setupWithProvider(useEnterpriseProvider: boolean): Promise { const registry = Registry.as(ConfigurationExtensions.Configuration); registry.registerConfiguration({ 'id': 'copilot.setup', @@ -1129,9 +1119,9 @@ class ChatSetupWelcomeContent extends Disposable { } }); - if (useGhe) { + if (useEnterpriseProvider) { await this.configurationService.updateValue(defaultChat.providerSetting, defaultChat.enterpriseProviderId, ConfigurationTarget.USER); - const success = await this.handleGheUri(); + const success = await this.handleEnterpriseInstance(); if (!success) { return; // not properly configured, abort } @@ -1140,10 +1130,10 @@ class ChatSetupWelcomeContent extends Disposable { await this.configurationService.updateValue(defaultChat.providerUriSetting, undefined, ConfigurationTarget.USER); } - return this.controller.setup(); + return this.controller.setup({ forceSignIn: true }); } - private async handleGheUri(): Promise { + private async handleEnterpriseInstance(): Promise { const uri = this.configurationService.getValue(defaultChat.providerUriSetting); if (uri) { return true; // already setup @@ -1151,8 +1141,8 @@ class ChatSetupWelcomeContent extends Disposable { let isSingleWord = false; const result = await this.quickInputService.input({ - prompt: localize('gheInstance', "What is your GHE.com instance?"), - placeHolder: localize('gheInstancePlaceholder', 'i.e. "octocat" or "https://octocat.ghe.com"...'), + prompt: localize('enterpriseInstance', "What is your {0} instance?", defaultChat.enterpriseProviderName), + placeHolder: localize('enterpriseInstancePlaceholder', 'i.e. "octocat" or "https://octocat.ghe.com"...'), validateInput: async value => { isSingleWord = false; if (!value) { @@ -1169,7 +1159,7 @@ class ChatSetupWelcomeContent extends Disposable { const regex = /^(https:\/\/)?([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.ghe\.com\/?$/; if (!regex.test(value)) { return { - content: localize('invalidGheInstance', 'Please enter a valid GHE.com instance (i.e. "octocat" or "https://octocat.ghe.com")'), + content: localize('invalidEnterpriseInstance', 'Please enter a valid {0} instance (i.e. "octocat" or "https://octocat.ghe.com")', defaultChat.enterpriseProviderName), severity: Severity.Error }; } @@ -1182,12 +1172,12 @@ class ChatSetupWelcomeContent extends Disposable { if (!result) { const { confirmed } = await this.dialogService.confirm({ type: Severity.Error, - message: localize('gheSetupError', "The provided GHE.com instance is invalid. Would you like to enter it again?", defaultChat.providerName), + message: localize('enterpriseSetupError', "The provided {0} instance is invalid. Would you like to enter it again?", defaultChat.enterpriseProviderName), primaryButton: localize('retry', "Retry") }); if (confirmed) { - return this.handleGheUri(); + return this.handleEnterpriseInstance(); } return false; From 42e96916ac665ce03432ca301dc373f492f1149a Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 22 Jan 2025 21:51:31 +0100 Subject: [PATCH 0786/3587] Git - handle local tracking branches (#238503) --- extensions/git/src/git.ts | 4 +++- extensions/git/src/historyProvider.ts | 27 +++++++++++++++++++++------ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index d235b926b16e..98d0790003c6 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -2451,7 +2451,9 @@ export class Repository { // Upstream commit if (HEAD && HEAD.upstream) { - const ref = `refs/remotes/${HEAD.upstream.remote}/${HEAD.upstream.name}`; + const ref = HEAD.upstream.remote !== '.' + ? `refs/remotes/${HEAD.upstream.remote}/${HEAD.upstream.name}` + : `refs/heads/${HEAD.upstream.name}`; const commit = await this.revParse(ref); HEAD = { ...HEAD, upstream: { ...HEAD.upstream, commit } }; } diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index f5605c354ab8..f15c5899fdaf 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -136,12 +136,27 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec historyItemRefName = this.repository.HEAD.name; // Remote - this._currentHistoryItemRemoteRef = this.repository.HEAD.upstream ? { - id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, - name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, - revision: this.repository.HEAD.upstream.commit, - icon: new ThemeIcon('cloud') - } : undefined; + if (this.repository.HEAD.upstream) { + if (this.repository.HEAD.upstream.remote === '.') { + // Local branch + this._currentHistoryItemRemoteRef = { + id: `refs/heads/${this.repository.HEAD.upstream.name}`, + name: this.repository.HEAD.upstream.name, + revision: this.repository.HEAD.upstream.commit, + icon: new ThemeIcon('gi-branch') + }; + } else { + // Remote branch + this._currentHistoryItemRemoteRef = { + id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + revision: this.repository.HEAD.upstream.commit, + icon: new ThemeIcon('cloud') + }; + } + } else { + this._currentHistoryItemRemoteRef = undefined; + } // Base if (this._HEAD?.name !== this.repository.HEAD.name) { From 87c13a70bc06be39aa49fee1506e828124e43be9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 22 Jan 2025 22:18:14 +0100 Subject: [PATCH 0787/3587] fix: update sign-in error message to reflect correct provider name (#238504) --- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index c94c9eacf308..4b11dd973bf5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -879,7 +879,7 @@ class ChatSetupController extends Disposable { if (!session && !this.willShutdown) { const { confirmed } = await this.dialogService.confirm({ type: Severity.Error, - message: localize('unknownSignInError', "Failed to sign in to {0}. Would you like to try again?", defaultChat.providerName), + message: localize('unknownSignInError', "Failed to sign in to {0}. Would you like to try again?", ChatSetupRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId ? defaultChat.enterpriseProviderName : defaultChat.providerName), detail: localize('unknownSignInErrorDetail', "You must be signed in to use Copilot."), primaryButton: localize('retry', "Retry") }); @@ -1090,7 +1090,7 @@ class ChatSetupWelcomeContent extends Disposable { switch (this.controller.step) { case ChatSetupStep.SigningIn: - buttonLabel = localize('setupChatSignIn', "$(loading~spin) Signing in to {0}...", defaultChat.providerName); + buttonLabel = localize('setupChatSignIn', "$(loading~spin) Signing in to {0}...", ChatSetupRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId ? defaultChat.enterpriseProviderName : defaultChat.providerName); break; case ChatSetupStep.Installing: buttonLabel = localize('setupChatInstalling', "$(loading~spin) Getting Copilot Ready..."); From 9d62bcde741e45928dca2e22d7c6d99c480fe4f3 Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Wed, 22 Jan 2025 14:24:54 -0800 Subject: [PATCH 0788/3587] fix editable check (#238482) * fix editable check * Revert "fix: set _lastFocusedWidget as undefined on widget blur (#234610)" This reverts commit acc7cf2c73361360267890d99b4818f7cba47131. --- src/vs/platform/list/browser/listService.ts | 5 ----- .../notebook/browser/view/renderers/webviewPreloads.ts | 3 ++- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index a92048240590..94c2acead573 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -90,11 +90,6 @@ export class ListService implements IListService { return combinedDisposable( widget.onDidFocus(() => this.setLastFocusedList(widget)), - widget.onDidBlur(() => { - if (this._lastFocusedWidget === widget) { - this.setLastFocusedList(undefined); - } - }), toDisposable(() => this.lists.splice(this.lists.indexOf(registeredList), 1)), widget.onDidDispose(() => { this.lists = this.lists.filter(l => l !== registeredList); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index da6d359d55e9..ff6f74f9096a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -188,7 +188,8 @@ async function webviewPreloads(ctx: PreloadContext) { }; const isEditableElement = (element: Element) => { - return element.tagName.toLowerCase() === 'input' || element.tagName.toLowerCase() === 'textarea' || 'editContext' in element; + return element.tagName.toLowerCase() === 'input' || element.tagName.toLowerCase() === 'textarea' + || ('editContext' in element && !!element.editContext); }; // check if an input element is focused within the output element From fe4ff630ab6c43cba8e5c2cb765bca7b0ff0a5f0 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 22 Jan 2025 15:34:02 -0800 Subject: [PATCH 0789/3587] chat: fix confirmation cutting off code part (#238509) Closes https://github.com/microsoft/vscode-copilot/issues/12031 --- .../chatContentParts/chatConfirmationContentPart.ts | 2 ++ .../chatContentParts/chatConfirmationWidget.ts | 12 ++++++++++-- .../chatContentParts/chatToolInvocationPart.ts | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts index b38aa13424bf..68a8d913c373 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts @@ -42,6 +42,8 @@ export class ChatConfirmationContentPart extends Disposable implements IChatCont const confirmationWidget = this._register(this.instantiationService.createInstance(ChatConfirmationWidget, confirmation.title, confirmation.message, buttons)); confirmationWidget.setShowButtons(!confirmation.isUsed); + this._register(confirmationWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + this._register(confirmationWidget.onDidClick(async e => { if (isResponseVM(element)) { const prompt = `${e.label}: "${confirmation.title}"`; diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts index df2b54b53e15..b6f42ef60cca 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts @@ -23,6 +23,9 @@ export class ChatConfirmationWidget extends Disposable { private _onDidClick = this._register(new Emitter()); get onDidClick(): Event { return this._onDidClick.event; } + private _onDidChangeHeight = this._register(new Emitter()); + get onDidChangeHeight(): Event { return this._onDidChangeHeight.event; } + private _domNode: HTMLElement; get domNode(): HTMLElement { return this._domNode; @@ -48,10 +51,15 @@ export class ChatConfirmationWidget extends Disposable { this._domNode = elements.root; const renderer = this.instantiationService.createInstance(MarkdownRenderer, {}); - const renderedTitle = this._register(renderer.render(new MarkdownString(title))); + const renderedTitle = this._register(renderer.render(new MarkdownString(title), { + asyncRenderCallback: () => this._onDidChangeHeight.fire(), + })); elements.title.appendChild(renderedTitle.element); - const renderedMessage = this._register(renderer.render(typeof message === 'string' ? new MarkdownString(message) : message)); + const renderedMessage = this._register(renderer.render( + typeof message === 'string' ? new MarkdownString(message) : message, + { asyncRenderCallback: () => this._onDidChangeHeight.fire() } + )); elements.message.appendChild(renderedMessage.element); buttons.forEach(buttonData => { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts index d079c05d0842..2d065692bdc6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts @@ -5,7 +5,7 @@ import * as dom from '../../../../../base/browser/dom.js'; import { Codicon } from '../../../../../base/common/codicons.js'; -import { Emitter } from '../../../../../base/common/event.js'; +import { Emitter, Relay } from '../../../../../base/common/event.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; @@ -43,6 +43,7 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa const subPart = partStore.add(instantiationService.createInstance(ChatToolInvocationSubPart, toolInvocation, context, renderer)); this.domNode.appendChild(subPart.domNode); + partStore.add(subPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); partStore.add(subPart.onNeedsRerender(() => { render(); this._onDidChangeHeight.fire(); @@ -66,6 +67,9 @@ class ChatToolInvocationSubPart extends Disposable { private _onNeedsRerender = this._register(new Emitter()); public readonly onNeedsRerender = this._onNeedsRerender.event; + private _onDidChangeHeight = this._register(new Relay()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + constructor( toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, context: IChatContentPartRenderContext, @@ -86,6 +90,7 @@ class ChatToolInvocationSubPart extends Disposable { this._register(confirmWidget.onDidClick(button => { toolInvocation.confirmed.complete(button.data); })); + this._onDidChangeHeight.input = confirmWidget.onDidChangeHeight; toolInvocation.confirmed.p.then(() => this._onNeedsRerender.fire()); } else { const content = typeof toolInvocation.invocationMessage === 'string' ? From 3390a6280b2da2fd0ccf0c76b2cafbd76ae7acea Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 22 Jan 2025 16:36:48 -0800 Subject: [PATCH 0790/3587] tools: fix edit tool not saving new files before running (#238515) Fixes https://github.com/microsoft/vscode-copilot/issues/12058 --- .../contrib/chat/browser/tools/tools.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/tools.ts b/src/vs/workbench/contrib/chat/browser/tools/tools.ts index b1a209f23625..29746370f035 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/tools.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/tools.ts @@ -5,7 +5,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; @@ -149,15 +149,26 @@ class EditTool implements IToolImpl { throw new Error(result.errorMessage); } + let dispose: IDisposable; await new Promise((resolve) => { - autorun((r) => { + // The file will not be modified until the first edits start streaming in, + // so wait until we see that it _was_ modified before waiting for it to be done. + let wasFileBeingModified = false; + + dispose = autorun((r) => { const currentEditingSession = this.chatEditingService.currentEditingSessionObs.read(r); const entries = currentEditingSession?.entries.read(r); const currentFile = entries?.find((e) => e.modifiedURI.toString() === uri.toString()); - if (currentFile && !currentFile.isCurrentlyBeingModified.read(r)) { - resolve(true); + if (currentFile) { + if (currentFile.isCurrentlyBeingModified.read(r)) { + wasFileBeingModified = true; + } else if (wasFileBeingModified) { + resolve(true); + } } }); + }).finally(() => { + dispose.dispose(); }); await this.textFileService.save(uri); From fc9c74300b636edac88ecf4d63bb7bdfab7402ea Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 22 Jan 2025 17:19:36 -0800 Subject: [PATCH 0791/3587] Skip image resize if we don't need to If the image is under 2048 we can just return the original data --- src/vs/workbench/contrib/chat/browser/imageUtils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/imageUtils.ts b/src/vs/workbench/contrib/chat/browser/imageUtils.ts index c176876c5380..194f7be40d19 100644 --- a/src/vs/workbench/contrib/chat/browser/imageUtils.ts +++ b/src/vs/workbench/contrib/chat/browser/imageUtils.ts @@ -27,6 +27,9 @@ export async function resizeImage(data: Uint8Array): Promise { const scaleFactor = 2048 / Math.max(width, height); width = Math.round(width * scaleFactor); height = Math.round(height * scaleFactor); + } else { + resolve(data); + return; } const scaleFactor = 768 / Math.min(width, height); From 459c76c1a7a71b286b0e674cf707c0850136c206 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 22 Jan 2025 17:57:50 -0800 Subject: [PATCH 0792/3587] Don't block pastes resolving copy data that we don't care about --- .../browser/copyPasteController.ts | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index ed17348099a6..4600a3360a2c 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -8,7 +8,7 @@ import { IAction } from '../../../../base/common/actions.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { CancelablePromise, createCancelablePromise, DeferredPromise, raceCancellation } from '../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { createStringDataTransferItem, matchesMimeType, UriList, VSDataTransfer } from '../../../../base/common/dataTransfer.js'; +import { createStringDataTransferItem, IReadonlyVSDataTransfer, matchesMimeType, UriList, VSDataTransfer } from '../../../../base/common/dataTransfer.js'; import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'; import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; @@ -74,6 +74,11 @@ export type PastePreference = | { readonly providerId: string } // Only used internally ; +interface CopyOperation { + readonly providerMimeTypes: readonly string[]; + readonly operation: CancelablePromise; +} + export class CopyPasteController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.copyPasteActionController'; @@ -97,7 +102,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi */ private static _currentCopyOperation?: { readonly handle: string; - readonly dataTransferPromise: CancelablePromise; + readonly operations: ReadonlyArray; }; private readonly _editor: ICodeEditor; @@ -222,31 +227,20 @@ export class CopyPasteController extends Disposable implements IEditorContributi defaultPastePayload }); - const promise = createCancelablePromise(async token => { - const results = coalesce(await Promise.all(providers.map(async provider => { - try { - return await provider.prepareDocumentPaste!(model, ranges, dataTransfer, token); - } catch (err) { - console.error(err); - return undefined; - } - }))); - - // Values from higher priority providers should overwrite values from lower priority ones. - // Reverse the array to so that the calls to `replace` below will do this - results.reverse(); - - for (const result of results) { - for (const [mime, value] of result) { - dataTransfer.replace(mime, value); - } - } - - return dataTransfer; + const operations = providers.map((provider): CopyOperation => { + return { + providerMimeTypes: provider.copyMimeTypes, + operation: createCancelablePromise(token => + provider.prepareDocumentPaste!(model, ranges, dataTransfer, token) + .catch(err => { + console.error(err); + return undefined; + })) + }; }); - CopyPasteController._currentCopyOperation?.dataTransferPromise.cancel(); - CopyPasteController._currentCopyOperation = { handle: handle, dataTransferPromise: promise }; + CopyPasteController._currentCopyOperation?.operations.forEach(entry => entry.operation.cancel()); + CopyPasteController._currentCopyOperation = { handle, operations }; } private async handlePaste(e: ClipboardEvent) { @@ -356,7 +350,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi const token = cts.token; try { - await this.mergeInDataFromCopy(dataTransfer, metadata, token); + await this.mergeInDataFromCopy(allProviders, dataTransfer, metadata, token); if (token.isCancellationRequested) { return; } @@ -371,7 +365,9 @@ export class CopyPasteController extends Disposable implements IEditorContributi const context: DocumentPasteContext = { triggerKind: DocumentPasteTriggerKind.Automatic, }; + console.time('getPasteEdits'); const editSession = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, token); + console.timeEnd('getPasteEdits'); disposables.add(editSession); if (token.isCancellationRequested) { return; @@ -448,7 +444,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi const disposables = new DisposableStore(); const tokenSource = disposables.add(new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token)); try { - await this.mergeInDataFromCopy(dataTransfer, metadata, tokenSource.token); + await this.mergeInDataFromCopy(allProviders, dataTransfer, metadata, tokenSource.token); if (tokenSource.token.isCancellationRequested) { return; } @@ -583,15 +579,26 @@ export class CopyPasteController extends Disposable implements IEditorContributi return undefined; } - private async mergeInDataFromCopy(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken): Promise { + private async mergeInDataFromCopy(allProviders: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken): Promise { if (metadata?.id && CopyPasteController._currentCopyOperation?.handle === metadata.id) { - const toMergeDataTransfer = await CopyPasteController._currentCopyOperation.dataTransferPromise; + // Only resolve providers that have data we may care about + const toResolve = CopyPasteController._currentCopyOperation.operations + .filter(op => allProviders.some(provider => provider.pasteMimeTypes.some(type => matchesMimeType(type, op.providerMimeTypes)))) + .map(op => op.operation); + + const toMergeResults = await Promise.all(toResolve); if (token.isCancellationRequested) { return; } - for (const [key, value] of toMergeDataTransfer) { - dataTransfer.replace(key, value); + // Values from higher priority providers should overwrite values from lower priority ones. + // Reverse the array to so that the calls to `DataTransfer.replace` later will do this + for (const toMergeData of toMergeResults.reverse()) { + if (toMergeData) { + for (const [key, value] of toMergeData) { + dataTransfer.replace(key, value); + } + } } } From 7b203a15ebc06bd7289c92518f99593f7bd626f8 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 22 Jan 2025 18:01:03 -0800 Subject: [PATCH 0793/3587] Remove logs --- .../contrib/dropOrPasteInto/browser/copyPasteController.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index 4600a3360a2c..6efeeb360f8e 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -365,9 +365,8 @@ export class CopyPasteController extends Disposable implements IEditorContributi const context: DocumentPasteContext = { triggerKind: DocumentPasteTriggerKind.Automatic, }; - console.time('getPasteEdits'); + const editSession = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, token); - console.timeEnd('getPasteEdits'); disposables.add(editSession); if (token.isCancellationRequested) { return; From 53c5905823029ebe839513b34c179c9277294e06 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 22 Jan 2025 20:16:13 -0600 Subject: [PATCH 0794/3587] fix terminal suggest layout issue (#238466) fix #238464 --- .../suggest/browser/simpleSuggestWidget.ts | 2 +- .../browser/simpleSuggestWidgetDetails.ts | 91 ++++++++++--------- 2 files changed, 49 insertions(+), 44 deletions(-) diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index 0c925bfd2b30..3001f462a92a 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -222,7 +222,7 @@ export class SimpleSuggestWidget extends Disposable { this._messageElement = dom.append(this.element.domNode, dom.$('.message')); - const details: SimpleSuggestDetailsWidget = this._register(instantiationService.createInstance(SimpleSuggestDetailsWidget)); + const details: SimpleSuggestDetailsWidget = this._register(instantiationService.createInstance(SimpleSuggestDetailsWidget, this._getFontInfo)); this._register(details.onDidClose(() => this.toggleDetails())); this._details = this._register(new SimpleSuggestDetailsOverlay(details, this._listElement)); this._register(dom.addDisposableListener(this._details.widget.domNode, 'blur', (e) => this._onDidBlurDetails.fire(e))); diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts index fdea7625c931..88408bfc7821 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts @@ -12,10 +12,10 @@ import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { ResizableHTMLElement } from '../../../../base/browser/ui/resizable/resizable.js'; import * as nls from '../../../../nls.js'; import { SimpleCompletionItem } from './simpleCompletionItem.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ISimpleSuggestWidgetFontInfo } from './simpleSuggestWidgetRenderer.js'; export function canExpandCompletionItem(item: SimpleCompletionItem | undefined): boolean { return !!item && Boolean(item.completion.documentation || item.completion.detail && item.completion.detail !== item.completion.label); @@ -48,7 +48,7 @@ export class SimpleSuggestDetailsWidget { private _size = new dom.Dimension(330, 0); constructor( - @IConfigurationService private readonly _configurationService: IConfigurationService, + private readonly _getFontInfo: () => ISimpleSuggestWidgetFontInfo, @IInstantiationService instaService: IInstantiationService, ) { this.domNode = dom.$('.suggest-details'); @@ -81,7 +81,7 @@ export class SimpleSuggestDetailsWidget { } getLayoutInfo() { - const lineHeight = this._configurationService.getValue('editor.lineHeight'); + const lineHeight = this._getFontInfo().lineHeight; const borderWidth = this._borderWidth; const borderHeight = borderWidth * 2; return { @@ -177,7 +177,7 @@ export class SimpleSuggestDetailsWidget { this._body.scrollTop = 0; - this.layout(this._size.width, this._type.clientHeight + this._docs.clientHeight + 20); + this.layout(this._size.width, this._type.clientHeight + this._docs.clientHeight + this.getLayoutInfo().verticalPadding); this._onDidChangeContents.fire(this); } @@ -250,7 +250,7 @@ export class SimpleSuggestDetailsOverlay { private _anchorBox?: dom.IDomNodePagePosition; // private _preferAlignAtTop: boolean = true; private _userSize?: dom.Dimension; - // private _topLeft?: TopLeftPosition; + private _topLeft?: TopLeftPosition; constructor( readonly widget: SimpleSuggestDetailsWidget, @@ -262,43 +262,43 @@ export class SimpleSuggestDetailsOverlay { this._resizable.domNode.appendChild(widget.domNode); this._resizable.enableSashes(false, true, true, false); - // let topLeftNow: TopLeftPosition | undefined; - // let sizeNow: dom.Dimension | undefined; - // let deltaTop: number = 0; - // let deltaLeft: number = 0; - // this._disposables.add(this._resizable.onDidWillResize(() => { - // topLeftNow = this._topLeft; - // sizeNow = this._resizable.size; - // })); - - // this._disposables.add(this._resizable.onDidResize(e => { - // if (topLeftNow && sizeNow) { - // this.widget.layout(e.dimension.width, e.dimension.height); - - // let updateTopLeft = false; - // if (e.west) { - // deltaLeft = sizeNow.width - e.dimension.width; - // updateTopLeft = true; - // } - // if (e.north) { - // deltaTop = sizeNow.height - e.dimension.height; - // updateTopLeft = true; - // } - // if (updateTopLeft) { - // this._applyTopLeft({ - // top: topLeftNow.top + deltaTop, - // left: topLeftNow.left + deltaLeft, - // }); - // } - // } - // if (e.done) { - // topLeftNow = undefined; - // sizeNow = undefined; - // deltaTop = 0; - // deltaLeft = 0; - // this._userSize = e.dimension; - // } - // })); + let topLeftNow: TopLeftPosition | undefined; + let sizeNow: dom.Dimension | undefined; + let deltaTop: number = 0; + let deltaLeft: number = 0; + this._disposables.add(this._resizable.onDidWillResize(() => { + topLeftNow = this._topLeft; + sizeNow = this._resizable.size; + })); + + this._disposables.add(this._resizable.onDidResize(e => { + if (topLeftNow && sizeNow) { + this.widget.layout(e.dimension.width, e.dimension.height); + + let updateTopLeft = false; + if (e.west) { + deltaLeft = sizeNow.width - e.dimension.width; + updateTopLeft = true; + } + if (e.north) { + deltaTop = sizeNow.height - e.dimension.height; + updateTopLeft = true; + } + if (updateTopLeft) { + this._applyTopLeft({ + top: topLeftNow.top + deltaTop, + left: topLeftNow.left + deltaLeft, + }); + } + } + if (e.done) { + topLeftNow = undefined; + sizeNow = undefined; + deltaTop = 0; + deltaLeft = 0; + this._userSize = e.dimension; + } + })); this._disposables.add(this.widget.onDidChangeContents(() => { if (this._anchorBox) { @@ -439,10 +439,15 @@ export class SimpleSuggestDetailsOverlay { } private _applyTopLeft(topLeft: { left: number; top: number }): void { - // this._topLeft = topLeft; + this._topLeft = topLeft; // this._editor.layoutOverlayWidget(this); this._resizable.domNode.style.top = `${topLeft.top}px`; this._resizable.domNode.style.left = `${topLeft.left}px`; this._resizable.domNode.style.position = 'absolute'; } } + +interface TopLeftPosition { + top: number; + left: number; +} From 213656757931920080610c1f338f42bb23e31e73 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 22 Jan 2025 21:18:46 -0800 Subject: [PATCH 0795/3587] integrate instructions into the Edits experience (#238486) * [edits]: integrate instructions into the Edits experience * [edits]: update preconditions for the chat execute actions * [edits]: cleanup * Update src/vs/workbench/contrib/chat/browser/chatWidget.ts Co-authored-by: Joyce Er * [edits]: refactor instruction attachments to chat variable conversion logic * [edits]: chat variable entries for instruction references now different for `child` and `root` references * [edits]: update variable ID to be unique --------- Co-authored-by: Joyce Er --- .../browser/actions/chatExecuteActions.ts | 58 +++++++++++--- .../instructionAttachments.ts | 8 ++ .../chatInstructionAttachmentsModel.ts | 78 ++++++++++++++++++- .../chatInstructionsAttachment.ts | 12 ++- .../contrib/chat/browser/chatInputPart.ts | 13 +--- .../contrib/chat/browser/chatWidget.ts | 18 ++++- .../promptSyntax/parsers/basePromptParser.ts | 8 +- .../promptSyntax/parsers/filePromptParser.ts | 2 +- 8 files changed, 167 insertions(+), 30 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 82fdf00c6394..d3aea50c7aac 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -133,13 +133,21 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { static readonly ID = 'workbench.action.edits.submit'; constructor() { + const precondition = ContextKeyExpr.and( + // if the input has prompt instructions attached, allow submitting requests even + // without text present - having instructions is enough context for a request + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + applyingChatEditsContextKey.toNegated(), + ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), + ); + super({ id: ChatEditingSessionSubmitAction.ID, title: localize2('edits.submit.label', "Send"), f1: false, category: CHAT_CATEGORY, icon: Codicon.send, - precondition: ContextKeyExpr.and(ChatContextKeys.inputHasText, ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey.toNegated()), + precondition, keybinding: { when: ChatContextKeys.inChatInput, primary: KeyCode.Enter, @@ -167,18 +175,23 @@ class SubmitWithoutDispatchingAction extends Action2 { static readonly ID = 'workbench.action.chat.submitWithoutDispatching'; constructor() { + const precondition = ContextKeyExpr.and( + // if the input has prompt instructions attached, allow submitting requests even + // without text present - having instructions is enough context for a request + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + ChatContextKeys.requestInProgress.negate(), + ContextKeyExpr.and(ContextKeyExpr.or( + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Editor), + )), + ); + super({ id: SubmitWithoutDispatchingAction.ID, title: localize2('interactive.submitWithoutDispatch.label', "Send"), f1: false, category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and( - ChatContextKeys.inputHasText, - ChatContextKeys.requestInProgress.negate(), - ContextKeyExpr.and(ContextKeyExpr.or( - ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), - ChatContextKeys.location.isEqualTo(ChatAgentLocation.Editor), - ))), + precondition, keybinding: { when: ChatContextKeys.inChatInput, primary: KeyMod.Alt | KeyMod.Shift | KeyCode.Enter, @@ -227,10 +240,18 @@ export class ChatSubmitSecondaryAgentAction extends Action2 { static readonly ID = 'workbench.action.chat.submitSecondaryAgent'; constructor() { + const precondition = ContextKeyExpr.and( + // if the input has prompt instructions attached, allow submitting requests even + // without text present - having instructions is enough context for a request + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + ChatContextKeys.inputHasAgent.negate(), + ChatContextKeys.requestInProgress.negate(), + ); + super({ id: ChatSubmitSecondaryAgentAction.ID, title: localize2({ key: 'actions.chat.submitSecondaryAgent', comment: ['Send input from the chat input box to the secondary agent'] }, "Submit to Secondary Agent"), - precondition: ContextKeyExpr.and(ChatContextKeys.inputHasText, ChatContextKeys.inputHasAgent.negate(), ChatContextKeys.requestInProgress.negate()), + precondition, menu: { id: MenuId.ChatExecuteSecondary, group: 'group_1', @@ -270,10 +291,18 @@ export class ChatSubmitSecondaryAgentAction extends Action2 { class SendToChatEditingAction extends Action2 { constructor() { + const precondition = ContextKeyExpr.and( + // if the input has prompt instructions attached, allow submitting requests even + // without text present - having instructions is enough context for a request + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + ChatContextKeys.inputHasAgent.negate(), + ChatContextKeys.requestInProgress.negate(), + ); + super({ id: 'workbench.action.chat.sendToChatEditing', title: localize2('chat.sendToChatEditing.label', "Send to Copilot Edits"), - precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), ChatContextKeys.inputHasAgent.negate(), ChatContextKeys.inputHasText), + precondition, category: CHAT_CATEGORY, f1: false, menu: { @@ -355,10 +384,17 @@ class SendToChatEditingAction extends Action2 { class SendToNewChatAction extends Action2 { constructor() { + const precondition = ContextKeyExpr.and( + // if the input has prompt instructions attached, allow submitting requests even + // without text present - having instructions is enough context for a request + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + ChatContextKeys.requestInProgress.negate(), + ); + super({ id: 'workbench.action.chat.sendToNewChat', title: localize2('chat.newChat.label', "Send to New Chat"), - precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), ChatContextKeys.inputHasText), + precondition, category: CHAT_CATEGORY, f1: false, menu: { diff --git a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionAttachments.ts b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionAttachments.ts index 2e8d11ca7f51..79d84ebf43a0 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionAttachments.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionAttachments.ts @@ -35,6 +35,14 @@ export class InstructionAttachmentsWidget extends Disposable { return this.model.references; } + /** + * Get the list of all prompt instruction attachment variables, including all + * nested child references of each attachment explicitly attached by user. + */ + public get chatAttachments() { + return this.model.chatAttachments; + } + /** * Check if child widget list is empty (no attachments present). */ diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts index e57cd6fc284b..0dcf1991deda 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts @@ -5,6 +5,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { Emitter } from '../../../../../base/common/event.js'; +import { IChatRequestVariableEntry } from '../../common/chatModel.js'; import { ChatInstructionsFileLocator } from './chatInstructionsFileLocator.js'; import { ChatInstructionsAttachmentModel } from './chatInstructionsAttachment.js'; import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; @@ -12,9 +13,42 @@ import { BasePromptParser } from '../../common/promptSyntax/parsers/basePromptPa import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +/** + * Common instructions attachment variable identifier. + */ +type TInstructionsId = 'vscode.prompt.instructions'; + +/** + * Instructions attachment variable identifier for + * the `root` reference. + */ +type TInstructionsRootId = `${TInstructionsId}.root`; + +/** + * Well-defined instructions attachment variable identifiers. + */ +type TInstructionsVariableIds = TInstructionsId | TInstructionsRootId; + +/** + * Utility to convert a reference `URI` to a chat variable + * entry with the specified variable {@linkcode id}. + */ +const toChatVariable = (id: TInstructionsVariableIds, uri: URI): IChatRequestVariableEntry => { + return { + id: `${id}__${uri}`, + name: uri.fsPath, + value: uri, + isSelection: false, + enabled: true, + isFile: true, + isDynamic: true, + isMarkedReadonly: true, + }; +}; + /** * Model for a collection of prompt instruction attachments. - * See {@linkcode ChatInstructionsAttachmentModel}. + * See {@linkcode ChatInstructionsAttachmentModel} for individual attachment. */ export class ChatInstructionAttachmentsModel extends Disposable { /** @@ -42,6 +76,48 @@ export class ChatInstructionAttachmentsModel extends Disposable { return result; } + /** + * Get the list of all prompt instruction attachment variables, including all + * nested child references of each attachment explicitly attached by user. + */ + public get chatAttachments(): readonly IChatRequestVariableEntry[] { + const result = []; + const attachments = [...this.attachments.values()]; + + for (const attachment of attachments) { + const { reference } = attachment; + + // the usual URIs list of prompt instructions is `bottom-up`, therefore + // we do the same herfe - first add all child references of the model + result.push( + ...reference.allValidReferencesUris.map((uri) => { + return toChatVariable('vscode.prompt.instructions', uri); + }), + ); + + // then add the root reference of the model itself + result.push( + toChatVariable('vscode.prompt.instructions.root', reference.uri), + ); + } + + return result; + } + + /** + * Promise that resolves when parsing of all attached prompt instruction + * files completes, including parsing of all its possible child references. + */ + public async allSettled(): Promise { + const attachments = [...this.attachments.values()]; + + await Promise.allSettled( + attachments.map((attachment) => { + return attachment.allSettled; + }), + ); + } + /** * Event that fires then this model is updated. * diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts index a53dcb7b3436..d03092c32fe2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts @@ -26,8 +26,8 @@ export class ChatInstructionsAttachmentModel extends Disposable { } /** - * Get `URI` for the main reference and `URI`s of all valid - * child references it may contain. + * Get `URI` for the main reference and `URI`s of all valid child + * references it may contain, including reference of this model itself. */ public get references(): readonly URI[] { const { reference } = this; @@ -47,6 +47,14 @@ export class ChatInstructionsAttachmentModel extends Disposable { ]; } + /** + * Promise that resolves when the prompt is fully parsed, + * including all its possible nested child references. + */ + public get allSettled(): Promise { + return this.reference.settledAll(); + } + /** * Get the top-level error of the prompt instructions * reference, if any. diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 8a676d3e3230..c904d8a399cb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -205,17 +205,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - for (const uri of this.instructionAttachmentsPart.references) { - contextArr.push({ - id: 'vscode.prompt.instructions', - name: basename(uri.path), - value: uri, - isSelection: false, - enabled: true, - isFile: true, - isDynamic: true, - }); - } + contextArr + .push(...this.instructionAttachmentsPart.chatAttachments); return contextArr; } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 88d43a4181e6..b3f85bfca821 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1004,7 +1004,17 @@ export class ChatWidget extends Disposable implements IChatWidget { `${query.prefix} ${editorValue}`; const isUserQuery = !query || 'prefix' in query; - + const { promptInstructions } = this.inputPart.attachmentModel; + const instructionsEnabled = promptInstructions.featureEnabled; + if (instructionsEnabled) { + // instruction files may have nested child references to other prompt + // files that are resolved asynchronously, hence we need to wait for + // the entire prompt instruction tree to be processed + const instructionsStarted = performance.now(); + await promptInstructions.allSettled(); + // allow-any-unicode-next-line + this.logService.trace(`[⏱] instructions tree resolved in ${performance.now() - instructionsStarted}ms`); + } let attachedContext = this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId); let workingSet: URI[] | undefined; @@ -1034,6 +1044,12 @@ export class ChatWidget extends Disposable implements IChatWidget { } } + // add prompt instruction references to the attached context, if enabled + if (instructionsEnabled) { + editingSessionAttachedContext + .push(...promptInstructions.chatAttachments); + } + for (const file of uniqueWorkingSetEntries) { // Make sure that any files that we sent are part of the working set // but do not permanently add file variables from previous requests to the working set diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index 2e531924c2d3..c063be400ae5 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -286,9 +286,11 @@ export abstract class BasePromptParser extend _stream: ChatPromptDecoder, error?: Error, ): this { - this.logService.warn( - `[prompt parser][${basename(this.uri)}]} received an error on the chat prompt decoder stream: ${error}`, - ); + if (error) { + this.logService.warn( + `[prompt parser][${basename(this.uri)}] received an error on the chat prompt decoder stream: ${error}`, + ); + } this._onUpdate.fire(); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts index ca47af1ed50e..5154016fcb8b 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from '../../../../../../base/common/uri.js'; import { BasePromptParser } from './basePromptParser.js'; +import { URI } from '../../../../../../base/common/uri.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; From 26bd8a0f229f10354006b702756b0c9d274f55cf Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 23 Jan 2025 08:20:25 +0100 Subject: [PATCH 0796/3587] Deep link to Copilot Chat fails if `?referrer` param is added (fix microsoft/vscode-copilot#12053) (#238528) --- .../contrib/chat/browser/chatSetup.ts | 11 ++++++++--- .../extensions/browser/extensionUrlHandler.ts | 18 ++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 4b11dd973bf5..0192a3a0fd0c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -67,6 +67,7 @@ import { StopWatch } from '../../../../base/common/stopwatch.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; +import { equalsIgnoreCase } from '../../../../base/common/strings.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -306,9 +307,13 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr } private registerUrlLinkHandler(): void { - this._register(ExtensionUrlHandlerOverrideRegistry.registerHandler(URI.parse(`${this.productService.urlProtocol}://${defaultChat.chatExtensionId}`), { - handleURL: async () => { - this.telemetryService.publicLog2('workbenchActionExecuted', { id: TRIGGER_SETUP_COMMAND_ID, from: 'url' }); + this._register(ExtensionUrlHandlerOverrideRegistry.registerHandler({ + canHandleURL: url => { + return url.scheme === this.productService.urlProtocol && equalsIgnoreCase(url.authority, defaultChat.chatExtensionId); + }, + handleURL: async url => { + const params = new URLSearchParams(url.query); + this.telemetryService.publicLog2('workbenchActionExecuted', { id: TRIGGER_SETUP_COMMAND_ID, from: params.get('referrer') ?? 'url' }); await this.commandService.executeCommand(TRIGGER_SETUP_COMMAND_ID); diff --git a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index b5f67878d99a..d1a25424a4e0 100644 --- a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -26,7 +26,6 @@ import { mainWindow } from '../../../../base/browser/window.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { ResourceMap } from '../../../../base/common/map.js'; const FIVE_MINUTES = 5 * 60 * 1000; const THIRTY_SECONDS = 30 * 1000; @@ -88,21 +87,28 @@ type ExtensionUrlHandlerClassification = { }; export interface IExtensionUrlHandlerOverride { + canHandleURL(uri: URI): boolean; handleURL(uri: URI): Promise; } export class ExtensionUrlHandlerOverrideRegistry { - private static readonly handlers = new ResourceMap(); + private static readonly handlers = new Set(); - static registerHandler(uri: URI, handler: IExtensionUrlHandlerOverride): IDisposable { - this.handlers.set(uri, handler); + static registerHandler(handler: IExtensionUrlHandlerOverride): IDisposable { + this.handlers.add(handler); - return toDisposable(() => this.handlers.delete(uri)); + return toDisposable(() => this.handlers.delete(handler)); } static getHandler(uri: URI): IExtensionUrlHandlerOverride | undefined { - return this.handlers.get(uri); + for (const handler of this.handlers) { + if (handler.canHandleURL(uri)) { + return handler; + } + } + + return undefined; } } From ce2c2f3c79a32b9917e32c61e058392dc5a1b6aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 23 Jan 2025 09:41:55 +0100 Subject: [PATCH 0797/3587] fix build (#238530) * fix build * fix build * one more fix --- build/azure-pipelines/common/publish.js | 37 +------------ build/azure-pipelines/common/publish.ts | 2 +- build/azure-pipelines/upload-cdn.js | 53 ++++--------------- build/azure-pipelines/upload-cdn.ts | 6 +-- build/azure-pipelines/upload-nlsmetadata.js | 51 ++++-------------- build/azure-pipelines/upload-nlsmetadata.ts | 6 +-- build/azure-pipelines/upload-sourcemaps.js | 6 +-- build/azure-pipelines/upload-sourcemaps.ts | 4 +- build/darwin/verify-macho.js | 37 +------------ build/darwin/verify-macho.ts | 2 +- build/lib/nls.js | 47 +++-------------- build/lib/nls.ts | 4 +- build/lib/optimize.js | 58 ++++++++++----------- build/lib/optimize.ts | 14 ++--- 14 files changed, 81 insertions(+), 246 deletions(-) diff --git a/build/azure-pipelines/common/publish.js b/build/azure-pipelines/common/publish.js index 48093086c348..599f12f47af1 100644 --- a/build/azure-pipelines/common/publish.js +++ b/build/azure-pipelines/common/publish.js @@ -3,39 +3,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; @@ -53,7 +20,7 @@ const os_1 = __importDefault(require("os")); const node_worker_threads_1 = require("node:worker_threads"); const msal_node_1 = require("@azure/msal-node"); const storage_blob_1 = require("@azure/storage-blob"); -const jws = __importStar(require("jws")); +const jws_1 = __importDefault(require("jws")); const node_timers_1 = require("node:timers"); function e(name) { const result = process.env[name]; @@ -283,7 +250,7 @@ class ESRPReleaseService { return await res.json(); } async generateJwsToken(message) { - return jws.sign({ + return jws_1.default.sign({ header: { alg: 'RS256', crit: ['exp', 'x5t'], diff --git a/build/azure-pipelines/common/publish.ts b/build/azure-pipelines/common/publish.ts index 79444bebf13c..39d189c05faa 100644 --- a/build/azure-pipelines/common/publish.ts +++ b/build/azure-pipelines/common/publish.ts @@ -17,7 +17,7 @@ import os from 'os'; import { Worker, isMainThread, workerData } from 'node:worker_threads'; import { ConfidentialClientApplication } from '@azure/msal-node'; import { BlobClient, BlobServiceClient, BlockBlobClient, ContainerClient } from '@azure/storage-blob'; -import * as jws from 'jws'; +import jws from 'jws'; import { clearInterval, setInterval } from 'node:timers'; function e(name: string): string { diff --git a/build/azure-pipelines/upload-cdn.js b/build/azure-pipelines/upload-cdn.js index a0ec9d93516a..3174c8dfbaf0 100644 --- a/build/azure-pipelines/upload-cdn.js +++ b/build/azure-pipelines/upload-cdn.js @@ -3,54 +3,21 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -const es = __importStar(require("event-stream")); +const event_stream_1 = __importDefault(require("event-stream")); const vinyl_1 = __importDefault(require("vinyl")); -const vfs = __importStar(require("vinyl-fs")); +const vinyl_fs_1 = __importDefault(require("vinyl-fs")); const gulp_filter_1 = __importDefault(require("gulp-filter")); const gulp_gzip_1 = __importDefault(require("gulp-gzip")); -const mime = __importStar(require("mime")); +const mime_1 = __importDefault(require("mime")); const identity_1 = require("@azure/identity"); const azure = require('gulp-azure-storage'); const commit = process.env['BUILD_SOURCEVERSION']; const credential = new identity_1.ClientAssertionCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], () => Promise.resolve(process.env['AZURE_ID_TOKEN'])); -mime.define({ +mime_1.default.define({ 'application/typescript': ['ts'], 'application/json': ['code-snippets'], }); @@ -118,17 +85,17 @@ async function main() { cacheControl: 'max-age=31536000, public' } }); - const all = vfs.src('**', { cwd: '../vscode-web', base: '../vscode-web', dot: true }) + const all = vinyl_fs_1.default.src('**', { cwd: '../vscode-web', base: '../vscode-web', dot: true }) .pipe((0, gulp_filter_1.default)(f => !f.isDirectory())); const compressed = all - .pipe((0, gulp_filter_1.default)(f => MimeTypesToCompress.has(mime.lookup(f.path)))) + .pipe((0, gulp_filter_1.default)(f => MimeTypesToCompress.has(mime_1.default.lookup(f.path)))) .pipe((0, gulp_gzip_1.default)({ append: false })) .pipe(azure.upload(options(true))); const uncompressed = all - .pipe((0, gulp_filter_1.default)(f => !MimeTypesToCompress.has(mime.lookup(f.path)))) + .pipe((0, gulp_filter_1.default)(f => !MimeTypesToCompress.has(mime_1.default.lookup(f.path)))) .pipe(azure.upload(options(false))); - const out = es.merge(compressed, uncompressed) - .pipe(es.through(function (f) { + const out = event_stream_1.default.merge(compressed, uncompressed) + .pipe(event_stream_1.default.through(function (f) { console.log('Uploaded:', f.relative); files.push(f.relative); this.emit('data', f); @@ -140,7 +107,7 @@ async function main() { contents: Buffer.from(files.join('\n')), stat: { mode: 0o666 } }); - const filesOut = es.readArray([listing]) + const filesOut = event_stream_1.default.readArray([listing]) .pipe((0, gulp_gzip_1.default)({ append: false })) .pipe(azure.upload(options(true))); console.log(`Uploading: files.txt (${files.length} files)`); // debug diff --git a/build/azure-pipelines/upload-cdn.ts b/build/azure-pipelines/upload-cdn.ts index 719ecd09c36a..8ca5e03f1f84 100644 --- a/build/azure-pipelines/upload-cdn.ts +++ b/build/azure-pipelines/upload-cdn.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; +import es from 'event-stream'; import Vinyl from 'vinyl'; -import * as vfs from 'vinyl-fs'; +import vfs from 'vinyl-fs'; import filter from 'gulp-filter'; import gzip from 'gulp-gzip'; -import * as mime from 'mime'; +import mime from 'mime'; import { ClientAssertionCredential } from '@azure/identity'; const azure = require('gulp-azure-storage'); diff --git a/build/azure-pipelines/upload-nlsmetadata.js b/build/azure-pipelines/upload-nlsmetadata.js index aac93a057325..146f804ce699 100644 --- a/build/azure-pipelines/upload-nlsmetadata.js +++ b/build/azure-pipelines/upload-nlsmetadata.js @@ -3,45 +3,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -const es = __importStar(require("event-stream")); -const vfs = __importStar(require("vinyl-fs")); +const event_stream_1 = __importDefault(require("event-stream")); +const vinyl_fs_1 = __importDefault(require("vinyl-fs")); const gulp_merge_json_1 = __importDefault(require("gulp-merge-json")); const gulp_gzip_1 = __importDefault(require("gulp-gzip")); const identity_1 = require("@azure/identity"); @@ -52,11 +19,11 @@ const commit = process.env['BUILD_SOURCEVERSION']; const credential = new identity_1.ClientAssertionCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], () => Promise.resolve(process.env['AZURE_ID_TOKEN'])); function main() { return new Promise((c, e) => { - const combinedMetadataJson = es.merge( + const combinedMetadataJson = event_stream_1.default.merge( // vscode: we are not using `out-build/nls.metadata.json` here because // it includes metadata for translators for `keys`. but for our purpose // we want only the `keys` and `messages` as `string`. - es.merge(vfs.src('out-build/nls.keys.json', { base: 'out-build' }), vfs.src('out-build/nls.messages.json', { base: 'out-build' })) + event_stream_1.default.merge(vinyl_fs_1.default.src('out-build/nls.keys.json', { base: 'out-build' }), vinyl_fs_1.default.src('out-build/nls.messages.json', { base: 'out-build' })) .pipe((0, gulp_merge_json_1.default)({ fileName: 'vscode.json', jsonSpace: '', @@ -73,7 +40,7 @@ function main() { } })), // extensions - vfs.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' })).pipe((0, gulp_merge_json_1.default)({ + vinyl_fs_1.default.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), vinyl_fs_1.default.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), vinyl_fs_1.default.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' })).pipe((0, gulp_merge_json_1.default)({ fileName: 'combined.nls.metadata.json', jsonSpace: '', concatArrays: true, @@ -129,11 +96,11 @@ function main() { return { [key]: parsedJson }; }, })); - const nlsMessagesJs = vfs.src('out-build/nls.messages.js', { base: 'out-build' }); - es.merge(combinedMetadataJson, nlsMessagesJs) + const nlsMessagesJs = vinyl_fs_1.default.src('out-build/nls.messages.js', { base: 'out-build' }); + event_stream_1.default.merge(combinedMetadataJson, nlsMessagesJs) .pipe((0, gulp_gzip_1.default)({ append: false })) - .pipe(vfs.dest('./nlsMetadata')) - .pipe(es.through(function (data) { + .pipe(vinyl_fs_1.default.dest('./nlsMetadata')) + .pipe(event_stream_1.default.through(function (data) { console.log(`Uploading ${data.path}`); // trigger artifact upload console.log(`##vso[artifact.upload containerfolder=nlsmetadata;artifactname=${data.basename}]${data.path}`); diff --git a/build/azure-pipelines/upload-nlsmetadata.ts b/build/azure-pipelines/upload-nlsmetadata.ts index 5c13f73a006a..7337156f5775 100644 --- a/build/azure-pipelines/upload-nlsmetadata.ts +++ b/build/azure-pipelines/upload-nlsmetadata.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; -import * as Vinyl from 'vinyl'; -import * as vfs from 'vinyl-fs'; +import es from 'event-stream'; +import Vinyl from 'vinyl'; +import vfs from 'vinyl-fs'; import merge from 'gulp-merge-json'; import gzip from 'gulp-gzip'; import { ClientAssertionCredential } from '@azure/identity'; diff --git a/build/azure-pipelines/upload-sourcemaps.js b/build/azure-pipelines/upload-sourcemaps.js index 68ee13dcf2d9..71ee20476638 100644 --- a/build/azure-pipelines/upload-sourcemaps.js +++ b/build/azure-pipelines/upload-sourcemaps.js @@ -45,7 +45,7 @@ const event_stream_1 = __importDefault(require("event-stream")); const vinyl_fs_1 = __importDefault(require("vinyl-fs")); const util = __importStar(require("../lib/util")); // @ts-ignore -const deps = __importStar(require("../lib/dependencies")); +const dependencies_1 = __importDefault(require("../lib/dependencies")); const identity_1 = require("@azure/identity"); const azure = require('gulp-azure-storage'); const root = path_1.default.dirname(path_1.default.dirname(__dirname)); @@ -66,8 +66,8 @@ function main() { if (!base) { const vs = src('out-vscode-min'); // client source-maps only sources.push(vs); - const productionDependencies = deps.getProductionDependencies(root); - const productionDependenciesSrc = productionDependencies.map(d => path_1.default.relative(root, d)).map(d => `./${d}/**/*.map`); + const productionDependencies = dependencies_1.default.getProductionDependencies(root); + const productionDependenciesSrc = productionDependencies.map((d) => path_1.default.relative(root, d)).map((d) => `./${d}/**/*.map`); const nodeModules = vinyl_fs_1.default.src(productionDependenciesSrc, { base: '.' }) .pipe(util.cleanNodeModules(path_1.default.join(root, 'build', '.moduleignore'))) .pipe(util.cleanNodeModules(path_1.default.join(root, 'build', `.moduleignore.${process.platform}`))); diff --git a/build/azure-pipelines/upload-sourcemaps.ts b/build/azure-pipelines/upload-sourcemaps.ts index b4a9f38e1297..ac453ed5310c 100644 --- a/build/azure-pipelines/upload-sourcemaps.ts +++ b/build/azure-pipelines/upload-sourcemaps.ts @@ -9,7 +9,7 @@ import Vinyl from 'vinyl'; import vfs from 'vinyl-fs'; import * as util from '../lib/util'; // @ts-ignore -import * as deps from '../lib/dependencies'; +import deps from '../lib/dependencies'; import { ClientAssertionCredential } from '@azure/identity'; const azure = require('gulp-azure-storage'); @@ -37,7 +37,7 @@ function main(): Promise { sources.push(vs); const productionDependencies = deps.getProductionDependencies(root); - const productionDependenciesSrc = productionDependencies.map(d => path.relative(root, d)).map(d => `./${d}/**/*.map`); + const productionDependenciesSrc = productionDependencies.map((d: string) => path.relative(root, d)).map((d: string) => `./${d}/**/*.map`); const nodeModules = vfs.src(productionDependenciesSrc, { base: '.' }) .pipe(util.cleanNodeModules(path.join(root, 'build', '.moduleignore'))) .pipe(util.cleanNodeModules(path.join(root, 'build', `.moduleignore.${process.platform}`))); diff --git a/build/darwin/verify-macho.js b/build/darwin/verify-macho.js index 2df99a351420..e7a4eb28d705 100644 --- a/build/darwin/verify-macho.js +++ b/build/darwin/verify-macho.js @@ -3,45 +3,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const assert_1 = __importDefault(require("assert")); -const path = __importStar(require("path")); +const path_1 = __importDefault(require("path")); const promises_1 = require("fs/promises"); const cross_spawn_promise_1 = require("@malept/cross-spawn-promise"); const MACHO_PREFIX = 'Mach-O '; @@ -131,7 +98,7 @@ async function checkMachOFiles(appPath, arch) { } if (info.isDirectory()) { for (const child of await (0, promises_1.readdir)(p)) { - await traverse(path.resolve(p, child)); + await traverse(path_1.default.resolve(p, child)); } } }; diff --git a/build/darwin/verify-macho.ts b/build/darwin/verify-macho.ts index 9e2f4b518f28..61ebc71aba24 100644 --- a/build/darwin/verify-macho.ts +++ b/build/darwin/verify-macho.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import * as path from 'path'; +import path from 'path'; import { open, stat, readdir, realpath } from 'fs/promises'; import { spawn, ExitCodeError } from '@malept/cross-spawn-promise'; diff --git a/build/lib/nls.js b/build/lib/nls.js index af648b40ed8a..12e60a36ec99 100644 --- a/build/lib/nls.js +++ b/build/lib/nls.js @@ -3,39 +3,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; @@ -44,8 +11,8 @@ exports.nls = nls; const lazy_js_1 = __importDefault(require("lazy.js")); const event_stream_1 = require("event-stream"); const vinyl_1 = __importDefault(require("vinyl")); -const sm = __importStar(require("source-map")); -const path = __importStar(require("path")); +const source_map_1 = __importDefault(require("source-map")); +const path_1 = __importDefault(require("path")); const gulp_sort_1 = __importDefault(require("gulp-sort")); var CollectStepResult; (function (CollectStepResult) { @@ -93,7 +60,7 @@ function nls(options) { } const root = f.sourceMap.sourceRoot; if (root) { - source = path.join(root, source); + source = path_1.default.join(root, source); } const typescript = f.sourceMap.sourcesContent[0]; if (!typescript) { @@ -328,7 +295,7 @@ var _nls; return model.toString(); } function patchSourcemap(patches, rsm, smc) { - const smg = new sm.SourceMapGenerator({ + const smg = new source_map_1.default.SourceMapGenerator({ file: rsm.file, sourceRoot: rsm.sourceRoot }); @@ -353,10 +320,10 @@ var _nls; generated.column += lengthDiff; patches.pop(); } - source = rsm.sourceRoot ? path.relative(rsm.sourceRoot, m.source) : m.source; + source = rsm.sourceRoot ? path_1.default.relative(rsm.sourceRoot, m.source) : m.source; source = source.replace(/\\/g, '/'); smg.addMapping({ source, name: m.name, original, generated }); - }, null, sm.SourceMapConsumer.GENERATED_ORDER); + }, null, source_map_1.default.SourceMapConsumer.GENERATED_ORDER); if (source) { smg.setSourceContent(source, smc.sourceContentFor(source)); } @@ -377,7 +344,7 @@ var _nls; } const nlsKeys = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.key)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.key))); const nlsMessages = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.value)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.value))); - const smc = new sm.SourceMapConsumer(sourcemap); + const smc = new source_map_1.default.SourceMapConsumer(sourcemap); const positionFrom = mappedPositionFrom.bind(null, sourcemap.sources[0]); // build patches const toPatch = (c) => { diff --git a/build/lib/nls.ts b/build/lib/nls.ts index 4194eb3c489b..ef2afc5d7c8a 100644 --- a/build/lib/nls.ts +++ b/build/lib/nls.ts @@ -7,8 +7,8 @@ import type * as ts from 'typescript'; import lazy from 'lazy.js'; import { duplex, through } from 'event-stream'; import File from 'vinyl'; -import * as sm from 'source-map'; -import * as path from 'path'; +import sm from 'source-map'; +import path from 'path'; import sort from 'gulp-sort'; declare class FileSourceMap extends File { diff --git a/build/lib/optimize.js b/build/lib/optimize.js index d45ff0d67d3c..d75a29786976 100644 --- a/build/lib/optimize.js +++ b/build/lib/optimize.js @@ -42,31 +42,31 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", { value: true }); exports.bundleTask = bundleTask; exports.minifyTask = minifyTask; -const es = __importStar(require("event-stream")); -const gulp = __importStar(require("gulp")); +const event_stream_1 = __importDefault(require("event-stream")); +const gulp_1 = __importDefault(require("gulp")); const gulp_filter_1 = __importDefault(require("gulp-filter")); -const path = __importStar(require("path")); -const fs = __importStar(require("fs")); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); const pump_1 = __importDefault(require("pump")); const vinyl_1 = __importDefault(require("vinyl")); const bundle = __importStar(require("./bundle")); const postcss_1 = require("./postcss"); -const esbuild = __importStar(require("esbuild")); -const sourcemaps = __importStar(require("gulp-sourcemaps")); +const esbuild_1 = __importDefault(require("esbuild")); +const gulp_sourcemaps_1 = __importDefault(require("gulp-sourcemaps")); const fancy_log_1 = __importDefault(require("fancy-log")); -const ansiColors = __importStar(require("ansi-colors")); -const REPO_ROOT_PATH = path.join(__dirname, '../..'); +const ansi_colors_1 = __importDefault(require("ansi-colors")); +const REPO_ROOT_PATH = path_1.default.join(__dirname, '../..'); const DEFAULT_FILE_HEADER = [ '/*!--------------------------------------------------------', ' * Copyright (C) Microsoft Corporation. All rights reserved.', ' *--------------------------------------------------------*/' ].join('\n'); function bundleESMTask(opts) { - const resourcesStream = es.through(); // this stream will contain the resources - const bundlesStream = es.through(); // this stream will contain the bundled files + const resourcesStream = event_stream_1.default.through(); // this stream will contain the resources + const bundlesStream = event_stream_1.default.through(); // this stream will contain the bundled files const entryPoints = opts.entryPoints.map(entryPoint => { if (typeof entryPoint === 'string') { - return { name: path.parse(entryPoint).name }; + return { name: path_1.default.parse(entryPoint).name }; } return entryPoint; }); @@ -80,7 +80,7 @@ function bundleESMTask(opts) { const files = []; const tasks = []; for (const entryPoint of entryPoints) { - (0, fancy_log_1.default)(`Bundled entry point: ${ansiColors.yellow(entryPoint.name)}...`); + (0, fancy_log_1.default)(`Bundled entry point: ${ansi_colors_1.default.yellow(entryPoint.name)}...`); // support for 'dest' via esbuild#in/out const dest = entryPoint.dest?.replace(/\.[^/.]+$/, '') ?? entryPoint.name; // banner contents @@ -90,14 +90,14 @@ function bundleESMTask(opts) { }; // TS Boilerplate if (!opts.skipTSBoilerplateRemoval?.(entryPoint.name)) { - const tslibPath = path.join(require.resolve('tslib'), '../tslib.es6.js'); - banner.js += await fs.promises.readFile(tslibPath, 'utf-8'); + const tslibPath = path_1.default.join(require.resolve('tslib'), '../tslib.es6.js'); + banner.js += await fs_1.default.promises.readFile(tslibPath, 'utf-8'); } const contentsMapper = { name: 'contents-mapper', setup(build) { build.onLoad({ filter: /\.js$/ }, async ({ path }) => { - const contents = await fs.promises.readFile(path, 'utf-8'); + const contents = await fs_1.default.promises.readFile(path, 'utf-8'); // TS Boilerplate let newContents; if (!opts.skipTSBoilerplateRemoval?.(entryPoint.name)) { @@ -121,11 +121,11 @@ function bundleESMTask(opts) { // We inline selected modules that are we depend on on startup without // a conditional `await import(...)` by hooking into the resolution. build.onResolve({ filter: /^minimist$/ }, () => { - return { path: path.join(REPO_ROOT_PATH, 'node_modules', 'minimist', 'index.js'), external: false }; + return { path: path_1.default.join(REPO_ROOT_PATH, 'node_modules', 'minimist', 'index.js'), external: false }; }); }, }; - const task = esbuild.build({ + const task = esbuild_1.default.build({ bundle: true, external: entryPoint.exclude, packages: 'external', // "external all the things", see https://esbuild.github.io/api/#packages @@ -144,11 +144,11 @@ function bundleESMTask(opts) { banner: entryPoint.name === 'vs/workbench/workbench.web.main' ? undefined : banner, // TODO@esm remove line when we stop supporting web-amd-esm-bridge entryPoints: [ { - in: path.join(REPO_ROOT_PATH, opts.src, `${entryPoint.name}.js`), + in: path_1.default.join(REPO_ROOT_PATH, opts.src, `${entryPoint.name}.js`), out: dest, } ], - outdir: path.join(REPO_ROOT_PATH, opts.src), + outdir: path_1.default.join(REPO_ROOT_PATH, opts.src), write: false, // enables res.outputFiles metafile: true, // enables res.metafile // minify: NOT enabled because we have a separate minify task that takes care of the TSLib banner as well @@ -162,7 +162,7 @@ function bundleESMTask(opts) { contents: Buffer.from(file.contents), sourceMap: sourceMapFile ? JSON.parse(sourceMapFile.text) : undefined, // support gulp-sourcemaps path: file.path, - base: path.join(REPO_ROOT_PATH, opts.src) + base: path_1.default.join(REPO_ROOT_PATH, opts.src) }; files.push(new vinyl_1.default(fileProps)); } @@ -174,13 +174,13 @@ function bundleESMTask(opts) { }; bundleAsync().then((output) => { // bundle output (JS, CSS, SVG...) - es.readArray(output.files).pipe(bundlesStream); + event_stream_1.default.readArray(output.files).pipe(bundlesStream); // forward all resources - gulp.src(opts.resources ?? [], { base: `${opts.src}`, allowEmpty: true }).pipe(resourcesStream); + gulp_1.default.src(opts.resources ?? [], { base: `${opts.src}`, allowEmpty: true }).pipe(resourcesStream); }); - const result = es.merge(bundlesStream, resourcesStream); + const result = event_stream_1.default.merge(bundlesStream, resourcesStream); return result - .pipe(sourcemaps.write('./', { + .pipe(gulp_sourcemaps_1.default.write('./', { sourceRoot: undefined, addComment: true, includeContent: true @@ -188,7 +188,7 @@ function bundleESMTask(opts) { } function bundleTask(opts) { return function () { - return bundleESMTask(opts.esm).pipe(gulp.dest(opts.out)); + return bundleESMTask(opts.esm).pipe(gulp_1.default.dest(opts.out)); }; } function minifyTask(src, sourceMapBaseUrl) { @@ -199,8 +199,8 @@ function minifyTask(src, sourceMapBaseUrl) { const jsFilter = (0, gulp_filter_1.default)('**/*.js', { restore: true }); const cssFilter = (0, gulp_filter_1.default)('**/*.css', { restore: true }); const svgFilter = (0, gulp_filter_1.default)('**/*.svg', { restore: true }); - (0, pump_1.default)(gulp.src([src + '/**', '!' + src + '/**/*.map']), jsFilter, sourcemaps.init({ loadMaps: true }), es.map((f, cb) => { - esbuild.build({ + (0, pump_1.default)(gulp_1.default.src([src + '/**', '!' + src + '/**/*.map']), jsFilter, gulp_sourcemaps_1.default.init({ loadMaps: true }), event_stream_1.default.map((f, cb) => { + esbuild_1.default.build({ entryPoints: [f.path], minify: true, sourcemap: 'external', @@ -223,12 +223,12 @@ function minifyTask(src, sourceMapBaseUrl) { cb(undefined, f); } }, cb); - }), jsFilter.restore, cssFilter, (0, postcss_1.gulpPostcss)([cssnano({ preset: 'default' })]), cssFilter.restore, svgFilter, svgmin(), svgFilter.restore, sourcemaps.write('./', { + }), jsFilter.restore, cssFilter, (0, postcss_1.gulpPostcss)([cssnano({ preset: 'default' })]), cssFilter.restore, svgFilter, svgmin(), svgFilter.restore, gulp_sourcemaps_1.default.write('./', { sourceMappingURL, sourceRoot: undefined, includeContent: true, addComment: true - }), gulp.dest(src + '-min'), (err) => cb(err)); + }), gulp_1.default.dest(src + '-min'), (err) => cb(err)); }; } //# sourceMappingURL=optimize.js.map \ No newline at end of file diff --git a/build/lib/optimize.ts b/build/lib/optimize.ts index 55566d4f2418..e12c85c3fa04 100644 --- a/build/lib/optimize.ts +++ b/build/lib/optimize.ts @@ -3,19 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as es from 'event-stream'; -import * as gulp from 'gulp'; +import es from 'event-stream'; +import gulp from 'gulp'; import filter from 'gulp-filter'; -import * as path from 'path'; -import * as fs from 'fs'; +import path from 'path'; +import fs from 'fs'; import pump from 'pump'; import VinylFile from 'vinyl'; import * as bundle from './bundle'; import { gulpPostcss } from './postcss'; -import * as esbuild from 'esbuild'; -import * as sourcemaps from 'gulp-sourcemaps'; +import esbuild from 'esbuild'; +import sourcemaps from 'gulp-sourcemaps'; import fancyLog from 'fancy-log'; -import * as ansiColors from 'ansi-colors'; +import ansiColors from 'ansi-colors'; const REPO_ROOT_PATH = path.join(__dirname, '../..'); From 6cd86f433db0b2f25fea828b627d5a7e2454e3e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 23 Jan 2025 10:23:39 +0100 Subject: [PATCH 0798/3587] fix flakiness in extension smoke tests (#238537) --- test/automation/src/extensions.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/test/automation/src/extensions.ts b/test/automation/src/extensions.ts index c881e4fd8dc6..3713faa6700b 100644 --- a/test/automation/src/extensions.ts +++ b/test/automation/src/extensions.ts @@ -52,8 +52,22 @@ export class Extensions extends Viewlet { async installExtension(id: string, waitUntilEnabled: boolean): Promise { await this.searchForExtension(id); - await this.code.waitAndClick(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[data-extension-id="${id}"] .extension-list-item .monaco-action-bar .action-item:not(.disabled) .extension-action.install`); - await this.code.waitForElement(`.extension-editor .monaco-action-bar .action-item:not(.disabled) .extension-action.uninstall`); + + // try to install extension 3 times + let attempt = 1; + while (true) { + await this.code.waitAndClick(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[data-extension-id="${id}"] .extension-list-item .monaco-action-bar .action-item:not(.disabled) .extension-action.install`); + + try { + await this.code.waitForElement(`.extension-editor .monaco-action-bar .action-item:not(.disabled) .extension-action.uninstall`); + break; + } catch (err) { + if (attempt++ === 3) { + throw err; + } + } + } + if (waitUntilEnabled) { await this.code.waitForElement(`.extension-editor .monaco-action-bar .action-item:not(.disabled) a[aria-label="Disable this extension"]`); } From 6459fc1f7227be9ba199ebd9e738632a50736eee Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 23 Jan 2025 10:47:35 +0100 Subject: [PATCH 0799/3587] do not make request for extensions with invalid ids (#238535) --- .../extensionManagement/common/extensionGalleryService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 1262c42d93a8..b8fee5a8c4ce 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -756,7 +756,6 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } if (!EXTENSION_IDENTIFIER_REGEX.test(extensionInfo.id)) { - toQuery.push(extensionInfo); return; } From 18d42bb060b9d375f70142552e417d9cf6bf91b8 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 23 Jan 2025 10:48:39 +0100 Subject: [PATCH 0800/3587] Use token store to know what needs to be erefreshed (#238539) --- src/vs/editor/common/model/tokenStore.ts | 16 ++ .../model/treeSitterTokenStoreService.ts | 15 +- .../browser/treeSitterTokenizationFeature.ts | 158 ++++++++++-------- .../treeSitterTokenizationFeature.test.ts | 7 +- 4 files changed, 128 insertions(+), 68 deletions(-) diff --git a/src/vs/editor/common/model/tokenStore.ts b/src/vs/editor/common/model/tokenStore.ts index b357069c5b4c..9ad6e415308c 100644 --- a/src/vs/editor/common/model/tokenStore.ts +++ b/src/vs/editor/common/model/tokenStore.ts @@ -387,6 +387,22 @@ export class TokenStore implements IDisposable { return needsRefresh; } + getNeedsRefresh(): { startOffset: number; endOffset: number }[] { + const result: { startOffset: number; endOffset: number }[] = []; + + this.traverseInOrderInRange(0, this._textModel.getValueLength(), (node, offset) => { + if (isLeaf(node) && node.needsRefresh) { + if ((result.length > 0) && (result[result.length - 1].endOffset === offset)) { + result[result.length - 1].endOffset += node.length; + } else { + result.push({ startOffset: offset, endOffset: offset + node.length }); + } + } + return false; + }); + return result; + } + public deepCopy(): TokenStore { const newStore = new TokenStore(this._textModel); newStore._root = this._copyNodeIterative(this._root); diff --git a/src/vs/editor/common/model/treeSitterTokenStoreService.ts b/src/vs/editor/common/model/treeSitterTokenStoreService.ts index 17d194b5bd5c..56302824adae 100644 --- a/src/vs/editor/common/model/treeSitterTokenStoreService.ts +++ b/src/vs/editor/common/model/treeSitterTokenStoreService.ts @@ -16,6 +16,7 @@ export interface ITreeSitterTokenizationStoreService { getTokens(model: ITextModel, line: number): Uint32Array | undefined; updateTokens(model: ITextModel, version: number, updates: { oldRangeLength: number; newTokens: TokenUpdate[] }[]): void; markForRefresh(model: ITextModel, range: Range): void; + getNeedsRefresh(model: ITextModel): { range: Range; startOffset: number; endOffset: number }[]; hasTokens(model: ITextModel, accurateForRange?: Range): boolean; } @@ -36,7 +37,7 @@ class TreeSitterTokenizationStoreService implements ITreeSitterTokenizationStore setTokens(model: ITextModel, tokens: TokenUpdate[]): void { const disposables = new DisposableStore(); const store = disposables.add(new TokenStore(model)); - this.tokens.set(model, { store: store, accurateVersion: model.getVersionId(), disposables, guessVersion: 0 }); + this.tokens.set(model, { store: store, accurateVersion: model.getVersionId(), disposables, guessVersion: model.getVersionId() }); store.buildStore(tokens); disposables.add(model.onDidChangeContent(e => { @@ -119,6 +120,18 @@ class TreeSitterTokenizationStoreService implements ITreeSitterTokenizationStore tree.markForRefresh(model.getOffsetAt(range.getStartPosition()), model.getOffsetAt(range.getEndPosition())); } + getNeedsRefresh(model: ITextModel): { range: Range; startOffset: number; endOffset: number }[] { + const needsRefreshOffsetRanges = this.tokens.get(model)?.store.getNeedsRefresh(); + if (!needsRefreshOffsetRanges) { + return []; + } + return needsRefreshOffsetRanges.map(range => ({ + range: Range.fromPositions(model.getPositionAt(range.startOffset), model.getPositionAt(range.endOffset)), + startOffset: range.startOffset, + endOffset: range.endOffset + })); + } + dispose(): void { for (const [, value] of this.tokens) { value.disposables.dispose(); diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts index ffac377bd1b0..88c69a7028cb 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts @@ -24,7 +24,6 @@ import { LanguageId } from '../../../../editor/common/encodedTokenAttributes.js' import { TokenUpdate } from '../../../../editor/common/model/tokenStore.js'; import { Range } from '../../../../editor/common/core/range.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; -import { Position } from '../../../../editor/common/core/position.js'; import { setTimeout0 } from '../../../../base/common/platform.js'; const ALLOWED_SUPPORT = ['typescript']; @@ -116,12 +115,15 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi super(); this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => this.reset())); this._register(this._treeSitterService.onDidUpdateTree((e) => { + if (e.versionId !== e.textModel.getVersionId()) { + return; + } const maxLine = e.textModel.getLineCount(); const ranges = e.ranges.map(range => ({ fromLineNumber: range.newRange.startLineNumber, toLineNumber: range.newRange.endLineNumber < maxLine ? range.newRange.endLineNumber : maxLine })); // First time we see a tree we need to build a token store. if (!this._tokenizationStoreService.hasTokens(e.textModel)) { - this._firstTreeUpdate(e.textModel, ranges); + this._firstTreeUpdate(e.textModel, e.versionId, ranges); } else { // Mark the range for refresh immediately for (const range of e.ranges) { @@ -143,7 +145,10 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi return this._rangeTokensAsUpdates(0, emptyTokens); } - private _firstTreeUpdate(textModel: ITextModel, ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[]) { + private _firstTreeUpdate(textModel: ITextModel, versionId: number, ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[]) { + if (versionId !== textModel.getVersionId()) { + return; + } const modelEndOffset = textModel.getValueLength(); const editorEndPosition = textModel.getPositionAt(modelEndOffset); const captures = this._getTreeAndCaptures(new Range(1, 1, editorEndPosition.lineNumber, editorEndPosition.column), textModel); @@ -151,43 +156,10 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi const tokens: TokenUpdate[] = this._createEmptyTokens(captures, modelEndOffset) ?? []; this._tokenizationStoreService.setTokens(textModel, tokens); - this._setViewPortTokens(textModel); - this._tokenizeEntireDocument(textModel, ranges); - + this._setViewPortTokens(textModel, versionId); } - private _tokenizeEntireDocument(textModel: ITextModel, ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[] = []) { - const modelEndOffset = textModel.getValueLength(); - - // Go in 10000 offset chunks to avoid long operations - const chunkSize = 10000; - let chunkEnd = modelEndOffset > chunkSize ? chunkSize : modelEndOffset; - let chunkStart = 0; - let chunkStartingPosition = new Position(1, 1); - const rangeChanges: RangeChange[] = []; - do { - const chunkEndPosition = textModel.getPositionAt(chunkEnd); - const chunkRange = Range.fromPositions(chunkStartingPosition, chunkEndPosition); - - rangeChanges.push({ - newRange: chunkRange, - newRangeStartOffset: chunkStart, - newRangeEndOffset: chunkEnd, - oldRangeLength: chunkEnd - chunkStart - }); - - chunkStart = chunkEnd; - if (chunkEnd < modelEndOffset && chunkEnd + chunkSize > modelEndOffset) { - chunkEnd = modelEndOffset; - } else { - chunkEnd = chunkEnd + chunkSize; - } - chunkStartingPosition = chunkEndPosition; - } while (chunkEnd <= modelEndOffset); - this._handleTreeUpdate({ ranges: rangeChanges, textModel, versionId: textModel.getVersionId() }, ranges); - } - - private _setViewPortTokens(textModel: ITextModel) { + private _setViewPortTokens(textModel: ITextModel, versionId: number) { const maxLine = textModel.getLineCount(); const editor = this._codeEditorService.listCodeEditors().find(editor => editor.getModel() === textModel); if (!editor) { @@ -210,54 +182,87 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi oldRangeLength: newRangeEndOffset - newRangeStartOffset }; } - this._handleTreeUpdate({ ranges: rangeChanges, textModel, versionId: textModel.getVersionId() }, ranges); + this._handleTreeUpdate({ ranges: rangeChanges, textModel, versionId }, ranges, 'viewport'); } /** * Do not await in this method, it will cause a race */ - private _handleTreeUpdate(e: TreeUpdateEvent, ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[]) { - // Get the captures immediately too while the text model is correct - const captures = e.ranges.map(range => this._getTreeAndCaptures(range.newRange, e.textModel)); + private _handleTreeUpdate(e: TreeUpdateEvent, ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[], note?: string) { + let rangeChanges: RangeChange[] = []; + const chunkSize = 10000; + + for (let i = 0; i < e.ranges.length; i++) { + const rangeLength = e.ranges[i].newRangeEndOffset - e.ranges[i].newRangeStartOffset; + if (e.ranges[i].oldRangeLength === rangeLength) { + if (rangeLength > chunkSize) { + // Split the range into chunks to avoid long operations + const fullRangeEndOffset = e.ranges[i].newRangeEndOffset; + let chunkStart = e.ranges[i].newRangeStartOffset; + let chunkEnd = chunkStart + chunkSize; + let chunkStartingPosition = e.ranges[i].newRange.getStartPosition(); + do { + const chunkEndPosition = e.textModel.getPositionAt(chunkEnd); + const chunkRange = Range.fromPositions(chunkStartingPosition, chunkEndPosition); + + rangeChanges.push({ + newRange: chunkRange, + newRangeStartOffset: chunkStart, + newRangeEndOffset: chunkEnd, + oldRangeLength: chunkEnd - chunkStart + }); + + chunkStart = chunkEnd; + if (chunkEnd < fullRangeEndOffset && chunkEnd + chunkSize > fullRangeEndOffset) { + chunkEnd = fullRangeEndOffset; + } else { + chunkEnd = chunkEnd + chunkSize; + } + chunkStartingPosition = chunkEndPosition; + } while (chunkEnd <= fullRangeEndOffset); + } else { + rangeChanges.push(e.ranges[i]); + } + } else { + rangeChanges = e.ranges; + break; + } + } + + // Get the captures immediately while the text model is correct + const captures = rangeChanges.map(range => this._getTreeAndCaptures(range.newRange, e.textModel)); // Don't block - this._updateTreeForRanges(e, ranges, captures); - } + this._updateTreeForRanges(e.textModel, rangeChanges, e.versionId, ranges, captures, note).then(() => { + const tree = this._getTree(e.textModel); + if (!e.textModel.isDisposed() && (tree?.versionId === e.textModel.getVersionId())) { + this._refreshNeedsRefresh(e.textModel); + } - private _clipRangeChangeToFileLength(range: RangeChange, textModel: ITextModel) { - const valueLength = textModel.getValueLength(); - const startPosition = range.newRange.getStartPosition(); - const endPosition = range.newRangeEndOffset > valueLength ? textModel.getPositionAt(valueLength) : range.newRange.getEndPosition(); - const endOffset = textModel.getOffsetAt(endPosition); - const newRangeChange = { - oldRangeLength: endOffset - range.newRangeStartOffset, - newRangeEndOffset: endOffset, - newRangeStartOffset: textModel.getOffsetAt(startPosition), - newRange: Range.fromPositions(startPosition, endPosition) - }; - return newRangeChange; + }); } - private async _updateTreeForRanges(e: TreeUpdateEvent, ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[], captures: { tree: ITreeSitterParseResult | undefined; captures: QueryCapture[] }[]) { + private async _updateTreeForRanges(textModel: ITextModel, rangeChanges: RangeChange[], versionId: number, ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[], captures: { tree: ITreeSitterParseResult | undefined; captures: QueryCapture[] }[], note?: string) { let tokenUpdate: { oldRangeLength: number; newTokens: TokenUpdate[] } | undefined; - for (let i = 0; i < e.ranges.length; i++) { - let capture = captures[i]; - let range = e.ranges[i]; - if (e.versionId < e.textModel.getVersionId()) { + + for (let i = 0; i < rangeChanges.length; i++) { + if (versionId < textModel.getVersionId()) { // Our captures have become invalid and we need to re-capture - range = this._clipRangeChangeToFileLength(range, e.textModel); - capture = this._getTreeAndCaptures(range.newRange, e.textModel); + break; } - const updates = this.getTokensInRange(e.textModel, range.newRange, range.newRangeStartOffset, range.newRangeEndOffset, capture); + const capture = captures[i]; + const range = rangeChanges[i]; + + const updates = this.getTokensInRange(textModel, range.newRange, range.newRangeStartOffset, range.newRangeEndOffset, capture); if (updates) { tokenUpdate = { oldRangeLength: range.oldRangeLength, newTokens: updates }; } else { tokenUpdate = { oldRangeLength: range.oldRangeLength, newTokens: [] }; } - this._tokenizationStoreService.updateTokens(e.textModel, e.versionId, [tokenUpdate]); + this._tokenizationStoreService.updateTokens(textModel, versionId, [tokenUpdate]); await new Promise(resolve => setTimeout0(resolve)); } this._onDidChangeTokens.fire({ - textModel: e.textModel, + textModel: textModel, changes: { semanticTokensApplied: false, ranges @@ -265,6 +270,27 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi }); } + private _refreshNeedsRefresh(textModel: ITextModel) { + const rangesToRefresh = this._tokenizationStoreService.getNeedsRefresh(textModel); + if (rangesToRefresh.length === 0) { + return; + } + const rangeChanges: RangeChange[] = new Array(rangesToRefresh.length); + const changedRanges: { readonly fromLineNumber: number; readonly toLineNumber: number }[] = new Array(rangesToRefresh.length); + + for (let i = 0; i < rangesToRefresh.length; i++) { + const range = rangesToRefresh[i]; + rangeChanges[i] = { + newRange: range.range, + newRangeStartOffset: range.startOffset, + newRangeEndOffset: range.endOffset, + oldRangeLength: range.endOffset - range.startOffset + }; + changedRanges[i] = { fromLineNumber: range.range.startLineNumber, toLineNumber: range.range.endLineNumber }; + } + this._handleTreeUpdate({ ranges: rangeChanges, textModel, versionId: textModel.getVersionId() }, changedRanges); + } + private _rangeTokensAsUpdates(rangeOffset: number, endOffsetToken: EndOffsetToken[]) { const updates: TokenUpdate[] = []; let lastEnd = 0; diff --git a/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts b/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts index 65c196380a05..74b7a9e926bb 100644 --- a/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts +++ b/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts @@ -68,6 +68,10 @@ class MockTelemetryService implements ITelemetryService { } class MockTokenStoreService implements ITreeSitterTokenizationStoreService { + getNeedsRefresh(model: ITextModel): { range: Range; startOffset: number; endOffset: number }[] { + throw new Error('Method not implemented.'); + } + _serviceBrand: undefined; setTokens(model: ITextModel, tokens: TokenUpdate[]): void { } @@ -162,8 +166,9 @@ suite('Tree Sitter TokenizationFeature', function () { return tokens[tokens.length - 1].startOffsetInclusive + tokens[tokens.length - 1].length; } + let nameNumber = 1; async function getModelAndPrepTree(content: string) { - const model = disposables.add(modelService.createModel(content, { languageId: 'typescript', onDidChange: Event.None }, URI.file('file.ts'))); + const model = disposables.add(modelService.createModel(content, { languageId: 'typescript', onDidChange: Event.None }, URI.file(`file${nameNumber++}.ts`))); const tree = disposables.add(await treeSitterParserService.getTextModelTreeSitter(model)); const treeParseResult = new Promise(resolve => { const disposable = treeSitterParserService.onDidUpdateTree(e => { From cc14c75c962284dfd2e2493dd487e3b81fb15d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 23 Jan 2025 11:34:30 +0100 Subject: [PATCH 0801/3587] fix build again (#238545) --- build/azure-pipelines/upload-sourcemaps.js | 4 ++-- build/azure-pipelines/upload-sourcemaps.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/azure-pipelines/upload-sourcemaps.js b/build/azure-pipelines/upload-sourcemaps.js index 71ee20476638..3a4e5044c0f4 100644 --- a/build/azure-pipelines/upload-sourcemaps.js +++ b/build/azure-pipelines/upload-sourcemaps.js @@ -45,7 +45,7 @@ const event_stream_1 = __importDefault(require("event-stream")); const vinyl_fs_1 = __importDefault(require("vinyl-fs")); const util = __importStar(require("../lib/util")); // @ts-ignore -const dependencies_1 = __importDefault(require("../lib/dependencies")); +const dependencies_1 = require("../lib/dependencies"); const identity_1 = require("@azure/identity"); const azure = require('gulp-azure-storage'); const root = path_1.default.dirname(path_1.default.dirname(__dirname)); @@ -66,7 +66,7 @@ function main() { if (!base) { const vs = src('out-vscode-min'); // client source-maps only sources.push(vs); - const productionDependencies = dependencies_1.default.getProductionDependencies(root); + const productionDependencies = (0, dependencies_1.getProductionDependencies)(root); const productionDependenciesSrc = productionDependencies.map((d) => path_1.default.relative(root, d)).map((d) => `./${d}/**/*.map`); const nodeModules = vinyl_fs_1.default.src(productionDependenciesSrc, { base: '.' }) .pipe(util.cleanNodeModules(path_1.default.join(root, 'build', '.moduleignore'))) diff --git a/build/azure-pipelines/upload-sourcemaps.ts b/build/azure-pipelines/upload-sourcemaps.ts index ac453ed5310c..527977e111d8 100644 --- a/build/azure-pipelines/upload-sourcemaps.ts +++ b/build/azure-pipelines/upload-sourcemaps.ts @@ -9,7 +9,7 @@ import Vinyl from 'vinyl'; import vfs from 'vinyl-fs'; import * as util from '../lib/util'; // @ts-ignore -import deps from '../lib/dependencies'; +import { getProductionDependencies } from '../lib/dependencies'; import { ClientAssertionCredential } from '@azure/identity'; const azure = require('gulp-azure-storage'); @@ -36,7 +36,7 @@ function main(): Promise { const vs = src('out-vscode-min'); // client source-maps only sources.push(vs); - const productionDependencies = deps.getProductionDependencies(root); + const productionDependencies = getProductionDependencies(root); const productionDependenciesSrc = productionDependencies.map((d: string) => path.relative(root, d)).map((d: string) => `./${d}/**/*.map`); const nodeModules = vfs.src(productionDependenciesSrc, { base: '.' }) .pipe(util.cleanNodeModules(path.join(root, 'build', '.moduleignore'))) From 05900b320e8902a711cc21e20c46d15063f0c63b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 23 Jan 2025 11:39:20 +0100 Subject: [PATCH 0802/3587] notifications - adjust positioning (#238546) When the center opens, it should show notifications exactly where the toast was --- .../parts/notifications/media/notificationsCenter.css | 6 +++--- .../parts/notifications/media/notificationsToasts.css | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css b/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css index a9e4fd0ebb0e..2025ec3ea7da 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css @@ -6,15 +6,15 @@ .monaco-workbench > .notifications-center { position: absolute; z-index: 1000; - right: 8px; - bottom: 31px; + right: 11px; /* attempt to position at same location as a toast */ + bottom: 33px; /* 22px status bar height + 11px (attempt to position at same location as a toast) */ display: none; overflow: hidden; border-radius: 4px; } .monaco-workbench.nostatusbar > .notifications-center { - bottom: 8px; + bottom: 11px; /* attempt to position at same location as a toast */ } .monaco-workbench > .notifications-center.visible { diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css b/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css index d4ef2314bc50..f12d6b925dfa 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css @@ -7,7 +7,7 @@ position: absolute; z-index: 1000; right: 3px; - bottom: 26px; + bottom: 25px; /* 22px status bar height + 3px */ display: none; overflow: hidden; } From dcd4531153afb132184fcc06d1a5b840be78f84f Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 23 Jan 2025 11:42:20 +0100 Subject: [PATCH 0803/3587] Show dialog when installing an extension from a publisher for the first time (#238540) * #215527 Notify users when installing an extension from a publisher for the first time * feedback * polish * trust publishers post installing * add verify publisher link * tweak wording * tweak wording * add quotes * add telelemtry --- src/vs/base/common/product.ts | 1 + .../abstractExtensionManagementService.ts | 14 ++ .../common/allowedExtensionsService.ts | 28 +++- .../common/extensionManagement.ts | 4 + .../contrib/chat/browser/chatAgentHover.ts | 2 +- .../browser/extensions.contribution.ts | 49 ++++++- .../extensions/browser/extensionsIcons.ts | 1 - .../extensions/browser/extensionsList.ts | 6 +- .../extensions/browser/extensionsWidgets.ts | 4 +- .../common/extensionManagementService.ts | 122 +++++++++++++++--- .../common/extensionsIcons.ts | 12 ++ .../common/media/extensionManagement.css | 12 ++ .../extensionManagementService.ts | 4 +- 13 files changed, 228 insertions(+), 31 deletions(-) create mode 100644 src/vs/workbench/services/extensionManagement/common/extensionsIcons.ts create mode 100644 src/vs/workbench/services/extensionManagement/common/media/extensionManagement.css diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 4567a7c8af64..ebd9e4381b25 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -106,6 +106,7 @@ export interface IProductConfiguration { }; readonly extensionPublisherOrgs?: readonly string[]; + readonly trustedExtensionPublishers?: readonly string[]; readonly extensionRecommendations?: IStringDictionary; readonly configBasedExtensionTips?: IStringDictionary; diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 4bef58907b79..dde862d66563 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -196,6 +196,20 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio results.push(...await this.installExtensions(installableExtensions)); } + const untrustedPublishers = new Set(); + for (const result of results) { + if (result.local && result.source && !URI.isUri(result.source) && !this.allowedExtensionsService.isTrusted(result.source)) { + untrustedPublishers.add(result.source.publisher); + } + } + if (untrustedPublishers.size) { + try { + await this.allowedExtensionsService.trustPublishers(...untrustedPublishers); + } catch (error) { + this.logService.error('Error while trusting publishers', getErrorMessage(error), ...untrustedPublishers); + } + } + return results; } diff --git a/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts b/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts index 0d00bc2bfeb3..b47dbd467230 100644 --- a/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts +++ b/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts @@ -6,7 +6,7 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { URI } from '../../../base/common/uri.js'; import * as nls from '../../../nls.js'; -import { IGalleryExtension, AllowedExtensionsConfigKey, IAllowedExtensionsService } from './extensionManagement.js'; +import { IGalleryExtension, AllowedExtensionsConfigKey, IAllowedExtensionsService, TrustedPublishersConfigKey } from './extensionManagement.js'; import { ExtensionType, IExtension, TargetPlatform } from '../../extensions/common/extensions.js'; import { IProductService } from '../../product/common/productService.js'; import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js'; @@ -34,6 +34,7 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte private allowedExtensions: AllowedExtensionsConfigValueType | undefined; private readonly publisherOrgs: string[]; + private readonly trustedPublishers: readonly string[]; private _onDidChangeAllowedExtensions = this._register(new Emitter()); readonly onDidChangeAllowedExtensions = this._onDidChangeAllowedExtensions.event; @@ -45,6 +46,7 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte super(); this.publisherOrgs = productService.extensionPublisherOrgs?.map(p => p.toLowerCase()) ?? []; this.allowedExtensions = this.getAllowedExtensionsValue(); + this.trustedPublishers = productService.trustedExtensionPublishers ?? []; this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AllowedExtensionsConfigKey)) { this.allowedExtensions = this.getAllowedExtensionsValue(); @@ -142,4 +144,28 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte return extensionReason; } + + isTrusted(extension: IGalleryExtension): boolean { + const publisher = extension.publisher.toLowerCase(); + if (this.trustedPublishers.includes(publisher) || this.trustedPublishers.includes(extension.publisherDisplayName.toLowerCase())) { + return true; + } + + // Check if the extension is allowed by publisher or extension id + if (this.allowedExtensions && (this.allowedExtensions[publisher] || this.allowedExtensions[extension.identifier.id.toLowerCase()])) { + return true; + } + + const trustedPublishers = (this.configurationService.getValue(TrustedPublishersConfigKey) ?? []).map(p => p.toLowerCase()); + return trustedPublishers.includes(publisher); + } + + async trustPublishers(...publishers: string[]): Promise { + const trustedPublishers = (this.configurationService.getValue(TrustedPublishersConfigKey) ?? []).map(p => p.toLowerCase()); + publishers = publishers.map(p => p.toLowerCase()).filter(p => !trustedPublishers.includes(p)); + if (publishers.length) { + trustedPublishers.push(...publishers); + await this.configurationService.updateValue(TrustedPublishersConfigKey, trustedPublishers); + } + } } diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index dd719f089e99..1381ea594c37 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -636,6 +636,9 @@ export interface IAllowedExtensionsService { isAllowed(extension: IGalleryExtension | IExtension): true | IMarkdownString; isAllowed(extension: { id: string; publisherDisplayName: string | undefined; version?: string; prerelease?: boolean; targetPlatform?: TargetPlatform }): true | IMarkdownString; + + isTrusted(extension: IGalleryExtension): boolean; + trustPublishers(...publishers: string[]): Promise; } export async function computeSize(location: URI, fileService: IFileService): Promise { @@ -659,3 +662,4 @@ export const ExtensionsLocalizedLabel = localize2('extensions', "Extensions"); export const PreferencesLocalizedLabel = localize2('preferences', 'Preferences'); export const UseUnpkgResourceApiConfigKey = 'extensions.gallery.useUnpkgResourceApi'; export const AllowedExtensionsConfigKey = 'extensions.allowed'; +export const TrustedPublishersConfigKey = 'extensions.trustedPublishers'; diff --git a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts index 0f083a319ab6..53485a258978 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts @@ -17,8 +17,8 @@ import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { getFullyQualifiedId, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../common/chatAgents.js'; import { showExtensionsWithIdsCommandId } from '../../extensions/browser/extensionsActions.js'; -import { verifiedPublisherIcon } from '../../extensions/browser/extensionsIcons.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; +import { verifiedPublisherIcon } from '../../../services/extensionManagement/common/extensionsIcons.js'; export class ChatAgentHover extends Disposable { public readonly domNode: HTMLElement; diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 4a55c93c25c8..c420bb92b681 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -8,7 +8,7 @@ import { KeyMod, KeyCode } from '../../../../base/common/keyCodes.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuRegistry, MenuId, registerAction2, Action2, IMenuItem, IAction2Options } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApiConfigKey, AllowedExtensionsConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApiConfigKey, AllowedExtensionsConfigKey, TrustedPublishersConfigKey, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; @@ -58,7 +58,7 @@ import { Schemas } from '../../../../base/common/network.js'; import { ShowRuntimeExtensionsAction } from './abstractRuntimeExtensionsEditor.js'; import { ExtensionEnablementWorkspaceTrustTransitionParticipant } from './extensionEnablementWorkspaceTrustTransitionParticipant.js'; import { clearSearchResultsIcon, configureRecommendedIcon, extensionsViewIcon, filterIcon, installWorkspaceRecommendedIcon, refreshIcon } from './extensionsIcons.js'; -import { EXTENSION_CATEGORIES } from '../../../../platform/extensions/common/extensions.js'; +import { EXTENSION_CATEGORIES, ExtensionType } from '../../../../platform/extensions/common/extensions.js'; import { Disposable, DisposableStore, IDisposable, isDisposable } from '../../../../base/common/lifecycle.js'; import { IDialogService, IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { mnemonicButtonLabel } from '../../../../base/common/labels.js'; @@ -71,7 +71,7 @@ import { Event } from '../../../../base/common/event.js'; import { UnsupportedExtensionsMigrationContrib } from './unsupportedExtensionsMigrationContribution.js'; import { isLinux, isNative, isWeb } from '../../../../base/common/platform.js'; import { ExtensionStorageService } from '../../../../platform/extensionManagement/common/extensionStorage.js'; -import { IStorageService } from '../../../../platform/storage/common/storage.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; import { CONTEXT_KEYBINDINGS_EDITOR } from '../../preferences/common/preferences.js'; import { ProgressLocation } from '../../../../platform/progress/common/progress.js'; @@ -271,6 +271,16 @@ Registry.as(ConfigurationExtensions.Configuration) scope: ConfigurationScope.APPLICATION, tags: ['onExp', 'usesOnlineServices'] }, + [TrustedPublishersConfigKey]: { + type: 'array', + markdownDescription: localize('extensions.trustedPublishers', "Specify a list of trusted publishers. Extensions from these publishers will be allowed to install without consent from the user."), + scope: ConfigurationScope.APPLICATION, + items: { + type: 'string', + pattern: '^[a-z0-9A-Z][a-z0-9-A-Z]*$', + patternErrorMessage: localize('extensions.trustedPublishers.patternError', "Publisher names must only contain letters and numbers."), + } + }, [AllowedExtensionsConfigKey]: { // Note: Type is set only to object because to support policies generation during build time, where single type is expected. type: 'object', @@ -1919,6 +1929,38 @@ class ExtensionStorageCleaner implements IWorkbenchContribution { } } +class TrustedPublishersInitializer implements IWorkbenchContribution { + constructor( + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, + @IProductService productService: IProductService, + @IStorageService storageService: IStorageService, + ) { + const trustedPublishersInitStatusKey = 'trusted-publishers-initialized'; + if (!storageService.get(trustedPublishersInitStatusKey, StorageScope.APPLICATION)) { + extensionManagementService.getInstalled(ExtensionType.User) + .then(async extensions => { + const trustedPublishers = new Set(); + for (const extension of extensions) { + if (!extension.publisherId) { + continue; + } + const publisher = extension.manifest.publisher.toLowerCase(); + if (productService.trustedExtensionPublishers?.includes(publisher) + || (extension.publisherDisplayName && productService.trustedExtensionPublishers?.includes(extension.publisherDisplayName.toLowerCase()))) { + continue; + } + trustedPublishers.add(publisher); + } + if (trustedPublishers.size) { + await allowedExtensionsService.trustPublishers(...trustedPublishers); + } + storageService.store(trustedPublishersInitStatusKey, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE); + }); + } + } +} + const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(StatusUpdater, LifecyclePhase.Eventually); @@ -1930,6 +1972,7 @@ workbenchRegistry.registerWorkbenchContribution(ExtensionDependencyChecker, Life workbenchRegistry.registerWorkbenchContribution(ExtensionEnablementWorkspaceTrustTransitionParticipant, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(ExtensionsCompletionItemsProvider, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(UnsupportedExtensionsMigrationContrib, LifecyclePhase.Eventually); +workbenchRegistry.registerWorkbenchContribution(TrustedPublishersInitializer, LifecyclePhase.Eventually); if (isWeb) { workbenchRegistry.registerWorkbenchContribution(ExtensionStorageCleaner, LifecyclePhase.Eventually); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts b/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts index 868e006ddabf..f7871ef4e2b7 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts @@ -24,7 +24,6 @@ export const syncIgnoredIcon = registerIcon('extensions-sync-ignored', Codicon.s export const remoteIcon = registerIcon('extensions-remote', Codicon.remote, localize('remoteIcon', 'Icon to indicate that an extension is remote in the extensions view and editor.')); export const installCountIcon = registerIcon('extensions-install-count', Codicon.cloudDownload, localize('installCountIcon', 'Icon shown along with the install count in the extensions view and editor.')); export const ratingIcon = registerIcon('extensions-rating', Codicon.star, localize('ratingIcon', 'Icon shown along with the rating in the extensions view and editor.')); -export const verifiedPublisherIcon = registerIcon('extensions-verified-publisher', Codicon.verifiedFilled, localize('verifiedPublisher', 'Icon used for the verified extension publisher in the extensions view and editor.')); export const preReleaseIcon = registerIcon('extensions-pre-release', Codicon.versions, localize('preReleaseIcon', 'Icon shown for extensions having pre-release versions in extensions view and editor.')); export const sponsorIcon = registerIcon('extensions-sponsor', Codicon.heartFilled, localize('sponsorIcon', 'Icon used for sponsoring extensions in the extensions view and editor.')); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 7293bfef2b0a..8e2e5ab9691c 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -15,7 +15,7 @@ import { Event } from '../../../../base/common/event.js'; import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService, IExtensionsViewState } from '../common/extensions.js'; import { ManageExtensionAction, ExtensionRuntimeStateAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ButtonWithDropDownExtensionAction, InstallDropdownAction, InstallingLabelAction, ButtonWithDropdownExtensionActionViewItem, DropDownExtensionAction, WebInstallAction, MigrateDeprecatedExtensionAction, SetLanguageAction, ClearLanguageAction, UpdateAction } from './extensionsActions.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; -import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionRuntimeStatusWidget, PreReleaseBookmarkWidget, extensionVerifiedPublisherIconColor, VerifiedPublisherWidget } from './extensionsWidgets.js'; +import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionRuntimeStatusWidget, PreReleaseBookmarkWidget, VerifiedPublisherWidget } from './extensionsWidgets.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; @@ -24,8 +24,8 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import { WORKBENCH_BACKGROUND } from '../../../common/theme.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; -import { verifiedPublisherIcon as verifiedPublisherThemeIcon } from './extensionsIcons.js'; import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { extensionVerifiedPublisherIconColor, verifiedPublisherIcon } from '../../../services/extensionManagement/common/extensionsIcons.js'; const EXTENSION_LIST_ELEMENT_HEIGHT = 72; @@ -250,6 +250,6 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = const verifiedPublisherIconColor = theme.getColor(extensionVerifiedPublisherIconColor); if (verifiedPublisherIconColor) { const disabledVerifiedPublisherIconColor = verifiedPublisherIconColor.transparent(.5).makeOpaque(WORKBENCH_BACKGROUND(theme)); - collector.addRule(`.extensions-list .monaco-list .monaco-list-row.disabled:not(.selected) .author .verified-publisher ${ThemeIcon.asCSSSelector(verifiedPublisherThemeIcon)} { color: ${disabledVerifiedPublisherIconColor}; }`); + collector.addRule(`.extensions-list .monaco-list .monaco-list-row.disabled:not(.selected) .author .verified-publisher ${ThemeIcon.asCSSSelector(verifiedPublisherIcon)} { color: ${disabledVerifiedPublisherIconColor}; }`); } }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 3f508b8bf899..17bba8cc4fa6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -22,7 +22,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { CountBadge } from '../../../../base/browser/ui/countBadge/countBadge.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IUserDataSyncEnablementService } from '../../../../platform/userDataSync/common/userDataSync.js'; -import { activationTimeIcon, errorIcon, infoIcon, installCountIcon, preReleaseIcon, ratingIcon, remoteIcon, sponsorIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon, verifiedPublisherIcon, warningIcon } from './extensionsIcons.js'; +import { activationTimeIcon, errorIcon, infoIcon, installCountIcon, preReleaseIcon, ratingIcon, remoteIcon, sponsorIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon, warningIcon } from './extensionsIcons.js'; import { registerColor, textLinkForeground } from '../../../../platform/theme/common/colorRegistry.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; @@ -45,6 +45,7 @@ import type { IManagedHover } from '../../../../base/browser/ui/hover/hover.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from '../../../services/extensionManagement/common/extensionFeatures.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; +import { extensionVerifiedPublisherIconColor, verifiedPublisherIcon } from '../../../services/extensionManagement/common/extensionsIcons.js'; export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { private _extension: IExtension | null = null; @@ -859,7 +860,6 @@ export class ExtensionRecommendationWidget extends ExtensionWidget { } export const extensionRatingIconColor = registerColor('extensionIcon.starForeground', { light: '#DF6100', dark: '#FF8E00', hcDark: '#FF8E00', hcLight: textLinkForeground }, localize('extensionIconStarForeground', "The icon color for extension ratings."), false); -export const extensionVerifiedPublisherIconColor = registerColor('extensionIcon.verifiedForeground', textLinkForeground, localize('extensionIconVerifiedForeground', "The icon color for extension verified publisher."), false); export const extensionPreReleaseIconColor = registerColor('extensionIcon.preReleaseForeground', { dark: '#1d9271', light: '#1d9271', hcDark: '#1d9271', hcLight: textLinkForeground }, localize('extensionPreReleaseForeground', "The icon color for pre-release extension."), false); export const extensionSponsorIconColor = registerColor('extensionIcon.sponsorForeground', { light: '#B51E78', dark: '#D758B3', hcDark: null, hcLight: '#B51E78' }, localize('extensionIcon.sponsorForeground', "The icon color for extension sponsor."), false); diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 7bc18909d7f1..6a3855890064 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -4,12 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event, EventMultiplexer } from '../../../../base/common/event.js'; +import './media/extensionManagement.css'; import { ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation, EXTENSION_INSTALL_SOURCE_CONTEXT, InstallExtensionInfo, IProductVersion, ExtensionInstallSource, DidUpdateExtensionMetadata, - UninstallExtensionInfo + UninstallExtensionInfo, + IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IResourceExtension, IWorkbenchExtensionManagementService, IWorkbenchInstallOptions, UninstallExtensionOnServerEvent } from './extensionManagement.js'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from '../../../../platform/extensions/common/extensions.js'; @@ -43,6 +45,9 @@ import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uri import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; +import { joinPath } from '../../../../base/common/resources.js'; +import { verifiedPublisherIcon } from './extensionsIcons.js'; +import { Codicon } from '../../../../base/common/codicons.js'; function isGalleryExtension(extension: IResourceExtension | IGalleryExtension): extension is IGalleryExtension { return extension.type === 'gallery'; @@ -98,6 +103,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench @ILogService private readonly logService: ILogService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, + @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); @@ -424,7 +430,20 @@ export class ExtensionManagementService extends Disposable implements IWorkbench const extensionsByServer = new Map(); await Promise.all(extensions.map(async ({ extension, options }) => { try { - const servers = await this.validateAndGetExtensionManagementServersToInstall(extension, options); + const manifest = await this.extensionGalleryService.getManifest(extension, CancellationToken.None); + if (!manifest) { + throw new Error(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", extension.displayName || extension.name)); + } + + if (options?.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] !== ExtensionInstallSource.SETTINGS_SYNC) { + await this.checkForWorkspaceTrust(manifest, false); + + if (!options?.donotIncludePackAndDependencies) { + await this.checkInstallingExtensionOnWeb(extension, manifest); + } + } + + const servers = await this.getExtensionManagementServersToInstall(extension, manifest, options); if (!options.isMachineScoped && this.isExtensionsSyncEnabled()) { if (this.extensionManagementServerService.localExtensionManagementServer && !servers.includes(this.extensionManagementServerService.localExtensionManagementServer) @@ -460,7 +479,22 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } async installFromGallery(gallery: IGalleryExtension, installOptions?: IWorkbenchInstallOptions): Promise { - const servers = await this.validateAndGetExtensionManagementServersToInstall(gallery, installOptions); + const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None); + if (!manifest) { + throw new Error(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", gallery.displayName || gallery.name)); + } + + if (installOptions?.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] !== ExtensionInstallSource.SETTINGS_SYNC) { + await this.checkForTrustedPublisher(gallery); + + await this.checkForWorkspaceTrust(manifest, false); + + if (!installOptions?.donotIncludePackAndDependencies) { + await this.checkInstallingExtensionOnWeb(gallery, manifest); + } + } + + const servers = await this.getExtensionManagementServersToInstall(gallery, manifest, installOptions); if (!installOptions || isUndefined(installOptions.isMachineScoped)) { const isMachineScoped = await this.hasToFlagExtensionsMachineScoped([gallery]); installOptions = { ...(installOptions || {}), isMachineScoped }; @@ -605,13 +639,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } } - private async validateAndGetExtensionManagementServersToInstall(gallery: IGalleryExtension, installOptions?: IWorkbenchInstallOptions): Promise { - - const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None); - if (!manifest) { - return Promise.reject(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", gallery.displayName || gallery.name)); - } - + private async getExtensionManagementServersToInstall(gallery: IGalleryExtension, manifest: IExtensionManifest, installOptions?: IWorkbenchInstallOptions): Promise { const servers: IExtensionManagementServer[] = []; if (installOptions?.servers?.length) { @@ -644,14 +672,6 @@ export class ExtensionManagementService extends Disposable implements IWorkbench throw error; } - if (installOptions?.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] !== ExtensionInstallSource.SETTINGS_SYNC) { - await this.checkForWorkspaceTrust(manifest, false); - } - - if (!installOptions?.donotIncludePackAndDependencies) { - await this.checkInstallingExtensionOnWeb(gallery, manifest); - } - return servers; } @@ -751,7 +771,71 @@ export class ExtensionManagementService extends Disposable implements IWorkbench throw new Error('No extension server found'); } - protected async checkForWorkspaceTrust(manifest: IExtensionManifest, requireTrust: boolean): Promise { + private async checkForTrustedPublisher(extension: IGalleryExtension): Promise { + if (this.allowedExtensionsService.isTrusted(extension)) { + return; + } + + type TrustPublisherClassification = { + owner: 'sandy081'; + comment: 'Report the action taken by the user on the publisher trust dialog'; + action: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The action taken by the user on the publisher trust dialog. Can be trust, learn more or cancel.' }; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the extension for which the publisher trust dialog was shown.' }; + }; + type TrustPublisherEvent = { + action: string; + extensionId: string; + }; + + const installButton: IPromptButton = { + label: localize({ key: 'trust and install', comment: ['&& denotes a mnemonic'] }, "Trust Publisher & &&Install"), + run: async () => { + this.telemetryService.publicLog2('extensions:trustPublisher', { action: 'trust', extensionId: extension.identifier.id }); + await this.allowedExtensionsService.trustPublishers(extension.publisher); + } + }; + + const learnMoreButton: IPromptButton = { + label: localize({ key: 'learnMore', comment: ['&& denotes a mnemonic'] }, "&&Learn More"), + run: () => { + this.telemetryService.publicLog2('extensions:trustPublisher', { action: 'learn', extensionId: extension.identifier.id }); + this.instantiationService.invokeFunction(accessor => accessor.get(ICommandService).executeCommand('vscode.open', URI.parse('https://code.visualstudio.com/docs/editor/extension-runtime-security'))); + throw new CancellationError(); + } + }; + + const customMessage = new MarkdownString('', { supportThemeIcons: true }); + customMessage.appendMarkdown(localize('message1', "The extension {0} is published by {1}. This is the first extension you're installing from this publisher.", `[${extension.displayName}](${this.productService.extensionsGallery!.itemUrl}?itemName=${extension.identifier.id})`, `[${extension.publisherDisplayName}](${joinPath(URI.parse(this.productService.extensionsGallery!.publisherUrl), extension.publisher)})`)); + customMessage.appendText('\n'); + if (extension.publisherDomain?.verified) { + const publisherVerifiedMessage = localize('verifiedPublisher', "This publisher has verified ownership of {0}.", `[${URI.parse(extension.publisherDomain.link).authority}](${extension.publisherDomain.link})`); + customMessage.appendMarkdown(`$(${verifiedPublisherIcon.id}) ${publisherVerifiedMessage}`); + } else { + customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublisher', "This publisher is **not** [verified](https://aka.ms/vscode-verify-publisher).")}`); + } + + customMessage.appendText('\n'); + customMessage.appendMarkdown(localize('message2', "{0} has no control over the behavior of third-party extensions, including how they manage your personal data. Please proceed only if you trust the publisher.", this.productService.nameLong)); + + await this.dialogService.prompt({ + message: localize('checkTrustedPublisherTitle', "Do you trust the publisher \"{0}\"?", extension.publisherDisplayName), + type: Severity.Warning, + buttons: [installButton, learnMoreButton], + cancelButton: { + run: () => { + this.telemetryService.publicLog2('extensions:trustPublisher', { action: 'cancel', extensionId: extension.identifier.id }); + throw new CancellationError(); + } + }, + custom: { + markdownDetails: [{ markdown: customMessage, classes: ['extensions-management-publisher-trust-dialog'] }], + closeOnLinkClick: true, + } + }); + + } + + private async checkForWorkspaceTrust(manifest: IExtensionManifest, requireTrust: boolean): Promise { if (requireTrust || this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(manifest) === false) { const buttons: WorkspaceTrustRequestButton[] = []; buttons.push({ label: localize('extensionInstallWorkspaceTrustButton', "Trust Workspace & Install"), type: 'ContinueWithTrust' }); diff --git a/src/vs/workbench/services/extensionManagement/common/extensionsIcons.ts b/src/vs/workbench/services/extensionManagement/common/extensionsIcons.ts new file mode 100644 index 000000000000..ef1ea959ed88 --- /dev/null +++ b/src/vs/workbench/services/extensionManagement/common/extensionsIcons.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../base/common/codicons.js'; +import { localize } from '../../../../nls.js'; +import { registerColor, textLinkForeground } from '../../../../platform/theme/common/colorRegistry.js'; +import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; + +export const verifiedPublisherIcon = registerIcon('extensions-verified-publisher', Codicon.verifiedFilled, localize('verifiedPublisher', 'Icon used for the verified extension publisher in the extensions view and editor.')); +export const extensionVerifiedPublisherIconColor = registerColor('extensionIcon.verifiedForeground', textLinkForeground, localize('extensionIconVerifiedForeground', "The icon color for extension verified publisher."), false); diff --git a/src/vs/workbench/services/extensionManagement/common/media/extensionManagement.css b/src/vs/workbench/services/extensionManagement/common/media/extensionManagement.css new file mode 100644 index 000000000000..1a03c09f4375 --- /dev/null +++ b/src/vs/workbench/services/extensionManagement/common/media/extensionManagement.css @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.rendered-markdown.extensions-management-publisher-trust-dialog .codicon { + vertical-align: sub; +} + +.rendered-markdown.extensions-management-publisher-trust-dialog .codicon.codicon-extensions-verified-publisher { + color: var(--vscode-extensionIcon-verifiedForeground); +} diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts index 90c9d4af88a4..403336b5f20f 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { generateUuid } from '../../../../base/common/uuid.js'; -import { ILocalExtension, IExtensionGalleryService, InstallOptions } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { ILocalExtension, IExtensionGalleryService, InstallOptions, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { URI } from '../../../../base/common/uri.js'; import { ExtensionManagementService as BaseExtensionManagementService } from '../common/extensionManagementService.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; @@ -46,6 +46,7 @@ export class ExtensionManagementService extends BaseExtensionManagementService { @ILogService logService: ILogService, @IInstantiationService instantiationService: IInstantiationService, @IExtensionsScannerService extensionsScannerService: IExtensionsScannerService, + @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, @ITelemetryService telemetryService: ITelemetryService, ) { super( @@ -64,6 +65,7 @@ export class ExtensionManagementService extends BaseExtensionManagementService { logService, instantiationService, extensionsScannerService, + allowedExtensionsService, telemetryService ); } From 4a5645c3df211e03db7c7c3e8307650bc9772c8b Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:06:28 +0100 Subject: [PATCH 0804/3587] Git - handle merge conflict refs (#238557) --- extensions/git/src/git.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 98d0790003c6..5e1eb1925dc7 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1367,7 +1367,7 @@ export class Repository { } async getObjectDetails(treeish: string, path: string): Promise<{ mode: string; object: string; size: number }> { - if (!treeish) { // index + if (!treeish || treeish === ':1' || treeish === ':2' || treeish === ':3') { // index const elements = await this.lsfiles(path); if (elements.length === 0) { From f7327b79f31df0b40176643974e5d60427d8dfd7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 23 Jan 2025 13:10:27 +0100 Subject: [PATCH 0805/3587] Remove TypeScript ignore comments from upload-sourcemaps files (#238556) --- build/azure-pipelines/upload-sourcemaps.js | 1 - build/azure-pipelines/upload-sourcemaps.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/build/azure-pipelines/upload-sourcemaps.js b/build/azure-pipelines/upload-sourcemaps.js index 3a4e5044c0f4..fe267275b5bc 100644 --- a/build/azure-pipelines/upload-sourcemaps.js +++ b/build/azure-pipelines/upload-sourcemaps.js @@ -44,7 +44,6 @@ const path_1 = __importDefault(require("path")); const event_stream_1 = __importDefault(require("event-stream")); const vinyl_fs_1 = __importDefault(require("vinyl-fs")); const util = __importStar(require("../lib/util")); -// @ts-ignore const dependencies_1 = require("../lib/dependencies"); const identity_1 = require("@azure/identity"); const azure = require('gulp-azure-storage'); diff --git a/build/azure-pipelines/upload-sourcemaps.ts b/build/azure-pipelines/upload-sourcemaps.ts index 527977e111d8..d2950368ee07 100644 --- a/build/azure-pipelines/upload-sourcemaps.ts +++ b/build/azure-pipelines/upload-sourcemaps.ts @@ -8,7 +8,6 @@ import es from 'event-stream'; import Vinyl from 'vinyl'; import vfs from 'vinyl-fs'; import * as util from '../lib/util'; -// @ts-ignore import { getProductionDependencies } from '../lib/dependencies'; import { ClientAssertionCredential } from '@azure/identity'; const azure = require('gulp-azure-storage'); From 560144803df49c5923c6894872864ea86b79eb8c Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 23 Jan 2025 13:32:12 +0100 Subject: [PATCH 0806/3587] update distro (#238559) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 499a2821d3bc..18ef1aece3db 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.97.0", - "distro": "3bc3cc2a38be414a418f2930e0aa7ba1aa39043a", + "distro": "0f416a8c0a95ea8996009a16346e8b469601abee", "author": { "name": "Microsoft Corporation" }, @@ -237,4 +237,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} +} \ No newline at end of file From 8ef59e9a68bc234a3474145495501edc94802035 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:57:38 +0100 Subject: [PATCH 0807/3587] Git - select the correct object for a merge conflict ref (#238560) --- extensions/git/src/git.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 5e1eb1925dc7..431d50b88afe 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1374,7 +1374,10 @@ export class Repository { throw new GitError({ message: 'Path not known by git', gitErrorCode: GitErrorCodes.UnknownPath }); } - const { mode, object } = elements[0]; + const { mode, object } = treeish !== '' + ? elements.find(e => e.stage === treeish.substring(1)) ?? elements[0] + : elements[0]; + const catFile = await this.exec(['cat-file', '-s', object]); const size = parseInt(catFile.stdout); From 9c03b0ae3005051644f2b7840989e006aa0d7df3 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 23 Jan 2025 14:10:41 +0100 Subject: [PATCH 0808/3587] debt - reduce telemetry use in my components (#238565) * debt - reduce telemetry use in my components * debt - reduce telemetry use in my components --- src/vs/base/common/actions.ts | 2 +- .../browser/parts/editor/editorGroupView.ts | 4 +- .../notifications/notificationsCommands.ts | 48 +------------ .../notifications/notificationsTelemetry.ts | 55 -------------- src/vs/workbench/browser/workbench.ts | 2 - .../browser/telemetry.contribution.ts | 72 ++----------------- 6 files changed, 9 insertions(+), 174 deletions(-) delete mode 100644 src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts diff --git a/src/vs/base/common/actions.ts b/src/vs/base/common/actions.ts index 5a271b2bdc86..fd2605942f3e 100644 --- a/src/vs/base/common/actions.ts +++ b/src/vs/base/common/actions.ts @@ -17,7 +17,7 @@ export type WorkbenchActionExecutedClassification = { id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the action that was run.' }; from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the component the action was run from.' }; detail?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Optional details about how the action was run, e.g which keybinding was used.' }; - owner: 'bpasero'; + owner: 'isidorn'; comment: 'Provides insight into actions that are executed within the workbench.'; }; diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 2b7d3ed86ebf..1dff715f7036 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -650,7 +650,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { /* __GDPR__ "editorOpened" : { - "owner": "bpasero", + "owner": "isidorn", "${include}": [ "${EditorTelemetryDescriptor}" ] @@ -687,7 +687,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { /* __GDPR__ "editorClosed" : { - "owner": "bpasero", + "owner": "isidorn", "${include}": [ "${EditorTelemetryDescriptor}" ] diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index c80c3561c464..04980171865f 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -12,12 +12,10 @@ import { MenuRegistry, MenuId } from '../../../../platform/actions/common/action import { localize, localize2 } from '../../../../nls.js'; import { IListService, WorkbenchList } from '../../../../platform/list/browser/listService.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { NotificationMetrics, NotificationMetricsClassification, notificationToMetrics } from './notificationsTelemetry.js'; import { NotificationFocusedContext, NotificationsCenterVisibleContext, NotificationsToastsVisibleContext } from '../../../common/contextkeys.js'; -import { INotificationService, INotificationSourceFilter, NotificationPriority, NotificationsFilter } from '../../../../platform/notification/common/notification.js'; +import { INotificationService, INotificationSourceFilter, NotificationsFilter } from '../../../../platform/notification/common/notification.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ActionRunner, IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from '../../../../base/common/actions.js'; -import { hash } from '../../../../base/common/hash.js'; import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; @@ -109,16 +107,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl weight: KeybindingWeight.WorkbenchContrib + 50, when: NotificationsCenterVisibleContext, primary: KeyCode.Escape, - handler: accessor => { - const telemetryService = accessor.get(ITelemetryService); - for (const notification of model.notifications) { - if (notification.visible) { - telemetryService.publicLog2('notification:hide', notificationToMetrics(notification.message.original, notification.sourceId, notification.priority === NotificationPriority.SILENT)); - } - } - - center.hide(); - } + handler: () => center.hide() }); // Toggle Notifications Center @@ -210,12 +199,6 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Hide Toasts CommandsRegistry.registerCommand(HIDE_NOTIFICATION_TOAST, accessor => { - const telemetryService = accessor.get(ITelemetryService); - for (const notification of model.notifications) { - if (notification.visible) { - telemetryService.publicLog2('notification:hide', notificationToMetrics(notification.message.original, notification.sourceId, notification.priority === NotificationPriority.SILENT)); - } - } toasts.hide(); }); @@ -342,22 +325,6 @@ export function registerNotificationCommands(center: INotificationsCenterControl } -interface NotificationActionMetrics { - readonly id: string; - readonly actionLabel: string; - readonly source: string; - readonly silent: boolean; -} - -type NotificationActionMetricsClassification = { - id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the action that was run from a notification.' }; - actionLabel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The label of the action that was run from a notification.' }; - source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the notification where an action was run.' }; - silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the notification where an action was run is silent or not.' }; - owner: 'bpasero'; - comment: 'Tracks when actions are fired from notifcations and how they were fired.'; -}; - export class NotificationActionRunner extends ActionRunner { constructor( @@ -370,17 +337,6 @@ export class NotificationActionRunner extends ActionRunner { protected override async runAction(action: IAction, context: unknown): Promise { this.telemetryService.publicLog2('workbenchActionExecuted', { id: action.id, from: 'message' }); - if (isNotificationViewItem(context)) { - // Log some additional telemetry specifically for actions - // that are triggered from within notifications. - this.telemetryService.publicLog2('notification:actionExecuted', { - id: hash(context.message.original.toString()).toString(), - actionLabel: action.label, - source: context.sourceId || 'core', - silent: context.priority === NotificationPriority.SILENT - }); - } - // Run and make sure to notify on any error again try { await super.runAction(action, context); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts b/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts deleted file mode 100644 index fd7b3f3da3a0..000000000000 --- a/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts +++ /dev/null @@ -1,55 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { INotificationService, NotificationMessage, NotificationPriority } from '../../../../platform/notification/common/notification.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { hash } from '../../../../base/common/hash.js'; - -export interface NotificationMetrics { - readonly id: string; - readonly silent: boolean; - readonly source?: string; -} - -export type NotificationMetricsClassification = { - id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the source of the notification.' }; - silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the notification is silent or not.' }; - source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the notification.' }; - owner: 'bpasero'; - comment: 'Helps us gain insights to what notifications are being shown, how many, and if they are silent or not.'; -}; - -export function notificationToMetrics(message: NotificationMessage, source: string | undefined, silent: boolean): NotificationMetrics { - return { - id: hash(message.toString()).toString(), - silent, - source: source || 'core' - }; -} - -export class NotificationsTelemetry extends Disposable implements IWorkbenchContribution { - - constructor( - @ITelemetryService private readonly telemetryService: ITelemetryService, - @INotificationService private readonly notificationService: INotificationService - ) { - super(); - this.registerListeners(); - } - - private registerListeners(): void { - this._register(this.notificationService.onDidAddNotification(notification => { - const source = notification.source && typeof notification.source !== 'string' ? notification.source.id : notification.source; - this.telemetryService.publicLog2('notification:show', notificationToMetrics(notification.message, source, notification.priority === NotificationPriority.SILENT)); - })); - - this._register(this.notificationService.onDidRemoveNotification(notification => { - const source = notification.source && typeof notification.source !== 'string' ? notification.source.id : notification.source; - this.telemetryService.publicLog2('notification:close', notificationToMetrics(notification.message, source, notification.priority === NotificationPriority.SILENT)); - })); - } -} diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index b40c9fa73a97..fd795d5ad615 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -26,7 +26,6 @@ import { NotificationService } from '../services/notification/common/notificatio import { NotificationsCenter } from './parts/notifications/notificationsCenter.js'; import { NotificationsAlerts } from './parts/notifications/notificationsAlerts.js'; import { NotificationsStatus } from './parts/notifications/notificationsStatus.js'; -import { NotificationsTelemetry } from './parts/notifications/notificationsTelemetry.js'; import { registerNotificationCommands } from './parts/notifications/notificationsCommands.js'; import { NotificationsToasts } from './parts/notifications/notificationsToasts.js'; import { setARIAContainer } from '../../base/browser/ui/aria/aria.js'; @@ -376,7 +375,6 @@ export class Workbench extends Layout { const notificationsToasts = this._register(instantiationService.createInstance(NotificationsToasts, this.mainContainer, notificationService.model)); this._register(instantiationService.createInstance(NotificationsAlerts, notificationService.model)); const notificationsStatus = instantiationService.createInstance(NotificationsStatus, notificationService.model); - this._register(instantiationService.createInstance(NotificationsTelemetry)); // Visibility this._register(notificationsCenter.onDidChangeVisibility(() => { diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index e77b3838e7d5..a350e7a74b09 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -31,7 +31,6 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '. import { isBoolean, isNumber, isString } from '../../../../base/common/types.js'; import { LayoutSettings } from '../../../services/layout/browser/layoutService.js'; import { AutoRestartConfigurationKey, AutoUpdateConfigurationKey } from '../../extensions/common/extensions.js'; -import { KEYWORD_ACTIVIATION_SETTING_ID } from '../../chat/common/chatService.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; type TelemetryData = { @@ -143,7 +142,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr const settingsType = this.getTypeIfSettings(e.model.resource); if (settingsType) { type SettingsReadClassification = { - owner: 'bpasero'; + owner: 'isidorn'; settingsType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of the settings file that was read.' }; comment: 'Track when a settings file was read, for example from an editor.'; }; @@ -151,7 +150,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr this.telemetryService.publicLog2<{ settingsType: string }, SettingsReadClassification>('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data } else { type FileGetClassification = { - owner: 'bpasero'; + owner: 'isidorn'; comment: 'Track when a file was read, for example from an editor.'; } & FileTelemetryDataFragment; @@ -163,14 +162,14 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr const settingsType = this.getTypeIfSettings(e.model.resource); if (settingsType) { type SettingsWrittenClassification = { - owner: 'bpasero'; + owner: 'isidorn'; settingsType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of the settings file that was written to.' }; comment: 'Track when a settings file was written to, for example from an editor.'; }; this.telemetryService.publicLog2<{ settingsType: string }, SettingsWrittenClassification>('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data } else { type FilePutClassfication = { - owner: 'bpasero'; + owner: 'isidorn'; comment: 'Track when a file was written to, for example from an editor.'; } & FileTelemetryDataFragment; this.telemetryService.publicLog2('filePUT', this.getTelemetryData(e.model.resource, e.reason)); @@ -306,15 +305,6 @@ class ConfigurationTelemetryContribution extends Disposable implements IWorkbenc }>('extensions.autoUpdate', { settingValue: this.getValueToReport(key, target), source }); return; - case 'files.autoSave': - this.telemetryService.publicLog2('files.autoSave', { settingValue: this.getValueToReport(key, target), source }); - return; - case 'editor.stickyScroll.enabled': this.telemetryService.publicLog2('editor.stickyScroll.enabled', { settingValue: this.getValueToReport(key, target), source }); return; - case KEYWORD_ACTIVIATION_SETTING_ID: - this.telemetryService.publicLog2('accessibility.voice.keywordActivation', { settingValue: this.getValueToReport(key, target), source }); - return; - - case 'window.zoomLevel': - this.telemetryService.publicLog2('window.zoomLevel', { settingValue: this.getValueToReport(key, target), source }); - return; - - case 'window.zoomPerWindow': - this.telemetryService.publicLog2('window.zoomPerWindow', { settingValue: this.getValueToReport(key, target), source }); - return; - case 'window.titleBarStyle': this.telemetryService.publicLog2('window.titleBarStyle', { settingValue: this.getValueToReport(key, target), source }); return; - case 'window.commandCenter': - this.telemetryService.publicLog2('window.commandCenter', { settingValue: this.getValueToReport(key, target), source }); - return; - - case 'chat.commandCenter.enabled': - this.telemetryService.publicLog2('chat.commandCenter.enabled', { settingValue: this.getValueToReport(key, target), source }); - return; - case 'window.customTitleBarVisibility': this.telemetryService.publicLog2('extensions.verifySignature', { settingValue: this.getValueToReport(key, target), source }); return; - case 'window.systemColorTheme': - this.telemetryService.publicLog2('window.systemColorTheme', { settingValue: this.getValueToReport(key, target), source }); - return; - case 'window.newWindowProfile': { const valueToReport = this.getValueToReport(key, target); From 9d2ecaccb1e2fa5b949d67db7e8cc7a34f43538d Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 23 Jan 2025 14:14:06 +0100 Subject: [PATCH 0809/3587] use aka link (#238566) --- .../extensionManagement/common/extensionManagementService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 6a3855890064..37e772e97e34 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -799,7 +799,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench label: localize({ key: 'learnMore', comment: ['&& denotes a mnemonic'] }, "&&Learn More"), run: () => { this.telemetryService.publicLog2('extensions:trustPublisher', { action: 'learn', extensionId: extension.identifier.id }); - this.instantiationService.invokeFunction(accessor => accessor.get(ICommandService).executeCommand('vscode.open', URI.parse('https://code.visualstudio.com/docs/editor/extension-runtime-security'))); + this.instantiationService.invokeFunction(accessor => accessor.get(ICommandService).executeCommand('vscode.open', URI.parse('https://aka.ms/vscode-extension-security'))); throw new CancellationError(); } }; From 8d5f6f0050af2abde8c9977f1a269ef6eade6915 Mon Sep 17 00:00:00 2001 From: Johannes Date: Thu, 23 Jan 2025 15:39:06 +0100 Subject: [PATCH 0810/3587] inline chat edits --- .../base/common/observableInternal/utils.ts | 10 +- src/vs/base/common/strings.ts | 37 +++- src/vs/editor/common/core/position.ts | 2 +- .../chat/browser/actions/chatClearActions.ts | 2 + .../contrib/chat/browser/chat.contribution.ts | 2 + src/vs/workbench/contrib/chat/browser/chat.ts | 2 +- .../chatEditingModifiedFileEntry.ts | 16 +- .../browser/chatEditing/chatEditingService.ts | 4 +- .../browser/chatEditing/chatEditingSession.ts | 8 +- .../chat/browser/chatEditorController.ts | 17 +- .../contrib/chat/browser/chatEditorOverlay.ts | 85 ++++++++- .../contrib/chat/browser/chatInputPart.ts | 4 +- .../contrib/chat/browser/chatViewPane.ts | 1 + .../contrib/chat/browser/chatWidget.ts | 72 ++++---- .../chat/browser/media/chatEditorOverlay.css | 29 +++ .../contrib/chat/common/chatEditingService.ts | 2 +- .../browser/inlineChatController2.ts | 174 ++++++++++++++---- .../browser/inlineChatSessionService.ts | 3 + .../browser/inlineChatSessionServiceImpl.ts | 34 +++- 19 files changed, 391 insertions(+), 113 deletions(-) diff --git a/src/vs/base/common/observableInternal/utils.ts b/src/vs/base/common/observableInternal/utils.ts index c42f12f7b8ef..56d3e692b495 100644 --- a/src/vs/base/common/observableInternal/utils.ts +++ b/src/vs/base/common/observableInternal/utils.ts @@ -202,20 +202,24 @@ export namespace observableFromEvent { } export function observableSignalFromEvent( - debugName: string, + owner: DebugOwner | string, event: Event ): IObservable { - return new FromEventObservableSignal(debugName, event); + return new FromEventObservableSignal(typeof owner === 'string' ? owner : new DebugNameData(owner, undefined, undefined), event); } class FromEventObservableSignal extends BaseObservable { private subscription: IDisposable | undefined; + public readonly debugName: string; constructor( - public readonly debugName: string, + debugNameDataOrName: DebugNameData | string, private readonly event: Event, ) { super(); + this.debugName = typeof debugNameDataOrName === 'string' + ? debugNameDataOrName + : debugNameDataOrName.getDebugName(this) ?? 'Observable Signal From Event'; } protected override onFirstObserverAdded(): void { diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index be89ea492994..c08ea72c5dca 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -749,7 +749,7 @@ export function isEmojiImprecise(x: number): boolean { * happens at favorable positions - such as whitespace or punctuation characters. * The return value can be longer than the given value of `n`. Leading whitespace is always trimmed. */ -export function lcut(text: string, n: number, prefix = '') { +export function lcut(text: string, n: number, prefix = ''): string { const trimmed = text.trimStart(); if (trimmed.length < n) { @@ -774,6 +774,37 @@ export function lcut(text: string, n: number, prefix = '') { return prefix + trimmed.substring(i).trimStart(); } +/** + * Given a string and a max length returns a shorted version. Shorting + * happens at favorable positions - such as whitespace or punctuation characters. + * The return value can be longer than the given value of `n`. Trailing whitespace is always trimmed. + */ +export function rcut(text: string, n: number, suffix = ''): string { + const trimmed = text.trimEnd(); + + if (trimmed.length < n) { + return trimmed; + } + + const re = /\b/g; + let lastWordBreak = trimmed.length; + let i = 0; + + while (re.test(trimmed)) { + if (trimmed.length - re.lastIndex > n) { + lastWordBreak = re.lastIndex; + } + i = re.lastIndex; + re.lastIndex += 1; + } + + if (lastWordBreak === trimmed.length) { + return trimmed; + } + + return (trimmed.substring(0, lastWordBreak) + suffix).trimEnd(); +} + // Escape codes, compiled from https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ // Plus additional markers for custom `\x1b]...\x07` instructions. const CSI_SEQUENCE = /(?:(?:\x1b\[|\x9B)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~])|(:?\x1b\].*?\x07)/g; @@ -1082,7 +1113,7 @@ class GraphemeBreakTree { function getGraphemeBreakRawData(): number[] { // generated using https://github.com/alexdima/unicode-utils/blob/main/grapheme-break.js - return JSON.parse('[0,0,0,51229,51255,12,44061,44087,12,127462,127487,6,7083,7085,5,47645,47671,12,54813,54839,12,128678,128678,14,3270,3270,5,9919,9923,14,45853,45879,12,49437,49463,12,53021,53047,12,71216,71218,7,128398,128399,14,129360,129374,14,2519,2519,5,4448,4519,9,9742,9742,14,12336,12336,14,44957,44983,12,46749,46775,12,48541,48567,12,50333,50359,12,52125,52151,12,53917,53943,12,69888,69890,5,73018,73018,5,127990,127990,14,128558,128559,14,128759,128760,14,129653,129655,14,2027,2035,5,2891,2892,7,3761,3761,5,6683,6683,5,8293,8293,4,9825,9826,14,9999,9999,14,43452,43453,5,44509,44535,12,45405,45431,12,46301,46327,12,47197,47223,12,48093,48119,12,48989,49015,12,49885,49911,12,50781,50807,12,51677,51703,12,52573,52599,12,53469,53495,12,54365,54391,12,65279,65279,4,70471,70472,7,72145,72147,7,119173,119179,5,127799,127818,14,128240,128244,14,128512,128512,14,128652,128652,14,128721,128722,14,129292,129292,14,129445,129450,14,129734,129743,14,1476,1477,5,2366,2368,7,2750,2752,7,3076,3076,5,3415,3415,5,4141,4144,5,6109,6109,5,6964,6964,5,7394,7400,5,9197,9198,14,9770,9770,14,9877,9877,14,9968,9969,14,10084,10084,14,43052,43052,5,43713,43713,5,44285,44311,12,44733,44759,12,45181,45207,12,45629,45655,12,46077,46103,12,46525,46551,12,46973,46999,12,47421,47447,12,47869,47895,12,48317,48343,12,48765,48791,12,49213,49239,12,49661,49687,12,50109,50135,12,50557,50583,12,51005,51031,12,51453,51479,12,51901,51927,12,52349,52375,12,52797,52823,12,53245,53271,12,53693,53719,12,54141,54167,12,54589,54615,12,55037,55063,12,69506,69509,5,70191,70193,5,70841,70841,7,71463,71467,5,72330,72342,5,94031,94031,5,123628,123631,5,127763,127765,14,127941,127941,14,128043,128062,14,128302,128317,14,128465,128467,14,128539,128539,14,128640,128640,14,128662,128662,14,128703,128703,14,128745,128745,14,129004,129007,14,129329,129330,14,129402,129402,14,129483,129483,14,129686,129704,14,130048,131069,14,173,173,4,1757,1757,1,2200,2207,5,2434,2435,7,2631,2632,5,2817,2817,5,3008,3008,5,3201,3201,5,3387,3388,5,3542,3542,5,3902,3903,7,4190,4192,5,6002,6003,5,6439,6440,5,6765,6770,7,7019,7027,5,7154,7155,7,8205,8205,13,8505,8505,14,9654,9654,14,9757,9757,14,9792,9792,14,9852,9853,14,9890,9894,14,9937,9937,14,9981,9981,14,10035,10036,14,11035,11036,14,42654,42655,5,43346,43347,7,43587,43587,5,44006,44007,7,44173,44199,12,44397,44423,12,44621,44647,12,44845,44871,12,45069,45095,12,45293,45319,12,45517,45543,12,45741,45767,12,45965,45991,12,46189,46215,12,46413,46439,12,46637,46663,12,46861,46887,12,47085,47111,12,47309,47335,12,47533,47559,12,47757,47783,12,47981,48007,12,48205,48231,12,48429,48455,12,48653,48679,12,48877,48903,12,49101,49127,12,49325,49351,12,49549,49575,12,49773,49799,12,49997,50023,12,50221,50247,12,50445,50471,12,50669,50695,12,50893,50919,12,51117,51143,12,51341,51367,12,51565,51591,12,51789,51815,12,52013,52039,12,52237,52263,12,52461,52487,12,52685,52711,12,52909,52935,12,53133,53159,12,53357,53383,12,53581,53607,12,53805,53831,12,54029,54055,12,54253,54279,12,54477,54503,12,54701,54727,12,54925,54951,12,55149,55175,12,68101,68102,5,69762,69762,7,70067,70069,7,70371,70378,5,70720,70721,7,71087,71087,5,71341,71341,5,71995,71996,5,72249,72249,7,72850,72871,5,73109,73109,5,118576,118598,5,121505,121519,5,127245,127247,14,127568,127569,14,127777,127777,14,127872,127891,14,127956,127967,14,128015,128016,14,128110,128172,14,128259,128259,14,128367,128368,14,128424,128424,14,128488,128488,14,128530,128532,14,128550,128551,14,128566,128566,14,128647,128647,14,128656,128656,14,128667,128673,14,128691,128693,14,128715,128715,14,128728,128732,14,128752,128752,14,128765,128767,14,129096,129103,14,129311,129311,14,129344,129349,14,129394,129394,14,129413,129425,14,129466,129471,14,129511,129535,14,129664,129666,14,129719,129722,14,129760,129767,14,917536,917631,5,13,13,2,1160,1161,5,1564,1564,4,1807,1807,1,2085,2087,5,2307,2307,7,2382,2383,7,2497,2500,5,2563,2563,7,2677,2677,5,2763,2764,7,2879,2879,5,2914,2915,5,3021,3021,5,3142,3144,5,3263,3263,5,3285,3286,5,3398,3400,7,3530,3530,5,3633,3633,5,3864,3865,5,3974,3975,5,4155,4156,7,4229,4230,5,5909,5909,7,6078,6085,7,6277,6278,5,6451,6456,7,6744,6750,5,6846,6846,5,6972,6972,5,7074,7077,5,7146,7148,7,7222,7223,5,7416,7417,5,8234,8238,4,8417,8417,5,9000,9000,14,9203,9203,14,9730,9731,14,9748,9749,14,9762,9763,14,9776,9783,14,9800,9811,14,9831,9831,14,9872,9873,14,9882,9882,14,9900,9903,14,9929,9933,14,9941,9960,14,9974,9974,14,9989,9989,14,10006,10006,14,10062,10062,14,10160,10160,14,11647,11647,5,12953,12953,14,43019,43019,5,43232,43249,5,43443,43443,5,43567,43568,7,43696,43696,5,43765,43765,7,44013,44013,5,44117,44143,12,44229,44255,12,44341,44367,12,44453,44479,12,44565,44591,12,44677,44703,12,44789,44815,12,44901,44927,12,45013,45039,12,45125,45151,12,45237,45263,12,45349,45375,12,45461,45487,12,45573,45599,12,45685,45711,12,45797,45823,12,45909,45935,12,46021,46047,12,46133,46159,12,46245,46271,12,46357,46383,12,46469,46495,12,46581,46607,12,46693,46719,12,46805,46831,12,46917,46943,12,47029,47055,12,47141,47167,12,47253,47279,12,47365,47391,12,47477,47503,12,47589,47615,12,47701,47727,12,47813,47839,12,47925,47951,12,48037,48063,12,48149,48175,12,48261,48287,12,48373,48399,12,48485,48511,12,48597,48623,12,48709,48735,12,48821,48847,12,48933,48959,12,49045,49071,12,49157,49183,12,49269,49295,12,49381,49407,12,49493,49519,12,49605,49631,12,49717,49743,12,49829,49855,12,49941,49967,12,50053,50079,12,50165,50191,12,50277,50303,12,50389,50415,12,50501,50527,12,50613,50639,12,50725,50751,12,50837,50863,12,50949,50975,12,51061,51087,12,51173,51199,12,51285,51311,12,51397,51423,12,51509,51535,12,51621,51647,12,51733,51759,12,51845,51871,12,51957,51983,12,52069,52095,12,52181,52207,12,52293,52319,12,52405,52431,12,52517,52543,12,52629,52655,12,52741,52767,12,52853,52879,12,52965,52991,12,53077,53103,12,53189,53215,12,53301,53327,12,53413,53439,12,53525,53551,12,53637,53663,12,53749,53775,12,53861,53887,12,53973,53999,12,54085,54111,12,54197,54223,12,54309,54335,12,54421,54447,12,54533,54559,12,54645,54671,12,54757,54783,12,54869,54895,12,54981,55007,12,55093,55119,12,55243,55291,10,66045,66045,5,68325,68326,5,69688,69702,5,69817,69818,5,69957,69958,7,70089,70092,5,70198,70199,5,70462,70462,5,70502,70508,5,70750,70750,5,70846,70846,7,71100,71101,5,71230,71230,7,71351,71351,5,71737,71738,5,72000,72000,7,72160,72160,5,72273,72278,5,72752,72758,5,72882,72883,5,73031,73031,5,73461,73462,7,94192,94193,7,119149,119149,7,121403,121452,5,122915,122916,5,126980,126980,14,127358,127359,14,127535,127535,14,127759,127759,14,127771,127771,14,127792,127793,14,127825,127867,14,127897,127899,14,127945,127945,14,127985,127986,14,128000,128007,14,128021,128021,14,128066,128100,14,128184,128235,14,128249,128252,14,128266,128276,14,128335,128335,14,128379,128390,14,128407,128419,14,128444,128444,14,128481,128481,14,128499,128499,14,128526,128526,14,128536,128536,14,128543,128543,14,128556,128556,14,128564,128564,14,128577,128580,14,128643,128645,14,128649,128649,14,128654,128654,14,128660,128660,14,128664,128664,14,128675,128675,14,128686,128689,14,128695,128696,14,128705,128709,14,128717,128719,14,128725,128725,14,128736,128741,14,128747,128748,14,128755,128755,14,128762,128762,14,128981,128991,14,129009,129023,14,129160,129167,14,129296,129304,14,129320,129327,14,129340,129342,14,129356,129356,14,129388,129392,14,129399,129400,14,129404,129407,14,129432,129442,14,129454,129455,14,129473,129474,14,129485,129487,14,129648,129651,14,129659,129660,14,129671,129679,14,129709,129711,14,129728,129730,14,129751,129753,14,129776,129782,14,917505,917505,4,917760,917999,5,10,10,3,127,159,4,768,879,5,1471,1471,5,1536,1541,1,1648,1648,5,1767,1768,5,1840,1866,5,2070,2073,5,2137,2139,5,2274,2274,1,2363,2363,7,2377,2380,7,2402,2403,5,2494,2494,5,2507,2508,7,2558,2558,5,2622,2624,7,2641,2641,5,2691,2691,7,2759,2760,5,2786,2787,5,2876,2876,5,2881,2884,5,2901,2902,5,3006,3006,5,3014,3016,7,3072,3072,5,3134,3136,5,3157,3158,5,3260,3260,5,3266,3266,5,3274,3275,7,3328,3329,5,3391,3392,7,3405,3405,5,3457,3457,5,3536,3537,7,3551,3551,5,3636,3642,5,3764,3772,5,3895,3895,5,3967,3967,7,3993,4028,5,4146,4151,5,4182,4183,7,4226,4226,5,4253,4253,5,4957,4959,5,5940,5940,7,6070,6070,7,6087,6088,7,6158,6158,4,6432,6434,5,6448,6449,7,6679,6680,5,6742,6742,5,6754,6754,5,6783,6783,5,6912,6915,5,6966,6970,5,6978,6978,5,7042,7042,7,7080,7081,5,7143,7143,7,7150,7150,7,7212,7219,5,7380,7392,5,7412,7412,5,8203,8203,4,8232,8232,4,8265,8265,14,8400,8412,5,8421,8432,5,8617,8618,14,9167,9167,14,9200,9200,14,9410,9410,14,9723,9726,14,9733,9733,14,9745,9745,14,9752,9752,14,9760,9760,14,9766,9766,14,9774,9774,14,9786,9786,14,9794,9794,14,9823,9823,14,9828,9828,14,9833,9850,14,9855,9855,14,9875,9875,14,9880,9880,14,9885,9887,14,9896,9897,14,9906,9916,14,9926,9927,14,9935,9935,14,9939,9939,14,9962,9962,14,9972,9972,14,9978,9978,14,9986,9986,14,9997,9997,14,10002,10002,14,10017,10017,14,10055,10055,14,10071,10071,14,10133,10135,14,10548,10549,14,11093,11093,14,12330,12333,5,12441,12442,5,42608,42610,5,43010,43010,5,43045,43046,5,43188,43203,7,43302,43309,5,43392,43394,5,43446,43449,5,43493,43493,5,43571,43572,7,43597,43597,7,43703,43704,5,43756,43757,5,44003,44004,7,44009,44010,7,44033,44059,12,44089,44115,12,44145,44171,12,44201,44227,12,44257,44283,12,44313,44339,12,44369,44395,12,44425,44451,12,44481,44507,12,44537,44563,12,44593,44619,12,44649,44675,12,44705,44731,12,44761,44787,12,44817,44843,12,44873,44899,12,44929,44955,12,44985,45011,12,45041,45067,12,45097,45123,12,45153,45179,12,45209,45235,12,45265,45291,12,45321,45347,12,45377,45403,12,45433,45459,12,45489,45515,12,45545,45571,12,45601,45627,12,45657,45683,12,45713,45739,12,45769,45795,12,45825,45851,12,45881,45907,12,45937,45963,12,45993,46019,12,46049,46075,12,46105,46131,12,46161,46187,12,46217,46243,12,46273,46299,12,46329,46355,12,46385,46411,12,46441,46467,12,46497,46523,12,46553,46579,12,46609,46635,12,46665,46691,12,46721,46747,12,46777,46803,12,46833,46859,12,46889,46915,12,46945,46971,12,47001,47027,12,47057,47083,12,47113,47139,12,47169,47195,12,47225,47251,12,47281,47307,12,47337,47363,12,47393,47419,12,47449,47475,12,47505,47531,12,47561,47587,12,47617,47643,12,47673,47699,12,47729,47755,12,47785,47811,12,47841,47867,12,47897,47923,12,47953,47979,12,48009,48035,12,48065,48091,12,48121,48147,12,48177,48203,12,48233,48259,12,48289,48315,12,48345,48371,12,48401,48427,12,48457,48483,12,48513,48539,12,48569,48595,12,48625,48651,12,48681,48707,12,48737,48763,12,48793,48819,12,48849,48875,12,48905,48931,12,48961,48987,12,49017,49043,12,49073,49099,12,49129,49155,12,49185,49211,12,49241,49267,12,49297,49323,12,49353,49379,12,49409,49435,12,49465,49491,12,49521,49547,12,49577,49603,12,49633,49659,12,49689,49715,12,49745,49771,12,49801,49827,12,49857,49883,12,49913,49939,12,49969,49995,12,50025,50051,12,50081,50107,12,50137,50163,12,50193,50219,12,50249,50275,12,50305,50331,12,50361,50387,12,50417,50443,12,50473,50499,12,50529,50555,12,50585,50611,12,50641,50667,12,50697,50723,12,50753,50779,12,50809,50835,12,50865,50891,12,50921,50947,12,50977,51003,12,51033,51059,12,51089,51115,12,51145,51171,12,51201,51227,12,51257,51283,12,51313,51339,12,51369,51395,12,51425,51451,12,51481,51507,12,51537,51563,12,51593,51619,12,51649,51675,12,51705,51731,12,51761,51787,12,51817,51843,12,51873,51899,12,51929,51955,12,51985,52011,12,52041,52067,12,52097,52123,12,52153,52179,12,52209,52235,12,52265,52291,12,52321,52347,12,52377,52403,12,52433,52459,12,52489,52515,12,52545,52571,12,52601,52627,12,52657,52683,12,52713,52739,12,52769,52795,12,52825,52851,12,52881,52907,12,52937,52963,12,52993,53019,12,53049,53075,12,53105,53131,12,53161,53187,12,53217,53243,12,53273,53299,12,53329,53355,12,53385,53411,12,53441,53467,12,53497,53523,12,53553,53579,12,53609,53635,12,53665,53691,12,53721,53747,12,53777,53803,12,53833,53859,12,53889,53915,12,53945,53971,12,54001,54027,12,54057,54083,12,54113,54139,12,54169,54195,12,54225,54251,12,54281,54307,12,54337,54363,12,54393,54419,12,54449,54475,12,54505,54531,12,54561,54587,12,54617,54643,12,54673,54699,12,54729,54755,12,54785,54811,12,54841,54867,12,54897,54923,12,54953,54979,12,55009,55035,12,55065,55091,12,55121,55147,12,55177,55203,12,65024,65039,5,65520,65528,4,66422,66426,5,68152,68154,5,69291,69292,5,69633,69633,5,69747,69748,5,69811,69814,5,69826,69826,5,69932,69932,7,70016,70017,5,70079,70080,7,70095,70095,5,70196,70196,5,70367,70367,5,70402,70403,7,70464,70464,5,70487,70487,5,70709,70711,7,70725,70725,7,70833,70834,7,70843,70844,7,70849,70849,7,71090,71093,5,71103,71104,5,71227,71228,7,71339,71339,5,71344,71349,5,71458,71461,5,71727,71735,5,71985,71989,7,71998,71998,5,72002,72002,7,72154,72155,5,72193,72202,5,72251,72254,5,72281,72283,5,72344,72345,5,72766,72766,7,72874,72880,5,72885,72886,5,73023,73029,5,73104,73105,5,73111,73111,5,92912,92916,5,94095,94098,5,113824,113827,4,119142,119142,7,119155,119162,4,119362,119364,5,121476,121476,5,122888,122904,5,123184,123190,5,125252,125258,5,127183,127183,14,127340,127343,14,127377,127386,14,127491,127503,14,127548,127551,14,127744,127756,14,127761,127761,14,127769,127769,14,127773,127774,14,127780,127788,14,127796,127797,14,127820,127823,14,127869,127869,14,127894,127895,14,127902,127903,14,127943,127943,14,127947,127950,14,127972,127972,14,127988,127988,14,127992,127994,14,128009,128011,14,128019,128019,14,128023,128041,14,128064,128064,14,128102,128107,14,128174,128181,14,128238,128238,14,128246,128247,14,128254,128254,14,128264,128264,14,128278,128299,14,128329,128330,14,128348,128359,14,128371,128377,14,128392,128393,14,128401,128404,14,128421,128421,14,128433,128434,14,128450,128452,14,128476,128478,14,128483,128483,14,128495,128495,14,128506,128506,14,128519,128520,14,128528,128528,14,128534,128534,14,128538,128538,14,128540,128542,14,128544,128549,14,128552,128555,14,128557,128557,14,128560,128563,14,128565,128565,14,128567,128576,14,128581,128591,14,128641,128642,14,128646,128646,14,128648,128648,14,128650,128651,14,128653,128653,14,128655,128655,14,128657,128659,14,128661,128661,14,128663,128663,14,128665,128666,14,128674,128674,14,128676,128677,14,128679,128685,14,128690,128690,14,128694,128694,14,128697,128702,14,128704,128704,14,128710,128714,14,128716,128716,14,128720,128720,14,128723,128724,14,128726,128727,14,128733,128735,14,128742,128744,14,128746,128746,14,128749,128751,14,128753,128754,14,128756,128758,14,128761,128761,14,128763,128764,14,128884,128895,14,128992,129003,14,129008,129008,14,129036,129039,14,129114,129119,14,129198,129279,14,129293,129295,14,129305,129310,14,129312,129319,14,129328,129328,14,129331,129338,14,129343,129343,14,129351,129355,14,129357,129359,14,129375,129387,14,129393,129393,14,129395,129398,14,129401,129401,14,129403,129403,14,129408,129412,14,129426,129431,14,129443,129444,14,129451,129453,14,129456,129465,14,129472,129472,14,129475,129482,14,129484,129484,14,129488,129510,14,129536,129647,14,129652,129652,14,129656,129658,14,129661,129663,14,129667,129670,14,129680,129685,14,129705,129708,14,129712,129718,14,129723,129727,14,129731,129733,14,129744,129750,14,129754,129759,14,129768,129775,14,129783,129791,14,917504,917504,4,917506,917535,4,917632,917759,4,918000,921599,4,0,9,4,11,12,4,14,31,4,169,169,14,174,174,14,1155,1159,5,1425,1469,5,1473,1474,5,1479,1479,5,1552,1562,5,1611,1631,5,1750,1756,5,1759,1764,5,1770,1773,5,1809,1809,5,1958,1968,5,2045,2045,5,2075,2083,5,2089,2093,5,2192,2193,1,2250,2273,5,2275,2306,5,2362,2362,5,2364,2364,5,2369,2376,5,2381,2381,5,2385,2391,5,2433,2433,5,2492,2492,5,2495,2496,7,2503,2504,7,2509,2509,5,2530,2531,5,2561,2562,5,2620,2620,5,2625,2626,5,2635,2637,5,2672,2673,5,2689,2690,5,2748,2748,5,2753,2757,5,2761,2761,7,2765,2765,5,2810,2815,5,2818,2819,7,2878,2878,5,2880,2880,7,2887,2888,7,2893,2893,5,2903,2903,5,2946,2946,5,3007,3007,7,3009,3010,7,3018,3020,7,3031,3031,5,3073,3075,7,3132,3132,5,3137,3140,7,3146,3149,5,3170,3171,5,3202,3203,7,3262,3262,7,3264,3265,7,3267,3268,7,3271,3272,7,3276,3277,5,3298,3299,5,3330,3331,7,3390,3390,5,3393,3396,5,3402,3404,7,3406,3406,1,3426,3427,5,3458,3459,7,3535,3535,5,3538,3540,5,3544,3550,7,3570,3571,7,3635,3635,7,3655,3662,5,3763,3763,7,3784,3789,5,3893,3893,5,3897,3897,5,3953,3966,5,3968,3972,5,3981,3991,5,4038,4038,5,4145,4145,7,4153,4154,5,4157,4158,5,4184,4185,5,4209,4212,5,4228,4228,7,4237,4237,5,4352,4447,8,4520,4607,10,5906,5908,5,5938,5939,5,5970,5971,5,6068,6069,5,6071,6077,5,6086,6086,5,6089,6099,5,6155,6157,5,6159,6159,5,6313,6313,5,6435,6438,7,6441,6443,7,6450,6450,5,6457,6459,5,6681,6682,7,6741,6741,7,6743,6743,7,6752,6752,5,6757,6764,5,6771,6780,5,6832,6845,5,6847,6862,5,6916,6916,7,6965,6965,5,6971,6971,7,6973,6977,7,6979,6980,7,7040,7041,5,7073,7073,7,7078,7079,7,7082,7082,7,7142,7142,5,7144,7145,5,7149,7149,5,7151,7153,5,7204,7211,7,7220,7221,7,7376,7378,5,7393,7393,7,7405,7405,5,7415,7415,7,7616,7679,5,8204,8204,5,8206,8207,4,8233,8233,4,8252,8252,14,8288,8292,4,8294,8303,4,8413,8416,5,8418,8420,5,8482,8482,14,8596,8601,14,8986,8987,14,9096,9096,14,9193,9196,14,9199,9199,14,9201,9202,14,9208,9210,14,9642,9643,14,9664,9664,14,9728,9729,14,9732,9732,14,9735,9741,14,9743,9744,14,9746,9746,14,9750,9751,14,9753,9756,14,9758,9759,14,9761,9761,14,9764,9765,14,9767,9769,14,9771,9773,14,9775,9775,14,9784,9785,14,9787,9791,14,9793,9793,14,9795,9799,14,9812,9822,14,9824,9824,14,9827,9827,14,9829,9830,14,9832,9832,14,9851,9851,14,9854,9854,14,9856,9861,14,9874,9874,14,9876,9876,14,9878,9879,14,9881,9881,14,9883,9884,14,9888,9889,14,9895,9895,14,9898,9899,14,9904,9905,14,9917,9918,14,9924,9925,14,9928,9928,14,9934,9934,14,9936,9936,14,9938,9938,14,9940,9940,14,9961,9961,14,9963,9967,14,9970,9971,14,9973,9973,14,9975,9977,14,9979,9980,14,9982,9985,14,9987,9988,14,9992,9996,14,9998,9998,14,10000,10001,14,10004,10004,14,10013,10013,14,10024,10024,14,10052,10052,14,10060,10060,14,10067,10069,14,10083,10083,14,10085,10087,14,10145,10145,14,10175,10175,14,11013,11015,14,11088,11088,14,11503,11505,5,11744,11775,5,12334,12335,5,12349,12349,14,12951,12951,14,42607,42607,5,42612,42621,5,42736,42737,5,43014,43014,5,43043,43044,7,43047,43047,7,43136,43137,7,43204,43205,5,43263,43263,5,43335,43345,5,43360,43388,8,43395,43395,7,43444,43445,7,43450,43451,7,43454,43456,7,43561,43566,5,43569,43570,5,43573,43574,5,43596,43596,5,43644,43644,5,43698,43700,5,43710,43711,5,43755,43755,7,43758,43759,7,43766,43766,5,44005,44005,5,44008,44008,5,44012,44012,7,44032,44032,11,44060,44060,11,44088,44088,11,44116,44116,11,44144,44144,11,44172,44172,11,44200,44200,11,44228,44228,11,44256,44256,11,44284,44284,11,44312,44312,11,44340,44340,11,44368,44368,11,44396,44396,11,44424,44424,11,44452,44452,11,44480,44480,11,44508,44508,11,44536,44536,11,44564,44564,11,44592,44592,11,44620,44620,11,44648,44648,11,44676,44676,11,44704,44704,11,44732,44732,11,44760,44760,11,44788,44788,11,44816,44816,11,44844,44844,11,44872,44872,11,44900,44900,11,44928,44928,11,44956,44956,11,44984,44984,11,45012,45012,11,45040,45040,11,45068,45068,11,45096,45096,11,45124,45124,11,45152,45152,11,45180,45180,11,45208,45208,11,45236,45236,11,45264,45264,11,45292,45292,11,45320,45320,11,45348,45348,11,45376,45376,11,45404,45404,11,45432,45432,11,45460,45460,11,45488,45488,11,45516,45516,11,45544,45544,11,45572,45572,11,45600,45600,11,45628,45628,11,45656,45656,11,45684,45684,11,45712,45712,11,45740,45740,11,45768,45768,11,45796,45796,11,45824,45824,11,45852,45852,11,45880,45880,11,45908,45908,11,45936,45936,11,45964,45964,11,45992,45992,11,46020,46020,11,46048,46048,11,46076,46076,11,46104,46104,11,46132,46132,11,46160,46160,11,46188,46188,11,46216,46216,11,46244,46244,11,46272,46272,11,46300,46300,11,46328,46328,11,46356,46356,11,46384,46384,11,46412,46412,11,46440,46440,11,46468,46468,11,46496,46496,11,46524,46524,11,46552,46552,11,46580,46580,11,46608,46608,11,46636,46636,11,46664,46664,11,46692,46692,11,46720,46720,11,46748,46748,11,46776,46776,11,46804,46804,11,46832,46832,11,46860,46860,11,46888,46888,11,46916,46916,11,46944,46944,11,46972,46972,11,47000,47000,11,47028,47028,11,47056,47056,11,47084,47084,11,47112,47112,11,47140,47140,11,47168,47168,11,47196,47196,11,47224,47224,11,47252,47252,11,47280,47280,11,47308,47308,11,47336,47336,11,47364,47364,11,47392,47392,11,47420,47420,11,47448,47448,11,47476,47476,11,47504,47504,11,47532,47532,11,47560,47560,11,47588,47588,11,47616,47616,11,47644,47644,11,47672,47672,11,47700,47700,11,47728,47728,11,47756,47756,11,47784,47784,11,47812,47812,11,47840,47840,11,47868,47868,11,47896,47896,11,47924,47924,11,47952,47952,11,47980,47980,11,48008,48008,11,48036,48036,11,48064,48064,11,48092,48092,11,48120,48120,11,48148,48148,11,48176,48176,11,48204,48204,11,48232,48232,11,48260,48260,11,48288,48288,11,48316,48316,11,48344,48344,11,48372,48372,11,48400,48400,11,48428,48428,11,48456,48456,11,48484,48484,11,48512,48512,11,48540,48540,11,48568,48568,11,48596,48596,11,48624,48624,11,48652,48652,11,48680,48680,11,48708,48708,11,48736,48736,11,48764,48764,11,48792,48792,11,48820,48820,11,48848,48848,11,48876,48876,11,48904,48904,11,48932,48932,11,48960,48960,11,48988,48988,11,49016,49016,11,49044,49044,11,49072,49072,11,49100,49100,11,49128,49128,11,49156,49156,11,49184,49184,11,49212,49212,11,49240,49240,11,49268,49268,11,49296,49296,11,49324,49324,11,49352,49352,11,49380,49380,11,49408,49408,11,49436,49436,11,49464,49464,11,49492,49492,11,49520,49520,11,49548,49548,11,49576,49576,11,49604,49604,11,49632,49632,11,49660,49660,11,49688,49688,11,49716,49716,11,49744,49744,11,49772,49772,11,49800,49800,11,49828,49828,11,49856,49856,11,49884,49884,11,49912,49912,11,49940,49940,11,49968,49968,11,49996,49996,11,50024,50024,11,50052,50052,11,50080,50080,11,50108,50108,11,50136,50136,11,50164,50164,11,50192,50192,11,50220,50220,11,50248,50248,11,50276,50276,11,50304,50304,11,50332,50332,11,50360,50360,11,50388,50388,11,50416,50416,11,50444,50444,11,50472,50472,11,50500,50500,11,50528,50528,11,50556,50556,11,50584,50584,11,50612,50612,11,50640,50640,11,50668,50668,11,50696,50696,11,50724,50724,11,50752,50752,11,50780,50780,11,50808,50808,11,50836,50836,11,50864,50864,11,50892,50892,11,50920,50920,11,50948,50948,11,50976,50976,11,51004,51004,11,51032,51032,11,51060,51060,11,51088,51088,11,51116,51116,11,51144,51144,11,51172,51172,11,51200,51200,11,51228,51228,11,51256,51256,11,51284,51284,11,51312,51312,11,51340,51340,11,51368,51368,11,51396,51396,11,51424,51424,11,51452,51452,11,51480,51480,11,51508,51508,11,51536,51536,11,51564,51564,11,51592,51592,11,51620,51620,11,51648,51648,11,51676,51676,11,51704,51704,11,51732,51732,11,51760,51760,11,51788,51788,11,51816,51816,11,51844,51844,11,51872,51872,11,51900,51900,11,51928,51928,11,51956,51956,11,51984,51984,11,52012,52012,11,52040,52040,11,52068,52068,11,52096,52096,11,52124,52124,11,52152,52152,11,52180,52180,11,52208,52208,11,52236,52236,11,52264,52264,11,52292,52292,11,52320,52320,11,52348,52348,11,52376,52376,11,52404,52404,11,52432,52432,11,52460,52460,11,52488,52488,11,52516,52516,11,52544,52544,11,52572,52572,11,52600,52600,11,52628,52628,11,52656,52656,11,52684,52684,11,52712,52712,11,52740,52740,11,52768,52768,11,52796,52796,11,52824,52824,11,52852,52852,11,52880,52880,11,52908,52908,11,52936,52936,11,52964,52964,11,52992,52992,11,53020,53020,11,53048,53048,11,53076,53076,11,53104,53104,11,53132,53132,11,53160,53160,11,53188,53188,11,53216,53216,11,53244,53244,11,53272,53272,11,53300,53300,11,53328,53328,11,53356,53356,11,53384,53384,11,53412,53412,11,53440,53440,11,53468,53468,11,53496,53496,11,53524,53524,11,53552,53552,11,53580,53580,11,53608,53608,11,53636,53636,11,53664,53664,11,53692,53692,11,53720,53720,11,53748,53748,11,53776,53776,11,53804,53804,11,53832,53832,11,53860,53860,11,53888,53888,11,53916,53916,11,53944,53944,11,53972,53972,11,54000,54000,11,54028,54028,11,54056,54056,11,54084,54084,11,54112,54112,11,54140,54140,11,54168,54168,11,54196,54196,11,54224,54224,11,54252,54252,11,54280,54280,11,54308,54308,11,54336,54336,11,54364,54364,11,54392,54392,11,54420,54420,11,54448,54448,11,54476,54476,11,54504,54504,11,54532,54532,11,54560,54560,11,54588,54588,11,54616,54616,11,54644,54644,11,54672,54672,11,54700,54700,11,54728,54728,11,54756,54756,11,54784,54784,11,54812,54812,11,54840,54840,11,54868,54868,11,54896,54896,11,54924,54924,11,54952,54952,11,54980,54980,11,55008,55008,11,55036,55036,11,55064,55064,11,55092,55092,11,55120,55120,11,55148,55148,11,55176,55176,11,55216,55238,9,64286,64286,5,65056,65071,5,65438,65439,5,65529,65531,4,66272,66272,5,68097,68099,5,68108,68111,5,68159,68159,5,68900,68903,5,69446,69456,5,69632,69632,7,69634,69634,7,69744,69744,5,69759,69761,5,69808,69810,7,69815,69816,7,69821,69821,1,69837,69837,1,69927,69931,5,69933,69940,5,70003,70003,5,70018,70018,7,70070,70078,5,70082,70083,1,70094,70094,7,70188,70190,7,70194,70195,7,70197,70197,7,70206,70206,5,70368,70370,7,70400,70401,5,70459,70460,5,70463,70463,7,70465,70468,7,70475,70477,7,70498,70499,7,70512,70516,5,70712,70719,5,70722,70724,5,70726,70726,5,70832,70832,5,70835,70840,5,70842,70842,5,70845,70845,5,70847,70848,5,70850,70851,5,71088,71089,7,71096,71099,7,71102,71102,7,71132,71133,5,71219,71226,5,71229,71229,5,71231,71232,5,71340,71340,7,71342,71343,7,71350,71350,7,71453,71455,5,71462,71462,7,71724,71726,7,71736,71736,7,71984,71984,5,71991,71992,7,71997,71997,7,71999,71999,1,72001,72001,1,72003,72003,5,72148,72151,5,72156,72159,7,72164,72164,7,72243,72248,5,72250,72250,1,72263,72263,5,72279,72280,7,72324,72329,1,72343,72343,7,72751,72751,7,72760,72765,5,72767,72767,5,72873,72873,7,72881,72881,7,72884,72884,7,73009,73014,5,73020,73021,5,73030,73030,1,73098,73102,7,73107,73108,7,73110,73110,7,73459,73460,5,78896,78904,4,92976,92982,5,94033,94087,7,94180,94180,5,113821,113822,5,118528,118573,5,119141,119141,5,119143,119145,5,119150,119154,5,119163,119170,5,119210,119213,5,121344,121398,5,121461,121461,5,121499,121503,5,122880,122886,5,122907,122913,5,122918,122922,5,123566,123566,5,125136,125142,5,126976,126979,14,126981,127182,14,127184,127231,14,127279,127279,14,127344,127345,14,127374,127374,14,127405,127461,14,127489,127490,14,127514,127514,14,127538,127546,14,127561,127567,14,127570,127743,14,127757,127758,14,127760,127760,14,127762,127762,14,127766,127768,14,127770,127770,14,127772,127772,14,127775,127776,14,127778,127779,14,127789,127791,14,127794,127795,14,127798,127798,14,127819,127819,14,127824,127824,14,127868,127868,14,127870,127871,14,127892,127893,14,127896,127896,14,127900,127901,14,127904,127940,14,127942,127942,14,127944,127944,14,127946,127946,14,127951,127955,14,127968,127971,14,127973,127984,14,127987,127987,14,127989,127989,14,127991,127991,14,127995,127999,5,128008,128008,14,128012,128014,14,128017,128018,14,128020,128020,14,128022,128022,14,128042,128042,14,128063,128063,14,128065,128065,14,128101,128101,14,128108,128109,14,128173,128173,14,128182,128183,14,128236,128237,14,128239,128239,14,128245,128245,14,128248,128248,14,128253,128253,14,128255,128258,14,128260,128263,14,128265,128265,14,128277,128277,14,128300,128301,14,128326,128328,14,128331,128334,14,128336,128347,14,128360,128366,14,128369,128370,14,128378,128378,14,128391,128391,14,128394,128397,14,128400,128400,14,128405,128406,14,128420,128420,14,128422,128423,14,128425,128432,14,128435,128443,14,128445,128449,14,128453,128464,14,128468,128475,14,128479,128480,14,128482,128482,14,128484,128487,14,128489,128494,14,128496,128498,14,128500,128505,14,128507,128511,14,128513,128518,14,128521,128525,14,128527,128527,14,128529,128529,14,128533,128533,14,128535,128535,14,128537,128537,14]'); + return JSON.parse('[0,0,0,51229,51255,12,44061,44087,12,127462,127487,6,7083,7085,5,47645,47671,12,54813,54839,12,128678,128678,14,3270,3270,5,9919,9923,14,45853,45879,12,49437,49463,12,53021,53047,12,71216,71218,7,128398,128399,14,129360,129374,14,2519,2519,5,4448,4519,9,9742,9742,14,12336,12336,14,44957,44983,12,46749,46775,12,48541,48567,12,50333,50359,12,52125,52151,12,53917,53943,12,69888,69890,5,73018,73018,5,127990,127990,14,128558,128559,14,128759,128760,14,129653,129655,14,2027,2035,5,2891,2892,7,3761,3761,5,6683,6683,5,8293,8293,4,9825,9826,14,9999,9999,14,43452,43453,5,44509,44535,12,45405,45431,12,46301,46327,12,47197,47223,12,48093,48119,12,48989,49015,12,49885,49911,12,50781,50807,12,51677,51703,12,52573,52599,12,53469,53495,12,54365,54391,12,65279,65279,4,70471,70472,7,72145,72147,7,119173,119179,5,127799,127818,14,128240,128244,14,128512,128512,14,128652,128652,14,128721,128722,14,129292,129292,14,129445,129450,14,129734,129743,14,1476,1477,5,2366,2368,7,2750,2752,7,3076,3076,5,3415,3415,5,4141,4144,5,6109,6109,5,6964,6964,5,7394,7400,5,9197,9198,14,9770,9770,14,9877,9877,14,9968,9969,14,10084,10084,14,43052,43052,5,43713,43713,5,44285,44311,12,44733,44759,12,45181,45207,12,45629,45655,12,46077,46103,12,46525,46551,12,46973,46999,12,47421,47447,12,47869,47895,12,48317,48343,12,48765,48791,12,49213,49239,12,49661,49687,12,50109,50135,12,50557,50583,12,51005,51031,12,51453,51479,12,51901,51927,12,52349,52375,12,52797,52823,12,53245,53271,12,53693,53719,12,54141,54167,12,54589,54615,12,55037,55063,12,69506,69509,5,70191,70193,5,70841,70841,7,71463,71467,5,72330,72342,5,94031,94031,5,123628,123631,5,127763,127765,14,127941,127941,14,128043,128062,14,128302,128317,14,128465,128467,14,128539,128539,14,128640,128640,14,128662,128662,14,128703,128703,14,128745,128745,14,129004,129007,14,129329,129330,14,129402,129402,14,129483,129483,14,129686,129704,14,130048,131069,14,173,173,4,1757,1757,1,2200,2207,5,2434,2435,7,2631,2632,5,2817,2817,5,3008,3008,5,3201,3201,5,3387,3388,5,3542,3542,5,3902,3903,7,4190,4192,5,6002,6003,5,6439,6440,5,6765,6770,7,7019,7027,5,7154,7155,7,8205,8205,13,8505,8505,14,9654,9654,14,9757,9757,14,9792,9792,14,9852,9853,14,9890,9894,14,9937,9937,14,9981,9981,14,10035,10036,14,11035,11036,14,42654,42655,5,43346,43347,7,43587,43587,5,44006,44007,7,44173,44199,12,44397,44423,12,44621,44647,12,44845,44871,12,45069,45095,12,45293,45319,12,45517,45543,12,45741,45767,12,45965,45991,12,46189,46215,12,46413,46439,12,46637,46663,12,46861,46887,12,47085,47111,12,47309,47335,12,47533,47559,12,47757,47783,12,47981,48007,12,48205,48231,12,48429,48455,12,48653,48679,12,48877,48903,12,49101,49127,12,49325,49351,12,49549,49575,12,49773,49799,12,49997,50023,12,50221,50247,12,50445,50471,12,50669,50695,12,50893,50919,12,51117,51143,12,51341,51367,12,51565,51591,12,51789,51815,12,52013,52039,12,52237,52263,12,52461,52487,12,52685,52711,12,52909,52935,12,53133,53159,12,53357,53383,12,53581,53607,12,53805,53831,12,54029,54055,12,54253,54279,12,54477,54503,12,54701,54727,12,54925,54951,12,55149,55175,12,68101,68102,5,69762,69762,7,70067,70069,7,70371,70378,5,70720,70721,7,71087,71087,5,71341,71341,5,71995,71996,5,72249,72249,7,72850,72871,5,73109,73109,5,118576,118598,5,121505,121519,5,127245,127247,14,127568,127569,14,127777,127777,14,127872,127891,14,127956,127967,14,128015,128016,14,128110,128172,14,128259,128259,14,128367,128368,14,128424,128424,14,128488,128488,14,128530,128532,14,128550,128551,14,128566,128566,14,128647,128647,14,128656,128656,14,128667,128673,14,128691,128693,14,128715,128715,14,128728,128732,14,128752,128752,14,128765,128767,14,129096,129103,14,129311,129311,14,129344,129349,14,129394,129394,14,129413,129425,14,129466,129471,14,129511,129535,14,129664,129666,14,129719,129722,14,129760,129767,14,917536,917631,5,13,13,2,1160,1161,5,1564,1564,4,1807,1807,1,2085,2087,5,2307,2307,7,2382,2383,7,2497,2500,5,2563,2563,7,2677,2677,5,2763,2764,7,2879,2879,5,2914,2915,5,3021,3021,5,3142,3144,5,3263,3263,5,3285,3286,5,3398,3400,7,3530,3530,5,3633,3633,5,3864,3865,5,3974,3975,5,4155,4156,7,4229,4230,5,5909,5909,7,6078,6085,7,6277,6278,5,6451,6456,7,6744,6750,5,6846,6846,5,6972,6972,5,7074,7077,5,7146,7148,7,7222,7223,5,7416,7417,5,8234,8238,4,8417,8417,5,9000,9000,14,9203,9203,14,9730,9731,14,9748,9749,14,9762,9763,14,9776,9783,14,9800,9811,14,9831,9831,14,9872,9873,14,9882,9882,14,9900,9903,14,9929,9933,14,9941,9960,14,9974,9974,14,9989,9989,14,10006,10006,14,10062,10062,14,10160,10160,14,11647,11647,5,12953,12953,14,43019,43019,5,43232,43249,5,43443,43443,5,43567,43568,7,43696,43696,5,43765,43765,7,44013,44013,5,44117,44143,12,44229,44255,12,44341,44367,12,44453,44479,12,44565,44591,12,44677,44703,12,44789,44815,12,44901,44927,12,45013,45039,12,45125,45151,12,45237,45263,12,45349,45375,12,45461,45487,12,45573,45599,12,45685,45711,12,45797,45823,12,45909,45935,12,46021,46047,12,46133,46159,12,46245,46271,12,46357,46383,12,46469,46495,12,46581,46607,12,46693,46719,12,46805,46831,12,46917,46943,12,47029,47055,12,47141,47167,12,47253,47279,12,47365,47391,12,47477,47503,12,47589,47615,12,47701,47727,12,47813,47839,12,47925,47951,12,48037,48063,12,48149,48175,12,48261,48287,12,48373,48399,12,48485,48511,12,48597,48623,12,48709,48735,12,48821,48847,12,48933,48959,12,49045,49071,12,49157,49183,12,49269,49295,12,49381,49407,12,49493,49519,12,49605,49631,12,49717,49743,12,49829,49855,12,49941,49967,12,50053,50079,12,50165,50191,12,50277,50303,12,50389,50415,12,50501,50527,12,50613,50639,12,50725,50751,12,50837,50863,12,50949,50975,12,51061,51087,12,51173,51199,12,51285,51311,12,51397,51423,12,51509,51535,12,51621,51647,12,51733,51759,12,51845,51871,12,51957,51983,12,52069,52095,12,52181,52207,12,52293,52319,12,52405,52431,12,52517,52543,12,52629,52655,12,52741,52767,12,52853,52879,12,52965,52991,12,53077,53103,12,53189,53215,12,53301,53327,12,53413,53439,12,53525,53551,12,53637,53663,12,53749,53775,12,53861,53887,12,53973,53999,12,54085,54111,12,54197,54223,12,54309,54335,12,54421,54447,12,54533,54559,12,54645,54671,12,54757,54783,12,54869,54895,12,54981,55007,12,55093,55119,12,55243,55291,10,66045,66045,5,68325,68326,5,69688,69702,5,69817,69818,5,69957,69958,7,70089,70092,5,70198,70199,5,70462,70462,5,70502,70508,5,70750,70750,5,70846,70846,7,71100,71101,5,71230,71230,7,71351,71351,5,71737,71738,5,72000,72000,7,72160,72160,5,72273,72278,5,72752,72758,5,72882,72883,5,73031,73031,5,73461,73462,7,94192,94193,7,119149,119149,7,121403,121452,5,122915,122916,5,126980,126980,14,127358,127359,14,127535,127535,14,127759,127759,14,127771,127771,14,127792,127793,14,127825,127867,14,127897,127899,14,127945,127945,14,127985,127986,14,128000,128007,14,128021,128021,14,128066,128100,14,128184,128235,14,128249,128252,14,128266,128276,14,128335,128335,14,128379,128390,14,128407,128419,14,128444,128444,14,128481,128481,14,128499,128499,14,128526,128526,14,128536,128536,14,128543,128543,14,128556,128556,14,128564,128564,14,128577,128580,14,128643,128645,14,128649,128649,14,128654,128654,14,128660,128660,14,128664,128664,14,128675,128675,14,128686,128689,14,128695,128696,14,128705,128709,14,128717,128719,14,128725,128725,14,128736,128741,14,128747,128748,14,128755,128755,14,128762,128762,14,128981,128991,14,129009,129023,14,129160,129167,14,129296,129304,14,129320,129327,14,129340,129342,14,129356,129356,14,129388,129392,14,129399,129400,14,129404,129407,14,129432,129442,14,129454,129455,14,129473,129474,14,129485,129487,14,129648,129651,14,129659,129660,14,129671,129679,14,129709,129711,14,129728,129730,14,129751,129753,14,129776,129782,14,917505,917505,4,917760,917999,5,10,10,3,127,159,4,768,879,5,1471,1471,5,1536,1541,1,1648,1648,5,1767,1768,5,1840,1866,5,2070,2073,5,2137,2139,5,2274,2274,1,2363,2363,7,2377,2380,7,2402,2403,5,2494,2494,5,2507,2508,7,2558,2558,5,2622,2624,7,2641,2641,5,2691,2691,7,2759,2760,5,2786,2787,5,2876,2876,5,2881,2884,5,2901,2902,5,3006,3006,5,3014,3016,7,3072,3072,5,3134,3136,5,3157,3158,5,3260,3260,5,3266,3266,5,3274,3275,7,3328,3329,5,3391,3392,7,3405,3405,5,3457,3457,5,3536,3537,7,3551,3551,5,3636,3642,5,3764,3772,5,3895,3895,5,3967,3967,7,3993,4028,5,4146,4151,5,4182,4183,7,4226,4226,5,4253,4253,5,4957,4959,5,5940,5940,7,6070,6070,7,6087,6088,7,6158,6158,4,6432,6434,5,6448,6449,7,6679,6680,5,6742,6742,5,6754,6754,5,6783,6783,5,6912,6915,5,6966,6970,5,6978,6978,5,7042,7042,7,7080,7081,5,7143,7143,7,7150,7150,7,7212,7219,5,7380,7392,5,7412,7412,5,8203,8203,4,8232,8232,4,8265,8265,14,8400,8412,5,8421,8432,5,8617,8618,14,9167,9167,14,9200,9200,14,9410,9410,14,9723,9726,14,9733,9733,14,9745,9745,14,9752,9752,14,9760,9760,14,9766,9766,14,9774,9774,14,9786,9786,14,9794,9794,14,9823,9823,14,9828,9828,14,9833,9850,14,9855,9855,14,9875,9875,14,9880,9880,14,9885,9887,14,9896,9897,14,9906,9916,14,9926,9927,14,9935,9935,14,9939,9939,14,9962,9962,14,9972,9972,14,9978,9978,14,9986,9986,14,9997,9997,14,10002,10002,14,10017,10017,14,10055,10055,14,10071,10071,14,10133,10135,14,10548,10549,14,11093,11093,14,12330,12333,5,12441,12442,5,42608,42610,5,43010,43010,5,43045,43046,5,43188,43203,7,43302,43309,5,43392,43394,5,43446,43449,5,43493,43493,5,43571,43572,7,43597,43597,7,43703,43704,5,43756,43757,5,44003,44004,7,44009,44010,7,44033,44059,12,44089,44115,12,44145,44171,12,44201,44227,12,44257,44283,12,44313,44339,12,44369,44395,12,44425,44451,12,44481,44507,12,44537,44563,12,44593,44619,12,44649,44675,12,44705,44731,12,44761,44787,12,44817,44843,12,44873,44899,12,44929,44955,12,44985,45011,12,45041,45067,12,45097,45123,12,45153,45179,12,45209,45235,12,45265,45291,12,45321,45347,12,45377,45403,12,45433,45459,12,45489,45515,12,45545,45571,12,45601,45627,12,45657,45683,12,45713,45739,12,45769,45795,12,45825,45851,12,45881,45907,12,45937,45963,12,45993,46019,12,46049,46075,12,46105,46131,12,46161,46187,12,46217,46243,12,46273,46299,12,46329,46355,12,46385,46411,12,46441,46467,12,46497,46523,12,46553,46579,12,46609,46635,12,46665,46691,12,46721,46747,12,46777,46803,12,46833,46859,12,46889,46915,12,46945,46971,12,47001,47027,12,47057,47083,12,47113,47139,12,47169,47195,12,47225,47251,12,47281,47307,12,47337,47363,12,47393,47419,12,47449,47475,12,47505,47531,12,47561,47587,12,47617,47643,12,47673,47699,12,47729,47755,12,47785,47811,12,47841,47867,12,47897,47923,12,47953,47979,12,48009,48035,12,48065,48091,12,48121,48147,12,48177,48203,12,48233,48259,12,48289,48315,12,48345,48371,12,48401,48427,12,48457,48483,12,48513,48539,12,48569,48595,12,48625,48651,12,48681,48707,12,48737,48763,12,48793,48819,12,48849,48875,12,48905,48931,12,48961,48987,12,49017,49043,12,49073,49099,12,49129,49155,12,49185,49211,12,49241,49267,12,49297,49323,12,49353,49379,12,49409,49435,12,49465,49491,12,49521,49547,12,49577,49603,12,49633,49659,12,49689,49715,12,49745,49771,12,49801,49827,12,49857,49883,12,49913,49939,12,49969,49995,12,50025,50051,12,50081,50107,12,50137,50163,12,50193,50219,12,50249,50275,12,50305,50331,12,50361,50387,12,50417,50443,12,50473,50499,12,50529,50555,12,50585,50611,12,50641,50667,12,50697,50723,12,50753,50779,12,50809,50835,12,50865,50891,12,50921,50947,12,50977,51003,12,51033,51059,12,51089,51115,12,51145,51171,12,51201,51227,12,51257,51283,12,51313,51339,12,51369,51395,12,51425,51451,12,51481,51507,12,51537,51563,12,51593,51619,12,51649,51675,12,51705,51731,12,51761,51787,12,51817,51843,12,51873,51899,12,51929,51955,12,51985,52011,12,52041,52067,12,52097,52123,12,52153,52179,12,52209,52235,12,52265,52291,12,52321,52347,12,52377,52403,12,52433,52459,12,52489,52515,12,52545,52571,12,52601,52627,12,52657,52683,12,52713,52739,12,52769,52795,12,52825,52851,12,52881,52907,12,52937,52963,12,52993,53019,12,53049,53075,12,53105,53131,12,53161,53187,12,53217,53243,12,53273,53299,12,53329,53355,12,53385,53411,12,53441,53467,12,53497,53523,12,53553,53579,12,53609,53635,12,53665,53691,12,53721,53747,12,53777,53803,12,53833,53859,12,53889,53915,12,53945,53971,12,54001,54027,12,54057,54083,12,54113,54139,12,54169,54195,12,54225,54251,12,54281,54307,12,54337,54363,12,54393,54419,12,54449,54475,12,54505,54531,12,54561,54587,12,54617,54643,12,54673,54699,12,54729,54755,12,54785,54811,12,54841,54867,12,54897,54923,12,54953,54979,12,55009,55035,12,55065,55091,12,55121,55147,12,55177,55203,12,55243,55291,10,64286,64286,5,65056,65071,5,65438,65439,5,65529,65531,4,66272,66272,5,68097,68099,5,68108,68111,5,68159,68159,5,68900,68903,5,69446,69456,5,69632,69632,7,69634,69634,7,69744,69744,5,69759,69761,5,69808,69810,7,69815,69816,7,69821,69821,1,69837,69837,1,69927,69931,5,69933,69940,5,70003,70003,5,70018,70018,7,70070,70078,5,70082,70083,1,70094,70094,7,70188,70190,7,70194,70195,7,70197,70197,7,70206,70206,5,70368,70370,7,70400,70401,5,70459,70460,5,70463,70463,7,70465,70468,7,70475,70477,7,70498,70499,7,70512,70516,5,70712,70719,5,70722,70724,5,70726,70726,5,70832,70832,5,70835,70840,5,70842,70842,5,70845,70845,5,70847,70848,5,70850,70851,5,71088,71089,7,71096,71099,7,71102,71102,7,71132,71133,5,71219,71226,5,71229,71229,5,71231,71232,5,71340,71340,7,71342,71343,7,71350,71350,7,71453,71455,5,71462,71462,7,71724,71726,7,71736,71736,7,71984,71984,5,71991,71992,7,71997,71997,7,71999,71999,1,72001,72001,1,72003,72003,5,72148,72151,5,72156,72159,7,72164,72164,7,72243,72248,5,72250,72250,1,72263,72263,5,72279,72280,7,72324,72329,1,72343,72343,7,72751,72751,7,72760,72765,5,72767,72767,5,72873,72873,7,72881,72881,7,72884,72884,7,73009,73014,5,73020,73021,5,73030,73030,1,73098,73102,7,73107,73108,7,73110,73110,7,73459,73460,5,78896,78904,4,92976,92982,5,94033,94087,7,113824,113827,4,119142,119142,7,119155,119162,4,119362,119364,5,121476,121476,5,122888,122904,5,123184,123190,5,125252,125258,5,127183,127183,14,127340,127343,14,127377,127386,14,127491,127503,14,127548,127551,14,127744,127756,14,127761,127761,14,127769,127769,14,127773,127774,14,127780,127788,14,127796,127797,14,127820,127823,14,127869,127869,14,127894,127895,14,127902,127903,14,127943,127943,14,127947,127950,14,127972,127972,14,127988,127988,14,127992,127994,14,128009,128011,14,128019,128019,14,128023,128041,14,128064,128064,14,128102,128107,14,128174,128181,14,128238,128238,14,128246,128247,14,128254,128254,14,128264,128264,14,128278,128299,14,128329,128330,14,128348,128359,14,128371,128377,14,128392,128393,14,128401,128404,14,128421,128421,14,128433,128434,14,128450,128452,14,128476,128478,14,128483,128483,14,128495,128495,14,128506,128506,14,128519,128520,14,128528,128528,14,128534,128534,14,128538,128538,14]'); } //#endregion @@ -1174,7 +1205,7 @@ export class AmbiguousCharacters { // Generated using https://github.com/hediet/vscode-unicode-data // Stored as key1, value1, key2, value2, ... return JSON.parse( - '{\"_common\":[8232,32,8233,32,5760,32,8192,32,8193,32,8194,32,8195,32,8196,32,8197,32,8198,32,8200,32,8201,32,8202,32,8287,32,8199,32,8239,32,2042,95,65101,95,65102,95,65103,95,8208,45,8209,45,8210,45,65112,45,1748,45,8259,45,727,45,8722,45,10134,45,11450,45,1549,44,1643,44,8218,44,184,44,42233,44,894,59,2307,58,2691,58,1417,58,1795,58,1796,58,5868,58,65072,58,6147,58,6153,58,8282,58,1475,58,760,58,42889,58,8758,58,720,58,42237,58,451,33,11601,33,660,63,577,63,2429,63,5038,63,42731,63,119149,46,8228,46,1793,46,1794,46,42510,46,68176,46,1632,46,1776,46,42232,46,1373,96,65287,96,8219,96,8242,96,1370,96,1523,96,8175,96,65344,96,900,96,8189,96,8125,96,8127,96,8190,96,697,96,884,96,712,96,714,96,715,96,756,96,699,96,701,96,700,96,702,96,42892,96,1497,96,2036,96,2037,96,5194,96,5836,96,94033,96,94034,96,65339,91,10088,40,10098,40,12308,40,64830,40,65341,93,10089,41,10099,41,12309,41,64831,41,10100,123,119060,123,10101,125,65342,94,8270,42,1645,42,8727,42,66335,42,5941,47,8257,47,8725,47,8260,47,9585,47,10187,47,10744,47,119354,47,12755,47,12339,47,11462,47,20031,47,12035,47,65340,92,65128,92,8726,92,10189,92,10741,92,10745,92,119311,92,119355,92,12756,92,20022,92,12034,92,42872,38,708,94,710,94,5869,43,10133,43,66203,43,8249,60,10094,60,706,60,119350,60,5176,60,5810,60,5120,61,11840,61,12448,61,42239,61,8250,62,10095,62,707,62,119351,62,5171,62,94015,62,8275,126,732,126,8128,126,8764,126,65372,124,65293,45,120784,50,120794,50,120804,50,120814,50,120824,50,130034,50,42842,50,423,50,1000,50,42564,50,5311,50,42735,50,119302,51,120785,51,120795,51,120805,51,120815,51,120825,51,130035,51,42923,51,540,51,439,51,42858,51,11468,51,1248,51,94011,51,71882,51,120786,52,120796,52,120806,52,120816,52,120826,52,130036,52,5070,52,71855,52,120787,53,120797,53,120807,53,120817,53,120827,53,130037,53,444,53,71867,53,120788,54,120798,54,120808,54,120818,54,120828,54,130038,54,11474,54,5102,54,71893,54,119314,55,120789,55,120799,55,120809,55,120819,55,120829,55,130039,55,66770,55,71878,55,2819,56,2538,56,2666,56,125131,56,120790,56,120800,56,120810,56,120820,56,120830,56,130040,56,547,56,546,56,66330,56,2663,57,2920,57,2541,57,3437,57,120791,57,120801,57,120811,57,120821,57,120831,57,130041,57,42862,57,11466,57,71884,57,71852,57,71894,57,9082,97,65345,97,119834,97,119886,97,119938,97,119990,97,120042,97,120094,97,120146,97,120198,97,120250,97,120302,97,120354,97,120406,97,120458,97,593,97,945,97,120514,97,120572,97,120630,97,120688,97,120746,97,65313,65,119808,65,119860,65,119912,65,119964,65,120016,65,120068,65,120120,65,120172,65,120224,65,120276,65,120328,65,120380,65,120432,65,913,65,120488,65,120546,65,120604,65,120662,65,120720,65,5034,65,5573,65,42222,65,94016,65,66208,65,119835,98,119887,98,119939,98,119991,98,120043,98,120095,98,120147,98,120199,98,120251,98,120303,98,120355,98,120407,98,120459,98,388,98,5071,98,5234,98,5551,98,65314,66,8492,66,119809,66,119861,66,119913,66,120017,66,120069,66,120121,66,120173,66,120225,66,120277,66,120329,66,120381,66,120433,66,42932,66,914,66,120489,66,120547,66,120605,66,120663,66,120721,66,5108,66,5623,66,42192,66,66178,66,66209,66,66305,66,65347,99,8573,99,119836,99,119888,99,119940,99,119992,99,120044,99,120096,99,120148,99,120200,99,120252,99,120304,99,120356,99,120408,99,120460,99,7428,99,1010,99,11429,99,43951,99,66621,99,128844,67,71922,67,71913,67,65315,67,8557,67,8450,67,8493,67,119810,67,119862,67,119914,67,119966,67,120018,67,120174,67,120226,67,120278,67,120330,67,120382,67,120434,67,1017,67,11428,67,5087,67,42202,67,66210,67,66306,67,66581,67,66844,67,8574,100,8518,100,119837,100,119889,100,119941,100,119993,100,120045,100,120097,100,120149,100,120201,100,120253,100,120305,100,120357,100,120409,100,120461,100,1281,100,5095,100,5231,100,42194,100,8558,68,8517,68,119811,68,119863,68,119915,68,119967,68,120019,68,120071,68,120123,68,120175,68,120227,68,120279,68,120331,68,120383,68,120435,68,5024,68,5598,68,5610,68,42195,68,8494,101,65349,101,8495,101,8519,101,119838,101,119890,101,119942,101,120046,101,120098,101,120150,101,120202,101,120254,101,120306,101,120358,101,120410,101,120462,101,43826,101,1213,101,8959,69,65317,69,8496,69,119812,69,119864,69,119916,69,120020,69,120072,69,120124,69,120176,69,120228,69,120280,69,120332,69,120384,69,120436,69,917,69,120492,69,120550,69,120608,69,120666,69,120724,69,11577,69,5036,69,42224,69,71846,69,71854,69,66182,69,119839,102,119891,102,119943,102,119995,102,120047,102,120099,102,120151,102,120203,102,120255,102,120307,102,120359,102,120411,102,120463,102,43829,102,42905,102,383,102,7837,102,1412,102,119315,70,8497,70,119813,70,119865,70,119917,70,120021,70,120073,70,120125,70,120177,70,120229,70,120281,70,120333,70,120385,70,120437,70,42904,70,988,70,120778,70,5556,70,42205,70,71874,70,71842,70,66183,70,66213,70,66853,70,65351,103,8458,103,119840,103,119892,103,119944,103,120048,103,120100,103,120152,103,120204,103,120256,103,120308,103,120360,103,120412,103,120464,103,609,103,7555,103,397,103,1409,103,119814,71,119866,71,119918,71,119970,71,120022,71,120074,71,120126,71,120178,71,120230,71,120282,71,120334,71,120386,71,120438,71,1292,71,5056,71,5107,71,42198,71,65352,104,8462,104,119841,104,119945,104,119997,104,120049,104,120101,104,120153,104,120205,104,120257,104,120309,104,120361,104,120413,104,120465,104,1211,104,1392,104,5058,104,65320,72,8459,72,8460,72,8461,72,119815,72,119867,72,119919,72,120023,72,120179,72,120231,72,120283,72,120335,72,120387,72,120439,72,919,72,120494,72,120552,72,120610,72,120668,72,120726,72,11406,72,5051,72,5500,72,42215,72,66255,72,731,105,9075,105,65353,105,8560,105,8505,105,8520,105,119842,105,119894,105,119946,105,119998,105,120050,105,120102,105,120154,105,120206,105,120258,105,120310,105,120362,105,120414,105,120466,105,120484,105,618,105,617,105,953,105,8126,105,890,105,120522,105,120580,105,120638,105,120696,105,120754,105,1110,105,42567,105,1231,105,43893,105,5029,105,71875,105,65354,106,8521,106,119843,106,119895,106,119947,106,119999,106,120051,106,120103,106,120155,106,120207,106,120259,106,120311,106,120363,106,120415,106,120467,106,1011,106,1112,106,65322,74,119817,74,119869,74,119921,74,119973,74,120025,74,120077,74,120129,74,120181,74,120233,74,120285,74,120337,74,120389,74,120441,74,42930,74,895,74,1032,74,5035,74,5261,74,42201,74,119844,107,119896,107,119948,107,120000,107,120052,107,120104,107,120156,107,120208,107,120260,107,120312,107,120364,107,120416,107,120468,107,8490,75,65323,75,119818,75,119870,75,119922,75,119974,75,120026,75,120078,75,120130,75,120182,75,120234,75,120286,75,120338,75,120390,75,120442,75,922,75,120497,75,120555,75,120613,75,120671,75,120729,75,11412,75,5094,75,5845,75,42199,75,66840,75,1472,108,8739,73,9213,73,65512,73,1633,108,1777,73,66336,108,125127,108,120783,73,120793,73,120803,73,120813,73,120823,73,130033,73,65321,73,8544,73,8464,73,8465,73,119816,73,119868,73,119920,73,120024,73,120128,73,120180,73,120232,73,120284,73,120336,73,120388,73,120440,73,65356,108,8572,73,8467,108,119845,108,119897,108,119949,108,120001,108,120053,108,120105,73,120157,73,120209,73,120261,73,120313,73,120365,73,120417,73,120469,73,448,73,120496,73,120554,73,120612,73,120670,73,120728,73,11410,73,1030,73,1216,73,1493,108,1503,108,1575,108,126464,108,126592,108,65166,108,65165,108,1994,108,11599,73,5825,73,42226,73,93992,73,66186,124,66313,124,119338,76,8556,76,8466,76,119819,76,119871,76,119923,76,120027,76,120079,76,120131,76,120183,76,120235,76,120287,76,120339,76,120391,76,120443,76,11472,76,5086,76,5290,76,42209,76,93974,76,71843,76,71858,76,66587,76,66854,76,65325,77,8559,77,8499,77,119820,77,119872,77,119924,77,120028,77,120080,77,120132,77,120184,77,120236,77,120288,77,120340,77,120392,77,120444,77,924,77,120499,77,120557,77,120615,77,120673,77,120731,77,1018,77,11416,77,5047,77,5616,77,5846,77,42207,77,66224,77,66321,77,119847,110,119899,110,119951,110,120003,110,120055,110,120107,110,120159,110,120211,110,120263,110,120315,110,120367,110,120419,110,120471,110,1400,110,1404,110,65326,78,8469,78,119821,78,119873,78,119925,78,119977,78,120029,78,120081,78,120185,78,120237,78,120289,78,120341,78,120393,78,120445,78,925,78,120500,78,120558,78,120616,78,120674,78,120732,78,11418,78,42208,78,66835,78,3074,111,3202,111,3330,111,3458,111,2406,111,2662,111,2790,111,3046,111,3174,111,3302,111,3430,111,3664,111,3792,111,4160,111,1637,111,1781,111,65359,111,8500,111,119848,111,119900,111,119952,111,120056,111,120108,111,120160,111,120212,111,120264,111,120316,111,120368,111,120420,111,120472,111,7439,111,7441,111,43837,111,959,111,120528,111,120586,111,120644,111,120702,111,120760,111,963,111,120532,111,120590,111,120648,111,120706,111,120764,111,11423,111,4351,111,1413,111,1505,111,1607,111,126500,111,126564,111,126596,111,65259,111,65260,111,65258,111,65257,111,1726,111,64428,111,64429,111,64427,111,64426,111,1729,111,64424,111,64425,111,64423,111,64422,111,1749,111,3360,111,4125,111,66794,111,71880,111,71895,111,66604,111,1984,79,2534,79,2918,79,12295,79,70864,79,71904,79,120782,79,120792,79,120802,79,120812,79,120822,79,130032,79,65327,79,119822,79,119874,79,119926,79,119978,79,120030,79,120082,79,120134,79,120186,79,120238,79,120290,79,120342,79,120394,79,120446,79,927,79,120502,79,120560,79,120618,79,120676,79,120734,79,11422,79,1365,79,11604,79,4816,79,2848,79,66754,79,42227,79,71861,79,66194,79,66219,79,66564,79,66838,79,9076,112,65360,112,119849,112,119901,112,119953,112,120005,112,120057,112,120109,112,120161,112,120213,112,120265,112,120317,112,120369,112,120421,112,120473,112,961,112,120530,112,120544,112,120588,112,120602,112,120646,112,120660,112,120704,112,120718,112,120762,112,120776,112,11427,112,65328,80,8473,80,119823,80,119875,80,119927,80,119979,80,120031,80,120083,80,120187,80,120239,80,120291,80,120343,80,120395,80,120447,80,929,80,120504,80,120562,80,120620,80,120678,80,120736,80,11426,80,5090,80,5229,80,42193,80,66197,80,119850,113,119902,113,119954,113,120006,113,120058,113,120110,113,120162,113,120214,113,120266,113,120318,113,120370,113,120422,113,120474,113,1307,113,1379,113,1382,113,8474,81,119824,81,119876,81,119928,81,119980,81,120032,81,120084,81,120188,81,120240,81,120292,81,120344,81,120396,81,120448,81,11605,81,119851,114,119903,114,119955,114,120007,114,120059,114,120111,114,120163,114,120215,114,120267,114,120319,114,120371,114,120423,114,120475,114,43847,114,43848,114,7462,114,11397,114,43905,114,119318,82,8475,82,8476,82,8477,82,119825,82,119877,82,119929,82,120033,82,120189,82,120241,82,120293,82,120345,82,120397,82,120449,82,422,82,5025,82,5074,82,66740,82,5511,82,42211,82,94005,82,65363,115,119852,115,119904,115,119956,115,120008,115,120060,115,120112,115,120164,115,120216,115,120268,115,120320,115,120372,115,120424,115,120476,115,42801,115,445,115,1109,115,43946,115,71873,115,66632,115,65331,83,119826,83,119878,83,119930,83,119982,83,120034,83,120086,83,120138,83,120190,83,120242,83,120294,83,120346,83,120398,83,120450,83,1029,83,1359,83,5077,83,5082,83,42210,83,94010,83,66198,83,66592,83,119853,116,119905,116,119957,116,120009,116,120061,116,120113,116,120165,116,120217,116,120269,116,120321,116,120373,116,120425,116,120477,116,8868,84,10201,84,128872,84,65332,84,119827,84,119879,84,119931,84,119983,84,120035,84,120087,84,120139,84,120191,84,120243,84,120295,84,120347,84,120399,84,120451,84,932,84,120507,84,120565,84,120623,84,120681,84,120739,84,11430,84,5026,84,42196,84,93962,84,71868,84,66199,84,66225,84,66325,84,119854,117,119906,117,119958,117,120010,117,120062,117,120114,117,120166,117,120218,117,120270,117,120322,117,120374,117,120426,117,120478,117,42911,117,7452,117,43854,117,43858,117,651,117,965,117,120534,117,120592,117,120650,117,120708,117,120766,117,1405,117,66806,117,71896,117,8746,85,8899,85,119828,85,119880,85,119932,85,119984,85,120036,85,120088,85,120140,85,120192,85,120244,85,120296,85,120348,85,120400,85,120452,85,1357,85,4608,85,66766,85,5196,85,42228,85,94018,85,71864,85,8744,118,8897,118,65366,118,8564,118,119855,118,119907,118,119959,118,120011,118,120063,118,120115,118,120167,118,120219,118,120271,118,120323,118,120375,118,120427,118,120479,118,7456,118,957,118,120526,118,120584,118,120642,118,120700,118,120758,118,1141,118,1496,118,71430,118,43945,118,71872,118,119309,86,1639,86,1783,86,8548,86,119829,86,119881,86,119933,86,119985,86,120037,86,120089,86,120141,86,120193,86,120245,86,120297,86,120349,86,120401,86,120453,86,1140,86,11576,86,5081,86,5167,86,42719,86,42214,86,93960,86,71840,86,66845,86,623,119,119856,119,119908,119,119960,119,120012,119,120064,119,120116,119,120168,119,120220,119,120272,119,120324,119,120376,119,120428,119,120480,119,7457,119,1121,119,1309,119,1377,119,71434,119,71438,119,71439,119,43907,119,71919,87,71910,87,119830,87,119882,87,119934,87,119986,87,120038,87,120090,87,120142,87,120194,87,120246,87,120298,87,120350,87,120402,87,120454,87,1308,87,5043,87,5076,87,42218,87,5742,120,10539,120,10540,120,10799,120,65368,120,8569,120,119857,120,119909,120,119961,120,120013,120,120065,120,120117,120,120169,120,120221,120,120273,120,120325,120,120377,120,120429,120,120481,120,5441,120,5501,120,5741,88,9587,88,66338,88,71916,88,65336,88,8553,88,119831,88,119883,88,119935,88,119987,88,120039,88,120091,88,120143,88,120195,88,120247,88,120299,88,120351,88,120403,88,120455,88,42931,88,935,88,120510,88,120568,88,120626,88,120684,88,120742,88,11436,88,11613,88,5815,88,42219,88,66192,88,66228,88,66327,88,66855,88,611,121,7564,121,65369,121,119858,121,119910,121,119962,121,120014,121,120066,121,120118,121,120170,121,120222,121,120274,121,120326,121,120378,121,120430,121,120482,121,655,121,7935,121,43866,121,947,121,8509,121,120516,121,120574,121,120632,121,120690,121,120748,121,1199,121,4327,121,71900,121,65337,89,119832,89,119884,89,119936,89,119988,89,120040,89,120092,89,120144,89,120196,89,120248,89,120300,89,120352,89,120404,89,120456,89,933,89,978,89,120508,89,120566,89,120624,89,120682,89,120740,89,11432,89,1198,89,5033,89,5053,89,42220,89,94019,89,71844,89,66226,89,119859,122,119911,122,119963,122,120015,122,120067,122,120119,122,120171,122,120223,122,120275,122,120327,122,120379,122,120431,122,120483,122,7458,122,43923,122,71876,122,66293,90,71909,90,65338,90,8484,90,8488,90,119833,90,119885,90,119937,90,119989,90,120041,90,120197,90,120249,90,120301,90,120353,90,120405,90,120457,90,918,90,120493,90,120551,90,120609,90,120667,90,120725,90,5059,90,42204,90,71849,90,65282,34,65284,36,65285,37,65286,38,65290,42,65291,43,65294,46,65295,47,65296,48,65297,49,65298,50,65299,51,65300,52,65301,53,65302,54,65303,55,65304,56,65305,57,65308,60,65309,61,65310,62,65312,64,65316,68,65318,70,65319,71,65324,76,65329,81,65330,82,65333,85,65334,86,65335,87,65343,95,65346,98,65348,100,65350,102,65355,107,65357,109,65358,110,65361,113,65362,114,65364,116,65365,117,65367,119,65370,122,65371,123,65373,125,119846,109],\"_default\":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"cs\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"de\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"es\":[8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"fr\":[65374,126,65306,58,65281,33,8216,96,8245,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"it\":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"ja\":[8211,45,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65292,44,65307,59],\"ko\":[8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"pl\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"pt-BR\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"qps-ploc\":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"ru\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,305,105,921,73,1009,112,215,120,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"tr\":[160,32,8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"zh-hans\":[65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41],\"zh-hant\":[8211,45,65374,126,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65307,59]}' + '{\"_common\":[8232,32,8233,32,5760,32,8192,32,8193,32,8194,32,8195,32,8196,32,8197,32,8198,32,8200,32,8201,32,8202,32,8287,32,8199,32,8239,32,2042,95,65101,95,65102,95,65103,95,8208,45,8209,45,8210,45,65112,45,1748,45,8259,45,727,45,8722,45,10134,45,11450,45,1549,44,1643,44,8218,44,184,44,42233,44,894,59,2307,58,2691,58,1417,58,1795,58,1796,58,5868,58,65072,58,6147,58,6153,58,8282,58,1475,58,760,58,42889,58,8758,58,720,58,42237,58,451,33,11601,33,660,63,577,63,2429,63,5038,63,42731,63,119149,46,8228,46,1793,46,1794,46,42510,46,68176,46,1632,46,1776,46,42232,46,1373,96,65287,96,8219,96,8242,96,1370,96,1523,96,8175,96,65344,96,900,96,8189,96,8125,96,8127,96,8190,96,697,96,884,96,712,96,714,96,715,96,756,96,699,96,701,96,700,96,702,96,42892,96,1497,96,2036,96,2037,96,5194,96,5836,96,94033,96,94034,96,65339,91,10088,40,10098,40,12308,40,64830,40,65341,93,10089,41,10099,41,12309,41,64831,41,10100,123,119060,123,10101,125,65342,94,8270,42,1645,42,8727,42,66335,42,5941,47,8257,47,8725,47,8260,47,9585,47,10187,47,10744,47,119354,47,12755,47,12339,47,11462,47,20031,47,12035,47,65340,92,65128,92,8726,92,10189,92,10741,92,10745,92,119311,92,119355,92,12756,92,20022,92,12034,92,42872,38,708,94,710,94,5869,43,10133,43,66203,43,8249,60,10094,60,706,60,119350,60,5176,60,5810,60,5120,61,11840,61,12448,61,42239,61,8250,62,10095,62,707,62,119351,62,5171,62,94015,62,8275,126,732,126,8128,126,8764,126,65372,124,65293,45,120784,50,120794,50,120804,50,120814,50,120824,50,130034,50,42842,50,423,50,1000,50,42564,50,5311,50,42735,50,119302,51,120785,51,120795,51,120805,51,120815,51,120825,51,130035,51,42923,51,540,51,439,51,42858,51,11468,51,1248,51,94011,51,71882,51,120786,52,120796,52,120806,52,120816,52,120826,52,130036,52,5070,52,71855,52,120787,53,120797,53,120807,53,120817,53,120827,53,130037,53,444,53,71867,53,120788,54,120798,54,120808,54,120818,54,120828,54,130038,54,11474,54,5102,54,71893,54,119314,55,120789,55,120799,55,120809,55,120819,55,120829,55,130039,55,66770,55,71878,55,2819,56,2538,56,2666,56,125131,56,120790,56,120800,56,120810,56,120820,56,120830,56,130040,56,547,56,546,56,66330,56,2663,57,2920,57,2541,57,3437,57,120791,57,120801,57,120811,57,120821,57,120831,57,130041,57,42862,57,11466,57,71884,57,71852,57,71894,57,9082,97,65345,97,119834,97,119886,97,119938,97,119990,97,120042,97,120094,97,120146,97,120198,97,120250,97,120302,97,120354,97,120406,97,120458,97,593,97,945,97,120514,97,120572,97,120630,97,120688,97,120746,97,65313,65,119808,65,119860,65,119912,65,119964,65,120016,65,120068,65,120120,65,120172,65,120224,65,120276,65,120328,65,120380,65,120432,65,913,65,120488,65,120546,65,120604,65,120662,65,120720,65,5034,65,5573,65,42222,65,94016,65,66208,65,119835,98,119887,98,119939,98,119991,98,120043,98,120095,98,120147,98,120199,98,120251,98,120303,98,120355,98,120407,98,120459,98,388,98,5071,98,5234,98,5551,98,65314,66,8492,66,119809,66,119861,66,119913,66,120017,66,120069,66,120121,66,120173,66,120225,66,120277,66,120329,66,120381,66,120433,66,42932,66,914,66,120489,66,120547,66,120605,66,120663,66,120721,66,5108,66,5623,66,42192,66,66178,66,66209,66,66305,66,65347,99,8573,99,119836,99,119888,99,119940,99,119992,99,120044,99,120096,99,120148,99,120200,99,120252,99,120304,99,120356,99,120408,99,120460,99,7428,99,1010,99,11429,99,43951,99,66621,99,128844,67,71922,67,71913,67,65315,67,8557,67,8450,67,8493,67,119810,67,119862,67,119914,67,119966,67,120018,67,120174,67,120226,67,120278,67,120330,67,120382,67,120434,67,1017,67,11428,67,5087,67,42202,67,66210,67,66306,67,66581,67,66844,67,8574,100,8518,100,119837,100,119889,100,119941,100,119993,100,120045,100,120097,100,120149,100,120201,100,120253,100,120305,100,120357,100,120409,100,120461,100,1281,100,5095,100,5231,100,42194,100,8558,68,8517,68,119811,68,119863,68,119915,68,119967,68,120019,68,120071,68,120123,68,120175,68,120227,68,120279,68,120331,68,120383,68,120435,68,5024,68,5598,68,5610,68,42195,68,8494,101,65349,101,8495,101,8519,101,119838,101,119890,101,119942,101,120046,101,120098,101,120150,101,120202,101,120254,101,120306,101,120358,101,120410,101,120462,101,43826,101,1213,101,8959,69,65317,69,8496,69,119812,69,119864,69,119916,69,120020,69,120072,69,120124,69,120176,69,120228,69,120280,69,120332,69,120384,69,120436,69,917,69,120492,69,120550,69,120608,69,120666,69,120724,69,11577,69,5036,69,42224,69,71846,69,71854,69,66182,69,119839,102,119891,102,119943,102,119995,102,120047,102,120099,102,120151,102,120203,102,120255,102,120307,102,120359,102,120411,102,120463,102,43829,102,42905,102,383,102,7837,102,1412,102,119315,70,8497,70,119813,70,119865,70,119917,70,120021,70,120073,70,120125,70,120177,70,120229,70,120281,70,120333,70,120385,70,120437,70,42904,70,988,70,120778,70,5556,70,42205,70,71874,70,71842,70,66183,70,66213,70,66853,70,65351,103,8458,103,119840,103,119892,103,119944,103,120048,103,120100,103,120152,103,120204,103,120256,103,120308,103,120360,103,120412,103,120464,103,609,103,7555,103,397,103,1409,103,119814,71,119866,71,119918,71,119970,71,120022,71,120074,71,120126,71,120178,71,120230,71,120282,71,120334,71,120386,71,120438,71,1292,71,5056,71,5107,71,42198,71,65352,104,8462,104,119841,104,119945,104,119997,104,120049,104,120101,104,120153,104,120205,104,120257,104,120309,104,120361,104,120413,104,120465,104,1211,104,1392,104,5058,104,65320,72,8459,72,8460,72,8461,72,119815,72,119867,72,119919,72,120023,72,120179,72,120231,72,120283,72,120335,72,120387,72,120439,72,919,72,120494,72,120552,72,120610,72,120668,72,120726,72,11406,72,5051,72,5500,72,42215,72,66255,72,731,105,9075,105,65353,105,8560,105,8505,105,8520,105,119842,105,119894,105,119946,105,119998,105,120050,105,120102,105,120154,105,120206,105,120258,105,120310,105,120362,105,120414,105,120466,105,120484,105,618,105,617,105,953,105,8126,105,890,105,120522,105,120580,105,120638,105,120696,105,120754,105,1110,105,42567,105,1231,105,43893,105,5029,105,71875,105,65354,106,8521,106,119843,106,119895,106,119947,106,119999,106,120051,106,120103,106,120155,106,120207,106,120259,106,120311,106,120363,106,120415,106,120467,106,1011,106,1112,106,65322,74,119817,74,119869,74,119921,74,119973,74,120025,74,120077,74,120129,74,120181,74,120233,74,120285,74,120337,74,120389,74,120441,74,42930,74,895,74,1032,74,5035,74,5261,74,42201,74,119844,107,119896,107,119948,107,120000,107,120052,107,120104,107,120156,107,120208,107,120260,107,120312,107,120364,107,120416,107,120468,107,8490,75,65323,75,119818,75,119870,75,119922,75,119974,75,120026,75,120078,75,120130,75,120182,75,120234,75,120286,75,120338,75,120390,75,120442,75,922,75,120497,75,120555,75,120613,75,120671,75,120729,75,11412,75,5094,75,5845,75,42199,75,66840,75,1472,108,8739,73,9213,73,65512,73,1633,108,1777,73,66336,108,125127,108,120783,73,120793,73,120803,73,120813,73,120823,73,130033,73,65321,73,8544,73,8464,73,8465,73,119816,73,119868,73,119920,73,120024,73,120128,73,120180,73,120232,73,120284,73,120336,73,120388,73,120440,73,65356,108,8572,73,8467,108,119845,108,119897,108,119949,108,120001,108,120053,108,120105,73,120157,73,120209,73,120261,73,120313,73,120365,73,120417,73,120469,73,448,73,120496,73,120554,73,120612,73,120670,73,120728,73,11410,73,1030,73,1216,73,1493,108,1503,108,1575,108,126464,108,126592,108,65166,108,65165,108,1994,108,11599,73,5825,73,42226,73,93992,73,66186,124,66313,124,119338,76,8556,76,8466,76,119819,76,119871,76,119923,76,120027,76,120079,76,120131,76,120183,76,120235,76,120287,76,120339,76,120391,76,120443,76,11472,76,5086,76,5290,76,42209,76,93974,76,71843,76,71858,76,66587,76,66854,76,65325,77,8559,77,8499,77,119820,77,119872,77,119924,77,120028,77,120080,77,120132,77,120184,77,120236,77,120288,77,120340,77,120392,77,120444,77,924,77,120499,77,120557,77,120615,77,120673,77,120731,77,1018,77,11416,77,5047,77,5616,77,5846,77,42207,77,66224,77,66321,77,119847,110,119899,110,119951,110,120003,110,120055,110,120107,110,120159,110,120211,110,120263,110,120315,110,120367,110,120419,110,120471,110,1400,110,1404,110,65326,78,8469,78,119821,78,119873,78,119925,78,119977,78,120029,78,120081,78,120185,78,120237,78,120289,78,120341,78,120393,78,120445,78,925,78,120500,78,120558,78,120616,78,120674,78,120732,78,11418,78,42208,78,66835,78,3074,111,3202,111,3330,111,3458,111,2406,111,2662,111,2790,111,3046,111,3174,111,3302,111,3430,111,3664,111,3792,111,4160,111,1637,111,1781,111,65359,111,8500,111,119848,111,119900,111,119952,111,120056,111,120108,111,120160,111,120212,111,120264,111,120316,111,120368,111,120420,111,120472,111,7439,111,7441,111,43837,111,959,111,120528,111,120586,111,120644,111,120702,111,120760,111,963,111,120532,111,120590,111,120648,111,120706,111,120764,111,11423,111,4351,111,1413,111,1505,111,1607,111,126500,111,126564,111,126596,111,65259,111,65260,111,65258,111,65257,111,1726,111,64428,111,64429,111,64427,111,64426,111,1729,111,64424,111,64425,111,64423,111,64422,111,1749,111,3360,111,4125,111,66794,111,71880,111,71895,111,66604,111,1984,79,2534,79,2918,79,12295,79,70864,79,71904,79,120782,79,120792,79,120802,79,120812,79,120822,79,130032,79,65327,79,119822,79,119874,79,119926,79,119978,79,120030,79,120082,79,120134,79,120186,79,120238,79,120290,79,120342,79,120394,79,120446,79,927,79,120502,79,120560,79,120618,79,120676,79,120734,79,11422,79,1365,79,11604,79,4816,79,2848,79,66754,79,42227,79,71861,79,66194,79,66219,79,66564,79,66838,79,9076,112,65360,112,119849,112,119901,112,119953,112,120005,112,120057,112,120109,112,120161,112,120213,112,120265,112,120317,112,120369,112,120421,112,120473,112,961,112,120530,112,120544,112,120588,112,120602,112,120646,112,120660,112,120704,112,120718,112,120762,112,120776,112,11427,112,65328,80,8473,80,119823,80,119875,80,119927,80,119979,80,120031,80,120083,80,120187,80,120239,80,120291,80,120343,80,120395,80,120447,80,929,80,120504,80,120562,80,120620,80,120678,80,120736,80,11426,80,5090,80,5229,80,42193,80,66197,80,119850,113,119902,113,119954,113,120006,113,120058,113,120110,113,120162,113,120214,113,120266,113,120318,113,120370,113,120422,113,120474,113,1307,113,1379,113,1382,113,8474,81,119824,81,119876,81,119928,81,119980,81,120032,81,120084,81,120188,81,120240,81,120292,81,120344,81,120396,81,120448,81,11605,81,119851,114,119903,114,119955,114,120007,114,120059,114,120111,114,120163,114,120215,114,120267,114,120319,114,120371,114,120423,114,120475,114,43847,114,43848,114,7462,114,11397,114,43905,114,119318,82,8475,82,8476,82,8477,82,119825,82,119877,82,119929,82,120033,82,120189,82,120241,82,120293,82,120345,82,120397,82,120449,82,422,82,5025,82,5074,82,66740,82,5511,82,42211,82,94005,82,65363,115,119852,115,119904,115,119956,115,120008,115,120060,115,120112,115,120164,115,120216,115,120268,115,120320,115,120372,115,120424,115,120476,115,42801,115,445,115,1109,115,43946,115,71873,115,66632,115,65331,83,119826,83,119878,83,119930,83,119982,83,120034,83,120086,83,120138,83,120190,83,120242,83,120294,83,120346,83,120398,83,120450,83,1029,83,1359,83,5077,83,5082,83,42210,83,94010,83,66198,83,66592,83,119853,116,119905,116,119957,116,120009,116,120061,116,120113,116,120165,116,120217,116,120269,116,120321,116,120373,116,120425,116,120477,116,8868,84,10201,84,128872,84,65332,84,119827,84,119879,84,119931,84,119983,84,120035,84,120087,84,120139,84,120191,84,120243,84,120295,84,120347,84,120399,84,120451,84,932,84,120507,84,120565,84,120623,84,120681,84,120739,84,11430,84,5026,84,42196,84,93962,84,71868,84,66199,84,66225,84,66325,84,119854,117,119906,117,119958,117,120010,117,120062,117,120114,117,120166,117,120218,117,120270,117,120322,117,120374,117,120426,117,120478,117,42911,117,7452,117,43854,117,43858,117,651,117,965,117,120534,117,120592,117,120650,117,120708,117,120766,117,1405,117,66806,117,71896,117,8746,85,8899,85,119828,85,119880,85,119932,85,119984,85,120036,85,120088,85,120140,85,120192,85,120244,85,120296,85,120348,85,120400,85,120452,85,1357,85,4608,85,66766,85,5196,85,42228,85,94018,85,71864,85,8744,118,8897,118,65366,118,8564,118,119855,118,119907,118,119959,118,120011,118,120063,118,120115,118,120167,118,120219,118,120271,118,120323,118,120375,118,120427,118,120479,118,7456,118,957,118,120526,118,120584,118,120642,118,120700,118,120758,118,1141,118,1496,118,71430,118,43945,118,71872,118,119309,86,1639,86,1783,86,8548,86,119829,86,119881,86,119933,86,119985,86,120037,86,120089,86,120141,86,120193,86,120245,86,120297,86,120349,86,120401,86,120453,86,1140,86,11576,86,5081,86,5167,86,42719,86,42214,86,93960,86,71840,86,66845,86,623,119,119856,119,119908,119,119960,119,120012,119,120064,119,120116,119,120168,119,120220,119,120272,119,120324,119,120376,119,120428,119,120480,119,7457,119,1121,119,1309,119,1377,119,71434,119,71438,119,71439,119,43907,119,71919,87,71910,87,119830,87,119882,87,119934,87,119986,87,120038,87,120090,87,120142,87,120194,87,120246,87,120298,87,120350,87,120402,87,120454,87,1308,87,5043,87,5076,87,42218,87,5742,120,10539,120,10540,120,10799,120,65368,120,8569,120,119857,120,119909,120,119961,120,120013,120,120065,120,120117,120,120169,120,120221,120,120273,120,120325,120,120377,120,120429,120,120481,120,5441,120,5501,120,5741,88,9587,88,66338,88,71916,88,65336,88,8553,88,119831,88,119883,88,119935,88,119987,88,120039,88,120091,88,120143,88,120195,88,120247,88,120299,88,120351,88,120403,88,120455,88,42931,88,935,88,120510,88,120568,88,120626,88,120684,88,120742,88,11436,88,11613,88,5815,88,42219,88,66192,88,66228,88,66327,88,66855,88,611,121,7564,121,65369,121,119858,121,119910,121,119962,121,120014,121,120066,121,120118,121,120170,121,120222,121,120274,121,120326,121,120378,121,120430,121,120482,121,655,121,7935,121,43866,121,947,121,8509,121,120516,121,120574,121,120632,121,120690,121,120748,121,1199,121,4327,121,71900,121,65337,89,119832,89,119884,89,119936,89,119988,89,120040,89,120092,89,120144,89,120196,89,120248,89,120300,89,120352,89,120404,89,120456,89,933,89,978,89,120508,89,120566,89,120624,89,120682,89,120740,89,11432,89,1198,89,5033,89,5053,89,42220,89,94019,89,71844,89,66226,89,119859,122,119911,122,119963,122,120015,122,120067,122,120119,122,120171,122,120223,122,120275,122,120327,122,120379,122,120431,122,120483,122,7458,122,43923,122,71876,122,66293,90,71909,90,65338,90,8484,90,8488,90,119833,90,119885,90,119937,90,119989,90,120041,90,120197,90,120249,90,120301,90,120353,90,120405,90,120457,90,918,90,120493,90,120551,90,120609,90,120667,90,120725,90,5059,90,42204,90,71849,90,65282,34,65284,36,65285,37,65286,38,65290,42,65291,43,65294,46,65295,47,65296,48,65297,49,65298,50,65299,51,65300,52,65301,53,65302,54,65303,55,65304,56,65305,57,65308,60,65309,61,65310,62,65312,64,65316,68,65318,70,65319,71,65324,76,65329,81,65330,82,65333,85,65334,86,65335,87,65343,95,65346,98,65348,100,65350,102,65355,107,65357,109,65358,110,65361,113,65362,114,65364,116,65365,117,65367,119,65370,122,65371,123,65373,125,119846,109],\"_default\":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"cs\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"de\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"es\":[8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"fr\":[65374,126,65306,58,65281,33,8216,96,8245,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"it\":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"ja\":[8211,45,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65292,44,65307,59],\"ko\":[8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"pl\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"pt-BR\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"qps-ploc\":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"ru\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,305,105,921,73,1009,112,215,120,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"tr\":[160,32,8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"zh-hans\":[65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41],\"zh-hant\":[8211,45,65374,126,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65307,59]}' ); }); diff --git a/src/vs/editor/common/core/position.ts b/src/vs/editor/common/core/position.ts index c6bc5da82ba3..e3033e953f9f 100644 --- a/src/vs/editor/common/core/position.ts +++ b/src/vs/editor/common/core/position.ts @@ -56,7 +56,7 @@ export class Position { * @param deltaColumn column delta */ delta(deltaLineNumber: number = 0, deltaColumn: number = 0): Position { - return this.with(this.lineNumber + deltaLineNumber, this.column + deltaColumn); + return this.with(Math.max(1, this.lineNumber + deltaLineNumber), Math.max(1, this.column + deltaColumn)); } /** diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index 8ab3798c9fb5..40ac18994392 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -19,6 +19,7 @@ import { ChatAgentLocation } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { ChatViewId, EditsViewId, IChatWidgetService } from '../chat.js'; +import { ctxIsGlobalEditingSession } from '../chatEditorController.js'; import { ChatEditorInput } from '../chatEditorInput.js'; import { ChatViewPane } from '../chatViewPane.js'; import { CHAT_CATEGORY } from './chatActions.js'; @@ -332,6 +333,7 @@ export function registerNewChatActions() { order: 2 }, { id: MenuId.ChatEditingEditorContent, + when: ctxIsGlobalEditingSession, group: 'navigate', order: 4, }], diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 37c4dbb7403a..d6b4115efb48 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -81,6 +81,7 @@ import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesCon import { ChatQuotasService, ChatQuotasStatusBarEntry, IChatQuotasService } from './chatQuotasService.js'; import { BuiltinToolsContribution } from './tools/tools.js'; import { ChatSetupContribution } from './chatSetup.js'; +import { ChatEditorOverlayController } from './chatEditorOverlay.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -334,6 +335,7 @@ registerChatDeveloperActions(); registerChatEditorActions(); registerEditorFeature(ChatPasteProvidersFeature); +registerEditorContribution(ChatEditorOverlayController.ID, ChatEditorOverlayController, EditorContributionInstantiation.Lazy); registerEditorContribution(ChatEditorController.ID, ChatEditorController, EditorContributionInstantiation.Eventually); registerSingleton(IChatService, ChatService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 080f2b23c151..bc18d5eeee3b 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -187,7 +187,7 @@ export interface IChatWidgetViewOptions { defaultElementHeight?: number; editorOverflowWidgetsDomNode?: HTMLElement; enableImplicitContext?: boolean; - enableWorkingSet?: boolean; + enableWorkingSet?: 'explicit' | 'implicit'; } export interface IChatViewViewContext { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 91821810a13b..e030552e8366 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { RunOnceScheduler } from '../../../../../base/common/async.js'; -import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Disposable, IReference, toDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; @@ -305,10 +304,9 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie this._clearCurrentEditLineDecoration(); // AUTO accept mode - if (!this.reviewMode.get()) { + if (!this.reviewMode.get() && !this._autoAcceptCtrl.get()) { const future = Date.now() + (this._autoAcceptTimeout.get() * 1000); - const cts = new CancellationTokenSource(); const update = () => { const reviewMode = this.reviewMode.get(); @@ -318,17 +316,15 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie return; } - if (cts.token.isCancellationRequested) { - this._autoAcceptCtrl.set(undefined, undefined); - return; - } - const remain = Math.round((future - Date.now()) / 1000); if (remain <= 0) { this.accept(undefined); } else { - this._autoAcceptCtrl.set(new AutoAcceptControl(remain, () => cts.cancel()), undefined); - setTimeout(update, 100); + const handle = setTimeout(update, 100); + this._autoAcceptCtrl.set(new AutoAcceptControl(remain, () => { + clearTimeout(handle); + this._autoAcceptCtrl.set(undefined, undefined); + }), undefined); } }; update(); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts index bddeb051851f..fa18d8628e0d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts @@ -242,7 +242,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic this._currentSessionDisposables.clear(); - const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, this._editingSessionFileLimitPromise, this._lookupEntry.bind(this)); + const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, true, this._editingSessionFileLimitPromise, this._lookupEntry.bind(this)); await session.init(); // listen for completed responses, run the code mapper and apply the edits to this edit session @@ -258,7 +258,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic } async createAdhocEditingSession(chatSessionId: string): Promise { - const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, this._editingSessionFileLimitPromise, this._lookupEntry.bind(this)); + const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, false, this._editingSessionFileLimitPromise, this._lookupEntry.bind(this)); await session.init(); const list = this._adhocSessionsObs.get(); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 27a7bebfb4f1..f6a2e9daa54f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -155,18 +155,14 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return this._onDidDispose.event; } - get isVisible(): boolean { - this._assertNotDisposed(); - return Boolean(this._editorPane && this._editorPane.isVisible()); - } - private _isToolsAgentSession = false; get isToolsAgentSession(): boolean { return this._isToolsAgentSession; } constructor( - public readonly chatSessionId: string, + readonly chatSessionId: string, + readonly isGlobalEditingSession: boolean, private editingSessionFileLimitPromise: Promise, private _lookupExternalEntry: (uri: URI) => ChatEditingModifiedFileEntry | undefined, @IInstantiationService private readonly _instantiationService: IInstantiationService, diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index 66127bd38a7c..c9137403d01c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -35,8 +35,9 @@ import { isDiffEditorForEntry } from './chatEditing/chatEditing.js'; import { basename, isEqual } from '../../../../base/common/resources.js'; import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; import { EditorsOrder, IEditorIdentifier, isDiffEditorInput } from '../../../common/editor.js'; -import { ChatEditorOverlayWidget } from './chatEditorOverlay.js'; +import { ChatEditorOverlayController } from './chatEditorOverlay.js'; +export const ctxIsGlobalEditingSession = new RawContextKey('chat.isGlobalEditingSession', undefined, localize('chat.ctxEditSessionIsGlobal', "The current editor is part of the global edit session")); export const ctxHasEditorModification = new RawContextKey('chat.hasEditorModifications', undefined, localize('chat.hasEditorModifications', "The current editor contains chat modifications")); export const ctxHasRequestInProgress = new RawContextKey('chat.ctxHasRequestInProgress', false, localize('chat.ctxHasRequestInProgress', "The current editor shows a file from an edit session which is still in progress")); export const ctxReviewModeEnabled = new RawContextKey('chat.ctxReviewModeEnabled', true, localize('chat.ctxReviewModeEnabled', "Review mode for chat changes is enabled")); @@ -54,8 +55,9 @@ export class ChatEditorController extends Disposable implements IEditorContribut private _viewZones: string[] = []; - private readonly _overlayWidget: ChatEditorOverlayWidget; + private readonly _overlayCtrl: ChatEditorOverlayController; + private readonly _ctxIsGlobalEditsSession: IContextKey; private readonly _ctxHasEditorModification: IContextKey; private readonly _ctxRequestInProgress: IContextKey; private readonly _ctxReviewModelEnabled: IContextKey; @@ -83,7 +85,8 @@ export class ChatEditorController extends Disposable implements IEditorContribut ) { super(); - this._overlayWidget = _instantiationService.createInstance(ChatEditorOverlayWidget, _editor); + this._overlayCtrl = ChatEditorOverlayController.get(_editor)!; + this._ctxIsGlobalEditsSession = ctxIsGlobalEditingSession.bindTo(contextKeyService); this._ctxHasEditorModification = ctxHasEditorModification.bindTo(contextKeyService); this._ctxRequestInProgress = ctxHasRequestInProgress.bindTo(contextKeyService); this._ctxReviewModelEnabled = ctxReviewModeEnabled.bindTo(contextKeyService); @@ -129,6 +132,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut const currentEditorEntry = entryForEditor.read(r); if (!currentEditorEntry) { + this._ctxIsGlobalEditsSession.reset(); this._clear(); didReval = false; return; @@ -141,6 +145,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut const { session, entries, idx, entry } = currentEditorEntry; + this._ctxIsGlobalEditsSession.set(session.isGlobalEditingSession); this._ctxReviewModelEnabled.set(entry.reviewMode.read(r)); // context @@ -148,9 +153,9 @@ export class ChatEditorController extends Disposable implements IEditorContribut // overlay widget if (entry.state.read(r) !== WorkingSetEntryState.Modified) { - this._overlayWidget.hide(); + this._overlayCtrl.hide(); } else { - this._overlayWidget.show(session, entry, entries[(idx + 1) % entries.length]); + this._overlayCtrl.showEntry(session, entry, entries[(idx + 1) % entries.length]); } // scrolling logic @@ -242,7 +247,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut private _clear() { this._clearDiffRendering(); - this._overlayWidget.hide(); + this._overlayCtrl.hide(); this._diffLineDecorations.clear(); this._currentChangeIndex.set(undefined, undefined); this._currentEntryIndex.set(undefined, undefined); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index c37d9a4d4109..5593f309fed7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -24,8 +24,12 @@ import { AcceptAction, navigationBearingFakeActionId, RejectAction } from './cha import { ChatEditorController } from './chatEditorController.js'; import './media/chatEditorOverlay.css'; import { findDiffEditorContainingCodeEditor } from '../../../../editor/browser/widget/diffEditor/commands.js'; +import { IChatService } from '../common/chatService.js'; +import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; +import { rcut } from '../../../../base/common/strings.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -export class ChatEditorOverlayWidget implements IOverlayWidget { +class ChatEditorOverlayWidget implements IOverlayWidget { readonly allowEditorOverflow = true; @@ -43,6 +47,8 @@ export class ChatEditorOverlayWidget implements IOverlayWidget { constructor( private readonly _editor: ICodeEditor, @IEditorService editorService: IEditorService, + @IHoverService private readonly _hoverService: IHoverService, + @IChatService private readonly _chatService: IChatService, @IInstantiationService private readonly _instaService: IInstantiationService, ) { this._domNode = document.createElement('div'); @@ -212,7 +218,32 @@ export class ChatEditorOverlayWidget implements IOverlayWidget { return { preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER }; } - show(session: IChatEditingSession, activeEntry: IModifiedFileEntry, next: IModifiedFileEntry) { + showRequest(session: IChatEditingSession) { + + this._showStore.clear(); + + const chatModel = this._chatService.getSession(session.chatSessionId); + const chatRequest = chatModel?.getRequests().at(-1); + + if (!chatRequest || !chatRequest.response) { + this.hide(); + return; + } + + this._domNode.classList.toggle('busy', true); + + const message = rcut(chatRequest.message.text, 47); + reset(this._progressNode, message); + + this._showStore.add(this._hoverService.setupDelayedHover(this._progressNode, { + content: chatRequest.message.text, + appearance: { showPointer: true } + })); + + this._show(); + } + + showEntry(session: IChatEditingSession, activeEntry: IModifiedFileEntry, next: IModifiedFileEntry) { this._showStore.clear(); @@ -226,8 +257,8 @@ export class ChatEditorOverlayWidget implements IOverlayWidget { this._showStore.add(autorun(r => { const value = activeEntry.rewriteRatio.read(r); reset(this._progressNode, (value === 0 - ? localize('generating', "Generating edits...") - : localize('applyingPercentage', "{0}% Applying edits...", Math.round(value * 100)))); + ? localize('generating', "Generating edits") + : localize('applyingPercentage', "{0}% Applying edits", Math.round(value * 100)))); })); this._showStore.add(autorun(r => { @@ -256,8 +287,13 @@ export class ChatEditorOverlayWidget implements IOverlayWidget { this._navigationBearings.set({ changeCount: changes, activeIdx, entriesCount: entries.length }, undefined); })); + this._show(); + + } + + private _show(): void { - const editorWithObs = observableFromEvent(this._editor.onDidLayoutChange, () => { + const editorWidthObs = observableFromEvent(this._editor.onDidLayoutChange, () => { const diffEditor = this._instaService.invokeFunction(findDiffEditorContainingCodeEditor, this._editor); return diffEditor ? diffEditor.getOriginalEditor().getLayoutInfo().contentWidth + diffEditor.getModifiedEditor().getLayoutInfo().contentWidth @@ -265,7 +301,7 @@ export class ChatEditorOverlayWidget implements IOverlayWidget { }); this._showStore.add(autorun(r => { - const width = editorWithObs.read(r); + const width = editorWidthObs.read(r); this._domNode.style.maxWidth = `${width - 20}px`; })); @@ -289,3 +325,40 @@ export class ChatEditorOverlayWidget implements IOverlayWidget { } } } + + +export class ChatEditorOverlayController implements IEditorContribution { + + static readonly ID = 'editor.contrib.chatEditorOverlayController'; + + static get(editor: ICodeEditor): ChatEditorOverlayController | undefined { + return editor.getContribution(ChatEditorOverlayController.ID) ?? undefined; + } + + private readonly _overlayWidget: ChatEditorOverlayWidget; + + constructor( + private readonly _editor: ICodeEditor, + @IInstantiationService private readonly _instaService: IInstantiationService, + ) { + this._overlayWidget = this._instaService.createInstance(ChatEditorOverlayWidget, this._editor); + + } + + dispose(): void { + this.hide(); + this._overlayWidget.dispose(); + } + + showRequest(session: IChatEditingSession) { + this._overlayWidget.showRequest(session); + } + + showEntry(session: IChatEditingSession, activeEntry: IModifiedFileEntry, next: IModifiedFileEntry) { + this._overlayWidget.showEntry(session, activeEntry, next); + } + + hide() { + this._overlayWidget.hide(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 4102bbfe91dd..7d6618c8b35b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -124,7 +124,7 @@ interface IChatInputPartOptions { }; editorOverflowWidgetsDomNode?: HTMLElement; enableImplicitContext?: boolean; - enableWorkingSet?: boolean; + renderWorkingSet?: boolean; } export interface IWorkingSetEntry { @@ -1138,7 +1138,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge async renderChatEditingSessionState(chatEditingSession: IChatEditingSession | null, chatWidget?: IChatWidget) { dom.setVisibility(Boolean(chatEditingSession), this.chatEditingSessionWidgetContainer); - if (!chatEditingSession || this.options.enableWorkingSet === false) { + if (!chatEditingSession || !this.options.renderWorkingSet) { dom.clearNode(this.chatEditingSessionWidgetContainer); this._chatEditsDisposables.clear(); this._chatEditList = undefined; diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index 742ae8cbf6b6..8455f189e6e3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -185,6 +185,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { }, enableImplicitContext: this.chatOptions.location === ChatAgentLocation.Panel, editorOverflowWidgetsDomNode: editorOverflowNode, + enableWorkingSet: this.chatOptions.location === ChatAgentLocation.EditingSession ? 'explicit' : undefined }, { listForeground: SIDE_BAR_FOREGROUND, diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index ad10fd0adea0..b37c4622c583 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -15,7 +15,7 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, combinedDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../base/common/map.js'; import { Schemas } from '../../../../base/common/network.js'; -import { autorun } from '../../../../base/common/observable.js'; +import { autorunWithStore, observableFromEvent } from '../../../../base/common/observable.js'; import { extUri, isEqual } from '../../../../base/common/resources.js'; import { isDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; @@ -187,6 +187,8 @@ export class ChatWidget extends Disposable implements IChatWidget { return this._viewModel; } + private _editingSession: IChatEditingSession | undefined; + private parsedChatRequest: IParsedChatRequest | undefined; get parsedInput() { if (this.parsedChatRequest === undefined) { @@ -249,38 +251,42 @@ export class ChatWidget extends Disposable implements IChatWidget { this._codeBlockModelCollection = this._register(instantiationService.createInstance(CodeBlockModelCollection)); - const chatEditingSessionDisposables = this._register(new DisposableStore()); - this._register(autorun(r => { - const session = this.chatEditingService.currentEditingSessionObs.read(r); + const viewModelObs = observableFromEvent(this, this.onDidChangeViewModel, () => this.viewModel); + + this._register(autorunWithStore((r, store) => { + + const viewModel = viewModelObs.read(r); + const sessions = chatEditingService.editingSessionsObs.read(r); + + const session = sessions.find(candidate => candidate.chatSessionId === viewModel?.sessionId); + this._editingSession = undefined; + this.renderChatEditingSessionState(); // this is necessary to make sure we dispose previous buttons, etc. + if (!session) { + // none or for a different chat widget return; } - if (session.chatSessionId !== this.viewModel?.sessionId) { - // this chat editing session is for a different chat widget - return; - } - // make sure to clean up anything related to the prev session (if any) - chatEditingSessionDisposables.clear(); - this.renderChatEditingSessionState(null); // this is necessary to make sure we dispose previous buttons, etc. - chatEditingSessionDisposables.add(session.onDidChange(() => { - this.renderChatEditingSessionState(session); + this._editingSession = session; + + store.add(session.onDidChange(() => { + this.renderChatEditingSessionState(); })); - chatEditingSessionDisposables.add(session.onDidDispose(() => { - chatEditingSessionDisposables.clear(); - this.renderChatEditingSessionState(null); + store.add(session.onDidDispose(() => { + this._editingSession = undefined; + this.renderChatEditingSessionState(); })); - chatEditingSessionDisposables.add(this.onDidChangeParsedInput(() => { - this.renderChatEditingSessionState(session); + store.add(this.onDidChangeParsedInput(() => { + this.renderChatEditingSessionState(); })); - chatEditingSessionDisposables.add(this.inputEditor.onDidChangeModelContent(() => { + store.add(this.inputEditor.onDidChangeModelContent(() => { if (this.getInput() === '') { this.refreshParsedInput(); - this.renderChatEditingSessionState(session); + this.renderChatEditingSessionState(); } })); - this.renderChatEditingSessionState(session); + this.renderChatEditingSessionState(); })); if (this._location.location === ChatAgentLocation.EditingSession) { @@ -289,7 +295,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const sessionId = this._viewModel?.sessionId; if (sessionId) { if (sessionId !== currentEditSession?.chatSessionId) { - currentEditSession = await this.chatEditingService.startOrContinueEditingSession(sessionId); + currentEditSession = await chatEditingService.startOrContinueEditingSession(sessionId); } } else { if (currentEditSession) { @@ -581,8 +587,11 @@ export class ChatWidget extends Disposable implements IChatWidget { } } - private async renderChatEditingSessionState(session: IChatEditingSession | null) { - this.inputPart.renderChatEditingSessionState(session, this); + private async renderChatEditingSessionState() { + if (!this.inputPart) { + return; + } + this.inputPart.renderChatEditingSessionState(this._editingSession ?? null, this); if (this.bodyDimension) { this.layout(this.bodyDimension.height, this.bodyDimension.width); @@ -764,7 +773,7 @@ export class ChatWidget extends Disposable implements IChatWidget { menus: { executeToolbar: MenuId.ChatExecute, ...this.viewOptions.menus }, editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode, enableImplicitContext: this.viewOptions.enableImplicitContext, - enableWorkingSet: this.viewOptions.enableWorkingSet + renderWorkingSet: this.viewOptions.enableWorkingSet === 'explicit' }, this.styles, () => this.collectInputState() @@ -830,9 +839,9 @@ export class ChatWidget extends Disposable implements IChatWidget { this._onDidChangeContentHeight.fire(); })); this._register(this.inputPart.attachmentModel.onDidChangeContext(() => { - if (this.chatEditingService.currentEditingSession && this.chatEditingService.currentEditingSession?.chatSessionId === this.viewModel?.sessionId) { + if (this._editingSession) { // TODO still needed? Do this inside input part and fire onDidChangeHeight? - this.renderChatEditingSessionState(this.chatEditingService.currentEditingSession); + this.renderChatEditingSessionState(); } })); this._register(this.inputEditor.onDidChangeModelContent(() => { @@ -873,8 +882,8 @@ export class ChatWidget extends Disposable implements IChatWidget { this.scrollToEnd(); } - if (this.chatEditingService.currentEditingSession && this.chatEditingService.currentEditingSession?.chatSessionId === this.viewModel?.sessionId) { - this.renderChatEditingSessionState(this.chatEditingService.currentEditingSession); + if (this._editingSession) { + this.renderChatEditingSessionState(); } })); this.viewModelDisposables.add(this.viewModel.onDidDisposeModel(() => { @@ -1009,12 +1018,13 @@ export class ChatWidget extends Disposable implements IChatWidget { let attachedContext = this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId); let workingSet: URI[] | undefined; - if (this.location === ChatAgentLocation.EditingSession) { - const currentEditingSession = this.chatEditingService.currentEditingSessionObs.get(); + 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[] = []; + // 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) { diff --git a/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css index eb1603f43d1b..90d4859bc6cc 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css @@ -38,6 +38,8 @@ padding: 0px 5px; font-size: 12px; font-variant-numeric: tabular-nums; + overflow: hidden; + white-space: nowrap; } .chat-editor-overlay-widget.busy .chat-editor-overlay-progress { @@ -49,6 +51,33 @@ /* font-style: italic; */ } +@keyframes ellipsis { + 0% { + content: ""; + } + 25% { + content: "."; + } + 50% { + content: ".."; + } + 75% { + content: "..."; + } + 100% { + content: ""; + } +} + +.chat-editor-overlay-widget.busy .chat-editor-overlay-progress .busy-label::after { + content: ""; + display: inline-flex; + white-space: nowrap; + overflow: hidden; + width: 3ch; + animation: ellipsis steps(4, end) 1s infinite; +} + .chat-editor-overlay-widget.busy .chat-editor-overlay-toolbar { display: none; } diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 71f168eb0973..ee3a0a6ef039 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -76,13 +76,13 @@ export interface WorkingSetDisplayMetadata { } export interface IChatEditingSession { + readonly isGlobalEditingSession: boolean; readonly chatSessionId: string; readonly onDidChange: Event; readonly onDidDispose: Event; readonly state: IObservable; readonly entries: IObservable; readonly workingSet: ResourceMap; - readonly isVisible: boolean; readonly isToolsAgentSession: boolean; addFileToWorkingSet(uri: URI, description?: string, kind?: WorkingSetEntryState.Transient | WorkingSetEntryState.Suggested): void; show(): Promise; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts index b83695ed642f..aa1e91b8f13c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts @@ -4,33 +4,41 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { autorun, observableFromEvent } from '../../../../base/common/observable.js'; +import { autorun, autorunWithStore, constObservable, derived, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; +import { isEqual } from '../../../../base/common/resources.js'; import { assertType } from '../../../../base/common/types.js'; import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; import { EditorAction2, ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; -import { ObservableCodeEditor, observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diffEditor/embeddedDiffEditorWidget.js'; import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; -import { localize2 } from '../../../../nls.js'; -import { IAction2Options, MenuId } from '../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { localize, localize2 } from '../../../../nls.js'; +import { IAction2Options, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { ctxIsGlobalEditingSession } from '../../chat/browser/chatEditorController.js'; +import { ChatEditorOverlayController } from '../../chat/browser/chatEditorOverlay.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; import { ChatAgentLocation } from '../../chat/common/chatAgents.js'; -import { isRequestVM } from '../../chat/common/chatViewModel.js'; +import { WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; import { CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_POSSIBLE, CTX_INLINE_CHAT_VISIBLE } from '../common/inlineChat.js'; -import { IInlineChatSessionService } from './inlineChatSessionService.js'; +import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; + +export const CTX_HAS_SESSION = new RawContextKey('inlineChatHasSession', undefined, localize('chat.hasInlineChatSession', "The current editor has an active inline chat session")); + + export class InlineChatController2 implements IEditorContribution { static readonly ID = 'editor.contrib.inlineChatController2'; @@ -41,20 +49,18 @@ export class InlineChatController2 implements IEditorContribution { private readonly _store = new DisposableStore(); - private readonly _editorObs: ObservableCodeEditor; - - // private readonly _session: IObservable; - // private readonly _zone: InlineChatZoneWidget; + private readonly _showWidgetOverrideObs = observableValue(this, false); constructor( private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instaService: IInstantiationService, @INotebookEditorService private readonly _notebookEditorService: INotebookEditorService, - @IInlineChatSessionService private readonly _inlineChatSessions: IInlineChatSessionService, + @IInlineChatSessionService inlineChatSessions: IInlineChatSessionService, @IContextKeyService contextKeyService: IContextKeyService, ) { + const ctxHasSession = CTX_HAS_SESSION.bindTo(contextKeyService); const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); const location: IChatWidgetLocationOptions = { @@ -87,48 +93,118 @@ export class InlineChatController2 implements IEditorContribution { const zone = this._instaService.createInstance(InlineChatZoneWidget, location, { - enableWorkingSet: false, - filter: item => isRequestVM(item) + enableWorkingSet: 'implicit', + // filter: item => isRequestVM(item), + rendererOptions: { + renderCodeBlockPills: true, + renderTextEditsAsSummary: uri => isEqual(uri, _editor.getModel()?.uri) + } }, this._editor ); + const overlay = ChatEditorOverlayController.get(_editor)!; - this._editorObs = observableCodeEditor(_editor); + const editorObs = observableCodeEditor(_editor); - const sessionObs = observableFromEvent(this, _inlineChatSessions.onDidChangeSessions, () => { - const model = _editor.getModel(); - if (!model) { - return undefined; - } - return _inlineChatSessions.getSession2(_editor, model.uri); + const sessionsSignal = observableSignalFromEvent(this, inlineChatSessions.onDidChangeSessions); + + const sessionObs = derived(r => { + sessionsSignal.read(r); + const model = editorObs.model.read(r); + const value = model && inlineChatSessions.getSession2(_editor, model.uri); + return value ?? undefined; }); + this._store.add(autorun(r => { - const position = this._editorObs.cursorPosition.read(r); - const value = sessionObs.read(r); - if (!value || !position) { + const session = sessionObs.read(r); + ctxHasSession.set(Boolean(session)); + })); + + const visibleSessionObs = observableValue(this, undefined); + + this._store.add(autorunWithStore((r, store) => { + + const session = sessionObs.read(r); + + if (!session) { + visibleSessionObs.set(undefined, undefined); + return; + } + + const { chatModel } = session; + const showShowUntil = this._showWidgetOverrideObs.read(r); + const hasNoRequests = chatModel.getRequests().length === 0; + + store.add(chatModel.onDidChange(e => { + if (e.kind === 'addRequest') { + transaction(tx => { + this._showWidgetOverrideObs.set(false, tx); + visibleSessionObs.set(undefined, tx); + }); + } + })); + + if (showShowUntil || hasNoRequests) { + visibleSessionObs.set(session, undefined); + } else { + visibleSessionObs.set(undefined, undefined); + } + })); + + this._store.add(autorun(r => { + + const session = visibleSessionObs.read(r); + + if (!session) { zone.hide(); + _editor.focus(); ctxInlineChatVisible.reset(); } else { - zone.widget.setChatModel(value.chatModel); ctxInlineChatVisible.set(true); + zone.widget.setChatModel(session.chatModel); if (!zone.position) { - zone.show(position); + zone.show(session.initialPosition); + } else { + zone.reveal(zone.position); } + zone.widget.focus(); + session.editingSession.getEntry(session.uri)?.autoAcceptController.get()?.cancel(); } })); + this._store.add(autorun(r => { + + const session = sessionObs.read(r); + const model = editorObs.model.read(r); + if (!session || !model) { + overlay.hide(); + return; + } + + const lastResponse = observableFromEvent(this, session.chatModel.onDidChange, () => session.chatModel.getRequests().at(-1)?.response); + const response = lastResponse.read(r); + + const isInProgress = response + ? observableFromEvent(this, response.onDidChange, () => !response.isComplete) + : constObservable(false); + + if (isInProgress.read(r)) { + overlay.showRequest(session.editingSession); + } else if (session.editingSession.getEntry(session.uri)?.state.get() !== WorkingSetEntryState.Modified) { + overlay.hide(); + } + })); } dispose(): void { this._store.dispose(); } - async start() { - assertType(this._editor.hasModel()); - const textModel = this._editor.getModel(); - await this._inlineChatSessions.createSession2(this._editor, textModel.uri, CancellationToken.None); + toggleWidgetUntilNextRequest() { + const value = this._showWidgetOverrideObs.get(); + this._showWidgetOverrideObs.set(!value, undefined); } } @@ -141,6 +217,7 @@ export class StartSessionAction2 extends EditorAction2 { precondition: ContextKeyExpr.and( CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_POSSIBLE, + CTX_HAS_SESSION.negate(), EditorContextKeys.writable, EditorContextKeys.editorSimpleInput.negate() ), @@ -226,12 +303,10 @@ export class StopSessionAction2 extends AbstractInlineChatAction { id: 'inlineChat2.stop', title: localize2('stop', "Stop"), f1: true, - category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and( - CTX_INLINE_CHAT_VISIBLE, + CTX_HAS_SESSION, CTX_INLINE_CHAT_VISIBLE ), keybinding: { - when: EditorContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.Escape }, @@ -247,3 +322,36 @@ export class StopSessionAction2 extends AbstractInlineChatAction { inlineChatSessions.getSession2(editor, textModel.uri)?.dispose(); } } + +class RevealWidget extends AbstractInlineChatAction { + constructor() { + super({ + id: 'inlineChat2.reveal', + title: localize2('reveal', "Toggle Inline Chat"), + f1: true, + icon: Codicon.copilot, + precondition: ContextKeyExpr.and( + CTX_HAS_SESSION, + ), + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyI + }, + menu: { + id: MenuId.ChatEditingEditorContent, + when: ContextKeyExpr.and( + CTX_HAS_SESSION, + ctxIsGlobalEditingSession.negate(), + ), + group: 'z', + order: 4, + } + }); + } + + runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController2, editor: ICodeEditor, ...args: any[]): void { + ctrl.toggleWidgetUntilNextRequest(); + } +} + +registerAction2(RevealWidget); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts index 80c212a5517e..93be522ac5be 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts @@ -7,6 +7,7 @@ import { Event } from '../../../../base/common/event.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { IActiveCodeEditor, ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { Position } from '../../../../editor/common/core/position.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { IValidEditOperation } from '../../../../editor/common/model.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; @@ -30,6 +31,8 @@ export interface IInlineChatSessionEndEvent extends IInlineChatSessionEvent { } export interface IInlineChatSession2 { + readonly initialPosition: Position; + readonly uri: URI; readonly chatModel: IChatModel; readonly editingSession: IChatEditingSession; dispose(): void; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 60b9f7fa3294..767d074b27e5 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -30,7 +30,9 @@ import { IInlineChatSession2, IInlineChatSessionEndEvent, IInlineChatSessionEven import { isEqual } from '../../../../base/common/resources.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; -import { IChatEditingService } from '../../chat/common/chatEditingService.js'; +import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; +import { assertType } from '../../../../base/common/types.js'; +import { autorun } from '../../../../base/common/observable.js'; type SessionData = { @@ -324,6 +326,8 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { async createSession2(editor: ICodeEditor, uri: URI, token: CancellationToken): Promise { + assertType(editor.hasModel()); + const key = this._key(editor, uri); if (this._sessions2.has(key)) { throw new Error('Session already exists'); @@ -336,16 +340,30 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const editingSession = await this._chatEditingService.createAdhocEditingSession(chatModel.sessionId); editingSession.addFileToWorkingSet(uri); + const store = new DisposableStore(); + store.add(toDisposable(() => { + editingSession.reject(); + this._sessions2.delete(key); + this._onDidChangeSessions.fire(this); + })); + store.add(editingSession); + store.add(chatModel); + + store.add(autorun(r => { + const entry = editingSession.readEntry(uri, r); + const state = entry?.state.read(r); + if (state === WorkingSetEntryState.Accepted || state === WorkingSetEntryState.Rejected) { + // self terminate + store.dispose(); + } + })); + const result: IInlineChatSession2 = { + uri, + initialPosition: editor.getPosition().delta(-1), chatModel, editingSession, - dispose: () => { - if (this._sessions2.delete(key)) { - editingSession.dispose(); - chatModel.dispose(); - this._onDidChangeSessions.fire(this); - } - } + dispose: store.dispose.bind(store) }; this._sessions2.set(key, result); this._onDidChangeSessions.fire(this); From efd5750e2d2d636c8a6584959ed7421e52198ed4 Mon Sep 17 00:00:00 2001 From: Johannes Date: Thu, 23 Jan 2025 15:46:54 +0100 Subject: [PATCH 0811/3587] undo change --- src/vs/base/common/strings.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index c08ea72c5dca..609d99f9ceae 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -1113,7 +1113,7 @@ class GraphemeBreakTree { function getGraphemeBreakRawData(): number[] { // generated using https://github.com/alexdima/unicode-utils/blob/main/grapheme-break.js - return JSON.parse('[0,0,0,51229,51255,12,44061,44087,12,127462,127487,6,7083,7085,5,47645,47671,12,54813,54839,12,128678,128678,14,3270,3270,5,9919,9923,14,45853,45879,12,49437,49463,12,53021,53047,12,71216,71218,7,128398,128399,14,129360,129374,14,2519,2519,5,4448,4519,9,9742,9742,14,12336,12336,14,44957,44983,12,46749,46775,12,48541,48567,12,50333,50359,12,52125,52151,12,53917,53943,12,69888,69890,5,73018,73018,5,127990,127990,14,128558,128559,14,128759,128760,14,129653,129655,14,2027,2035,5,2891,2892,7,3761,3761,5,6683,6683,5,8293,8293,4,9825,9826,14,9999,9999,14,43452,43453,5,44509,44535,12,45405,45431,12,46301,46327,12,47197,47223,12,48093,48119,12,48989,49015,12,49885,49911,12,50781,50807,12,51677,51703,12,52573,52599,12,53469,53495,12,54365,54391,12,65279,65279,4,70471,70472,7,72145,72147,7,119173,119179,5,127799,127818,14,128240,128244,14,128512,128512,14,128652,128652,14,128721,128722,14,129292,129292,14,129445,129450,14,129734,129743,14,1476,1477,5,2366,2368,7,2750,2752,7,3076,3076,5,3415,3415,5,4141,4144,5,6109,6109,5,6964,6964,5,7394,7400,5,9197,9198,14,9770,9770,14,9877,9877,14,9968,9969,14,10084,10084,14,43052,43052,5,43713,43713,5,44285,44311,12,44733,44759,12,45181,45207,12,45629,45655,12,46077,46103,12,46525,46551,12,46973,46999,12,47421,47447,12,47869,47895,12,48317,48343,12,48765,48791,12,49213,49239,12,49661,49687,12,50109,50135,12,50557,50583,12,51005,51031,12,51453,51479,12,51901,51927,12,52349,52375,12,52797,52823,12,53245,53271,12,53693,53719,12,54141,54167,12,54589,54615,12,55037,55063,12,69506,69509,5,70191,70193,5,70841,70841,7,71463,71467,5,72330,72342,5,94031,94031,5,123628,123631,5,127763,127765,14,127941,127941,14,128043,128062,14,128302,128317,14,128465,128467,14,128539,128539,14,128640,128640,14,128662,128662,14,128703,128703,14,128745,128745,14,129004,129007,14,129329,129330,14,129402,129402,14,129483,129483,14,129686,129704,14,130048,131069,14,173,173,4,1757,1757,1,2200,2207,5,2434,2435,7,2631,2632,5,2817,2817,5,3008,3008,5,3201,3201,5,3387,3388,5,3542,3542,5,3902,3903,7,4190,4192,5,6002,6003,5,6439,6440,5,6765,6770,7,7019,7027,5,7154,7155,7,8205,8205,13,8505,8505,14,9654,9654,14,9757,9757,14,9792,9792,14,9852,9853,14,9890,9894,14,9937,9937,14,9981,9981,14,10035,10036,14,11035,11036,14,42654,42655,5,43346,43347,7,43587,43587,5,44006,44007,7,44173,44199,12,44397,44423,12,44621,44647,12,44845,44871,12,45069,45095,12,45293,45319,12,45517,45543,12,45741,45767,12,45965,45991,12,46189,46215,12,46413,46439,12,46637,46663,12,46861,46887,12,47085,47111,12,47309,47335,12,47533,47559,12,47757,47783,12,47981,48007,12,48205,48231,12,48429,48455,12,48653,48679,12,48877,48903,12,49101,49127,12,49325,49351,12,49549,49575,12,49773,49799,12,49997,50023,12,50221,50247,12,50445,50471,12,50669,50695,12,50893,50919,12,51117,51143,12,51341,51367,12,51565,51591,12,51789,51815,12,52013,52039,12,52237,52263,12,52461,52487,12,52685,52711,12,52909,52935,12,53133,53159,12,53357,53383,12,53581,53607,12,53805,53831,12,54029,54055,12,54253,54279,12,54477,54503,12,54701,54727,12,54925,54951,12,55149,55175,12,68101,68102,5,69762,69762,7,70067,70069,7,70371,70378,5,70720,70721,7,71087,71087,5,71341,71341,5,71995,71996,5,72249,72249,7,72850,72871,5,73109,73109,5,118576,118598,5,121505,121519,5,127245,127247,14,127568,127569,14,127777,127777,14,127872,127891,14,127956,127967,14,128015,128016,14,128110,128172,14,128259,128259,14,128367,128368,14,128424,128424,14,128488,128488,14,128530,128532,14,128550,128551,14,128566,128566,14,128647,128647,14,128656,128656,14,128667,128673,14,128691,128693,14,128715,128715,14,128728,128732,14,128752,128752,14,128765,128767,14,129096,129103,14,129311,129311,14,129344,129349,14,129394,129394,14,129413,129425,14,129466,129471,14,129511,129535,14,129664,129666,14,129719,129722,14,129760,129767,14,917536,917631,5,13,13,2,1160,1161,5,1564,1564,4,1807,1807,1,2085,2087,5,2307,2307,7,2382,2383,7,2497,2500,5,2563,2563,7,2677,2677,5,2763,2764,7,2879,2879,5,2914,2915,5,3021,3021,5,3142,3144,5,3263,3263,5,3285,3286,5,3398,3400,7,3530,3530,5,3633,3633,5,3864,3865,5,3974,3975,5,4155,4156,7,4229,4230,5,5909,5909,7,6078,6085,7,6277,6278,5,6451,6456,7,6744,6750,5,6846,6846,5,6972,6972,5,7074,7077,5,7146,7148,7,7222,7223,5,7416,7417,5,8234,8238,4,8417,8417,5,9000,9000,14,9203,9203,14,9730,9731,14,9748,9749,14,9762,9763,14,9776,9783,14,9800,9811,14,9831,9831,14,9872,9873,14,9882,9882,14,9900,9903,14,9929,9933,14,9941,9960,14,9974,9974,14,9989,9989,14,10006,10006,14,10062,10062,14,10160,10160,14,11647,11647,5,12953,12953,14,43019,43019,5,43232,43249,5,43443,43443,5,43567,43568,7,43696,43696,5,43765,43765,7,44013,44013,5,44117,44143,12,44229,44255,12,44341,44367,12,44453,44479,12,44565,44591,12,44677,44703,12,44789,44815,12,44901,44927,12,45013,45039,12,45125,45151,12,45237,45263,12,45349,45375,12,45461,45487,12,45573,45599,12,45685,45711,12,45797,45823,12,45909,45935,12,46021,46047,12,46133,46159,12,46245,46271,12,46357,46383,12,46469,46495,12,46581,46607,12,46693,46719,12,46805,46831,12,46917,46943,12,47029,47055,12,47141,47167,12,47253,47279,12,47365,47391,12,47477,47503,12,47589,47615,12,47701,47727,12,47813,47839,12,47925,47951,12,48037,48063,12,48149,48175,12,48261,48287,12,48373,48399,12,48485,48511,12,48597,48623,12,48709,48735,12,48821,48847,12,48933,48959,12,49045,49071,12,49157,49183,12,49269,49295,12,49381,49407,12,49493,49519,12,49605,49631,12,49717,49743,12,49829,49855,12,49941,49967,12,50053,50079,12,50165,50191,12,50277,50303,12,50389,50415,12,50501,50527,12,50613,50639,12,50725,50751,12,50837,50863,12,50949,50975,12,51061,51087,12,51173,51199,12,51285,51311,12,51397,51423,12,51509,51535,12,51621,51647,12,51733,51759,12,51845,51871,12,51957,51983,12,52069,52095,12,52181,52207,12,52293,52319,12,52405,52431,12,52517,52543,12,52629,52655,12,52741,52767,12,52853,52879,12,52965,52991,12,53077,53103,12,53189,53215,12,53301,53327,12,53413,53439,12,53525,53551,12,53637,53663,12,53749,53775,12,53861,53887,12,53973,53999,12,54085,54111,12,54197,54223,12,54309,54335,12,54421,54447,12,54533,54559,12,54645,54671,12,54757,54783,12,54869,54895,12,54981,55007,12,55093,55119,12,55243,55291,10,66045,66045,5,68325,68326,5,69688,69702,5,69817,69818,5,69957,69958,7,70089,70092,5,70198,70199,5,70462,70462,5,70502,70508,5,70750,70750,5,70846,70846,7,71100,71101,5,71230,71230,7,71351,71351,5,71737,71738,5,72000,72000,7,72160,72160,5,72273,72278,5,72752,72758,5,72882,72883,5,73031,73031,5,73461,73462,7,94192,94193,7,119149,119149,7,121403,121452,5,122915,122916,5,126980,126980,14,127358,127359,14,127535,127535,14,127759,127759,14,127771,127771,14,127792,127793,14,127825,127867,14,127897,127899,14,127945,127945,14,127985,127986,14,128000,128007,14,128021,128021,14,128066,128100,14,128184,128235,14,128249,128252,14,128266,128276,14,128335,128335,14,128379,128390,14,128407,128419,14,128444,128444,14,128481,128481,14,128499,128499,14,128526,128526,14,128536,128536,14,128543,128543,14,128556,128556,14,128564,128564,14,128577,128580,14,128643,128645,14,128649,128649,14,128654,128654,14,128660,128660,14,128664,128664,14,128675,128675,14,128686,128689,14,128695,128696,14,128705,128709,14,128717,128719,14,128725,128725,14,128736,128741,14,128747,128748,14,128755,128755,14,128762,128762,14,128981,128991,14,129009,129023,14,129160,129167,14,129296,129304,14,129320,129327,14,129340,129342,14,129356,129356,14,129388,129392,14,129399,129400,14,129404,129407,14,129432,129442,14,129454,129455,14,129473,129474,14,129485,129487,14,129648,129651,14,129659,129660,14,129671,129679,14,129709,129711,14,129728,129730,14,129751,129753,14,129776,129782,14,917505,917505,4,917760,917999,5,10,10,3,127,159,4,768,879,5,1471,1471,5,1536,1541,1,1648,1648,5,1767,1768,5,1840,1866,5,2070,2073,5,2137,2139,5,2274,2274,1,2363,2363,7,2377,2380,7,2402,2403,5,2494,2494,5,2507,2508,7,2558,2558,5,2622,2624,7,2641,2641,5,2691,2691,7,2759,2760,5,2786,2787,5,2876,2876,5,2881,2884,5,2901,2902,5,3006,3006,5,3014,3016,7,3072,3072,5,3134,3136,5,3157,3158,5,3260,3260,5,3266,3266,5,3274,3275,7,3328,3329,5,3391,3392,7,3405,3405,5,3457,3457,5,3536,3537,7,3551,3551,5,3636,3642,5,3764,3772,5,3895,3895,5,3967,3967,7,3993,4028,5,4146,4151,5,4182,4183,7,4226,4226,5,4253,4253,5,4957,4959,5,5940,5940,7,6070,6070,7,6087,6088,7,6158,6158,4,6432,6434,5,6448,6449,7,6679,6680,5,6742,6742,5,6754,6754,5,6783,6783,5,6912,6915,5,6966,6970,5,6978,6978,5,7042,7042,7,7080,7081,5,7143,7143,7,7150,7150,7,7212,7219,5,7380,7392,5,7412,7412,5,8203,8203,4,8232,8232,4,8265,8265,14,8400,8412,5,8421,8432,5,8617,8618,14,9167,9167,14,9200,9200,14,9410,9410,14,9723,9726,14,9733,9733,14,9745,9745,14,9752,9752,14,9760,9760,14,9766,9766,14,9774,9774,14,9786,9786,14,9794,9794,14,9823,9823,14,9828,9828,14,9833,9850,14,9855,9855,14,9875,9875,14,9880,9880,14,9885,9887,14,9896,9897,14,9906,9916,14,9926,9927,14,9935,9935,14,9939,9939,14,9962,9962,14,9972,9972,14,9978,9978,14,9986,9986,14,9997,9997,14,10002,10002,14,10017,10017,14,10055,10055,14,10071,10071,14,10133,10135,14,10548,10549,14,11093,11093,14,12330,12333,5,12441,12442,5,42608,42610,5,43010,43010,5,43045,43046,5,43188,43203,7,43302,43309,5,43392,43394,5,43446,43449,5,43493,43493,5,43571,43572,7,43597,43597,7,43703,43704,5,43756,43757,5,44003,44004,7,44009,44010,7,44033,44059,12,44089,44115,12,44145,44171,12,44201,44227,12,44257,44283,12,44313,44339,12,44369,44395,12,44425,44451,12,44481,44507,12,44537,44563,12,44593,44619,12,44649,44675,12,44705,44731,12,44761,44787,12,44817,44843,12,44873,44899,12,44929,44955,12,44985,45011,12,45041,45067,12,45097,45123,12,45153,45179,12,45209,45235,12,45265,45291,12,45321,45347,12,45377,45403,12,45433,45459,12,45489,45515,12,45545,45571,12,45601,45627,12,45657,45683,12,45713,45739,12,45769,45795,12,45825,45851,12,45881,45907,12,45937,45963,12,45993,46019,12,46049,46075,12,46105,46131,12,46161,46187,12,46217,46243,12,46273,46299,12,46329,46355,12,46385,46411,12,46441,46467,12,46497,46523,12,46553,46579,12,46609,46635,12,46665,46691,12,46721,46747,12,46777,46803,12,46833,46859,12,46889,46915,12,46945,46971,12,47001,47027,12,47057,47083,12,47113,47139,12,47169,47195,12,47225,47251,12,47281,47307,12,47337,47363,12,47393,47419,12,47449,47475,12,47505,47531,12,47561,47587,12,47617,47643,12,47673,47699,12,47729,47755,12,47785,47811,12,47841,47867,12,47897,47923,12,47953,47979,12,48009,48035,12,48065,48091,12,48121,48147,12,48177,48203,12,48233,48259,12,48289,48315,12,48345,48371,12,48401,48427,12,48457,48483,12,48513,48539,12,48569,48595,12,48625,48651,12,48681,48707,12,48737,48763,12,48793,48819,12,48849,48875,12,48905,48931,12,48961,48987,12,49017,49043,12,49073,49099,12,49129,49155,12,49185,49211,12,49241,49267,12,49297,49323,12,49353,49379,12,49409,49435,12,49465,49491,12,49521,49547,12,49577,49603,12,49633,49659,12,49689,49715,12,49745,49771,12,49801,49827,12,49857,49883,12,49913,49939,12,49969,49995,12,50025,50051,12,50081,50107,12,50137,50163,12,50193,50219,12,50249,50275,12,50305,50331,12,50361,50387,12,50417,50443,12,50473,50499,12,50529,50555,12,50585,50611,12,50641,50667,12,50697,50723,12,50753,50779,12,50809,50835,12,50865,50891,12,50921,50947,12,50977,51003,12,51033,51059,12,51089,51115,12,51145,51171,12,51201,51227,12,51257,51283,12,51313,51339,12,51369,51395,12,51425,51451,12,51481,51507,12,51537,51563,12,51593,51619,12,51649,51675,12,51705,51731,12,51761,51787,12,51817,51843,12,51873,51899,12,51929,51955,12,51985,52011,12,52041,52067,12,52097,52123,12,52153,52179,12,52209,52235,12,52265,52291,12,52321,52347,12,52377,52403,12,52433,52459,12,52489,52515,12,52545,52571,12,52601,52627,12,52657,52683,12,52713,52739,12,52769,52795,12,52825,52851,12,52881,52907,12,52937,52963,12,52993,53019,12,53049,53075,12,53105,53131,12,53161,53187,12,53217,53243,12,53273,53299,12,53329,53355,12,53385,53411,12,53441,53467,12,53497,53523,12,53553,53579,12,53609,53635,12,53665,53691,12,53721,53747,12,53777,53803,12,53833,53859,12,53889,53915,12,53945,53971,12,54001,54027,12,54057,54083,12,54113,54139,12,54169,54195,12,54225,54251,12,54281,54307,12,54337,54363,12,54393,54419,12,54449,54475,12,54505,54531,12,54561,54587,12,54617,54643,12,54673,54699,12,54729,54755,12,54785,54811,12,54841,54867,12,54897,54923,12,54953,54979,12,55009,55035,12,55065,55091,12,55121,55147,12,55177,55203,12,55243,55291,10,64286,64286,5,65056,65071,5,65438,65439,5,65529,65531,4,66272,66272,5,68097,68099,5,68108,68111,5,68159,68159,5,68900,68903,5,69446,69456,5,69632,69632,7,69634,69634,7,69744,69744,5,69759,69761,5,69808,69810,7,69815,69816,7,69821,69821,1,69837,69837,1,69927,69931,5,69933,69940,5,70003,70003,5,70018,70018,7,70070,70078,5,70082,70083,1,70094,70094,7,70188,70190,7,70194,70195,7,70197,70197,7,70206,70206,5,70368,70370,7,70400,70401,5,70459,70460,5,70463,70463,7,70465,70468,7,70475,70477,7,70498,70499,7,70512,70516,5,70712,70719,5,70722,70724,5,70726,70726,5,70832,70832,5,70835,70840,5,70842,70842,5,70845,70845,5,70847,70848,5,70850,70851,5,71088,71089,7,71096,71099,7,71102,71102,7,71132,71133,5,71219,71226,5,71229,71229,5,71231,71232,5,71340,71340,7,71342,71343,7,71350,71350,7,71453,71455,5,71462,71462,7,71724,71726,7,71736,71736,7,71984,71984,5,71991,71992,7,71997,71997,7,71999,71999,1,72001,72001,1,72003,72003,5,72148,72151,5,72156,72159,7,72164,72164,7,72243,72248,5,72250,72250,1,72263,72263,5,72279,72280,7,72324,72329,1,72343,72343,7,72751,72751,7,72760,72765,5,72767,72767,5,72873,72873,7,72881,72881,7,72884,72884,7,73009,73014,5,73020,73021,5,73030,73030,1,73098,73102,7,73107,73108,7,73110,73110,7,73459,73460,5,78896,78904,4,92976,92982,5,94033,94087,7,113824,113827,4,119142,119142,7,119155,119162,4,119362,119364,5,121476,121476,5,122888,122904,5,123184,123190,5,125252,125258,5,127183,127183,14,127340,127343,14,127377,127386,14,127491,127503,14,127548,127551,14,127744,127756,14,127761,127761,14,127769,127769,14,127773,127774,14,127780,127788,14,127796,127797,14,127820,127823,14,127869,127869,14,127894,127895,14,127902,127903,14,127943,127943,14,127947,127950,14,127972,127972,14,127988,127988,14,127992,127994,14,128009,128011,14,128019,128019,14,128023,128041,14,128064,128064,14,128102,128107,14,128174,128181,14,128238,128238,14,128246,128247,14,128254,128254,14,128264,128264,14,128278,128299,14,128329,128330,14,128348,128359,14,128371,128377,14,128392,128393,14,128401,128404,14,128421,128421,14,128433,128434,14,128450,128452,14,128476,128478,14,128483,128483,14,128495,128495,14,128506,128506,14,128519,128520,14,128528,128528,14,128534,128534,14,128538,128538,14]'); + return JSON.parse('[0,0,0,51229,51255,12,44061,44087,12,127462,127487,6,7083,7085,5,47645,47671,12,54813,54839,12,128678,128678,14,3270,3270,5,9919,9923,14,45853,45879,12,49437,49463,12,53021,53047,12,71216,71218,7,128398,128399,14,129360,129374,14,2519,2519,5,4448,4519,9,9742,9742,14,12336,12336,14,44957,44983,12,46749,46775,12,48541,48567,12,50333,50359,12,52125,52151,12,53917,53943,12,69888,69890,5,73018,73018,5,127990,127990,14,128558,128559,14,128759,128760,14,129653,129655,14,2027,2035,5,2891,2892,7,3761,3761,5,6683,6683,5,8293,8293,4,9825,9826,14,9999,9999,14,43452,43453,5,44509,44535,12,45405,45431,12,46301,46327,12,47197,47223,12,48093,48119,12,48989,49015,12,49885,49911,12,50781,50807,12,51677,51703,12,52573,52599,12,53469,53495,12,54365,54391,12,65279,65279,4,70471,70472,7,72145,72147,7,119173,119179,5,127799,127818,14,128240,128244,14,128512,128512,14,128652,128652,14,128721,128722,14,129292,129292,14,129445,129450,14,129734,129743,14,1476,1477,5,2366,2368,7,2750,2752,7,3076,3076,5,3415,3415,5,4141,4144,5,6109,6109,5,6964,6964,5,7394,7400,5,9197,9198,14,9770,9770,14,9877,9877,14,9968,9969,14,10084,10084,14,43052,43052,5,43713,43713,5,44285,44311,12,44733,44759,12,45181,45207,12,45629,45655,12,46077,46103,12,46525,46551,12,46973,46999,12,47421,47447,12,47869,47895,12,48317,48343,12,48765,48791,12,49213,49239,12,49661,49687,12,50109,50135,12,50557,50583,12,51005,51031,12,51453,51479,12,51901,51927,12,52349,52375,12,52797,52823,12,53245,53271,12,53693,53719,12,54141,54167,12,54589,54615,12,55037,55063,12,69506,69509,5,70191,70193,5,70841,70841,7,71463,71467,5,72330,72342,5,94031,94031,5,123628,123631,5,127763,127765,14,127941,127941,14,128043,128062,14,128302,128317,14,128465,128467,14,128539,128539,14,128640,128640,14,128662,128662,14,128703,128703,14,128745,128745,14,129004,129007,14,129329,129330,14,129402,129402,14,129483,129483,14,129686,129704,14,130048,131069,14,173,173,4,1757,1757,1,2200,2207,5,2434,2435,7,2631,2632,5,2817,2817,5,3008,3008,5,3201,3201,5,3387,3388,5,3542,3542,5,3902,3903,7,4190,4192,5,6002,6003,5,6439,6440,5,6765,6770,7,7019,7027,5,7154,7155,7,8205,8205,13,8505,8505,14,9654,9654,14,9757,9757,14,9792,9792,14,9852,9853,14,9890,9894,14,9937,9937,14,9981,9981,14,10035,10036,14,11035,11036,14,42654,42655,5,43346,43347,7,43587,43587,5,44006,44007,7,44173,44199,12,44397,44423,12,44621,44647,12,44845,44871,12,45069,45095,12,45293,45319,12,45517,45543,12,45741,45767,12,45965,45991,12,46189,46215,12,46413,46439,12,46637,46663,12,46861,46887,12,47085,47111,12,47309,47335,12,47533,47559,12,47757,47783,12,47981,48007,12,48205,48231,12,48429,48455,12,48653,48679,12,48877,48903,12,49101,49127,12,49325,49351,12,49549,49575,12,49773,49799,12,49997,50023,12,50221,50247,12,50445,50471,12,50669,50695,12,50893,50919,12,51117,51143,12,51341,51367,12,51565,51591,12,51789,51815,12,52013,52039,12,52237,52263,12,52461,52487,12,52685,52711,12,52909,52935,12,53133,53159,12,53357,53383,12,53581,53607,12,53805,53831,12,54029,54055,12,54253,54279,12,54477,54503,12,54701,54727,12,54925,54951,12,55149,55175,12,68101,68102,5,69762,69762,7,70067,70069,7,70371,70378,5,70720,70721,7,71087,71087,5,71341,71341,5,71995,71996,5,72249,72249,7,72850,72871,5,73109,73109,5,118576,118598,5,121505,121519,5,127245,127247,14,127568,127569,14,127777,127777,14,127872,127891,14,127956,127967,14,128015,128016,14,128110,128172,14,128259,128259,14,128367,128368,14,128424,128424,14,128488,128488,14,128530,128532,14,128550,128551,14,128566,128566,14,128647,128647,14,128656,128656,14,128667,128673,14,128691,128693,14,128715,128715,14,128728,128732,14,128752,128752,14,128765,128767,14,129096,129103,14,129311,129311,14,129344,129349,14,129394,129394,14,129413,129425,14,129466,129471,14,129511,129535,14,129664,129666,14,129719,129722,14,129760,129767,14,917536,917631,5,13,13,2,1160,1161,5,1564,1564,4,1807,1807,1,2085,2087,5,2307,2307,7,2382,2383,7,2497,2500,5,2563,2563,7,2677,2677,5,2763,2764,7,2879,2879,5,2914,2915,5,3021,3021,5,3142,3144,5,3263,3263,5,3285,3286,5,3398,3400,7,3530,3530,5,3633,3633,5,3864,3865,5,3974,3975,5,4155,4156,7,4229,4230,5,5909,5909,7,6078,6085,7,6277,6278,5,6451,6456,7,6744,6750,5,6846,6846,5,6972,6972,5,7074,7077,5,7146,7148,7,7222,7223,5,7416,7417,5,8234,8238,4,8417,8417,5,9000,9000,14,9203,9203,14,9730,9731,14,9748,9749,14,9762,9763,14,9776,9783,14,9800,9811,14,9831,9831,14,9872,9873,14,9882,9882,14,9900,9903,14,9929,9933,14,9941,9960,14,9974,9974,14,9989,9989,14,10006,10006,14,10062,10062,14,10160,10160,14,11647,11647,5,12953,12953,14,43019,43019,5,43232,43249,5,43443,43443,5,43567,43568,7,43696,43696,5,43765,43765,7,44013,44013,5,44117,44143,12,44229,44255,12,44341,44367,12,44453,44479,12,44565,44591,12,44677,44703,12,44789,44815,12,44901,44927,12,45013,45039,12,45125,45151,12,45237,45263,12,45349,45375,12,45461,45487,12,45573,45599,12,45685,45711,12,45797,45823,12,45909,45935,12,46021,46047,12,46133,46159,12,46245,46271,12,46357,46383,12,46469,46495,12,46581,46607,12,46693,46719,12,46805,46831,12,46917,46943,12,47029,47055,12,47141,47167,12,47253,47279,12,47365,47391,12,47477,47503,12,47589,47615,12,47701,47727,12,47813,47839,12,47925,47951,12,48037,48063,12,48149,48175,12,48261,48287,12,48373,48399,12,48485,48511,12,48597,48623,12,48709,48735,12,48821,48847,12,48933,48959,12,49045,49071,12,49157,49183,12,49269,49295,12,49381,49407,12,49493,49519,12,49605,49631,12,49717,49743,12,49829,49855,12,49941,49967,12,50053,50079,12,50165,50191,12,50277,50303,12,50389,50415,12,50501,50527,12,50613,50639,12,50725,50751,12,50837,50863,12,50949,50975,12,51061,51087,12,51173,51199,12,51285,51311,12,51397,51423,12,51509,51535,12,51621,51647,12,51733,51759,12,51845,51871,12,51957,51983,12,52069,52095,12,52181,52207,12,52293,52319,12,52405,52431,12,52517,52543,12,52629,52655,12,52741,52767,12,52853,52879,12,52965,52991,12,53077,53103,12,53189,53215,12,53301,53327,12,53413,53439,12,53525,53551,12,53637,53663,12,53749,53775,12,53861,53887,12,53973,53999,12,54085,54111,12,54197,54223,12,54309,54335,12,54421,54447,12,54533,54559,12,54645,54671,12,54757,54783,12,54869,54895,12,54981,55007,12,55093,55119,12,55243,55291,10,66045,66045,5,68325,68326,5,69688,69702,5,69817,69818,5,69957,69958,7,70089,70092,5,70198,70199,5,70462,70462,5,70502,70508,5,70750,70750,5,70846,70846,7,71100,71101,5,71230,71230,7,71351,71351,5,71737,71738,5,72000,72000,7,72160,72160,5,72273,72278,5,72752,72758,5,72882,72883,5,73031,73031,5,73461,73462,7,94192,94193,7,119149,119149,7,121403,121452,5,122915,122916,5,126980,126980,14,127358,127359,14,127535,127535,14,127759,127759,14,127771,127771,14,127792,127793,14,127825,127867,14,127897,127899,14,127945,127945,14,127985,127986,14,128000,128007,14,128021,128021,14,128066,128100,14,128184,128235,14,128249,128252,14,128266,128276,14,128335,128335,14,128379,128390,14,128407,128419,14,128444,128444,14,128481,128481,14,128499,128499,14,128526,128526,14,128536,128536,14,128543,128543,14,128556,128556,14,128564,128564,14,128577,128580,14,128643,128645,14,128649,128649,14,128654,128654,14,128660,128660,14,128664,128664,14,128675,128675,14,128686,128689,14,128695,128696,14,128705,128709,14,128717,128719,14,128725,128725,14,128736,128741,14,128747,128748,14,128755,128755,14,128762,128762,14,128981,128991,14,129009,129023,14,129160,129167,14,129296,129304,14,129320,129327,14,129340,129342,14,129356,129356,14,129388,129392,14,129399,129400,14,129404,129407,14,129432,129442,14,129454,129455,14,129473,129474,14,129485,129487,14,129648,129651,14,129659,129660,14,129671,129679,14,129709,129711,14,129728,129730,14,129751,129753,14,129776,129782,14,917505,917505,4,917760,917999,5,10,10,3,127,159,4,768,879,5,1471,1471,5,1536,1541,1,1648,1648,5,1767,1768,5,1840,1866,5,2070,2073,5,2137,2139,5,2274,2274,1,2363,2363,7,2377,2380,7,2402,2403,5,2494,2494,5,2507,2508,7,2558,2558,5,2622,2624,7,2641,2641,5,2691,2691,7,2759,2760,5,2786,2787,5,2876,2876,5,2881,2884,5,2901,2902,5,3006,3006,5,3014,3016,7,3072,3072,5,3134,3136,5,3157,3158,5,3260,3260,5,3266,3266,5,3274,3275,7,3328,3329,5,3391,3392,7,3405,3405,5,3457,3457,5,3536,3537,7,3551,3551,5,3636,3642,5,3764,3772,5,3895,3895,5,3967,3967,7,3993,4028,5,4146,4151,5,4182,4183,7,4226,4226,5,4253,4253,5,4957,4959,5,5940,5940,7,6070,6070,7,6087,6088,7,6158,6158,4,6432,6434,5,6448,6449,7,6679,6680,5,6742,6742,5,6754,6754,5,6783,6783,5,6912,6915,5,6966,6970,5,6978,6978,5,7042,7042,7,7080,7081,5,7143,7143,7,7150,7150,7,7212,7219,5,7380,7392,5,7412,7412,5,8203,8203,4,8232,8232,4,8265,8265,14,8400,8412,5,8421,8432,5,8617,8618,14,9167,9167,14,9200,9200,14,9410,9410,14,9723,9726,14,9733,9733,14,9745,9745,14,9752,9752,14,9760,9760,14,9766,9766,14,9774,9774,14,9786,9786,14,9794,9794,14,9823,9823,14,9828,9828,14,9833,9850,14,9855,9855,14,9875,9875,14,9880,9880,14,9885,9887,14,9896,9897,14,9906,9916,14,9926,9927,14,9935,9935,14,9939,9939,14,9962,9962,14,9972,9972,14,9978,9978,14,9986,9986,14,9997,9997,14,10002,10002,14,10017,10017,14,10055,10055,14,10071,10071,14,10133,10135,14,10548,10549,14,11093,11093,14,12330,12333,5,12441,12442,5,42608,42610,5,43010,43010,5,43045,43046,5,43188,43203,7,43302,43309,5,43392,43394,5,43446,43449,5,43493,43493,5,43571,43572,7,43597,43597,7,43703,43704,5,43756,43757,5,44003,44004,7,44009,44010,7,44033,44059,12,44089,44115,12,44145,44171,12,44201,44227,12,44257,44283,12,44313,44339,12,44369,44395,12,44425,44451,12,44481,44507,12,44537,44563,12,44593,44619,12,44649,44675,12,44705,44731,12,44761,44787,12,44817,44843,12,44873,44899,12,44929,44955,12,44985,45011,12,45041,45067,12,45097,45123,12,45153,45179,12,45209,45235,12,45265,45291,12,45321,45347,12,45377,45403,12,45433,45459,12,45489,45515,12,45545,45571,12,45601,45627,12,45657,45683,12,45713,45739,12,45769,45795,12,45825,45851,12,45881,45907,12,45937,45963,12,45993,46019,12,46049,46075,12,46105,46131,12,46161,46187,12,46217,46243,12,46273,46299,12,46329,46355,12,46385,46411,12,46441,46467,12,46497,46523,12,46553,46579,12,46609,46635,12,46665,46691,12,46721,46747,12,46777,46803,12,46833,46859,12,46889,46915,12,46945,46971,12,47001,47027,12,47057,47083,12,47113,47139,12,47169,47195,12,47225,47251,12,47281,47307,12,47337,47363,12,47393,47419,12,47449,47475,12,47505,47531,12,47561,47587,12,47617,47643,12,47673,47699,12,47729,47755,12,47785,47811,12,47841,47867,12,47897,47923,12,47953,47979,12,48009,48035,12,48065,48091,12,48121,48147,12,48177,48203,12,48233,48259,12,48289,48315,12,48345,48371,12,48401,48427,12,48457,48483,12,48513,48539,12,48569,48595,12,48625,48651,12,48681,48707,12,48737,48763,12,48793,48819,12,48849,48875,12,48905,48931,12,48961,48987,12,49017,49043,12,49073,49099,12,49129,49155,12,49185,49211,12,49241,49267,12,49297,49323,12,49353,49379,12,49409,49435,12,49465,49491,12,49521,49547,12,49577,49603,12,49633,49659,12,49689,49715,12,49745,49771,12,49801,49827,12,49857,49883,12,49913,49939,12,49969,49995,12,50025,50051,12,50081,50107,12,50137,50163,12,50193,50219,12,50249,50275,12,50305,50331,12,50361,50387,12,50417,50443,12,50473,50499,12,50529,50555,12,50585,50611,12,50641,50667,12,50697,50723,12,50753,50779,12,50809,50835,12,50865,50891,12,50921,50947,12,50977,51003,12,51033,51059,12,51089,51115,12,51145,51171,12,51201,51227,12,51257,51283,12,51313,51339,12,51369,51395,12,51425,51451,12,51481,51507,12,51537,51563,12,51593,51619,12,51649,51675,12,51705,51731,12,51761,51787,12,51817,51843,12,51873,51899,12,51929,51955,12,51985,52011,12,52041,52067,12,52097,52123,12,52153,52179,12,52209,52235,12,52265,52291,12,52321,52347,12,52377,52403,12,52433,52459,12,52489,52515,12,52545,52571,12,52601,52627,12,52657,52683,12,52713,52739,12,52769,52795,12,52825,52851,12,52881,52907,12,52937,52963,12,52993,53019,12,53049,53075,12,53105,53131,12,53161,53187,12,53217,53243,12,53273,53299,12,53329,53355,12,53385,53411,12,53441,53467,12,53497,53523,12,53553,53579,12,53609,53635,12,53665,53691,12,53721,53747,12,53777,53803,12,53833,53859,12,53889,53915,12,53945,53971,12,54001,54027,12,54057,54083,12,54113,54139,12,54169,54195,12,54225,54251,12,54281,54307,12,54337,54363,12,54393,54419,12,54449,54475,12,54505,54531,12,54561,54587,12,54617,54643,12,54673,54699,12,54729,54755,12,54785,54811,12,54841,54867,12,54897,54923,12,54953,54979,12,55009,55035,12,55065,55091,12,55121,55147,12,55177,55203,12,65024,65039,5,65520,65528,4,66422,66426,5,68152,68154,5,69291,69292,5,69633,69633,5,69747,69748,5,69811,69814,5,69826,69826,5,69932,69932,7,70016,70017,5,70079,70080,7,70095,70095,5,70196,70196,5,70367,70367,5,70402,70403,7,70464,70464,5,70487,70487,5,70709,70711,7,70725,70725,7,70833,70834,7,70843,70844,7,70849,70849,7,71090,71093,5,71103,71104,5,71227,71228,7,71339,71339,5,71344,71349,5,71458,71461,5,71727,71735,5,71985,71989,7,71998,71998,5,72002,72002,7,72154,72155,5,72193,72202,5,72251,72254,5,72281,72283,5,72344,72345,5,72766,72766,7,72874,72880,5,72885,72886,5,73023,73029,5,73104,73105,5,73111,73111,5,92912,92916,5,94095,94098,5,113824,113827,4,119142,119142,7,119155,119162,4,119362,119364,5,121476,121476,5,122888,122904,5,123184,123190,5,125252,125258,5,127183,127183,14,127340,127343,14,127377,127386,14,127491,127503,14,127548,127551,14,127744,127756,14,127761,127761,14,127769,127769,14,127773,127774,14,127780,127788,14,127796,127797,14,127820,127823,14,127869,127869,14,127894,127895,14,127902,127903,14,127943,127943,14,127947,127950,14,127972,127972,14,127988,127988,14,127992,127994,14,128009,128011,14,128019,128019,14,128023,128041,14,128064,128064,14,128102,128107,14,128174,128181,14,128238,128238,14,128246,128247,14,128254,128254,14,128264,128264,14,128278,128299,14,128329,128330,14,128348,128359,14,128371,128377,14,128392,128393,14,128401,128404,14,128421,128421,14,128433,128434,14,128450,128452,14,128476,128478,14,128483,128483,14,128495,128495,14,128506,128506,14,128519,128520,14,128528,128528,14,128534,128534,14,128538,128538,14,128540,128542,14,128544,128549,14,128552,128555,14,128557,128557,14,128560,128563,14,128565,128565,14,128567,128576,14,128581,128591,14,128641,128642,14,128646,128646,14,128648,128648,14,128650,128651,14,128653,128653,14,128655,128655,14,128657,128659,14,128661,128661,14,128663,128663,14,128665,128666,14,128674,128674,14,128676,128677,14,128679,128685,14,128690,128690,14,128694,128694,14,128697,128702,14,128704,128704,14,128710,128714,14,128716,128716,14,128720,128720,14,128723,128724,14,128726,128727,14,128733,128735,14,128742,128744,14,128746,128746,14,128749,128751,14,128753,128754,14,128756,128758,14,128761,128761,14,128763,128764,14,128884,128895,14,128992,129003,14,129008,129008,14,129036,129039,14,129114,129119,14,129198,129279,14,129293,129295,14,129305,129310,14,129312,129319,14,129328,129328,14,129331,129338,14,129343,129343,14,129351,129355,14,129357,129359,14,129375,129387,14,129393,129393,14,129395,129398,14,129401,129401,14,129403,129403,14,129408,129412,14,129426,129431,14,129443,129444,14,129451,129453,14,129456,129465,14,129472,129472,14,129475,129482,14,129484,129484,14,129488,129510,14,129536,129647,14,129652,129652,14,129656,129658,14,129661,129663,14,129667,129670,14,129680,129685,14,129705,129708,14,129712,129718,14,129723,129727,14,129731,129733,14,129744,129750,14,129754,129759,14,129768,129775,14,129783,129791,14,917504,917504,4,917506,917535,4,917632,917759,4,918000,921599,4,0,9,4,11,12,4,14,31,4,169,169,14,174,174,14,1155,1159,5,1425,1469,5,1473,1474,5,1479,1479,5,1552,1562,5,1611,1631,5,1750,1756,5,1759,1764,5,1770,1773,5,1809,1809,5,1958,1968,5,2045,2045,5,2075,2083,5,2089,2093,5,2192,2193,1,2250,2273,5,2275,2306,5,2362,2362,5,2364,2364,5,2369,2376,5,2381,2381,5,2385,2391,5,2433,2433,5,2492,2492,5,2495,2496,7,2503,2504,7,2509,2509,5,2530,2531,5,2561,2562,5,2620,2620,5,2625,2626,5,2635,2637,5,2672,2673,5,2689,2690,5,2748,2748,5,2753,2757,5,2761,2761,7,2765,2765,5,2810,2815,5,2818,2819,7,2878,2878,5,2880,2880,7,2887,2888,7,2893,2893,5,2903,2903,5,2946,2946,5,3007,3007,7,3009,3010,7,3018,3020,7,3031,3031,5,3073,3075,7,3132,3132,5,3137,3140,7,3146,3149,5,3170,3171,5,3202,3203,7,3262,3262,7,3264,3265,7,3267,3268,7,3271,3272,7,3276,3277,5,3298,3299,5,3330,3331,7,3390,3390,5,3393,3396,5,3402,3404,7,3406,3406,1,3426,3427,5,3458,3459,7,3535,3535,5,3538,3540,5,3544,3550,7,3570,3571,7,3635,3635,7,3655,3662,5,3763,3763,7,3784,3789,5,3893,3893,5,3897,3897,5,3953,3966,5,3968,3972,5,3981,3991,5,4038,4038,5,4145,4145,7,4153,4154,5,4157,4158,5,4184,4185,5,4209,4212,5,4228,4228,7,4237,4237,5,4352,4447,8,4520,4607,10,5906,5908,5,5938,5939,5,5970,5971,5,6068,6069,5,6071,6077,5,6086,6086,5,6089,6099,5,6155,6157,5,6159,6159,5,6313,6313,5,6435,6438,7,6441,6443,7,6450,6450,5,6457,6459,5,6681,6682,7,6741,6741,7,6743,6743,7,6752,6752,5,6757,6764,5,6771,6780,5,6832,6845,5,6847,6862,5,6916,6916,7,6965,6965,5,6971,6971,7,6973,6977,7,6979,6980,7,7040,7041,5,7073,7073,7,7078,7079,7,7082,7082,7,7142,7142,5,7144,7145,5,7149,7149,5,7151,7153,5,7204,7211,7,7220,7221,7,7376,7378,5,7393,7393,7,7405,7405,5,7415,7415,7,7616,7679,5,8204,8204,5,8206,8207,4,8233,8233,4,8252,8252,14,8288,8292,4,8294,8303,4,8413,8416,5,8418,8420,5,8482,8482,14,8596,8601,14,8986,8987,14,9096,9096,14,9193,9196,14,9199,9199,14,9201,9202,14,9208,9210,14,9642,9643,14,9664,9664,14,9728,9729,14,9732,9732,14,9735,9741,14,9743,9744,14,9746,9746,14,9750,9751,14,9753,9756,14,9758,9759,14,9761,9761,14,9764,9765,14,9767,9769,14,9771,9773,14,9775,9775,14,9784,9785,14,9787,9791,14,9793,9793,14,9795,9799,14,9812,9822,14,9824,9824,14,9827,9827,14,9829,9830,14,9832,9832,14,9851,9851,14,9854,9854,14,9856,9861,14,9874,9874,14,9876,9876,14,9878,9879,14,9881,9881,14,9883,9884,14,9888,9889,14,9895,9895,14,9898,9899,14,9904,9905,14,9917,9918,14,9924,9925,14,9928,9928,14,9934,9934,14,9936,9936,14,9938,9938,14,9940,9940,14,9961,9961,14,9963,9967,14,9970,9971,14,9973,9973,14,9975,9977,14,9979,9980,14,9982,9985,14,9987,9988,14,9992,9996,14,9998,9998,14,10000,10001,14,10004,10004,14,10013,10013,14,10024,10024,14,10052,10052,14,10060,10060,14,10067,10069,14,10083,10083,14,10085,10087,14,10145,10145,14,10175,10175,14,11013,11015,14,11088,11088,14,11503,11505,5,11744,11775,5,12334,12335,5,12349,12349,14,12951,12951,14,42607,42607,5,42612,42621,5,42736,42737,5,43014,43014,5,43043,43044,7,43047,43047,7,43136,43137,7,43204,43205,5,43263,43263,5,43335,43345,5,43360,43388,8,43395,43395,7,43444,43445,7,43450,43451,7,43454,43456,7,43561,43566,5,43569,43570,5,43573,43574,5,43596,43596,5,43644,43644,5,43698,43700,5,43710,43711,5,43755,43755,7,43758,43759,7,43766,43766,5,44005,44005,5,44008,44008,5,44012,44012,7,44032,44032,11,44060,44060,11,44088,44088,11,44116,44116,11,44144,44144,11,44172,44172,11,44200,44200,11,44228,44228,11,44256,44256,11,44284,44284,11,44312,44312,11,44340,44340,11,44368,44368,11,44396,44396,11,44424,44424,11,44452,44452,11,44480,44480,11,44508,44508,11,44536,44536,11,44564,44564,11,44592,44592,11,44620,44620,11,44648,44648,11,44676,44676,11,44704,44704,11,44732,44732,11,44760,44760,11,44788,44788,11,44816,44816,11,44844,44844,11,44872,44872,11,44900,44900,11,44928,44928,11,44956,44956,11,44984,44984,11,45012,45012,11,45040,45040,11,45068,45068,11,45096,45096,11,45124,45124,11,45152,45152,11,45180,45180,11,45208,45208,11,45236,45236,11,45264,45264,11,45292,45292,11,45320,45320,11,45348,45348,11,45376,45376,11,45404,45404,11,45432,45432,11,45460,45460,11,45488,45488,11,45516,45516,11,45544,45544,11,45572,45572,11,45600,45600,11,45628,45628,11,45656,45656,11,45684,45684,11,45712,45712,11,45740,45740,11,45768,45768,11,45796,45796,11,45824,45824,11,45852,45852,11,45880,45880,11,45908,45908,11,45936,45936,11,45964,45964,11,45992,45992,11,46020,46020,11,46048,46048,11,46076,46076,11,46104,46104,11,46132,46132,11,46160,46160,11,46188,46188,11,46216,46216,11,46244,46244,11,46272,46272,11,46300,46300,11,46328,46328,11,46356,46356,11,46384,46384,11,46412,46412,11,46440,46440,11,46468,46468,11,46496,46496,11,46524,46524,11,46552,46552,11,46580,46580,11,46608,46608,11,46636,46636,11,46664,46664,11,46692,46692,11,46720,46720,11,46748,46748,11,46776,46776,11,46804,46804,11,46832,46832,11,46860,46860,11,46888,46888,11,46916,46916,11,46944,46944,11,46972,46972,11,47000,47000,11,47028,47028,11,47056,47056,11,47084,47084,11,47112,47112,11,47140,47140,11,47168,47168,11,47196,47196,11,47224,47224,11,47252,47252,11,47280,47280,11,47308,47308,11,47336,47336,11,47364,47364,11,47392,47392,11,47420,47420,11,47448,47448,11,47476,47476,11,47504,47504,11,47532,47532,11,47560,47560,11,47588,47588,11,47616,47616,11,47644,47644,11,47672,47672,11,47700,47700,11,47728,47728,11,47756,47756,11,47784,47784,11,47812,47812,11,47840,47840,11,47868,47868,11,47896,47896,11,47924,47924,11,47952,47952,11,47980,47980,11,48008,48008,11,48036,48036,11,48064,48064,11,48092,48092,11,48120,48120,11,48148,48148,11,48176,48176,11,48204,48204,11,48232,48232,11,48260,48260,11,48288,48288,11,48316,48316,11,48344,48344,11,48372,48372,11,48400,48400,11,48428,48428,11,48456,48456,11,48484,48484,11,48512,48512,11,48540,48540,11,48568,48568,11,48596,48596,11,48624,48624,11,48652,48652,11,48680,48680,11,48708,48708,11,48736,48736,11,48764,48764,11,48792,48792,11,48820,48820,11,48848,48848,11,48876,48876,11,48904,48904,11,48932,48932,11,48960,48960,11,48988,48988,11,49016,49016,11,49044,49044,11,49072,49072,11,49100,49100,11,49128,49128,11,49156,49156,11,49184,49184,11,49212,49212,11,49240,49240,11,49268,49268,11,49296,49296,11,49324,49324,11,49352,49352,11,49380,49380,11,49408,49408,11,49436,49436,11,49464,49464,11,49492,49492,11,49520,49520,11,49548,49548,11,49576,49576,11,49604,49604,11,49632,49632,11,49660,49660,11,49688,49688,11,49716,49716,11,49744,49744,11,49772,49772,11,49800,49800,11,49828,49828,11,49856,49856,11,49884,49884,11,49912,49912,11,49940,49940,11,49968,49968,11,49996,49996,11,50024,50024,11,50052,50052,11,50080,50080,11,50108,50108,11,50136,50136,11,50164,50164,11,50192,50192,11,50220,50220,11,50248,50248,11,50276,50276,11,50304,50304,11,50332,50332,11,50360,50360,11,50388,50388,11,50416,50416,11,50444,50444,11,50472,50472,11,50500,50500,11,50528,50528,11,50556,50556,11,50584,50584,11,50612,50612,11,50640,50640,11,50668,50668,11,50696,50696,11,50724,50724,11,50752,50752,11,50780,50780,11,50808,50808,11,50836,50836,11,50864,50864,11,50892,50892,11,50920,50920,11,50948,50948,11,50976,50976,11,51004,51004,11,51032,51032,11,51060,51060,11,51088,51088,11,51116,51116,11,51144,51144,11,51172,51172,11,51200,51200,11,51228,51228,11,51256,51256,11,51284,51284,11,51312,51312,11,51340,51340,11,51368,51368,11,51396,51396,11,51424,51424,11,51452,51452,11,51480,51480,11,51508,51508,11,51536,51536,11,51564,51564,11,51592,51592,11,51620,51620,11,51648,51648,11,51676,51676,11,51704,51704,11,51732,51732,11,51760,51760,11,51788,51788,11,51816,51816,11,51844,51844,11,51872,51872,11,51900,51900,11,51928,51928,11,51956,51956,11,51984,51984,11,52012,52012,11,52040,52040,11,52068,52068,11,52096,52096,11,52124,52124,11,52152,52152,11,52180,52180,11,52208,52208,11,52236,52236,11,52264,52264,11,52292,52292,11,52320,52320,11,52348,52348,11,52376,52376,11,52404,52404,11,52432,52432,11,52460,52460,11,52488,52488,11,52516,52516,11,52544,52544,11,52572,52572,11,52600,52600,11,52628,52628,11,52656,52656,11,52684,52684,11,52712,52712,11,52740,52740,11,52768,52768,11,52796,52796,11,52824,52824,11,52852,52852,11,52880,52880,11,52908,52908,11,52936,52936,11,52964,52964,11,52992,52992,11,53020,53020,11,53048,53048,11,53076,53076,11,53104,53104,11,53132,53132,11,53160,53160,11,53188,53188,11,53216,53216,11,53244,53244,11,53272,53272,11,53300,53300,11,53328,53328,11,53356,53356,11,53384,53384,11,53412,53412,11,53440,53440,11,53468,53468,11,53496,53496,11,53524,53524,11,53552,53552,11,53580,53580,11,53608,53608,11,53636,53636,11,53664,53664,11,53692,53692,11,53720,53720,11,53748,53748,11,53776,53776,11,53804,53804,11,53832,53832,11,53860,53860,11,53888,53888,11,53916,53916,11,53944,53944,11,53972,53972,11,54000,54000,11,54028,54028,11,54056,54056,11,54084,54084,11,54112,54112,11,54140,54140,11,54168,54168,11,54196,54196,11,54224,54224,11,54252,54252,11,54280,54280,11,54308,54308,11,54336,54336,11,54364,54364,11,54392,54392,11,54420,54420,11,54448,54448,11,54476,54476,11,54504,54504,11,54532,54532,11,54560,54560,11,54588,54588,11,54616,54616,11,54644,54644,11,54672,54672,11,54700,54700,11,54728,54728,11,54756,54756,11,54784,54784,11,54812,54812,11,54840,54840,11,54868,54868,11,54896,54896,11,54924,54924,11,54952,54952,11,54980,54980,11,55008,55008,11,55036,55036,11,55064,55064,11,55092,55092,11,55120,55120,11,55148,55148,11,55176,55176,11,55216,55238,9,64286,64286,5,65056,65071,5,65438,65439,5,65529,65531,4,66272,66272,5,68097,68099,5,68108,68111,5,68159,68159,5,68900,68903,5,69446,69456,5,69632,69632,7,69634,69634,7,69744,69744,5,69759,69761,5,69808,69810,7,69815,69816,7,69821,69821,1,69837,69837,1,69927,69931,5,69933,69940,5,70003,70003,5,70018,70018,7,70070,70078,5,70082,70083,1,70094,70094,7,70188,70190,7,70194,70195,7,70197,70197,7,70206,70206,5,70368,70370,7,70400,70401,5,70459,70460,5,70463,70463,7,70465,70468,7,70475,70477,7,70498,70499,7,70512,70516,5,70712,70719,5,70722,70724,5,70726,70726,5,70832,70832,5,70835,70840,5,70842,70842,5,70845,70845,5,70847,70848,5,70850,70851,5,71088,71089,7,71096,71099,7,71102,71102,7,71132,71133,5,71219,71226,5,71229,71229,5,71231,71232,5,71340,71340,7,71342,71343,7,71350,71350,7,71453,71455,5,71462,71462,7,71724,71726,7,71736,71736,7,71984,71984,5,71991,71992,7,71997,71997,7,71999,71999,1,72001,72001,1,72003,72003,5,72148,72151,5,72156,72159,7,72164,72164,7,72243,72248,5,72250,72250,1,72263,72263,5,72279,72280,7,72324,72329,1,72343,72343,7,72751,72751,7,72760,72765,5,72767,72767,5,72873,72873,7,72881,72881,7,72884,72884,7,73009,73014,5,73020,73021,5,73030,73030,1,73098,73102,7,73107,73108,7,73110,73110,7,73459,73460,5,78896,78904,4,92976,92982,5,94033,94087,7,94180,94180,5,113821,113822,5,118528,118573,5,119141,119141,5,119143,119145,5,119150,119154,5,119163,119170,5,119210,119213,5,121344,121398,5,121461,121461,5,121499,121503,5,122880,122886,5,122907,122913,5,122918,122922,5,123566,123566,5,125136,125142,5,126976,126979,14,126981,127182,14,127184,127231,14,127279,127279,14,127344,127345,14,127374,127374,14,127405,127461,14,127489,127490,14,127514,127514,14,127538,127546,14,127561,127567,14,127570,127743,14,127757,127758,14,127760,127760,14,127762,127762,14,127766,127768,14,127770,127770,14,127772,127772,14,127775,127776,14,127778,127779,14,127789,127791,14,127794,127795,14,127798,127798,14,127819,127819,14,127824,127824,14,127868,127868,14,127870,127871,14,127892,127893,14,127896,127896,14,127900,127901,14,127904,127940,14,127942,127942,14,127944,127944,14,127946,127946,14,127951,127955,14,127968,127971,14,127973,127984,14,127987,127987,14,127989,127989,14,127991,127991,14,127995,127999,5,128008,128008,14,128012,128014,14,128017,128018,14,128020,128020,14,128022,128022,14,128042,128042,14,128063,128063,14,128065,128065,14,128101,128101,14,128108,128109,14,128173,128173,14,128182,128183,14,128236,128237,14,128239,128239,14,128245,128245,14,128248,128248,14,128253,128253,14,128255,128258,14,128260,128263,14,128265,128265,14,128277,128277,14,128300,128301,14,128326,128328,14,128331,128334,14,128336,128347,14,128360,128366,14,128369,128370,14,128378,128378,14,128391,128391,14,128394,128397,14,128400,128400,14,128405,128406,14,128420,128420,14,128422,128423,14,128425,128432,14,128435,128443,14,128445,128449,14,128453,128464,14,128468,128475,14,128479,128480,14,128482,128482,14,128484,128487,14,128489,128494,14,128496,128498,14,128500,128505,14,128507,128511,14,128513,128518,14,128521,128525,14,128527,128527,14,128529,128529,14,128533,128533,14,128535,128535,14,128537,128537,14]'); } //#endregion @@ -1205,7 +1205,7 @@ export class AmbiguousCharacters { // Generated using https://github.com/hediet/vscode-unicode-data // Stored as key1, value1, key2, value2, ... return JSON.parse( - '{\"_common\":[8232,32,8233,32,5760,32,8192,32,8193,32,8194,32,8195,32,8196,32,8197,32,8198,32,8200,32,8201,32,8202,32,8287,32,8199,32,8239,32,2042,95,65101,95,65102,95,65103,95,8208,45,8209,45,8210,45,65112,45,1748,45,8259,45,727,45,8722,45,10134,45,11450,45,1549,44,1643,44,8218,44,184,44,42233,44,894,59,2307,58,2691,58,1417,58,1795,58,1796,58,5868,58,65072,58,6147,58,6153,58,8282,58,1475,58,760,58,42889,58,8758,58,720,58,42237,58,451,33,11601,33,660,63,577,63,2429,63,5038,63,42731,63,119149,46,8228,46,1793,46,1794,46,42510,46,68176,46,1632,46,1776,46,42232,46,1373,96,65287,96,8219,96,8242,96,1370,96,1523,96,8175,96,65344,96,900,96,8189,96,8125,96,8127,96,8190,96,697,96,884,96,712,96,714,96,715,96,756,96,699,96,701,96,700,96,702,96,42892,96,1497,96,2036,96,2037,96,5194,96,5836,96,94033,96,94034,96,65339,91,10088,40,10098,40,12308,40,64830,40,65341,93,10089,41,10099,41,12309,41,64831,41,10100,123,119060,123,10101,125,65342,94,8270,42,1645,42,8727,42,66335,42,5941,47,8257,47,8725,47,8260,47,9585,47,10187,47,10744,47,119354,47,12755,47,12339,47,11462,47,20031,47,12035,47,65340,92,65128,92,8726,92,10189,92,10741,92,10745,92,119311,92,119355,92,12756,92,20022,92,12034,92,42872,38,708,94,710,94,5869,43,10133,43,66203,43,8249,60,10094,60,706,60,119350,60,5176,60,5810,60,5120,61,11840,61,12448,61,42239,61,8250,62,10095,62,707,62,119351,62,5171,62,94015,62,8275,126,732,126,8128,126,8764,126,65372,124,65293,45,120784,50,120794,50,120804,50,120814,50,120824,50,130034,50,42842,50,423,50,1000,50,42564,50,5311,50,42735,50,119302,51,120785,51,120795,51,120805,51,120815,51,120825,51,130035,51,42923,51,540,51,439,51,42858,51,11468,51,1248,51,94011,51,71882,51,120786,52,120796,52,120806,52,120816,52,120826,52,130036,52,5070,52,71855,52,120787,53,120797,53,120807,53,120817,53,120827,53,130037,53,444,53,71867,53,120788,54,120798,54,120808,54,120818,54,120828,54,130038,54,11474,54,5102,54,71893,54,119314,55,120789,55,120799,55,120809,55,120819,55,120829,55,130039,55,66770,55,71878,55,2819,56,2538,56,2666,56,125131,56,120790,56,120800,56,120810,56,120820,56,120830,56,130040,56,547,56,546,56,66330,56,2663,57,2920,57,2541,57,3437,57,120791,57,120801,57,120811,57,120821,57,120831,57,130041,57,42862,57,11466,57,71884,57,71852,57,71894,57,9082,97,65345,97,119834,97,119886,97,119938,97,119990,97,120042,97,120094,97,120146,97,120198,97,120250,97,120302,97,120354,97,120406,97,120458,97,593,97,945,97,120514,97,120572,97,120630,97,120688,97,120746,97,65313,65,119808,65,119860,65,119912,65,119964,65,120016,65,120068,65,120120,65,120172,65,120224,65,120276,65,120328,65,120380,65,120432,65,913,65,120488,65,120546,65,120604,65,120662,65,120720,65,5034,65,5573,65,42222,65,94016,65,66208,65,119835,98,119887,98,119939,98,119991,98,120043,98,120095,98,120147,98,120199,98,120251,98,120303,98,120355,98,120407,98,120459,98,388,98,5071,98,5234,98,5551,98,65314,66,8492,66,119809,66,119861,66,119913,66,120017,66,120069,66,120121,66,120173,66,120225,66,120277,66,120329,66,120381,66,120433,66,42932,66,914,66,120489,66,120547,66,120605,66,120663,66,120721,66,5108,66,5623,66,42192,66,66178,66,66209,66,66305,66,65347,99,8573,99,119836,99,119888,99,119940,99,119992,99,120044,99,120096,99,120148,99,120200,99,120252,99,120304,99,120356,99,120408,99,120460,99,7428,99,1010,99,11429,99,43951,99,66621,99,128844,67,71922,67,71913,67,65315,67,8557,67,8450,67,8493,67,119810,67,119862,67,119914,67,119966,67,120018,67,120174,67,120226,67,120278,67,120330,67,120382,67,120434,67,1017,67,11428,67,5087,67,42202,67,66210,67,66306,67,66581,67,66844,67,8574,100,8518,100,119837,100,119889,100,119941,100,119993,100,120045,100,120097,100,120149,100,120201,100,120253,100,120305,100,120357,100,120409,100,120461,100,1281,100,5095,100,5231,100,42194,100,8558,68,8517,68,119811,68,119863,68,119915,68,119967,68,120019,68,120071,68,120123,68,120175,68,120227,68,120279,68,120331,68,120383,68,120435,68,5024,68,5598,68,5610,68,42195,68,8494,101,65349,101,8495,101,8519,101,119838,101,119890,101,119942,101,120046,101,120098,101,120150,101,120202,101,120254,101,120306,101,120358,101,120410,101,120462,101,43826,101,1213,101,8959,69,65317,69,8496,69,119812,69,119864,69,119916,69,120020,69,120072,69,120124,69,120176,69,120228,69,120280,69,120332,69,120384,69,120436,69,917,69,120492,69,120550,69,120608,69,120666,69,120724,69,11577,69,5036,69,42224,69,71846,69,71854,69,66182,69,119839,102,119891,102,119943,102,119995,102,120047,102,120099,102,120151,102,120203,102,120255,102,120307,102,120359,102,120411,102,120463,102,43829,102,42905,102,383,102,7837,102,1412,102,119315,70,8497,70,119813,70,119865,70,119917,70,120021,70,120073,70,120125,70,120177,70,120229,70,120281,70,120333,70,120385,70,120437,70,42904,70,988,70,120778,70,5556,70,42205,70,71874,70,71842,70,66183,70,66213,70,66853,70,65351,103,8458,103,119840,103,119892,103,119944,103,120048,103,120100,103,120152,103,120204,103,120256,103,120308,103,120360,103,120412,103,120464,103,609,103,7555,103,397,103,1409,103,119814,71,119866,71,119918,71,119970,71,120022,71,120074,71,120126,71,120178,71,120230,71,120282,71,120334,71,120386,71,120438,71,1292,71,5056,71,5107,71,42198,71,65352,104,8462,104,119841,104,119945,104,119997,104,120049,104,120101,104,120153,104,120205,104,120257,104,120309,104,120361,104,120413,104,120465,104,1211,104,1392,104,5058,104,65320,72,8459,72,8460,72,8461,72,119815,72,119867,72,119919,72,120023,72,120179,72,120231,72,120283,72,120335,72,120387,72,120439,72,919,72,120494,72,120552,72,120610,72,120668,72,120726,72,11406,72,5051,72,5500,72,42215,72,66255,72,731,105,9075,105,65353,105,8560,105,8505,105,8520,105,119842,105,119894,105,119946,105,119998,105,120050,105,120102,105,120154,105,120206,105,120258,105,120310,105,120362,105,120414,105,120466,105,120484,105,618,105,617,105,953,105,8126,105,890,105,120522,105,120580,105,120638,105,120696,105,120754,105,1110,105,42567,105,1231,105,43893,105,5029,105,71875,105,65354,106,8521,106,119843,106,119895,106,119947,106,119999,106,120051,106,120103,106,120155,106,120207,106,120259,106,120311,106,120363,106,120415,106,120467,106,1011,106,1112,106,65322,74,119817,74,119869,74,119921,74,119973,74,120025,74,120077,74,120129,74,120181,74,120233,74,120285,74,120337,74,120389,74,120441,74,42930,74,895,74,1032,74,5035,74,5261,74,42201,74,119844,107,119896,107,119948,107,120000,107,120052,107,120104,107,120156,107,120208,107,120260,107,120312,107,120364,107,120416,107,120468,107,8490,75,65323,75,119818,75,119870,75,119922,75,119974,75,120026,75,120078,75,120130,75,120182,75,120234,75,120286,75,120338,75,120390,75,120442,75,922,75,120497,75,120555,75,120613,75,120671,75,120729,75,11412,75,5094,75,5845,75,42199,75,66840,75,1472,108,8739,73,9213,73,65512,73,1633,108,1777,73,66336,108,125127,108,120783,73,120793,73,120803,73,120813,73,120823,73,130033,73,65321,73,8544,73,8464,73,8465,73,119816,73,119868,73,119920,73,120024,73,120128,73,120180,73,120232,73,120284,73,120336,73,120388,73,120440,73,65356,108,8572,73,8467,108,119845,108,119897,108,119949,108,120001,108,120053,108,120105,73,120157,73,120209,73,120261,73,120313,73,120365,73,120417,73,120469,73,448,73,120496,73,120554,73,120612,73,120670,73,120728,73,11410,73,1030,73,1216,73,1493,108,1503,108,1575,108,126464,108,126592,108,65166,108,65165,108,1994,108,11599,73,5825,73,42226,73,93992,73,66186,124,66313,124,119338,76,8556,76,8466,76,119819,76,119871,76,119923,76,120027,76,120079,76,120131,76,120183,76,120235,76,120287,76,120339,76,120391,76,120443,76,11472,76,5086,76,5290,76,42209,76,93974,76,71843,76,71858,76,66587,76,66854,76,65325,77,8559,77,8499,77,119820,77,119872,77,119924,77,120028,77,120080,77,120132,77,120184,77,120236,77,120288,77,120340,77,120392,77,120444,77,924,77,120499,77,120557,77,120615,77,120673,77,120731,77,1018,77,11416,77,5047,77,5616,77,5846,77,42207,77,66224,77,66321,77,119847,110,119899,110,119951,110,120003,110,120055,110,120107,110,120159,110,120211,110,120263,110,120315,110,120367,110,120419,110,120471,110,1400,110,1404,110,65326,78,8469,78,119821,78,119873,78,119925,78,119977,78,120029,78,120081,78,120185,78,120237,78,120289,78,120341,78,120393,78,120445,78,925,78,120500,78,120558,78,120616,78,120674,78,120732,78,11418,78,42208,78,66835,78,3074,111,3202,111,3330,111,3458,111,2406,111,2662,111,2790,111,3046,111,3174,111,3302,111,3430,111,3664,111,3792,111,4160,111,1637,111,1781,111,65359,111,8500,111,119848,111,119900,111,119952,111,120056,111,120108,111,120160,111,120212,111,120264,111,120316,111,120368,111,120420,111,120472,111,7439,111,7441,111,43837,111,959,111,120528,111,120586,111,120644,111,120702,111,120760,111,963,111,120532,111,120590,111,120648,111,120706,111,120764,111,11423,111,4351,111,1413,111,1505,111,1607,111,126500,111,126564,111,126596,111,65259,111,65260,111,65258,111,65257,111,1726,111,64428,111,64429,111,64427,111,64426,111,1729,111,64424,111,64425,111,64423,111,64422,111,1749,111,3360,111,4125,111,66794,111,71880,111,71895,111,66604,111,1984,79,2534,79,2918,79,12295,79,70864,79,71904,79,120782,79,120792,79,120802,79,120812,79,120822,79,130032,79,65327,79,119822,79,119874,79,119926,79,119978,79,120030,79,120082,79,120134,79,120186,79,120238,79,120290,79,120342,79,120394,79,120446,79,927,79,120502,79,120560,79,120618,79,120676,79,120734,79,11422,79,1365,79,11604,79,4816,79,2848,79,66754,79,42227,79,71861,79,66194,79,66219,79,66564,79,66838,79,9076,112,65360,112,119849,112,119901,112,119953,112,120005,112,120057,112,120109,112,120161,112,120213,112,120265,112,120317,112,120369,112,120421,112,120473,112,961,112,120530,112,120544,112,120588,112,120602,112,120646,112,120660,112,120704,112,120718,112,120762,112,120776,112,11427,112,65328,80,8473,80,119823,80,119875,80,119927,80,119979,80,120031,80,120083,80,120187,80,120239,80,120291,80,120343,80,120395,80,120447,80,929,80,120504,80,120562,80,120620,80,120678,80,120736,80,11426,80,5090,80,5229,80,42193,80,66197,80,119850,113,119902,113,119954,113,120006,113,120058,113,120110,113,120162,113,120214,113,120266,113,120318,113,120370,113,120422,113,120474,113,1307,113,1379,113,1382,113,8474,81,119824,81,119876,81,119928,81,119980,81,120032,81,120084,81,120188,81,120240,81,120292,81,120344,81,120396,81,120448,81,11605,81,119851,114,119903,114,119955,114,120007,114,120059,114,120111,114,120163,114,120215,114,120267,114,120319,114,120371,114,120423,114,120475,114,43847,114,43848,114,7462,114,11397,114,43905,114,119318,82,8475,82,8476,82,8477,82,119825,82,119877,82,119929,82,120033,82,120189,82,120241,82,120293,82,120345,82,120397,82,120449,82,422,82,5025,82,5074,82,66740,82,5511,82,42211,82,94005,82,65363,115,119852,115,119904,115,119956,115,120008,115,120060,115,120112,115,120164,115,120216,115,120268,115,120320,115,120372,115,120424,115,120476,115,42801,115,445,115,1109,115,43946,115,71873,115,66632,115,65331,83,119826,83,119878,83,119930,83,119982,83,120034,83,120086,83,120138,83,120190,83,120242,83,120294,83,120346,83,120398,83,120450,83,1029,83,1359,83,5077,83,5082,83,42210,83,94010,83,66198,83,66592,83,119853,116,119905,116,119957,116,120009,116,120061,116,120113,116,120165,116,120217,116,120269,116,120321,116,120373,116,120425,116,120477,116,8868,84,10201,84,128872,84,65332,84,119827,84,119879,84,119931,84,119983,84,120035,84,120087,84,120139,84,120191,84,120243,84,120295,84,120347,84,120399,84,120451,84,932,84,120507,84,120565,84,120623,84,120681,84,120739,84,11430,84,5026,84,42196,84,93962,84,71868,84,66199,84,66225,84,66325,84,119854,117,119906,117,119958,117,120010,117,120062,117,120114,117,120166,117,120218,117,120270,117,120322,117,120374,117,120426,117,120478,117,42911,117,7452,117,43854,117,43858,117,651,117,965,117,120534,117,120592,117,120650,117,120708,117,120766,117,1405,117,66806,117,71896,117,8746,85,8899,85,119828,85,119880,85,119932,85,119984,85,120036,85,120088,85,120140,85,120192,85,120244,85,120296,85,120348,85,120400,85,120452,85,1357,85,4608,85,66766,85,5196,85,42228,85,94018,85,71864,85,8744,118,8897,118,65366,118,8564,118,119855,118,119907,118,119959,118,120011,118,120063,118,120115,118,120167,118,120219,118,120271,118,120323,118,120375,118,120427,118,120479,118,7456,118,957,118,120526,118,120584,118,120642,118,120700,118,120758,118,1141,118,1496,118,71430,118,43945,118,71872,118,119309,86,1639,86,1783,86,8548,86,119829,86,119881,86,119933,86,119985,86,120037,86,120089,86,120141,86,120193,86,120245,86,120297,86,120349,86,120401,86,120453,86,1140,86,11576,86,5081,86,5167,86,42719,86,42214,86,93960,86,71840,86,66845,86,623,119,119856,119,119908,119,119960,119,120012,119,120064,119,120116,119,120168,119,120220,119,120272,119,120324,119,120376,119,120428,119,120480,119,7457,119,1121,119,1309,119,1377,119,71434,119,71438,119,71439,119,43907,119,71919,87,71910,87,119830,87,119882,87,119934,87,119986,87,120038,87,120090,87,120142,87,120194,87,120246,87,120298,87,120350,87,120402,87,120454,87,1308,87,5043,87,5076,87,42218,87,5742,120,10539,120,10540,120,10799,120,65368,120,8569,120,119857,120,119909,120,119961,120,120013,120,120065,120,120117,120,120169,120,120221,120,120273,120,120325,120,120377,120,120429,120,120481,120,5441,120,5501,120,5741,88,9587,88,66338,88,71916,88,65336,88,8553,88,119831,88,119883,88,119935,88,119987,88,120039,88,120091,88,120143,88,120195,88,120247,88,120299,88,120351,88,120403,88,120455,88,42931,88,935,88,120510,88,120568,88,120626,88,120684,88,120742,88,11436,88,11613,88,5815,88,42219,88,66192,88,66228,88,66327,88,66855,88,611,121,7564,121,65369,121,119858,121,119910,121,119962,121,120014,121,120066,121,120118,121,120170,121,120222,121,120274,121,120326,121,120378,121,120430,121,120482,121,655,121,7935,121,43866,121,947,121,8509,121,120516,121,120574,121,120632,121,120690,121,120748,121,1199,121,4327,121,71900,121,65337,89,119832,89,119884,89,119936,89,119988,89,120040,89,120092,89,120144,89,120196,89,120248,89,120300,89,120352,89,120404,89,120456,89,933,89,978,89,120508,89,120566,89,120624,89,120682,89,120740,89,11432,89,1198,89,5033,89,5053,89,42220,89,94019,89,71844,89,66226,89,119859,122,119911,122,119963,122,120015,122,120067,122,120119,122,120171,122,120223,122,120275,122,120327,122,120379,122,120431,122,120483,122,7458,122,43923,122,71876,122,66293,90,71909,90,65338,90,8484,90,8488,90,119833,90,119885,90,119937,90,119989,90,120041,90,120197,90,120249,90,120301,90,120353,90,120405,90,120457,90,918,90,120493,90,120551,90,120609,90,120667,90,120725,90,5059,90,42204,90,71849,90,65282,34,65284,36,65285,37,65286,38,65290,42,65291,43,65294,46,65295,47,65296,48,65297,49,65298,50,65299,51,65300,52,65301,53,65302,54,65303,55,65304,56,65305,57,65308,60,65309,61,65310,62,65312,64,65316,68,65318,70,65319,71,65324,76,65329,81,65330,82,65333,85,65334,86,65335,87,65343,95,65346,98,65348,100,65350,102,65355,107,65357,109,65358,110,65361,113,65362,114,65364,116,65365,117,65367,119,65370,122,65371,123,65373,125,119846,109],\"_default\":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"cs\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"de\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"es\":[8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"fr\":[65374,126,65306,58,65281,33,8216,96,8245,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"it\":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"ja\":[8211,45,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65292,44,65307,59],\"ko\":[8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"pl\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"pt-BR\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"qps-ploc\":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"ru\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,305,105,921,73,1009,112,215,120,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"tr\":[160,32,8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"zh-hans\":[65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41],\"zh-hant\":[8211,45,65374,126,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65307,59]}' + '{\"_common\":[8232,32,8233,32,5760,32,8192,32,8193,32,8194,32,8195,32,8196,32,8197,32,8198,32,8200,32,8201,32,8202,32,8287,32,8199,32,8239,32,2042,95,65101,95,65102,95,65103,95,8208,45,8209,45,8210,45,65112,45,1748,45,8259,45,727,45,8722,45,10134,45,11450,45,1549,44,1643,44,8218,44,184,44,42233,44,894,59,2307,58,2691,58,1417,58,1795,58,1796,58,5868,58,65072,58,6147,58,6153,58,8282,58,1475,58,760,58,42889,58,8758,58,720,58,42237,58,451,33,11601,33,660,63,577,63,2429,63,5038,63,42731,63,119149,46,8228,46,1793,46,1794,46,42510,46,68176,46,1632,46,1776,46,42232,46,1373,96,65287,96,8219,96,8242,96,1370,96,1523,96,8175,96,65344,96,900,96,8189,96,8125,96,8127,96,8190,96,697,96,884,96,712,96,714,96,715,96,756,96,699,96,701,96,700,96,702,96,42892,96,1497,96,2036,96,2037,96,5194,96,5836,96,94033,96,94034,96,65339,91,10088,40,10098,40,12308,40,64830,40,65341,93,10089,41,10099,41,12309,41,64831,41,10100,123,119060,123,10101,125,65342,94,8270,42,1645,42,8727,42,66335,42,5941,47,8257,47,8725,47,8260,47,9585,47,10187,47,10744,47,119354,47,12755,47,12339,47,11462,47,20031,47,12035,47,65340,92,65128,92,8726,92,10189,92,10741,92,10745,92,119311,92,119355,92,12756,92,20022,92,12034,92,42872,38,708,94,710,94,5869,43,10133,43,66203,43,8249,60,10094,60,706,60,119350,60,5176,60,5810,60,5120,61,11840,61,12448,61,42239,61,8250,62,10095,62,707,62,119351,62,5171,62,94015,62,8275,126,732,126,8128,126,8764,126,65372,124,65293,45,120784,50,120794,50,120804,50,120814,50,120824,50,130034,50,42842,50,423,50,1000,50,42564,50,5311,50,42735,50,119302,51,120785,51,120795,51,120805,51,120815,51,120825,51,130035,51,42923,51,540,51,439,51,42858,51,11468,51,1248,51,94011,51,71882,51,120786,52,120796,52,120806,52,120816,52,120826,52,130036,52,5070,52,71855,52,120787,53,120797,53,120807,53,120817,53,120827,53,130037,53,444,53,71867,53,120788,54,120798,54,120808,54,120818,54,120828,54,130038,54,11474,54,5102,54,71893,54,119314,55,120789,55,120799,55,120809,55,120819,55,120829,55,130039,55,66770,55,71878,55,2819,56,2538,56,2666,56,125131,56,120790,56,120800,56,120810,56,120820,56,120830,56,130040,56,547,56,546,56,66330,56,2663,57,2920,57,2541,57,3437,57,120791,57,120801,57,120811,57,120821,57,120831,57,130041,57,42862,57,11466,57,71884,57,71852,57,71894,57,9082,97,65345,97,119834,97,119886,97,119938,97,119990,97,120042,97,120094,97,120146,97,120198,97,120250,97,120302,97,120354,97,120406,97,120458,97,593,97,945,97,120514,97,120572,97,120630,97,120688,97,120746,97,65313,65,119808,65,119860,65,119912,65,119964,65,120016,65,120068,65,120120,65,120172,65,120224,65,120276,65,120328,65,120380,65,120432,65,913,65,120488,65,120546,65,120604,65,120662,65,120720,65,5034,65,5573,65,42222,65,94016,65,66208,65,119835,98,119887,98,119939,98,119991,98,120043,98,120095,98,120147,98,120199,98,120251,98,120303,98,120355,98,120407,98,120459,98,388,98,5071,98,5234,98,5551,98,65314,66,8492,66,119809,66,119861,66,119913,66,120017,66,120069,66,120121,66,120173,66,120225,66,120277,66,120329,66,120381,66,120433,66,42932,66,914,66,120489,66,120547,66,120605,66,120663,66,120721,66,5108,66,5623,66,42192,66,66178,66,66209,66,66305,66,65347,99,8573,99,119836,99,119888,99,119940,99,119992,99,120044,99,120096,99,120148,99,120200,99,120252,99,120304,99,120356,99,120408,99,120460,99,7428,99,1010,99,11429,99,43951,99,66621,99,128844,67,71922,67,71913,67,65315,67,8557,67,8450,67,8493,67,119810,67,119862,67,119914,67,119966,67,120018,67,120174,67,120226,67,120278,67,120330,67,120382,67,120434,67,1017,67,11428,67,5087,67,42202,67,66210,67,66306,67,66581,67,66844,67,8574,100,8518,100,119837,100,119889,100,119941,100,119993,100,120045,100,120097,100,120149,100,120201,100,120253,100,120305,100,120357,100,120409,100,120461,100,1281,100,5095,100,5231,100,42194,100,8558,68,8517,68,119811,68,119863,68,119915,68,119967,68,120019,68,120071,68,120123,68,120175,68,120227,68,120279,68,120331,68,120383,68,120435,68,5024,68,5598,68,5610,68,42195,68,8494,101,65349,101,8495,101,8519,101,119838,101,119890,101,119942,101,120046,101,120098,101,120150,101,120202,101,120254,101,120306,101,120358,101,120410,101,120462,101,43826,101,1213,101,8959,69,65317,69,8496,69,119812,69,119864,69,119916,69,120020,69,120072,69,120124,69,120176,69,120228,69,120280,69,120332,69,120384,69,120436,69,917,69,120492,69,120550,69,120608,69,120666,69,120724,69,11577,69,5036,69,42224,69,71846,69,71854,69,66182,69,119839,102,119891,102,119943,102,119995,102,120047,102,120099,102,120151,102,120203,102,120255,102,120307,102,120359,102,120411,102,120463,102,43829,102,42905,102,383,102,7837,102,1412,102,119315,70,8497,70,119813,70,119865,70,119917,70,120021,70,120073,70,120125,70,120177,70,120229,70,120281,70,120333,70,120385,70,120437,70,42904,70,988,70,120778,70,5556,70,42205,70,71874,70,71842,70,66183,70,66213,70,66853,70,65351,103,8458,103,119840,103,119892,103,119944,103,120048,103,120100,103,120152,103,120204,103,120256,103,120308,103,120360,103,120412,103,120464,103,609,103,7555,103,397,103,1409,103,119814,71,119866,71,119918,71,119970,71,120022,71,120074,71,120126,71,120178,71,120230,71,120282,71,120334,71,120386,71,120438,71,1292,71,5056,71,5107,71,42198,71,65352,104,8462,104,119841,104,119945,104,119997,104,120049,104,120101,104,120153,104,120205,104,120257,104,120309,104,120361,104,120413,104,120465,104,1211,104,1392,104,5058,104,65320,72,8459,72,8460,72,8461,72,119815,72,119867,72,119919,72,120023,72,120179,72,120231,72,120283,72,120335,72,120387,72,120439,72,919,72,120494,72,120552,72,120610,72,120668,72,120726,72,11406,72,5051,72,5500,72,42215,72,66255,72,731,105,9075,105,65353,105,8560,105,8505,105,8520,105,119842,105,119894,105,119946,105,119998,105,120050,105,120102,105,120154,105,120206,105,120258,105,120310,105,120362,105,120414,105,120466,105,120484,105,618,105,617,105,953,105,8126,105,890,105,120522,105,120580,105,120638,105,120696,105,120754,105,1110,105,42567,105,1231,105,43893,105,5029,105,71875,105,65354,106,8521,106,119843,106,119895,106,119947,106,119999,106,120051,106,120103,106,120155,106,120207,106,120259,106,120311,106,120363,106,120415,106,120467,106,1011,106,1112,106,65322,74,119817,74,119869,74,119921,74,119973,74,120025,74,120077,74,120129,74,120181,74,120233,74,120285,74,120337,74,120389,74,120441,74,42930,74,895,74,1032,74,5035,74,5261,74,42201,74,119844,107,119896,107,119948,107,120000,107,120052,107,120104,107,120156,107,120208,107,120260,107,120312,107,120364,107,120416,107,120468,107,8490,75,65323,75,119818,75,119870,75,119922,75,119974,75,120026,75,120078,75,120130,75,120182,75,120234,75,120286,75,120338,75,120390,75,120442,75,922,75,120497,75,120555,75,120613,75,120671,75,120729,75,11412,75,5094,75,5845,75,42199,75,66840,75,1472,108,8739,73,9213,73,65512,73,1633,108,1777,73,66336,108,125127,108,120783,73,120793,73,120803,73,120813,73,120823,73,130033,73,65321,73,8544,73,8464,73,8465,73,119816,73,119868,73,119920,73,120024,73,120128,73,120180,73,120232,73,120284,73,120336,73,120388,73,120440,73,65356,108,8572,73,8467,108,119845,108,119897,108,119949,108,120001,108,120053,108,120105,73,120157,73,120209,73,120261,73,120313,73,120365,73,120417,73,120469,73,448,73,120496,73,120554,73,120612,73,120670,73,120728,73,11410,73,1030,73,1216,73,1493,108,1503,108,1575,108,126464,108,126592,108,65166,108,65165,108,1994,108,11599,73,5825,73,42226,73,93992,73,66186,124,66313,124,119338,76,8556,76,8466,76,119819,76,119871,76,119923,76,120027,76,120079,76,120131,76,120183,76,120235,76,120287,76,120339,76,120391,76,120443,76,11472,76,5086,76,5290,76,42209,76,93974,76,71843,76,71858,76,66587,76,66854,76,65325,77,8559,77,8499,77,119820,77,119872,77,119924,77,120028,77,120080,77,120132,77,120184,77,120236,77,120288,77,120340,77,120392,77,120444,77,924,77,120499,77,120557,77,120615,77,120673,77,120731,77,1018,77,11416,77,5047,77,5616,77,5846,77,42207,77,66224,77,66321,77,119847,110,119899,110,119951,110,120003,110,120055,110,120107,110,120159,110,120211,110,120263,110,120315,110,120367,110,120419,110,120471,110,1400,110,1404,110,65326,78,8469,78,119821,78,119873,78,119925,78,119977,78,120029,78,120081,78,120185,78,120237,78,120289,78,120341,78,120393,78,120445,78,925,78,120500,78,120558,78,120616,78,120674,78,120732,78,11418,78,42208,78,66835,78,3074,111,3202,111,3330,111,3458,111,2406,111,2662,111,2790,111,3046,111,3174,111,3302,111,3430,111,3664,111,3792,111,4160,111,1637,111,1781,111,65359,111,8500,111,119848,111,119900,111,119952,111,120056,111,120108,111,120160,111,120212,111,120264,111,120316,111,120368,111,120420,111,120472,111,7439,111,7441,111,43837,111,959,111,120528,111,120586,111,120644,111,120702,111,120760,111,963,111,120532,111,120590,111,120648,111,120706,111,120764,111,11423,111,4351,111,1413,111,1505,111,1607,111,126500,111,126564,111,126596,111,65259,111,65260,111,65258,111,65257,111,1726,111,64428,111,64429,111,64427,111,64426,111,1729,111,64424,111,64425,111,64423,111,64422,111,1749,111,3360,111,4125,111,66794,111,71880,111,71895,111,66604,111,1984,79,2534,79,2918,79,12295,79,70864,79,71904,79,120782,79,120792,79,120802,79,120812,79,120822,79,130032,79,65327,79,119822,79,119874,79,119926,79,119978,79,120030,79,120082,79,120134,79,120186,79,120238,79,120290,79,120342,79,120394,79,120446,79,927,79,120502,79,120560,79,120618,79,120676,79,120734,79,11422,79,1365,79,11604,79,4816,79,2848,79,66754,79,42227,79,71861,79,66194,79,66219,79,66564,79,66838,79,9076,112,65360,112,119849,112,119901,112,119953,112,120005,112,120057,112,120109,112,120161,112,120213,112,120265,112,120317,112,120369,112,120421,112,120473,112,961,112,120530,112,120544,112,120588,112,120602,112,120646,112,120660,112,120704,112,120718,112,120762,112,120776,112,11427,112,65328,80,8473,80,119823,80,119875,80,119927,80,119979,80,120031,80,120083,80,120187,80,120239,80,120291,80,120343,80,120395,80,120447,80,929,80,120504,80,120562,80,120620,80,120678,80,120736,80,11426,80,5090,80,5229,80,42193,80,66197,80,119850,113,119902,113,119954,113,120006,113,120058,113,120110,113,120162,113,120214,113,120266,113,120318,113,120370,113,120422,113,120474,113,1307,113,1379,113,1382,113,8474,81,119824,81,119876,81,119928,81,119980,81,120032,81,120084,81,120188,81,120240,81,120292,81,120344,81,120396,81,120448,81,11605,81,119851,114,119903,114,119955,114,120007,114,120059,114,120111,114,120163,114,120215,114,120267,114,120319,114,120371,114,120423,114,120475,114,43847,114,43848,114,7462,114,11397,114,43905,114,119318,82,8475,82,8476,82,8477,82,119825,82,119877,82,119929,82,120033,82,120189,82,120241,82,120293,82,120345,82,120397,82,120449,82,422,82,5025,82,5074,82,66740,82,5511,82,42211,82,94005,82,65363,115,119852,115,119904,115,119956,115,120008,115,120060,115,120112,115,120164,115,120216,115,120268,115,120320,115,120372,115,120424,115,120476,115,42801,115,445,115,1109,115,43946,115,71873,115,66632,115,65331,83,119826,83,119878,83,119930,83,119982,83,120034,83,120086,83,120138,83,120190,83,120242,83,120294,83,120346,83,120398,83,120450,83,1029,83,1359,83,5077,83,5082,83,42210,83,94010,83,66198,83,66592,83,119853,116,119905,116,119957,116,120009,116,120061,116,120113,116,120165,116,120217,116,120269,116,120321,116,120373,116,120425,116,120477,116,8868,84,10201,84,128872,84,65332,84,119827,84,119879,84,119931,84,119983,84,120035,84,120087,84,120139,84,120191,84,120243,84,120295,84,120347,84,120399,84,120451,84,932,84,120507,84,120565,84,120623,84,120681,84,120739,84,11430,84,5026,84,42196,84,93962,84,71868,84,66199,84,66225,84,66325,84,119854,117,119906,117,119958,117,120010,117,120062,117,120114,117,120166,117,120218,117,120270,117,120322,117,120374,117,120426,117,120478,117,42911,117,7452,117,43854,117,43858,117,651,117,965,117,120534,117,120592,117,120650,117,120708,117,120766,117,1405,117,66806,117,71896,117,8746,85,8899,85,119828,85,119880,85,119932,85,119984,85,120036,85,120088,85,120140,85,120192,85,120244,85,120296,85,120348,85,120400,85,120452,85,1357,85,4608,85,66766,85,5196,85,42228,85,94018,85,71864,85,8744,118,8897,118,65366,118,8564,118,119855,118,119907,118,119959,118,120011,118,120063,118,120115,118,120167,118,120219,118,120271,118,120323,118,120375,118,120427,118,120479,118,7456,118,957,118,120526,118,120584,118,120642,118,120700,118,120758,118,1141,118,1496,118,71430,118,43945,118,71872,118,119309,86,1639,86,1783,86,8548,86,119829,86,119881,86,119933,86,119985,86,120037,86,120089,86,120141,86,120193,86,120245,86,120297,86,120349,86,120401,86,120453,86,1140,86,11576,86,5081,86,5167,86,42719,86,42214,86,93960,86,71840,86,66845,86,623,119,119856,119,119908,119,119960,119,120012,119,120064,119,120116,119,120168,119,120220,119,120272,119,120324,119,120376,119,120428,119,120480,119,7457,119,1121,119,1309,119,1377,119,71434,119,71438,119,71439,119,43907,119,71919,87,71910,87,119830,87,119882,87,119934,87,119986,87,120038,87,120090,87,120142,87,120194,87,120246,87,120298,87,120350,87,120402,87,120454,87,1308,87,5043,87,5076,87,42218,87,5742,120,10539,120,10540,120,10799,120,65368,120,8569,120,119857,120,119909,120,119961,120,120013,120,120065,120,120117,120,120169,120,120221,120,120273,120,120325,120,120377,120,120429,120,120481,120,5441,120,5501,120,5741,88,9587,88,66338,88,71916,88,65336,88,8553,88,119831,88,119883,88,119935,88,119987,88,120039,88,120091,88,120143,88,120195,88,120247,88,120299,88,120351,88,120403,88,120455,88,42931,88,935,88,120510,88,120568,88,120626,88,120684,88,120742,88,11436,88,11613,88,5815,88,42219,88,66192,88,66228,88,66327,88,66855,88,611,121,7564,121,65369,121,119858,121,119910,121,119962,121,120014,121,120066,121,120118,121,120170,121,120222,121,120274,121,120326,121,120378,121,120430,121,120482,121,655,121,7935,121,43866,121,947,121,8509,121,120516,121,120574,121,120632,121,120690,121,120748,121,1199,121,4327,121,71900,121,65337,89,119832,89,119884,89,119936,89,119988,89,120040,89,120092,89,120144,89,120196,89,120248,89,120300,89,120352,89,120404,89,120456,89,933,89,978,89,120508,89,120566,89,120624,89,120682,89,120740,89,11432,89,1198,89,5033,89,5053,89,42220,89,94019,89,71844,89,66226,89,119859,122,119911,122,119963,122,120015,122,120067,122,120119,122,120171,122,120223,122,120275,122,120327,122,120379,122,120431,122,120483,122,7458,122,43923,122,71876,122,66293,90,71909,90,65338,90,8484,90,8488,90,119833,90,119885,90,119937,90,119989,90,120041,90,120197,90,120249,90,120301,90,120353,90,120405,90,120457,90,918,90,120493,90,120551,90,120609,90,120667,90,120725,90,5059,90,42204,90,71849,90,65282,34,65284,36,65285,37,65286,38,65290,42,65291,43,65294,46,65295,47,65296,48,65297,49,65298,50,65299,51,65300,52,65301,53,65302,54,65303,55,65304,56,65305,57,65308,60,65309,61,65310,62,65312,64,65316,68,65318,70,65319,71,65324,76,65329,81,65330,82,65333,85,65334,86,65335,87,65343,95,65346,98,65348,100,65350,102,65355,107,65357,109,65358,110,65361,113,65362,114,65364,116,65365,117,65367,119,65370,122,65371,123,65373,125,119846,109],\"_default\":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"cs\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"de\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"es\":[8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"fr\":[65374,126,65306,58,65281,33,8216,96,8245,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"it\":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"ja\":[8211,45,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65292,44,65307,59],\"ko\":[8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"pl\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"pt-BR\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"qps-ploc\":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"ru\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,305,105,921,73,1009,112,215,120,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"tr\":[160,32,8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"zh-hans\":[65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41],\"zh-hant\":[8211,45,65374,126,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65307,59]}' ); }); From 4d437938eeb4b2e0b073777a835a7dcf9e36acf2 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:51:27 +0100 Subject: [PATCH 0812/3587] Remove NES accepted decoration effect (#238575) * remove NES accepted decoration effect * :lipstick: --- .../browser/model/inlineCompletionsModel.ts | 20 ++----------------- .../browser/view/inlineEdits/view.css | 4 ---- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index 0a545b965c41..f66cd70c9bdd 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { mapFindFirst } from '../../../../../base/common/arraysFind.js'; -import { disposableTimeout } from '../../../../../base/common/async.js'; import { itemsEquals } from '../../../../../base/common/equals.js'; import { BugIndicatingError, onUnexpectedError, onUnexpectedExternalError } from '../../../../../base/common/errors.js'; -import { Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; import { IObservable, IObservableWithChange, IReader, ITransaction, autorun, derived, derivedHandleChanges, derivedOpts, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from '../../../../../base/common/observable.js'; import { commonPrefixLength, firstNonWhitespaceIndex } from '../../../../../base/common/strings.js'; import { isDefined } from '../../../../../base/common/types.js'; @@ -19,7 +18,6 @@ import { EditorOption } from '../../../../common/config/editorOptions.js'; import { CursorColumns } from '../../../../common/core/cursorColumns.js'; import { EditOperation } from '../../../../common/core/editOperation.js'; import { LineRange } from '../../../../common/core/lineRange.js'; -import { OffsetRange } from '../../../../common/core/offsetRange.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; import { Selection } from '../../../../common/core/selection.js'; @@ -28,7 +26,7 @@ import { TextLength } from '../../../../common/core/textLength.js'; import { ScrollType } from '../../../../common/editorCommon.js'; import { Command, InlineCompletion, InlineCompletionContext, InlineCompletionTriggerKind, PartialAcceptTriggerKind } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; -import { EndOfLinePreference, IModelDecorationOptions, ITextModel, TrackedRangeStickiness } from '../../../../common/model.js'; +import { EndOfLinePreference, ITextModel } from '../../../../common/model.js'; import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js'; import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js'; import { SnippetController2 } from '../../../snippet/browser/snippetController2.js'; @@ -56,14 +54,6 @@ export class InlineCompletionsModel extends Disposable { private readonly _editorObs = observableCodeEditor(this._editor); - private readonly _acceptCompletionDecorationTimer = this._register(new MutableDisposable()); - private readonly _acceptCompletionDecorationCollection = this._editor.createDecorationsCollection(); - private readonly _acceptCompletionDecorationOptions: IModelDecorationOptions = { - description: 'inline-completion-accepted', - className: 'inline-completion-accepted', - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges - }; - private readonly _suggestPreviewEnabled = this._editorObs.getOption(EditorOption.suggest).map(v => v.preview); private readonly _suggestPreviewMode = this._editorObs.getOption(EditorOption.suggest).map(v => v.previewMode); private readonly _inlineSuggestMode = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => v.mode); @@ -585,7 +575,6 @@ export class InlineCompletionsModel extends Disposable { } let completion: InlineCompletionItem; - const acceptDecorationOffsetRanges: OffsetRange[] = []; const state = this.state.get(); if (state?.kind === 'ghostText') { @@ -595,7 +584,6 @@ export class InlineCompletionsModel extends Disposable { completion = state.inlineCompletion.toInlineCompletion(undefined); } else if (state?.kind === 'inlineEdit') { completion = state.inlineCompletion.toInlineCompletion(undefined); - acceptDecorationOffsetRanges.push(...(state.inlineCompletion.inlineEdit?.get()?.getNewTextRanges() ?? [])); } else { return; } @@ -624,10 +612,6 @@ export class InlineCompletionsModel extends Disposable { ...completion.additionalTextEdits ]); editor.setSelections(selections, 'inlineCompletionAccept'); - - const newTextRanges = acceptDecorationOffsetRanges.map(r => Range.fromPositions(this.textModel.getPositionAt(r.start), this.textModel.getPositionAt(r.endExclusive))); - this._acceptCompletionDecorationCollection.set(newTextRanges.map(r => ({ range: r, options: this._acceptCompletionDecorationOptions }))); - this._acceptCompletionDecorationTimer.value = disposableTimeout(() => this._acceptCompletionDecorationCollection.clear(), 2500); } // Reset before invoking the command, as the command might cause a follow up trigger (which we don't want to reset). diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index 6fb2883ec042..f3c76c922f96 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -210,10 +210,6 @@ } } -.monaco-editor .inline-completion-accepted { - background-color: var(--vscode-inlineEdit-acceptedBackground); -} - .inline-edits-view-gutter-indicator .codicon{ margin-top: 1px; /* TODO: Move into gutter DOM initialization */ } From cf2afd63fa943bfaa8b6c61a9a1854296d83e566 Mon Sep 17 00:00:00 2001 From: Johannes Date: Thu, 23 Jan 2025 15:54:55 +0100 Subject: [PATCH 0813/3587] tweaks --- src/vs/base/common/strings.ts | 2 -- src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts | 1 - src/vs/workbench/contrib/chat/browser/chatWidget.ts | 1 - 3 files changed, 4 deletions(-) diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 609d99f9ceae..c4913e9bfa98 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -788,13 +788,11 @@ export function rcut(text: string, n: number, suffix = ''): string { const re = /\b/g; let lastWordBreak = trimmed.length; - let i = 0; while (re.test(trimmed)) { if (trimmed.length - re.lastIndex > n) { lastWordBreak = re.lastIndex; } - i = re.lastIndex; re.lastIndex += 1; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index 5593f309fed7..347b2697d90e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -288,7 +288,6 @@ class ChatEditorOverlayWidget implements IOverlayWidget { })); this._show(); - } private _show(): void { diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index b37c4622c583..d601614b2aac 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1024,7 +1024,6 @@ export class ChatWidget extends Disposable implements IChatWidget { 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[] = []; - // 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) { From a039b56fe9005231a017ba7ed7a679869ecc7dbb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 08:05:51 -0800 Subject: [PATCH 0814/3587] Bump undici from 7.2.0 to 7.3.0 in /remote (#238563) Bumps [undici](https://github.com/nodejs/undici) from 7.2.0 to 7.3.0. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v7.2.0...v7.3.0) --- updated-dependencies: - dependency-name: undici dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- remote/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/remote/package-lock.json b/remote/package-lock.json index ae966cf9cda7..e039f2ab2f6f 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -1409,9 +1409,9 @@ } }, "node_modules/undici": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.2.0.tgz", - "integrity": "sha512-klt+0S55GBViA9nsq48/NSCo4YX5mjydjypxD7UmHh/brMu8h/Mhd/F7qAeoH2NOO8SDTk6kjnTFc4WpzmfYpQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.3.0.tgz", + "integrity": "sha512-Qy96NND4Dou5jKoSJ2gm8ax8AJM/Ey9o9mz7KN1bb9GP+G0l20Zw8afxTnY2f4b7hmhn/z8aC2kfArVQlAhFBw==", "license": "MIT", "engines": { "node": ">=20.18.1" From 54372d33fb414621d4deff20df48d66b8650755a Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 23 Jan 2025 17:13:42 +0100 Subject: [PATCH 0815/3587] support filter by category in all logs (#238582) --- .../output/browser/output.contribution.ts | 57 +-------------- .../contrib/output/browser/outputServices.ts | 30 ++++---- .../contrib/output/browser/outputView.ts | 73 +++++++++++++------ .../output/common/outputChannelModel.ts | 8 +- .../services/output/common/output.ts | 10 +-- 5 files changed, 78 insertions(+), 100 deletions(-) diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 2e16d0fd7adf..b1cbad16c0d9 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -10,7 +10,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuId, registerAction2, Action2, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { OutputService } from './outputServices.js'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, OUTPUT_FILTER_FOCUS_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT, isSingleSourceOutputChannelDescriptor, isMultiSourceOutputChannelDescriptor, HIDE_SOURCE_FILTER_CONTEXT } from '../../../services/output/common/output.js'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, IOutputChannelRegistry, Extensions, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, OUTPUT_FILTER_FOCUS_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT, isSingleSourceOutputChannelDescriptor } from '../../../services/output/common/output.js'; import { OutputViewPane } from './outputView.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; @@ -26,7 +26,7 @@ import { ContextKeyExpr, ContextKeyExpression } from '../../../../platform/conte import { Codicon } from '../../../../base/common/codicons.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; -import { Disposable, DisposableMap, DisposableStore, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { ILoggerService, LogLevel, LogLevelToLocalizedString, LogLevelToString } from '../../../../platform/log/common/log.js'; import { IDefaultLogLevelsService } from '../../logs/common/defaultLogLevels.js'; @@ -41,7 +41,6 @@ import { ViewAction } from '../../../browser/parts/views/viewPane.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { basename } from '../../../../base/common/resources.js'; -import { escapeRegExpCharacters } from '../../../../base/common/strings.js'; const IMPORTED_LOG_ID_PREFIX = 'importedLog.'; @@ -117,7 +116,6 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { this.registerOpenLogFileAction(); this.registerConfigureActiveOutputLogLevelAction(); this.registerLogLevelFilterActions(); - this.registerSourceFilterAction(); this.registerClearFilterActions(); this.registerExportLogsAction(); this.registerImportLogAction(); @@ -684,57 +682,6 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { registerLogLevel(LogLevel.Error, SHOW_ERROR_FILTER_CONTEXT); } - private registerSourceFilterAction(): void { - const registeredChannels = new DisposableMap(); - this._register(toDisposable(() => dispose(registeredChannels.values()))); - const registerOutputChannels = (channels: IOutputChannelDescriptor[]) => { - for (const channel of channels) { - if (!isMultiSourceOutputChannelDescriptor(channel)) { - continue; - } - const disposables = new DisposableStore(); - registeredChannels.set(channel.id, disposables); - for (const source of channel.source) { - if (!source.name) { - continue; - } - const sourceFilter = `${channel.id}:${source.name}`; - disposables.add(registerAction2(class extends Action2 { - constructor() { - super({ - id: `workbench.actions.${OUTPUT_VIEW_ID}.toggle.${sourceFilter}`, - title: source.name!, - toggled: ContextKeyExpr.regex(HIDE_SOURCE_FILTER_CONTEXT.key, new RegExp(`.*,${escapeRegExpCharacters(sourceFilter)},.*`)).negate(), - menu: { - id: viewFilterSubmenu, - group: '1_source_filter', - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), ACTIVE_OUTPUT_CHANNEL_CONTEXT.isEqualTo(channel.id)), - } - }); - } - async run(accessor: ServicesAccessor): Promise { - const outputService = accessor.get(IOutputService); - outputService.filters.toggleSource(sourceFilter); - } - })); - } - } - }; - registerOutputChannels(this.outputService.getChannelDescriptors()); - const outputChannelRegistry = Registry.as(Extensions.OutputChannels); - this._register(outputChannelRegistry.onDidRegisterChannel(e => { - const channel = this.outputService.getChannelDescriptor(e); - if (channel) { - registerOutputChannels([channel]); - } - })); - this._register(outputChannelRegistry.onDidUpdateChannelSources(e => { - registeredChannels.deleteAndDispose(e.id); - registerOutputChannels([e]); - })); - this._register(outputChannelRegistry.onDidRemoveChannel(e => registeredChannels.deleteAndDispose(e.id))); - } - private registerClearFilterActions(): void { this._register(registerAction2(class extends ViewAction { constructor() { diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 2cbd03b88259..a140d38c0e64 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -10,7 +10,7 @@ import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js' import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; -import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, IOutputViewFilters, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT, IMultiSourceOutputChannelDescriptor, isSingleSourceOutputChannelDescriptor, HIDE_SOURCE_FILTER_CONTEXT, isMultiSourceOutputChannelDescriptor, ILogEntry } from '../../../services/output/common/output.js'; +import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_FILE_OUTPUT, CONTEXT_ACTIVE_OUTPUT_LEVEL_SETTABLE, CONTEXT_ACTIVE_OUTPUT_LEVEL, CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT, IOutputViewFilters, SHOW_DEBUG_FILTER_CONTEXT, SHOW_ERROR_FILTER_CONTEXT, SHOW_INFO_FILTER_CONTEXT, SHOW_TRACE_FILTER_CONTEXT, SHOW_WARNING_FILTER_CONTEXT, CONTEXT_ACTIVE_LOG_FILE_OUTPUT, IMultiSourceOutputChannelDescriptor, isSingleSourceOutputChannelDescriptor, HIDE_CATEGORY_FILTER_CONTEXT, isMultiSourceOutputChannelDescriptor, ILogEntry } from '../../../services/output/common/output.js'; import { OutputLinkProvider } from './outputLinkProvider.js'; import { ITextModelService, ITextModelContentProvider } from '../../../../editor/common/services/resolverService.js'; import { ITextModel } from '../../../../editor/common/model.js'; @@ -113,7 +113,7 @@ class OutputViewFilters extends Disposable implements IOutputViewFilters { this._info.set(options.info); this._warning.set(options.warning); this._error.set(options.error); - this._sources.set(options.sources); + this._categories.set(options.sources); this.filterHistory = options.filterHistory; } @@ -186,29 +186,29 @@ class OutputViewFilters extends Disposable implements IOutputViewFilters { } } - private readonly _sources = HIDE_SOURCE_FILTER_CONTEXT.bindTo(this.contextKeyService); - get sources(): string { - return this._sources.get() || ','; + private readonly _categories = HIDE_CATEGORY_FILTER_CONTEXT.bindTo(this.contextKeyService); + get categories(): string { + return this._categories.get() || ','; } - set sources(sources: string) { - this._sources.set(sources); + set categories(categories: string) { + this._categories.set(categories); this._onDidChange.fire(); } - toggleSource(source: string): void { - const sources = this.sources; - if (this.hasSource(source)) { - this.sources = sources.replace(`,${source},`, ','); + toggleCategory(category: string): void { + const categories = this.categories; + if (this.hasCategory(category)) { + this.categories = categories.replace(`,${category},`, ','); } else { - this.sources = `${sources}${source},`; + this.categories = `${categories}${category},`; } } - hasSource(source: string): boolean { - if (source === ',') { + hasCategory(category: string): boolean { + if (category === ',') { return false; } - return this.sources.includes(`,${source},`); + return this.categories.includes(`,${category},`); } } diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 6cf5141a650b..d3d24dd49f3d 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -10,10 +10,10 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IContextKeyService, IContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKeyService, IContextKey, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IEditorOpenContext } from '../../../common/editor.js'; import { AbstractTextResourceEditor } from '../../../browser/parts/editor/textResourceEditor.js'; -import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT, ILogEntry } from '../../../services/output/common/output.js'; +import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT, ILogEntry, HIDE_CATEGORY_FILTER_CONTEXT } from '../../../services/output/common/output.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; @@ -46,6 +46,9 @@ import { Range } from '../../../../editor/common/core/range.js'; import { FindDecorations } from '../../../../editor/contrib/find/browser/findDecorations.js'; import { Memento, MementoObject } from '../../../common/memento.js'; import { Markers } from '../../markers/common/markers.js'; +import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { viewFilterSubmenu } from '../../../browser/parts/views/viewFilter.js'; +import { escapeRegExpCharacters } from '../../../../base/common/strings.js'; export class OutputViewPane extends FilterViewPane { @@ -96,7 +99,7 @@ export class OutputViewPane extends FilterViewPane { filters.info = this.panelState['showInfo'] ?? true; filters.warning = this.panelState['showWarning'] ?? true; filters.error = this.panelState['showError'] ?? true; - filters.sources = this.panelState['sourcesFilter'] ?? ''; + filters.categories = this.panelState['categories'] ?? ''; this.scrollLockContextKey = CONTEXT_OUTPUT_SCROLL_LOCK.bindTo(this.contextKeyService); @@ -186,7 +189,7 @@ export class OutputViewPane extends FilterViewPane { private checkMoreFilters(): void { const filters = this.outputService.filters; - this.filterWidget.checkMoreFilters(!filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error || (!!this.channelId && filters.sources.includes(`,${this.channelId}:`))); + this.filterWidget.checkMoreFilters(!filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error || (!!this.channelId && filters.categories.includes(`,${this.channelId}:`))); } private clearInput(): void { @@ -207,7 +210,7 @@ export class OutputViewPane extends FilterViewPane { this.panelState['showInfo'] = filters.info; this.panelState['showWarning'] = filters.warning; this.panelState['showError'] = filters.error; - this.panelState['sourcesFilter'] = filters.sources; + this.panelState['categories'] = filters.categories; this.memento.saveMemento(); super.saveState(); @@ -349,11 +352,12 @@ export class FilterController extends Disposable implements IEditorContribution private readonly modelDisposables: DisposableStore = this._register(new DisposableStore()); private hiddenAreas: Range[] = []; + private readonly categories = new Map(); private readonly decorationsCollection: IEditorDecorationsCollection; constructor( private readonly editor: ICodeEditor, - @IOutputService private readonly outputService: IOutputService + @IOutputService private readonly outputService: IOutputService, ) { super(); this.decorationsCollection = editor.createDecorationsCollection(); @@ -364,6 +368,7 @@ export class FilterController extends Disposable implements IEditorContribution private onDidChangeModel(): void { this.modelDisposables.clear(); this.hiddenAreas = []; + this.categories.clear(); if (!this.editor.hasModel()) { return; @@ -396,40 +401,66 @@ export class FilterController extends Disposable implements IEditorContribution } private filterIncremental(model: ITextModel, fromLineNumber: number): void { - const { findMatches, hiddenAreas } = this.computeDecorations(model, fromLineNumber); + const { findMatches, hiddenAreas, categories: sources } = this.compute(model, fromLineNumber); this.hiddenAreas.push(...hiddenAreas); this.editor.setHiddenAreas(this.hiddenAreas, this); if (findMatches.length) { this.decorationsCollection.append(findMatches); } + if (sources.size) { + const that = this; + for (const [categoryFilter, categoryName] of sources) { + if (this.categories.has(categoryFilter)) { + continue; + } + this.categories.set(categoryFilter, categoryName); + this.modelDisposables.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.${OUTPUT_VIEW_ID}.toggle.${categoryFilter}`, + title: categoryName, + toggled: ContextKeyExpr.regex(HIDE_CATEGORY_FILTER_CONTEXT.key, new RegExp(`.*,${escapeRegExpCharacters(categoryFilter)},.*`)).negate(), + menu: { + id: viewFilterSubmenu, + group: '1_category_filter', + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID)), + } + }); + } + async run(): Promise { + that.outputService.filters.toggleCategory(categoryFilter); + } + })); + } + } } - private computeDecorations(model: ITextModel, fromLineNumber: number): { findMatches: IModelDeltaDecoration[]; hiddenAreas: Range[] } { + private compute(model: ITextModel, fromLineNumber: number): { findMatches: IModelDeltaDecoration[]; hiddenAreas: Range[]; categories: Map } { const filters = this.outputService.filters; const activeChannel = this.outputService.getActiveChannel(); const findMatches: IModelDeltaDecoration[] = []; const hiddenAreas: Range[] = []; + const categories = new Map(); const logEntries = activeChannel?.getLogEntries(); if (activeChannel && logEntries?.length) { const hasLogLevelFilter = !filters.trace || !filters.debug || !filters.info || !filters.warning || !filters.error; - if (!hasLogLevelFilter && !filters.text && !filters.sources.includes(activeChannel.id)) { - return { findMatches, hiddenAreas }; - } - const fromLogLevelEntryIndex = logEntries.findIndex(entry => fromLineNumber >= entry.range.startLineNumber && fromLineNumber <= entry.range.endLineNumber); if (fromLogLevelEntryIndex === -1) { - return { findMatches, hiddenAreas }; + return { findMatches, hiddenAreas, categories }; } for (let i = fromLogLevelEntryIndex; i < logEntries.length; i++) { const entry = logEntries[i]; + if (entry.category) { + categories.set(`${activeChannel.id}:${entry.category}`, entry.category); + } if (hasLogLevelFilter && !this.shouldShowLogLevel(entry, filters)) { hiddenAreas.push(entry.range); continue; } - if (!this.shouldShowSource(activeChannel.id, entry, filters)) { + if (!this.shouldShowCategory(activeChannel.id, entry, filters)) { hiddenAreas.push(entry.range); continue; } @@ -444,15 +475,15 @@ export class FilterController extends Disposable implements IEditorContribution } } } - return { findMatches, hiddenAreas }; + return { findMatches, hiddenAreas, categories }; } if (!filters.text) { - return { findMatches, hiddenAreas }; + return { findMatches, hiddenAreas, categories }; } const lineCount = model.getLineCount(); - for (let lineNumber = fromLineNumber + 1; lineNumber <= lineCount; lineNumber++) { + for (let lineNumber = fromLineNumber; lineNumber <= lineCount; lineNumber++) { const lineRange = new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber)); const matches = model.findMatches(filters.text, lineRange, false, false, null, false); if (matches.length) { @@ -463,7 +494,7 @@ export class FilterController extends Disposable implements IEditorContribution hiddenAreas.push(lineRange); } } - return { findMatches, hiddenAreas }; + return { findMatches, hiddenAreas, categories }; } private shouldShowLogLevel(entry: ILogEntry, filters: IOutputViewFilters): boolean { @@ -482,10 +513,10 @@ export class FilterController extends Disposable implements IEditorContribution return true; } - private shouldShowSource(activeChannelId: string, entry: ILogEntry, filters: IOutputViewFilters): boolean { - if (!entry.source) { + private shouldShowCategory(activeChannelId: string, entry: ILogEntry, filters: IOutputViewFilters): boolean { + if (!entry.category) { return true; } - return !filters.hasSource(`${activeChannelId}:${entry.source}`); + return !filters.hasCategory(`${activeChannelId}:${entry.category}`); } } diff --git a/src/vs/workbench/contrib/output/common/outputChannelModel.ts b/src/vs/workbench/contrib/output/common/outputChannelModel.ts index 8d78deac91f1..a650e8e297c0 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModel.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModel.ts @@ -26,7 +26,7 @@ import { isCancellationError } from '../../../../base/common/errors.js'; import { TextModel } from '../../../../editor/common/model/textModel.js'; import { binarySearch, sortedDiff } from '../../../../base/common/arrays.js'; -const LOG_ENTRY_REGEX = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s(\[(info|trace|debug|error|warning)\])\s(\[(.*?)\]\s)?/; +const LOG_ENTRY_REGEX = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s(\[(info|trace|debug|error|warning)\])\s(\[(.*?)\])?/; function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntry | null { const lineContent = model.getLineContent(lineNumber); @@ -36,7 +36,7 @@ function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntry | nul const timestampRange = new Range(lineNumber, 1, lineNumber, match[1].length); const logLevel = parseLogLevel(match[3]); const logLevelRange = new Range(lineNumber, timestampRange.endColumn + 1, lineNumber, timestampRange.endColumn + 1 + match[2].length); - const source = match[5]; + const category = match[5]; const startLine = lineNumber; let endLine = lineNumber; @@ -48,7 +48,7 @@ function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntry | nul endLine++; } const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)); - return { range, timestamp, timestampRange, logLevel, logLevelRange, source }; + return { range, timestamp, timestampRange, logLevel, logLevelRange, category }; } return null; } @@ -383,7 +383,7 @@ class MultiFileContentProvider extends Disposable implements IContentProvider { const content = name ? `${lineContent.substring(0, logEntry.logLevelRange.endColumn)} [${name}]${lineContent.substring(logEntry.logLevelRange.endColumn)}` : lineContent; return [{ ...logEntry, - source: name, + category: name, range: new Range(logEntry.range.startLineNumber, logEntry.logLevelRange.startColumn, logEntry.range.endLineNumber, name ? logEntry.range.endColumn + name.length + 3 : logEntry.range.endColumn), }, content]; }; diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index 8d916e2bcb9c..0617017a0bc0 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -50,7 +50,7 @@ export const SHOW_INFO_FILTER_CONTEXT = new RawContextKey('output.filte export const SHOW_WARNING_FILTER_CONTEXT = new RawContextKey('output.filter.warning', true); export const SHOW_ERROR_FILTER_CONTEXT = new RawContextKey('output.filter.error', true); export const OUTPUT_FILTER_FOCUS_CONTEXT = new RawContextKey('outputFilterFocus', false); -export const HIDE_SOURCE_FILTER_CONTEXT = new RawContextKey('output.filter.sources', ''); +export const HIDE_CATEGORY_FILTER_CONTEXT = new RawContextKey('output.filter.categories', ''); export interface IOutputViewFilters { readonly onDidChange: Event; @@ -60,9 +60,9 @@ export interface IOutputViewFilters { info: boolean; warning: boolean; error: boolean; - sources: string; - toggleSource(source: string): void; - hasSource(source: string): boolean; + categories: string; + toggleCategory(category: string): void; + hasCategory(category: string): boolean; } export const IOutputService = createDecorator('outputService'); @@ -152,7 +152,7 @@ export interface ILogEntry { readonly timestampRange: Range; readonly logLevel: LogLevel; readonly logLevelRange: Range; - readonly source: string | undefined; + readonly category: string | undefined; } export interface IOutputChannel { From 4fd2ab399831d8aea5e4da9189ae8c32e4a71ca1 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 23 Jan 2025 10:30:05 -0600 Subject: [PATCH 0816/3587] add terminal suggest status bar to display actions (#238508) --- src/vs/platform/actions/common/actions.ts | 1 + .../browser/terminal.suggest.contribution.ts | 26 +++++- .../suggest/browser/terminalSuggestAddon.ts | 8 +- .../suggest/common/terminal.suggest.ts | 1 + .../common/terminalSuggestConfiguration.ts | 10 ++- .../suggest/browser/media/suggest.css | 12 +++ .../suggest/browser/simpleSuggestWidget.ts | 81 +++++++++++-------- 7 files changed, 102 insertions(+), 37 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index f87a62ed7ebd..0e10b0d232f2 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -108,6 +108,7 @@ export class MenuId { static readonly MenubarSwitchEditorMenu = new MenuId('MenubarSwitchEditorMenu'); static readonly MenubarSwitchGroupMenu = new MenuId('MenubarSwitchGroupMenu'); static readonly MenubarTerminalMenu = new MenuId('MenubarTerminalMenu'); + static readonly MenubarTerminalSuggestStatusMenu = new MenuId('MenubarTerminalSuggestStatusMenu'); static readonly MenubarViewMenu = new MenuId('MenubarViewMenu'); static readonly MenubarHomeMenu = new MenuId('MenubarHomeMenu'); static readonly OpenEditorsContext = new MenuId('OpenEditorsContext'); diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 15175d7ceacf..442c19efa34b 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -30,6 +30,8 @@ import { PwshCompletionProviderAddon } from './pwshCompletionProviderAddon.js'; import { SimpleSuggestContext } from '../../../../services/suggest/browser/simpleSuggestWidget.js'; import { SuggestDetailsClassName } from '../../../../services/suggest/browser/simpleSuggestWidgetDetails.js'; import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; +import { MenuId } from '../../../../../platform/actions/common/actions.js'; +import { IPreferencesService } from '../../../../services/preferences/common/preferences.js'; registerSingleton(ITerminalCompletionService, TerminalCompletionService, InstantiationType.Delayed); @@ -318,7 +320,7 @@ registerActiveInstanceAction({ registerActiveInstanceAction({ id: TerminalSuggestCommandId.AcceptSelectedSuggestion, - title: localize2('workbench.action.terminal.acceptSelectedSuggestion', 'Accept Selected Suggestion'), + title: localize2('workbench.action.terminal.acceptSelectedSuggestion', 'Insert'), f1: false, precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.focus, TerminalContextKeys.isOpen, TerminalContextKeys.suggestWidgetVisible), keybinding: { @@ -326,6 +328,11 @@ registerActiveInstanceAction({ // Tab is bound to other workbench keybindings that this needs to beat weight: KeybindingWeight.WorkbenchContrib + 1 }, + menu: { + id: MenuId.MenubarTerminalSuggestStatusMenu, + order: 1, + group: 'left' + }, run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.acceptSelectedSuggestion() }); @@ -356,6 +363,23 @@ registerActiveInstanceAction({ run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.hideSuggestWidget() }); +registerActiveInstanceAction({ + id: TerminalSuggestCommandId.ConfigureSettings, + title: localize2('workbench.action.terminal.configureSuggestSettings', 'Configure'), + f1: false, + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.focus, TerminalContextKeys.isOpen, TerminalContextKeys.suggestWidgetVisible), + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Comma, + weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.MenubarTerminalSuggestStatusMenu, + group: 'right', + order: 1 + }, + run: (activeInstance, c, accessor) => accessor.get(IPreferencesService).openSettings({ query: terminalSuggestConfigSection }) +}); + registerActiveInstanceAction({ id: TerminalSuggestCommandId.ClearSuggestCache, title: localize2('workbench.action.terminal.clearSuggestCache', 'Clear Suggest Cache'), diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 50f04542a564..c5cf96ae22a2 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -22,7 +22,7 @@ import { activeContrastBorder } from '../../../../../platform/theme/common/color import { ITerminalConfigurationService } from '../../../terminal/browser/terminal.js'; import type { IXtermCore } from '../../../terminal/browser/xterm-private.js'; import { TerminalStorageKeys } from '../../../terminal/common/terminalStorageKeys.js'; -import { terminalSuggestConfigSection, type ITerminalSuggestConfiguration } from '../common/terminalSuggestConfiguration.js'; +import { terminalSuggestConfigSection, TerminalSuggestSettingId, type ITerminalSuggestConfiguration } from '../common/terminalSuggestConfiguration.js'; import { SimpleCompletionItem } from '../../../../services/suggest/browser/simpleCompletionItem.js'; import { LineContext, SimpleCompletionModel } from '../../../../services/suggest/browser/simpleCompletionModel.js'; import { ISimpleSelectedSuggestion, SimpleSuggestWidget } from '../../../../services/suggest/browser/simpleSuggestWidget.js'; @@ -32,6 +32,7 @@ import { TerminalShellType } from '../../../../../platform/terminal/common/termi import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { MenuId } from '../../../../../platform/actions/common/actions.js'; export interface ISuggestController { isPasting: boolean; @@ -408,7 +409,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._container!, this._instantiationService.createInstance(PersistedWidgetSize), () => fontInfo, - {} + { + statusBarMenuId: MenuId.MenubarTerminalSuggestStatusMenu, + showStatusBarSettingId: TerminalSuggestSettingId.ShowStatusBar + }, )); this._suggestWidget.list.style(getListStyles({ listInactiveFocusBackground: editorSuggestWidgetSelectedBackground, diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts index 049ce2b29768..30d9c86ae469 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts @@ -16,6 +16,7 @@ export const enum TerminalSuggestCommandId { ResetWidgetSize = 'workbench.action.terminal.resetSuggestWidgetSize', ToggleDetails = 'workbench.action.terminal.suggestToggleDetails', ToggleDetailsFocus = 'workbench.action.terminal.suggestToggleDetailsFocus', + ConfigureSettings = 'workbench.action.terminal.configureSuggestSettings', } export const defaultTerminalSuggestCommandsToSkipShell = [ diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts index 6b2dd0618103..c126651bc712 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts @@ -17,6 +17,7 @@ export const enum TerminalSuggestSettingId { EnableExtensionCompletions = 'terminal.integrated.suggest.enableExtensionCompletions', WindowsExecutableExtensions = 'terminal.integrated.suggest.windowsExecutableExtensions', Providers = 'terminal.integrated.suggest.providers', + ShowStatusBar = 'terminal.integrated.suggest.showStatusBar', } export const windowsDefaultExecutableExtensions: string[] = [ @@ -134,7 +135,14 @@ export const terminalSuggestConfiguration: IStringDictionary; - private readonly _status?: SuggestWidgetStatus; + private _status?: SuggestWidgetStatus; private readonly _details: SimpleSuggestDetailsOverlay; private readonly _showTimeout = this._register(new TimeoutTimer()); @@ -109,9 +114,9 @@ export class SimpleSuggestWidget extends Disposable { private readonly _container: HTMLElement, private readonly _persistedSize: IPersistedWidgetSizeDelegate, private readonly _getFontInfo: () => ISimpleSuggestWidgetFontInfo, - options: IWorkbenchSuggestWidgetOptions, + private readonly _options: IWorkbenchSuggestWidgetOptions, @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, @IStorageService private readonly _storageService: IStorageService, @IContextKeyService _contextKeyService: IContextKeyService ) { @@ -170,7 +175,7 @@ export class SimpleSuggestWidget extends Disposable { state = undefined; })); - const applyIconStyle = () => this.element.domNode.classList.toggle('no-icons', !configurationService.getValue('editor.suggest.showIcons')); + const applyIconStyle = () => this.element.domNode.classList.toggle('no-icons', !_configurationService.getValue('editor.suggest.showIcons')); applyIconStyle(); const renderer = new SimpleSuggestWidgetItemRenderer(_getFontInfo); @@ -227,8 +232,8 @@ export class SimpleSuggestWidget extends Disposable { this._details = this._register(new SimpleSuggestDetailsOverlay(details, this._listElement)); this._register(dom.addDisposableListener(this._details.widget.domNode, 'blur', (e) => this._onDidBlurDetails.fire(e))); - if (options.statusBarMenuId) { - this._status = this._register(instantiationService.createInstance(SuggestWidgetStatus, this.element.domNode, options.statusBarMenuId)); + if (_options.statusBarMenuId && _options.showStatusBarSettingId && _configurationService.getValue(_options.showStatusBarSettingId)) { + this._status = this._register(instantiationService.createInstance(SuggestWidgetStatus, this.element.domNode, _options.statusBarMenuId)); this.element.domNode.classList.toggle('with-status-bar', true); } @@ -236,10 +241,25 @@ export class SimpleSuggestWidget extends Disposable { this._register(this._list.onTap(e => this._onListMouseDownOrTap(e))); this._register(this._list.onDidChangeFocus(e => this._onListFocus(e))); this._register(this._list.onDidChangeSelection(e => this._onListSelection(e))); - this._register(configurationService.onDidChangeConfiguration(e => { + this._register(_configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('editor.suggest.showIcons')) { applyIconStyle(); } + if (_options.statusBarMenuId && _options.showStatusBarSettingId && e.affectsConfiguration(_options.showStatusBarSettingId)) { + const showStatusBar: boolean = _configurationService.getValue(_options.showStatusBarSettingId); + if (showStatusBar && !this._status) { + this._status = this._register(instantiationService.createInstance(SuggestWidgetStatus, this.element.domNode, _options.statusBarMenuId)); + this._status.show(); + } else if (showStatusBar && this._status) { + this._status.show(); + } else if (this._status) { + this._status.element.remove(); + this._status.dispose(); + this._status = undefined; + this._layout(undefined); + } + this.element.domNode.classList.toggle('with-status-bar', showStatusBar); + } })); } @@ -428,10 +448,7 @@ export class SimpleSuggestWidget extends Disposable { dom.hide(this._messageElement, this._listElement, this._status.element); } dom.hide(this._listElement); - if (this._status) { - dom.hide(this._status?.element); - } - // this._details.hide(true); + this._details.hide(true); this._status?.hide(); // this._contentWidget.hide(); // this._ctxSuggestWidgetVisible.reset(); @@ -449,53 +466,52 @@ export class SimpleSuggestWidget extends Disposable { this._messageElement.textContent = SimpleSuggestWidget.LOADING_MESSAGE; dom.hide(this._listElement); if (this._status) { - dom.hide(this._status?.element); + dom.hide(this._status.element); } dom.show(this._messageElement); - // this._details.hide(); + this._details.hide(); this._show(); - // this._focusedItem = undefined; + this._focusedItem = undefined; break; case State.Empty: this.element.domNode.classList.add('message'); this._messageElement.textContent = SimpleSuggestWidget.NO_SUGGESTIONS_MESSAGE; dom.hide(this._listElement); if (this._status) { - dom.hide(this._status?.element); + dom.hide(this._status.element); } dom.show(this._messageElement); - // this._details.hide(); + this._details.hide(); this._show(); - // this._focusedItem = undefined; + this._focusedItem = undefined; break; case State.Open: dom.hide(this._messageElement); - dom.show(this._listElement); - if (this._status) { - dom.show(this._status?.element); - } + this._showListAndStatus(); this._show(); break; case State.Frozen: dom.hide(this._messageElement); - dom.show(this._listElement); - if (this._status) { - dom.show(this._status?.element); - } + this._showListAndStatus(); this._show(); break; case State.Details: dom.hide(this._messageElement); - dom.show(this._listElement); - if (this._status) { - dom.show(this._status?.element); - } - // this._details.show(); + this._showListAndStatus(); + this._details.show(); this._show(); break; } } + private _showListAndStatus(): void { + if (this._status) { + dom.show(this._listElement, this._status.element); + } else { + dom.show(this._listElement); + } + } + private _show(): void { // this._layout(this._persistedSize.restore()); // dom.show(this.element.domNode); @@ -628,7 +644,7 @@ export class SimpleSuggestWidget extends Disposable { // status bar if (this._status) { - this._status.element.style.lineHeight = `${info.itemHeight}px`; + this._status.element.style.height = `${info.itemHeight}px`; } // if (this._state === State.Empty || this._state === State.Loading) { @@ -734,7 +750,6 @@ export class SimpleSuggestWidget extends Disposable { this._listElement.style.height = `${height - statusBarHeight}px`; this._listElement.style.width = `${width}px`; - this._listElement.style.height = `${height}px`; this.element.layout(height, width); this._positionDetails(); @@ -749,7 +764,7 @@ export class SimpleSuggestWidget extends Disposable { private _getLayoutInfo() { const fontInfo = this._getFontInfo(); const itemHeight = clamp(Math.ceil(fontInfo.lineHeight), 8, 1000); - const statusBarHeight = 0; //!this.editor.getOption(EditorOption.suggest).showStatusBar || this._state === State.Empty || this._state === State.Loading ? 0 : itemHeight; + const statusBarHeight = !this._options.statusBarMenuId || !this._options.showStatusBarSettingId || !this._configurationService.getValue(this._options.showStatusBarSettingId) || this._state === State.Empty || this._state === State.Loading ? 0 : itemHeight; const borderWidth = 1; //this._details.widget.borderWidth; const borderHeight = 2 * borderWidth; From 70e928a4b2e0599afe73e1894c70aca0001409e1 Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Thu, 23 Jan 2025 08:52:41 -0800 Subject: [PATCH 0817/3587] set word wrap on correct element (#238493) * set word wrap on correct element * update test --- extensions/notebook-renderers/src/index.ts | 4 ++-- .../notebook-renderers/src/test/notebookRenderer.test.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/extensions/notebook-renderers/src/index.ts b/extensions/notebook-renderers/src/index.ts index 8f5fa908cb93..1c81a249d82c 100644 --- a/extensions/notebook-renderers/src/index.ts +++ b/extensions/notebook-renderers/src/index.ts @@ -404,9 +404,9 @@ function renderText(outputInfo: OutputItem, outputElement: HTMLElement, ctx: IRi const outputOptions = { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false, linkifyFilePaths: ctx.settings.linkifyFilePaths }; const content = createOutputContent(outputInfo.id, text, outputOptions); content.classList.add('output-plaintext'); - outputElement.classList.toggle('word-wrap', ctx.settings.outputWordWrap); + content.classList.toggle('word-wrap', ctx.settings.outputWordWrap); disposableStore.push(ctx.onDidChangeSettings(e => { - outputElement.classList.toggle('word-wrap', e.outputWordWrap); + content.classList.toggle('word-wrap', e.outputWordWrap); })); content.classList.toggle('scrollable', outputScrolling); diff --git a/extensions/notebook-renderers/src/test/notebookRenderer.test.ts b/extensions/notebook-renderers/src/test/notebookRenderer.test.ts index dfc7e2b15f87..9dc8f6c845e2 100644 --- a/extensions/notebook-renderers/src/test/notebookRenderer.test.ts +++ b/extensions/notebook-renderers/src/test/notebookRenderer.test.ts @@ -152,8 +152,12 @@ suite('Notebook builtin output renderer', () => { const inserted = outputElement.firstChild as HTMLElement; assert.ok(inserted, `nothing appended to output element: ${outputElement.innerHTML}`); assert.ok(outputElement.classList.contains('remove-padding'), `Padding should be removed for scrollable outputs ${outputElement.classList}`); - assert.ok(outputElement.classList.contains('word-wrap') && inserted.classList.contains('scrollable'), - `output content classList should contain word-wrap and scrollable ${inserted.classList}`); + if (mimeType === 'text/plain') { + assert.ok(inserted.classList.contains('word-wrap'), `Word wrap should be enabled for text/plain ${outputElement.classList}`); + } else { + assert.ok(outputElement.classList.contains('word-wrap') && inserted.classList.contains('scrollable'), + `output content classList should contain word-wrap and scrollable ${inserted.classList}`); + } assert.ok(inserted.innerHTML.indexOf('>content -1, `Content was not added to output element: ${outputElement.innerHTML}`); }); From a4b3b739be33758372eb7bd3e997d7b8d2b0becc Mon Sep 17 00:00:00 2001 From: Johannes Date: Thu, 23 Jan 2025 18:13:10 +0100 Subject: [PATCH 0818/3587] put toggle icon into navigate group --- .../contrib/inlineChat/browser/inlineChatController2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts index aa1e91b8f13c..b444ddebd81b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts @@ -343,7 +343,7 @@ class RevealWidget extends AbstractInlineChatAction { CTX_HAS_SESSION, ctxIsGlobalEditingSession.negate(), ), - group: 'z', + group: 'navigate', order: 4, } }); From 915a1200bd7620c43ce5c32ef270e47a5c1132e3 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:41:28 +0100 Subject: [PATCH 0819/3587] Fix inline completion reuse (#238587) * #12094 * fix https://github.com/microsoft/vscode-copilot/issues/12094 --- .../inlineCompletions/browser/model/inlineCompletionsSource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index f585567164c1..d1d476f5e7de 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -153,7 +153,7 @@ export class InlineCompletionsSource extends Disposable { } // Reuse Inline Edit if possible - if (activeInlineCompletion && activeInlineCompletion.isInlineEdit && activeInlineCompletion.canBeReused(this._textModel, position)) { + if (activeInlineCompletion && activeInlineCompletion.isInlineEdit && (activeInlineCompletion.canBeReused(this._textModel, position) || updatedCompletions.has(activeInlineCompletion.inlineCompletion) /* Inline Edit wins over completions if it's already been shown*/)) { updatedCompletions.dispose(); return false; } From ab172453fd2fe51a2bcf556c64542ee45675b3e8 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:47:17 +0100 Subject: [PATCH 0820/3587] Support showing multi line changes as overlay (#238585) * multi line replacement * fix test? --- src/vs/editor/common/core/positionToOffset.ts | 22 +- .../browser/view/inlineEdits/view.css | 27 ++ .../browser/view/inlineEdits/view.ts | 81 ++--- .../view/inlineEdits/wordReplacementView.ts | 287 +++++++++++++++++- .../core/positionOffsetTransformer.test.ts | 2 +- 5 files changed, 380 insertions(+), 39 deletions(-) diff --git a/src/vs/editor/common/core/positionToOffset.ts b/src/vs/editor/common/core/positionToOffset.ts index 65e4962128ae..3548c8d0e898 100644 --- a/src/vs/editor/common/core/positionToOffset.ts +++ b/src/vs/editor/common/core/positionToOffset.ts @@ -32,7 +32,27 @@ export class PositionOffsetTransformer { } getOffset(position: Position): number { - return this.lineStartOffsetByLineIdx[position.lineNumber - 1] + position.column - 1; + const valPos = this._validatePosition(position); + return this.lineStartOffsetByLineIdx[valPos.lineNumber - 1] + valPos.column - 1; + } + + private _validatePosition(position: Position): Position { + if (position.lineNumber < 1) { + return new Position(1, 1); + } + const lineCount = this.textLength.lineCount + 1; + if (position.lineNumber > lineCount) { + const lineLength = this.getLineLength(lineCount); + return new Position(lineCount, lineLength + 1); + } + if (position.column < 1) { + return new Position(position.lineNumber, 1); + } + const lineLength = this.getLineLength(position.lineNumber); + if (position.column - 1 > lineLength) { + return new Position(position.lineNumber, lineLength + 1); + } + return position; } getOffsetRange(range: Range): OffsetRange { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index f3c76c922f96..60f8e058e6bf 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -189,6 +189,33 @@ .inlineCompletions.strike-through { text-decoration-thickness: 1px; } + + /* line replacement bubbles */ + + .inlineCompletions-modified-bubble{ + background: var(--vscode-inlineEdit-modifiedChangedTextBackground); + } + + .inlineCompletions-original-bubble{ + background: var(--vscode-inlineEdit-originalChangedTextBackground); + border-radius: 4px; + } + + .inlineCompletions-modified-bubble, + .inlineCompletions-original-bubble { + pointer-events: none; + } + + .inlineCompletions-modified-bubble.start { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + } + + .inlineCompletions-modified-bubble.end { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + } + } .monaco-menu-option { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index c20f5e187f02..f80edbd6aa60 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -11,7 +11,6 @@ import { observableCodeEditor } from '../../../../../browser/observableCodeEdito import { EditorOption } from '../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../common/core/lineRange.js'; import { Position } from '../../../../../common/core/position.js'; -import { Range } from '../../../../../common/core/range.js'; import { SingleTextEdit, StringText } from '../../../../../common/core/textEdit.js'; import { TextLength } from '../../../../../common/core/textLength.js'; import { DetailedLineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../../../../../common/diff/rangeMapping.js'; @@ -25,7 +24,7 @@ import { InlineEditsSideBySideDiff } from './sideBySideDiff.js'; import { applyEditToModifiedRangeMappings, createReindentEdit } from './utils.js'; import './view.css'; import { InlineEditWithChanges } from './viewAndDiffProducer.js'; -import { WordInsertView, WordReplacementView } from './wordReplacementView.js'; +import { LineReplacementView, WordInsertView, WordReplacementView } from './wordReplacementView.js'; export class InlineEditsView extends Disposable { private readonly _editorObs = observableCodeEditor(this._editor); @@ -156,7 +155,7 @@ export class InlineEditsView extends Disposable { }).recomputeInitiallyAndOnChange(this._store); protected readonly _lineReplacementView = mapObservableArrayCached(this, this._uiState.map(s => s?.state?.kind === 'lineReplacement' ? [s.state] : []), (e, store) => { // TODO: no need for map here, how can this be done with observables - return store.add(this._instantiationService.createInstance(WordReplacementView, this._editorObs, e.edit, e.replacements)); + return store.add(this._instantiationService.createInstance(LineReplacementView, this._editorObs, e.originalRange, e.modifiedRange, e.modifiedLines, e.replacements)); }).recomputeInitiallyAndOnChange(this._store); private readonly _useGutterIndicator = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useGutterIndicator); @@ -188,14 +187,15 @@ export class InlineEditsView extends Disposable { private determineView(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText): string { // Check if we can use the previous view if it is the same InlineCompletion as previously shown - if (this._previousView?.id === edit.inlineCompletion.id) { - const canUseCache = edit.userJumpedToIt === this._previousView.userJumpedToIt || - !(this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible' && this._previousView.view !== 'mixedLines') && - !(this._useInterleavedLinesDiff.read(reader) === 'afterJump' && this._previousView.view !== 'interleavedLines'); - - if (canUseCache) { - return this._previousView.view; - } + const canUseCache = this._previousView?.id === edit.inlineCompletion.id; + const reconsiderViewAfterJump = edit.userJumpedToIt !== this._previousView?.userJumpedToIt && + ( + (this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible' && this._previousView?.view !== 'mixedLines') || + (this._useInterleavedLinesDiff.read(reader) === 'afterJump' && this._previousView?.view !== 'interleavedLines') + ); + + if (canUseCache && !reconsiderViewAfterJump) { + return this._previousView!.view; } if (edit.isCollapsed) { @@ -218,13 +218,13 @@ export class InlineEditsView extends Disposable { return 'deletion'; } - if (diff.length === 1 && diff[0].original.length === 1 && diff[0].modified.length === 1) { - const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); - if (allInnerChangesNotTooLong && isSingleInnerEdit) { - return 'wordReplacements'; - } else { - return 'lineReplacement'; - } + const numOriginalLines = edit.originalLineRange.length; + const numModifiedLines = edit.modifiedLineRange.length; + const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); + if (allInnerChangesNotTooLong && isSingleInnerEdit && numOriginalLines === 1 && numModifiedLines === 1) { + return 'wordReplacements'; + } else if (numOriginalLines > 0 && numModifiedLines > 0) { + return 'lineReplacement'; } if ( @@ -257,7 +257,7 @@ export class InlineEditsView extends Disposable { const inner = diff.flatMap(d => d.innerChanges ?? []); if (view === 'deletion') { - const trimLength = getPrefixTrimLength(edit.originalText.getLineAt(edit.originalLineRange.startLineNumber), newText.getLineAt(edit.modifiedLineRange.startLineNumber)); + const trimLength = getPrefixTrimLength(edit, inner, newText); const widgetStartColumn = Math.min(trimLength, ...inner.map(m => m.originalRange.startLineNumber !== m.originalRange.endLineNumber ? 0 : m.originalRange.startColumn - 1)); return { kind: 'deletion' as const, widgetStartColumn }; } @@ -275,20 +275,12 @@ export class InlineEditsView extends Disposable { } if (view === 'lineReplacement') { - const originalLine = edit.originalText.getLineAt(edit.originalLineRange.startLineNumber); - const editedLine = newText.getLineAt(edit.modifiedLineRange.startLineNumber); - const trimLength = Math.min(getPrefixTrimLength(originalLine, editedLine), replacements[0].range.startColumn - 1); - - const textEdit = edit.lineEdit.toSingleTextEdit(edit.originalText); - const lineEdit = new SingleTextEdit( - new Range(textEdit.range.startLineNumber, textEdit.range.startColumn + trimLength, textEdit.range.endLineNumber, textEdit.range.endColumn), - textEdit.text.slice(trimLength) - ); - return { kind: 'lineReplacement' as const, - edit: lineEdit, - replacements, + originalRange: edit.originalLineRange, + modifiedRange: edit.modifiedLineRange, + modifiedLines: edit.modifiedLineRange.mapToLineArray(line => newText.getLineAt(line)), + replacements: inner.map(m => ({ originalRange: m.originalRange, modifiedRange: m.modifiedRange })), }; } @@ -338,10 +330,27 @@ function isSingleLineDeletion(diff: DetailedLineRangeMapping[]): boolean { } } -function getPrefixTrimLength(originalLine: string, editedLine: string) { - let startTrim = 0; - while (originalLine[startTrim] === editedLine[startTrim] && (originalLine[startTrim] === ' ' || originalLine[startTrim] === '\t')) { - startTrim++; +function getPrefixTrimLength(edit: InlineEditWithChanges, innerChanges: RangeMapping[], newText: StringText) { + if (innerChanges.some(m => m.originalRange.startLineNumber !== m.originalRange.endLineNumber)) { + return 0; + } + + let minTrimLength = Number.MAX_SAFE_INTEGER; + for (let i = 0; i < edit.originalLineRange.length; i++) { + const lineNumber = edit.originalLineRange.startLineNumber + i; + const originalLine = edit.originalText.getLineAt(lineNumber); + const editedLine = newText.getLineAt(lineNumber); + const trimLength = getLinePrefixTrimLength(originalLine, editedLine); + minTrimLength = Math.min(minTrimLength, trimLength); + } + + return Math.min(minTrimLength, ...innerChanges.map(m => m.originalRange.startColumn - 1)); + + function getLinePrefixTrimLength(originalLine: string, editedLine: string) { + let startTrim = 0; + while (originalLine[startTrim] === editedLine[startTrim] && (originalLine[startTrim] === ' ' || originalLine[startTrim] === '\t')) { + startTrim++; + } + return startTrim; } - return startTrim; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index 51086055147b..2becea6662d2 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { Disposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { constObservable, derived, mapObservableArrayCached } from '../../../../../../base/common/observable.js'; import { editorHoverStatusBarBackground } from '../../../../../../platform/theme/common/colorRegistry.js'; import { registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; @@ -20,6 +20,10 @@ import { LineTokens } from '../../../../../common/tokens/lineTokens.js'; import { TokenArray } from '../../../../../common/tokens/tokenArray.js'; import { mapOutFalsy, n, rectToProps } from './utils.js'; import { localize } from '../../../../../../nls.js'; +import { Range } from '../../../../../common/core/range.js'; +import { LineRange } from '../../../../../common/core/lineRange.js'; +import { InlineDecoration, InlineDecorationType } from '../../../../../common/viewModel.js'; +import { IModelDecorationOptions, TrackedRangeStickiness } from '../../../../../common/model.js'; export const transparentHoverBackground = registerColor( 'inlineEdit.wordReplacementView.background', { @@ -253,6 +257,287 @@ export class WordReplacementView extends Disposable { } } +export class LineReplacementView extends Disposable { + + private readonly _originalBubblesDecorationCollection = this._editor.editor.createDecorationsCollection(); + private readonly _originalBubblesDecorationOptions: IModelDecorationOptions = { + description: 'inlineCompletions-original-bubble', + className: 'inlineCompletions-original-bubble', + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + }; + + private readonly maxPrefixTrim = derived(this, reader => { + const maxPrefixTrim = Math.max(...this._replacements.flatMap(r => [r.originalRange, r.modifiedRange]).map(r => r.isSingleLine() ? r.startColumn - 1 : 0)); + if (maxPrefixTrim === 0) { + return 0; + } + + const textModel = this._editor.editor.getModel()!; + + const getLineTrimColLength = (line: string) => { + let i = 0; + while (i < line.length && line[i] === ' ') { i++; } + return i; + }; + + // TODO: make sure this works for tabs + return Math.min( + maxPrefixTrim, + ...this._originalRange.mapToLineArray(line => getLineTrimColLength(textModel.getLineContent(line))), + ...this._modifiedLines.map(line => getLineTrimColLength(line)) + ); + }); + + private readonly _modifiedLineElements = derived(reader => { + + const maxPrefixTrim = this.maxPrefixTrim.read(reader); + + const lines = []; + let requiredWidth = 0; + + const modifiedBubbles = rangesToBubbleRanges(this._replacements.map(r => r.modifiedRange)).map(r => new Range(r.startLineNumber, r.startColumn - maxPrefixTrim, r.endLineNumber, r.endColumn - maxPrefixTrim)); + + const textModel = this._editor.model.get()!; + const startLineNumber = this._modifiedRange.startLineNumber; + for (let i = 0; i < this._modifiedRange.length; i++) { + const line = document.createElement('div'); + const lineNumber = startLineNumber + i; + const modLine = this._modifiedLines[i].replace(/\t\n/g, '').slice(maxPrefixTrim); + + const t = textModel.tokenization.tokenizeLinesAt(lineNumber, [modLine])?.[0]; + let tokens: LineTokens; + if (t) { + tokens = TokenArray.fromLineTokens(t).toLineTokens(modLine, this._languageService.languageIdCodec); + } else { + tokens = LineTokens.createEmpty(modLine, this._languageService.languageIdCodec); + } + + const decorations = []; + for (const modified of modifiedBubbles.filter(b => b.startLineNumber === lineNumber)) { + decorations.push(new InlineDecoration(new Range(1, modified.startColumn, 1, modified.endColumn), 'inlineCompletions-modified-bubble', InlineDecorationType.Regular)); + decorations.push(new InlineDecoration(new Range(1, modified.startColumn, 1, modified.startColumn + 1), 'start', InlineDecorationType.Regular)); + decorations.push(new InlineDecoration(new Range(1, modified.endColumn - 1, 1, modified.endColumn), 'end', InlineDecorationType.Regular)); + } + + const result = renderLines(new LineSource([tokens]), RenderOptions.fromEditor(this._editor.editor).withSetWidth(false), decorations, line, true); + + requiredWidth = Math.max(requiredWidth, result.minWidthInPx); + + lines.push(line); + } + + return { lines, requiredWidth: requiredWidth - 10 }; // TODO: Width is always too large, why? + }); + + private readonly _layout = derived(this, reader => { + const { requiredWidth } = this._modifiedLineElements.read(reader); + + const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); + const contentLeft = this._editor.layoutInfoContentLeft.read(reader); + const scrollLeft = this._editor.scrollLeft.read(reader); + const scrollTop = this._editor.scrollTop.read(reader); + const editorLeftOffset = contentLeft - scrollLeft; + const w = this._editor.getOption(EditorOption.fontInfo).read(reader).typicalHalfwidthCharacterWidth; + const PADDING = 4; + + const editorModel = this._editor.editor.getModel()!; + const maxPrefixTrim = this.maxPrefixTrim.read(reader); + + // TODO, correctly count tabs + const originalLineContents: string[] = []; + this._originalRange.forEach(line => originalLineContents.push(editorModel.getLineContent(line))); + const maxOriginalLineLength = Math.max(...originalLineContents.map(l => l.length)) - maxPrefixTrim; + const maxLineWidth = Math.max(maxOriginalLineLength * w, requiredWidth); + + const startLineNumber = this._originalRange.startLineNumber; + const endLineNumber = this._originalRange.endLineNumberExclusive - 1; + const topOfOriginalLines = this._editor.editor.getTopForLineNumber(startLineNumber) - scrollTop; + const bottomOfOriginalLines = this._editor.editor.getBottomForLineNumber(endLineNumber) - scrollTop; + + if (bottomOfOriginalLines <= 0) { + return undefined; + } + + const prefixTrimOffset = maxPrefixTrim * w; + + // Box Widget positioning + const originalLine = Rect.fromLeftTopWidthHeight( + editorLeftOffset + prefixTrimOffset, + topOfOriginalLines, + maxLineWidth, + bottomOfOriginalLines - topOfOriginalLines + PADDING + ); + const modifiedLine = Rect.fromLeftTopWidthHeight( + originalLine.left, + originalLine.bottom + PADDING, + originalLine.width, + this._modifiedRange.length * lineHeight + ); + const background = Rect.hull([originalLine, modifiedLine]).withMargin(PADDING); + + const lowerBackground = background.intersectVertical(new OffsetRange(originalLine.bottom, Number.MAX_SAFE_INTEGER)); + const lowerText = new Rect(lowerBackground.left + PADDING, lowerBackground.top + PADDING, lowerBackground.right, lowerBackground.bottom); + + return { + originalLine, + modifiedLine, + background, + lowerBackground, + lowerText, + padding: PADDING, + minContentWidthRequired: prefixTrimOffset + maxLineWidth + PADDING * 2, + }; + }); + + private readonly _div = n.div({ + class: 'line-replacement', + }, [ + derived(reader => { + const layout = mapOutFalsy(this._layout).read(reader); + if (!layout) { + return []; + } + + const layoutProps = layout.read(reader); + const scrollLeft = this._editor.scrollLeft.read(reader); + let contentLeft = this._editor.layoutInfoContentLeft.read(reader); + let contentWidth = this._editor.contentWidth.read(reader); + const contentHeight = this._editor.editor.getContentHeight(); + + if (scrollLeft === 0) { + contentLeft -= layoutProps.padding; + contentWidth += layoutProps.padding; + } + + const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); + const modifiedLines = this._modifiedLineElements.read(reader).lines; + modifiedLines.forEach(l => { + l.style.width = `${layout.read(reader).lowerText.width}px`; + l.style.height = `${lineHeight}px`; + l.style.position = 'relative'; + }); + + return [ + n.div({ + style: { + position: 'absolute', + top: 0, + left: contentLeft, + width: contentWidth, + height: contentHeight, + overflow: 'hidden', + pointerEvents: 'none', + } + }, [ + n.div({ // overlay to make sure the code is not visible between original and modified lines + style: { + position: 'absolute', + top: layoutProps.lowerBackground.top - layoutProps.padding, + left: layoutProps.lowerBackground.left - contentLeft, + width: layoutProps.lowerBackground.width, + height: layoutProps.padding * 2, + background: 'var(--vscode-editor-background)', + }, + }), + n.div({ // styling for the modified lines widget + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).lowerBackground.moveLeft(contentLeft)), + borderRadius: '4px', + background: 'var(--vscode-editor-background)', + boxShadow: 'var(--vscode-scrollbar-shadow) 0 6px 6px -6px', + borderTop: '1px solid var(--vscode-editorHoverWidget-border)', + overflow: 'hidden', + }, + }, [ + n.div({ // adds background color to modified lines widget (may be transparent) + style: { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + background: 'var(--vscode-diffEditor-insertedLineBackground)', + opacity: '0.5', + }, + }) + ]), + n.div({ + style: { + position: 'absolute', + padding: '0px', + boxSizing: 'border-box', + ...rectToProps(reader => layout.read(reader).lowerText.moveLeft(contentLeft)), + fontFamily: this._editor.getOption(EditorOption.fontFamily), + fontSize: this._editor.getOption(EditorOption.fontSize), + fontWeight: this._editor.getOption(EditorOption.fontWeight), + pointerEvents: 'none', + } + }, [...modifiedLines]), + n.div({ + style: { + position: 'absolute', + ...rectToProps(reader => layout.read(reader).background.moveLeft(contentLeft)), + borderRadius: '4px', + + border: '1px solid var(--vscode-editorHoverWidget-border)', + //background: 'rgba(122, 122, 122, 0.12)', looks better + background: 'var(--vscode-inlineEdit-wordReplacementView-background)', + pointerEvents: 'none', + boxSizing: 'border-box', + } + }, []), + ]) + ]; + }) + ]).keepUpdated(this._store); + + constructor( + private readonly _editor: ObservableCodeEditor, + private readonly _originalRange: LineRange, + private readonly _modifiedRange: LineRange, + private readonly _modifiedLines: string[], + private readonly _replacements: readonly Replacement[], + @ILanguageService private readonly _languageService: ILanguageService, + ) { + super(); + + this._register(toDisposable(() => this._originalBubblesDecorationCollection.clear())); + + const originalBubbles = rangesToBubbleRanges(this._replacements.map(r => r.originalRange)); + this._originalBubblesDecorationCollection.set(originalBubbles.map(r => ({ range: r, options: this._originalBubblesDecorationOptions }))); + + this._register(this._editor.createOverlayWidget({ + domNode: this._div.element, + minContentWidthInPx: derived(reader => { // TODO: is this helping? + return this._layout.read(reader)?.minContentWidthRequired ?? 0; + }), + position: constObservable({ preference: { top: 0, left: 0 } }), + allowEditorOverflow: false, + })); + } +} + +function rangesToBubbleRanges(ranges: Range[]): Range[] { + const result: Range[] = []; + while (ranges.length) { + let range = ranges.shift()!; + if (range.startLineNumber !== range.endLineNumber) { + ranges.push(new Range(range.startLineNumber + 1, 1, range.endLineNumber, range.endColumn)); + range = new Range(range.startLineNumber, range.startColumn, range.startLineNumber, Number.MAX_SAFE_INTEGER); // TODO: this is not correct + } + + result.push(range); + } + return result; + +} + +export interface Replacement { + originalRange: Range; + modifiedRange: Range; +} + export class WordInsertView extends Disposable { private readonly _start = this._editor.observePosition(constObservable(this._edit.range.getStartPosition()), this._store); diff --git a/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts b/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts index e44b41bd6d6a..3cf6744390ac 100644 --- a/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts +++ b/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts @@ -52,7 +52,7 @@ suite('PositionOffsetTransformer', () => { }); test('getOffset', () => { - for (let i = 0; i < str.length + 2; i++) { + for (let i = 0; i < str.length + 1; i++) { assert.strictEqual(t.getOffset(t.getPosition(i)), i); } }); From 5bca37ff5d79a286b74306ffe09dba05ea608d88 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 23 Jan 2025 12:55:42 -0600 Subject: [PATCH 0821/3587] apply font, styling for terminal suggest widget (#238593) --- .../suggest/browser/terminalSuggestAddon.ts | 35 +++++++++++++------ .../suggest/browser/media/suggest.css | 22 ++++++++++++ .../suggest/browser/simpleSuggestWidget.ts | 3 +- .../browser/simpleSuggestWidgetDetails.ts | 27 +++++++++++++- 4 files changed, 74 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index c5cf96ae22a2..1fdfe2f4262c 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -28,7 +28,7 @@ import { LineContext, SimpleCompletionModel } from '../../../../services/suggest import { ISimpleSelectedSuggestion, SimpleSuggestWidget } from '../../../../services/suggest/browser/simpleSuggestWidget.js'; import type { ISimpleSuggestWidgetFontInfo } from '../../../../services/suggest/browser/simpleSuggestWidgetRenderer.js'; import { ITerminalCompletionService, TerminalCompletionItemKind } from './terminalCompletionService.js'; -import { TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; +import { TerminalSettingId, TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; @@ -80,6 +80,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest readonly onAcceptedCompletion = this._onAcceptedCompletion.event; private readonly _onDidReceiveCompletions = this._register(new Emitter()); readonly onDidReceiveCompletions = this._onDidReceiveCompletions.event; + private readonly _onDidFontConfigurationChange = this._register(new Emitter()); + readonly onDidFontConfigurationChange = this._onDidFontConfigurationChange.event; private _kindToIconMap = new Map([ [TerminalCompletionItemKind.File, Codicon.file], @@ -393,22 +395,33 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest }); } + private _getFontInfo(): ISimpleSuggestWidgetFontInfo { + const c = this._terminalConfigurationService.config; + const font = this._terminalConfigurationService.getFont(dom.getActiveWindow()); + const fontInfo: ISimpleSuggestWidgetFontInfo = { + fontFamily: font.fontFamily, + fontSize: font.fontSize, + lineHeight: Math.ceil(1.5 * font.fontSize), + fontWeight: c.fontWeight.toString(), + letterSpacing: font.letterSpacing + }; + return fontInfo; + } private _ensureSuggestWidget(terminal: Terminal): SimpleSuggestWidget { if (!this._suggestWidget) { - const c = this._terminalConfigurationService.config; - const font = this._terminalConfigurationService.getFont(dom.getActiveWindow()); - const fontInfo: ISimpleSuggestWidgetFontInfo = { - fontFamily: font.fontFamily, - fontSize: font.fontSize, - lineHeight: Math.ceil(1.5 * font.fontSize), - fontWeight: c.fontWeight.toString(), - letterSpacing: font.letterSpacing - }; + + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(TerminalSettingId.FontFamily) || e.affectsConfiguration(TerminalSettingId.FontSize) || e.affectsConfiguration(TerminalSettingId.LineHeight) || e.affectsConfiguration(TerminalSettingId.FontFamily) || e.affectsConfiguration('editor.fontSize') || e.affectsConfiguration('editor.fontFamily')) { + this._onDidFontConfigurationChange.fire(); + } + } + )); this._suggestWidget = this._register(this._instantiationService.createInstance( SimpleSuggestWidget, this._container!, this._instantiationService.createInstance(PersistedWidgetSize), - () => fontInfo, + this._getFontInfo.bind(this), + this.onDidFontConfigurationChange, { statusBarMenuId: MenuId.MenubarTerminalSuggestStatusMenu, showStatusBarSettingId: TerminalSuggestSettingId.ShowStatusBar diff --git a/src/vs/workbench/services/suggest/browser/media/suggest.css b/src/vs/workbench/services/suggest/browser/media/suggest.css index 7aed847d1986..1bcec527a44c 100644 --- a/src/vs/workbench/services/suggest/browser/media/suggest.css +++ b/src/vs/workbench/services/suggest/browser/media/suggest.css @@ -201,6 +201,28 @@ height: 0.7em; display: inline-block; } + +/** ReadMore Icon styles **/ + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .header > .codicon-close, +.workbench-suggest-widget .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right > .readMore::before { + color: inherit; + opacity: 1; + font-size: 14px; + cursor: pointer; +} + +.monaco-workbench .workbench-suggest-widget .suggest-details .codicon.codicon-close { + position: absolute; + top: 6px; + right: 2px; +} + +.workbench-suggest-widget .suggest-details > .monaco-scrollable-element > .body > .header > .codicon-close:hover, +.workbench-suggest-widget .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right > .readMore:hover { + opacity: 1; +} + /** signature, qualifier, type/details opacity **/ .workbench-suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right > .details-label { diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index 354c8bd0078c..bbb56eefaeb1 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -114,6 +114,7 @@ export class SimpleSuggestWidget extends Disposable { private readonly _container: HTMLElement, private readonly _persistedSize: IPersistedWidgetSizeDelegate, private readonly _getFontInfo: () => ISimpleSuggestWidgetFontInfo, + private readonly _onDidFontConfigurationChange: Event, private readonly _options: IWorkbenchSuggestWidgetOptions, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -227,7 +228,7 @@ export class SimpleSuggestWidget extends Disposable { this._messageElement = dom.append(this.element.domNode, dom.$('.message')); - const details: SimpleSuggestDetailsWidget = this._register(instantiationService.createInstance(SimpleSuggestDetailsWidget, this._getFontInfo)); + const details: SimpleSuggestDetailsWidget = this._register(instantiationService.createInstance(SimpleSuggestDetailsWidget, this._getFontInfo, this._onDidFontConfigurationChange)); this._register(details.onDidClose(() => this.toggleDetails())); this._details = this._register(new SimpleSuggestDetailsOverlay(details, this._listElement)); this._register(dom.addDisposableListener(this._details.widget.domNode, 'blur', (e) => this._onDidBlurDetails.fire(e))); diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts index 88408bfc7821..a0727fd9a93b 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts @@ -49,7 +49,8 @@ export class SimpleSuggestDetailsWidget { constructor( private readonly _getFontInfo: () => ISimpleSuggestWidgetFontInfo, - @IInstantiationService instaService: IInstantiationService, + onDidFontInfoChange: Event, + @IInstantiationService instaService: IInstantiationService ) { this.domNode = dom.$('.suggest-details'); this.domNode.classList.add('no-docs'); @@ -72,6 +73,30 @@ export class SimpleSuggestDetailsWidget { this._type = dom.append(this._header, dom.$('p.type')); this._docs = dom.append(this._body, dom.$('p.docs')); + + this._configureFont(); + + this._disposables.add(onDidFontInfoChange(() => this._configureFont())); + } + + private _configureFont(): void { + const fontInfo = this._getFontInfo(); + const fontFamily = fontInfo.fontFamily; + + const fontSize = fontInfo.fontSize; + const lineHeight = fontInfo.lineHeight; + const fontWeight = fontInfo.fontWeight; + const fontSizePx = `${fontSize}px`; + const lineHeightPx = `${lineHeight}px`; + + this.domNode.style.fontSize = fontSizePx; + this.domNode.style.lineHeight = `${lineHeight / fontSize}`; + this.domNode.style.fontWeight = fontWeight; + // this.domNode.style.fontFeatureSettings = fontInfo.fontFeatureSettings; + this._type.style.fontFamily = fontFamily; + this._close.style.height = lineHeightPx; + this._close.style.width = lineHeightPx; + } dispose(): void { From 2a0e73a1b97fff1ffa27e3e9763ee7b887ef38e9 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 23 Jan 2025 20:25:58 +0100 Subject: [PATCH 0822/3587] SCM - fix graph hover pills (#238602) --- src/vs/workbench/contrib/scm/browser/media/scm.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index b616cb2a26de..5a6eebc287e1 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -496,11 +496,11 @@ } .monaco-hover.history-item-hover p:last-child { - margin-bottom: 0; + margin-bottom: 2px !important; } .monaco-hover.history-item-hover p:last-child span:not(.codicon) { - margin-bottom: 2px !important; + padding: 2px 0; } .monaco-hover.history-item-hover hr { From 565595ee4066499480f5708feb2fab56b189d050 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Thu, 23 Jan 2025 12:00:42 -0800 Subject: [PATCH 0823/3587] [instructions]: update chat variable IDs creation logic (#238606) [instructions]: update chat variable IDs creation logic to account for non prompt file references --- .../chatInstructionAttachmentsModel.ts | 57 ++++++++++++------- .../promptSyntax/parsers/basePromptParser.ts | 5 +- .../common/promptSyntax/parsers/types.d.ts | 5 ++ 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts index 0dcf1991deda..79a0a0d7c71c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts @@ -7,6 +7,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { Emitter } from '../../../../../base/common/event.js'; import { IChatRequestVariableEntry } from '../../common/chatModel.js'; import { ChatInstructionsFileLocator } from './chatInstructionsFileLocator.js'; +import { IPromptFileReference } from '../../common/promptSyntax/parsers/types.js'; import { ChatInstructionsAttachmentModel } from './chatInstructionsAttachment.js'; import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; import { BasePromptParser } from '../../common/promptSyntax/parsers/basePromptParser.js'; @@ -14,28 +15,42 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; /** - * Common instructions attachment variable identifier. + * Utility to convert a {@link reference} to a chat variable entry. + * The `id` of the chat variable can be one of the following: + * + * - `vscode.prompt.instructions__`: for all non-root prompt file references + * - `vscode.prompt.instructions.root__`: for *root* prompt file references + * - ``: for the rest of references(the ones that do not point to a prompt file) + * + * @param reference A reference object to convert to a chat variable entry. + * @param isRoot If the reference is the root reference in the references tree. + * This object most likely was explicitly attached by the user. */ -type TInstructionsId = 'vscode.prompt.instructions'; - -/** - * Instructions attachment variable identifier for - * the `root` reference. - */ -type TInstructionsRootId = `${TInstructionsId}.root`; +const toChatVariable = ( + reference: Pick, + isRoot: boolean, +): IChatRequestVariableEntry => { + const { uri } = reference; + + // default `id` is the stringified `URI` + let id = `${uri}`; + + // for prompt files, we add a prefix to the `id` + if (reference.isPromptSnippet) { + // the default prefix that is used for all prompt files + let prefix = 'vscode.prompt.instructions'; + // if the reference is the root object, add the `.root` suffix + if (isRoot) { + prefix += '.root'; + } -/** - * Well-defined instructions attachment variable identifiers. - */ -type TInstructionsVariableIds = TInstructionsId | TInstructionsRootId; + // final `id` for all `prompt files` starts with the well-defined + // part that the copilot extension(or other chatbot) can rely on + id = `${prefix}__${id}`; + } -/** - * Utility to convert a reference `URI` to a chat variable - * entry with the specified variable {@linkcode id}. - */ -const toChatVariable = (id: TInstructionsVariableIds, uri: URI): IChatRequestVariableEntry => { return { - id: `${id}__${uri}`, + id, name: uri.fsPath, value: uri, isSelection: false, @@ -90,14 +105,14 @@ export class ChatInstructionAttachmentsModel extends Disposable { // the usual URIs list of prompt instructions is `bottom-up`, therefore // we do the same herfe - first add all child references of the model result.push( - ...reference.allValidReferencesUris.map((uri) => { - return toChatVariable('vscode.prompt.instructions', uri); + ...reference.allValidReferences.map((link) => { + return toChatVariable(link, false); }), ); // then add the root reference of the model itself result.push( - toChatVariable('vscode.prompt.instructions.root', reference.uri), + toChatVariable(reference, true), ); } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index c063be400ae5..4c7faf561b3a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -401,7 +401,9 @@ export abstract class BasePromptParser extend return this.allReferences // filter out unresolved references .filter((reference) => { - return !reference.resolveFailed; + const { errorCondition } = reference; + + return !errorCondition || (errorCondition instanceof NonPromptSnippetFile); }); } @@ -443,6 +445,7 @@ export abstract class BasePromptParser extend return errorCondition; }); + result.push(...childErrorConditions); return result; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts index a2671e9e7aa5..2f99c31d7931 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts @@ -55,6 +55,11 @@ export interface IPromptReference extends IDisposable { */ readonly linkRange: IRange | undefined; + /** + * Whether the current reference points to a prompt snippet file. + */ + readonly isPromptSnippet: boolean; + /** * Flag that indicates if resolving this reference failed. * The `undefined` means that no attempt to resolve the reference From 23d0bf78c62d1fead056b4817d5c0a4b6f988ee1 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:35:39 -0800 Subject: [PATCH 0824/3587] add references to `ChatReferenceBinaryData` (#238516) * add reference to binary data api * remove comment * fix tests * fix tests try #2 * more testing * commenting out for testing * add back history filter * add under isimage check * some fixes * make sure to dispose --- .../workbench/api/common/extHostTypeConverters.ts | 2 +- src/vs/workbench/api/common/extHostTypes.ts | 4 +++- .../chatAttachmentsContentPart.ts | 10 ++++++++++ .../contrib/chat/browser/chatDragAndDrop.ts | 5 +++-- .../contrib/chat/browser/chatInputPart.ts | 15 +++++++++++++-- .../workbench/contrib/chat/browser/imageUtils.ts | 8 +++++--- .../vscode.proposed.chatReferenceBinaryData.d.ts | 5 +++++ 7 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 42beaa8feba2..f535d17683ba 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2799,7 +2799,7 @@ export namespace ChatPromptReference { 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)))) : value, + 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, modelDescription: variable.modelDescription, isReadonly: hasReadonlyProposal ? variable.isMarkedReadonly : undefined, }; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 748ecb99ad62..26ae6d8182ea 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4698,9 +4698,11 @@ export class ChatRequestNotebookData implements vscode.ChatRequestNotebookData { export class ChatReferenceBinaryData implements vscode.ChatReferenceBinaryData { mimeType: string; data: () => Thenable; - constructor(mimeType: string, data: () => Thenable) { + reference?: vscode.Uri; + constructor(mimeType: string, data: () => Thenable, reference?: vscode.Uri) { this.mimeType = mimeType; this.data = data; + this.reference = reference; } } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index 5fcd5d455c9d..599955eefcfb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -140,6 +140,16 @@ export class ChatAttachmentsContentPart extends Disposable { widget.appendChild(pillIcon); widget.appendChild(textLabel); + if (attachment.references) { + widget.style.cursor = 'pointer'; + const clickHandler = () => { + if (attachment.references && URI.isUri(attachment.references[0].reference)) { + this.openResource(attachment.references[0].reference, false, undefined); + } + }; + this.attachedContextDisposables.add(dom.addDisposableListener(widget, 'click', clickHandler)); + } + if (isAttachmentPartialOrOmitted) { hoverElement.textContent = localize('chat.imageAttachmentHover', "Image was not sent to the model."); textLabel.style.textDecoration = 'line-through'; diff --git a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts b/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts index 8b428308f7ce..2550c96d86c6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts @@ -431,7 +431,7 @@ async function getImageAttachContext(editor: EditorInput | IDraggedResourceEdito return undefined; } - if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(editor.resource.path)) { + if (/\.(png|jpg|jpeg|gif|webp)$/i.test(editor.resource.path)) { const fileName = basename(editor.resource); const readFile = await fileService.readFile(editor.resource); const resizedImage = await resizeImage(readFile.value.buffer); @@ -443,7 +443,8 @@ async function getImageAttachContext(editor: EditorInput | IDraggedResourceEdito icon: Codicon.fileMedia, isDynamic: true, isImage: true, - isFile: false + isFile: false, + references: [{ reference: editor.resource, kind: 'reference' }] }; } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index c904d8a399cb..84fe799c2ab4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -371,7 +371,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.getInputState = (): IChatInputState => { return { ...getContribsInputState(), - chatContextAttachments: this._attachmentModel.attachments.filter(attachment => !attachment.isImage), + chatContextAttachments: this._attachmentModel.attachments, }; }; this.inputEditorMaxHeight = this.options.renderStyle === 'compact' ? INPUT_EDITOR_MAX_HEIGHT / 3 : INPUT_EDITOR_MAX_HEIGHT; @@ -574,7 +574,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - private saveCurrentValue(inputState: any): void { + private saveCurrentValue(inputState: IChatInputState): void { + inputState.chatContextAttachments = inputState.chatContextAttachments?.filter(attachment => !attachment.isImage); const newEntry = { text: this._inputEditor.getValue(), state: inputState }; this.history.replaceLast(newEntry); } @@ -935,6 +936,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge widget.appendChild(pillIcon); widget.appendChild(textLabel); + if (attachment.references) { + widget.style.cursor = 'pointer'; + const clickHandler = () => { + if (attachment.references && URI.isUri(attachment.references[0].reference)) { + this.openResource(attachment.references[0].reference, false, undefined); + } + }; + store.add(addDisposableListener(widget, 'click', clickHandler)); + } + if (!supportsVision) { widget.classList.add('warning'); hoverElement.textContent = localize('chat.imageAttachmentHover', "{0} does not support images.", this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel)?.name : this.currentLanguageModel); diff --git a/src/vs/workbench/contrib/chat/browser/imageUtils.ts b/src/vs/workbench/contrib/chat/browser/imageUtils.ts index 194f7be40d19..20114790df86 100644 --- a/src/vs/workbench/contrib/chat/browser/imageUtils.ts +++ b/src/vs/workbench/contrib/chat/browser/imageUtils.ts @@ -22,14 +22,16 @@ export async function resizeImage(data: Uint8Array): Promise { URL.revokeObjectURL(url); let { width, height } = img; + if (width < 768 || height < 768) { + resolve(data); + return; + } + // Calculate the new dimensions while maintaining the aspect ratio if (width > 2048 || height > 2048) { const scaleFactor = 2048 / Math.max(width, height); width = Math.round(width * scaleFactor); height = Math.round(height * scaleFactor); - } else { - resolve(data); - return; } const scaleFactor = 768 / Math.min(width, height); diff --git a/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts b/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts index 72a193d368ed..ec10006fbe60 100644 --- a/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts +++ b/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts @@ -24,6 +24,11 @@ declare module 'vscode' { */ data(): Thenable; + /** + * + */ + readonly reference?: Uri; + /** * @param mimeType The MIME type of the binary data. * @param data The binary data of the reference. From 7d82760cbf7900a8c4e44197693c6a7be6dc18d7 Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Thu, 23 Jan 2025 13:29:28 -0800 Subject: [PATCH 0825/3587] Fix URI check for nb inline values (#238613) fix uri condition --- .../contrib/notebookVariables/notebookInlineVariables.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts index 702e3a94fc00..9f26801fd323 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts @@ -74,7 +74,7 @@ export class NotebookInlineVariablesController extends Disposable implements INo return; } - if (this.notebookEditor.textModel?.uri && isEqual(this.notebookEditor.textModel.uri, event.notebook)) { + if (!this.notebookEditor.textModel?.uri || !isEqual(this.notebookEditor.textModel.uri, event.notebook)) { return; } From 29d525c1d3e5daec9f35d0fbcd2744d8210a0c0b Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 23 Jan 2025 16:11:23 -0600 Subject: [PATCH 0826/3587] use `terminalShellType` for terminal suggest (#238381) --- extensions/terminal-suggest/package.json | 3 +- .../src/terminalSuggestMain.ts | 59 +++++++++++++++---- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/extensions/terminal-suggest/package.json b/extensions/terminal-suggest/package.json index 981112636bcb..82e488dd9f52 100644 --- a/extensions/terminal-suggest/package.json +++ b/extensions/terminal-suggest/package.json @@ -15,7 +15,8 @@ ], "enabledApiProposals": [ "terminalCompletionProvider", - "terminalShellEnv" + "terminalShellEnv", + "terminalShellType" ], "scripts": { "compile": "npx gulp compile-extension:terminal-suggest", diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 8ba754efab23..b0511a31985d 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -22,7 +22,7 @@ let cachedAvailableCommandsPath: string | undefined; let cachedWindowsExecutableExtensions: { [key: string]: boolean | undefined } | undefined; const cachedWindowsExecutableExtensionsSettingId = 'terminal.integrated.suggest.windowsExecutableExtensions'; let cachedAvailableCommands: Set | undefined; -const cachedBuiltinCommands: Map = new Map(); +const cachedBuiltinCommands: Map = new Map(); export const availableSpecs: Fig.Spec[] = [ cdSpec, @@ -33,34 +33,37 @@ for (const spec of upstreamSpecs) { availableSpecs.push(require(`./completions/upstream/${spec}`).default); } -async function getBuiltinCommands(shell: string, existingCommands?: Set): Promise { +async function getBuiltinCommands(shellType: TerminalShellType, existingCommands?: Set): Promise { try { - const shellType = path.basename(shell, path.extname(shell)); const cachedCommands = cachedBuiltinCommands.get(shellType); if (cachedCommands) { return cachedCommands; } const filter = (cmd: string) => cmd && !existingCommands?.has(cmd); + const shell = getShell(shellType); + if (!shell) { + return; + } const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell }; let commands: string[] | undefined; switch (shellType) { - case 'bash': { + case TerminalShellType.Bash: { const bashOutput = execSync('compgen -b', options); commands = bashOutput.split('\n').filter(filter); break; } - case 'zsh': { + case TerminalShellType.Zsh: { const zshOutput = execSync('printf "%s\\n" ${(k)builtins}', options); commands = zshOutput.split('\n').filter(filter); break; } - case 'fish': { + case TerminalShellType.Fish: { // TODO: Ghost text in the command line prevents completions from working ATM for fish const fishOutput = execSync('functions -n', options); commands = fishOutput.split(', ').filter(filter); break; } - case 'pwsh': { + case TerminalShellType.PowerShell: { const output = await new Promise((resolve, reject) => { exec('Get-Command -All | Select-Object Name, CommandType, DisplayName, Definition | ConvertTo-Json', { ...options, @@ -119,14 +122,13 @@ export async function activate(context: vscode.ExtensionContext) { return; } - // TODO: Leverage shellType when available https://github.com/microsoft/vscode/issues/230165 - const shellPath = ('shellPath' in terminal.creationOptions ? terminal.creationOptions.shellPath : undefined) ?? vscode.env.shell; - if (!shellPath) { + const shellType: TerminalShellType | undefined = 'shellType' in terminal.state ? terminal.state.shellType as TerminalShellType : undefined; + if (!shellType) { return; } const commandsInPath = await getCommandsInPath(terminal.shellIntegration?.env); - const builtinCommands = await getBuiltinCommands(shellPath, commandsInPath?.labels) ?? []; + const builtinCommands = await getBuiltinCommands(shellType, commandsInPath?.labels) ?? []; if (!commandsInPath?.completionResources) { return; } @@ -545,3 +547,38 @@ function getFriendlyFilePath(uri: vscode.Uri, pathSeparator: string): string { } return path; } + +// TODO: remove once API is finalized +export enum TerminalShellType { + Sh = 1, + Bash = 2, + Fish = 3, + Csh = 4, + Ksh = 5, + Zsh = 6, + CommandPrompt = 7, + GitBash = 8, + PowerShell = 9, + Python = 10, + Julia = 11, + NuShell = 12, + Node = 13 +} + + +function getShell(shellType: TerminalShellType): string | undefined { + switch (shellType) { + case TerminalShellType.Bash: + return 'bash'; + case TerminalShellType.Fish: + return 'fish'; + case TerminalShellType.Zsh: + return 'zsh'; + case TerminalShellType.PowerShell: + return 'pwsh'; + default: { + return undefined; + } + } +} + From 737074e1b73876310a3bc9db0a8cf66882bacfec Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 23 Jan 2025 23:31:03 +0100 Subject: [PATCH 0827/3587] Add wiggle animation on hover for inline edits (#238590) * wiggle on hover * :lipstick: --- .../browser/view/inlineEdits/deletionView.ts | 17 +++-------- .../view/inlineEdits/gutterIndicatorView.ts | 8 ++---- .../view/inlineEdits/inlineDiffView.ts | 7 +++-- .../view/inlineEdits/sideBySideDiff.ts | 6 +++- .../browser/view/inlineEdits/view.css | 28 ++++++++++++++++++- .../browser/view/inlineEdits/view.ts | 10 ++++++- .../view/inlineEdits/wordReplacementView.ts | 17 +++++++++-- 7 files changed, 66 insertions(+), 27 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts index 390b07779c24..595e683fd161 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts @@ -10,10 +10,11 @@ import { Point } from '../../../../../browser/point.js'; import { EditorOption } from '../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../common/core/lineRange.js'; import { Position } from '../../../../../common/core/position.js'; +import { IInlineEditsView } from './sideBySideDiff.js'; import { createRectangle, mapOutFalsy, maxContentWidthInRange, n } from './utils.js'; import { InlineEditWithChanges } from './viewAndDiffProducer.js'; -export class InlineEditsDeletionView extends Disposable { +export class InlineEditsDeletionView extends Disposable implements IInlineEditsView { private readonly _editorObs = observableCodeEditor(this._editor); constructor( @@ -41,17 +42,6 @@ export class InlineEditsDeletionView extends Disposable { private readonly _display = derived(this, reader => !!this._uiState.read(reader) ? 'block' : 'none'); - private readonly previewRef = n.ref(); - private readonly toolbarRef = n.ref(); - - private readonly _editorContainer = n.div({ - class: ['editorContainer', this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !v.edits.experimental.useGutterIndicator && 'showHover')], - style: { position: 'absolute' }, - }, [ - n.div({ class: 'preview', style: {}, ref: this.previewRef }), - n.div({ class: 'toolbar', style: {}, ref: this.toolbarRef }), - ]).keepUpdated(this._store); - private readonly _originalStartPosition = derived(this, (reader) => { const inlineEdit = this._edit.read(reader); return inlineEdit ? new Position(inlineEdit.originalLineRange.startLineNumber, 1) : null; @@ -170,7 +160,8 @@ export class InlineEditsDeletionView extends Disposable { display: this._display, }, }, [ - [this._editorContainer, this._foregroundSvg], + [this._foregroundSvg], ]).keepUpdated(this._store); + readonly isHovered = constObservable(false); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index 7dc1c30042ac..9d392a863efb 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -73,7 +73,7 @@ export class InlineEditsGutterIndicator extends Disposable { private readonly _editorObs: ObservableCodeEditor, private readonly _originalRange: IObservable, private readonly _model: IObservable, - private readonly _shouldShowHover: IObservable, + private readonly _isHoveringOverInlineEdit: IObservable, private readonly _focusIsInMenu: ISettableObservable, @IHoverService private readonly _hoverService: HoverService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -88,11 +88,7 @@ export class InlineEditsGutterIndicator extends Disposable { })); this._register(autorun(reader => { - if (this._shouldShowHover.read(reader)) { - this._showHover(); - } else { - this._hoverService.hideHover(); - } + this._indicator.element.classList.toggle('wiggle', this._isHoveringOverInlineEdit.read(reader)); })); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts index a910c8854d73..d6c99f67fe18 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { autorunWithStore, derived, IObservable, observableFromEvent } from '../../../../../../base/common/observable.js'; +import { autorunWithStore, constObservable, derived, IObservable, observableFromEvent } from '../../../../../../base/common/observable.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { rangeIsSingleLine } from '../../../../../browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.js'; @@ -18,6 +18,7 @@ import { DetailedLineRangeMapping } from '../../../../../common/diff/rangeMappin import { EndOfLinePreference, IModelDeltaDecoration, ITextModel } from '../../../../../common/model.js'; import { ModelDecorationOptions } from '../../../../../common/model/textModel.js'; import { InlineDecoration, InlineDecorationType } from '../../../../../common/viewModel.js'; +import { IInlineEditsView } from './sideBySideDiff.js'; import { classNames } from './utils.js'; export interface IOriginalEditorInlineDiffViewState { @@ -28,11 +29,13 @@ export interface IOriginalEditorInlineDiffViewState { modifiedCodeEditor: ICodeEditor; } -export class OriginalEditorInlineDiffView extends Disposable { +export class OriginalEditorInlineDiffView extends Disposable implements IInlineEditsView { public static supportsInlineDiffRendering(mapping: DetailedLineRangeMapping): boolean { return allowsTrueInlineDiffRendering(mapping); } + readonly isHovered = constObservable(false); + constructor( private readonly _originalEditor: ICodeEditor, private readonly _state: IObservable, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index c0023b811da7..5ba16a1ef5d0 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -107,7 +107,11 @@ export const acceptedDecorationBackgroundColor = registerColor( true ); -export class InlineEditsSideBySideDiff extends Disposable { +export interface IInlineEditsView { + isHovered: IObservable; +} + +export class InlineEditsSideBySideDiff extends Disposable implements IInlineEditsView { private readonly _editorObs = observableCodeEditor(this._editor); constructor( diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index 60f8e058e6bf..bda6e8daff90 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -237,6 +237,32 @@ } } -.inline-edits-view-gutter-indicator .codicon{ +.inline-edits-view-gutter-indicator .codicon { margin-top: 1px; /* TODO: Move into gutter DOM initialization */ } + +@keyframes wiggle { + 0% { + transform: rotate(0) scale(1); + } + + 15%, + 45% { + transform: rotate(.04turn) scale(1.1); + } + + 30%, + 60% { + transform: rotate(-.04turn) scale(1.2); + } + + 100% { + transform: rotate(0) scale(1); + } +} + +.inline-edits-view-gutter-indicator.wiggle .icon { + animation-duration: .8s; + animation-iteration-count: 1; + animation-name: wiggle; +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index f80edbd6aa60..8c4991e88257 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -160,6 +160,14 @@ export class InlineEditsView extends Disposable { private readonly _useGutterIndicator = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useGutterIndicator); + private readonly _inlineEditsIsHovered = derived(this, reader => { + return this._sideBySide.isHovered.read(reader) + || this._wordReplacementViews.read(reader).some(v => v.isHovered.read(reader)) + || this._deletion.isHovered.read(reader) + || this._inlineDiffView.isHovered.read(reader) + || this._lineReplacementView.read(reader).some(v => v.isHovered.read(reader)); + }); + protected readonly _indicator = this._register(autorunWithStore((reader, store) => { if (this._useGutterIndicator.read(reader)) { store.add(this._instantiationService.createInstance( @@ -167,7 +175,7 @@ export class InlineEditsView extends Disposable { this._editorObs, this._uiState.map(s => s && s.originalDisplayRange), this._model, - this._sideBySide.isHovered, + this._inlineEditsIsHovered, this._focusIsInMenu, )); } else { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index 2becea6662d2..bfba2fe07d8b 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -20,6 +20,7 @@ import { LineTokens } from '../../../../../common/tokens/lineTokens.js'; import { TokenArray } from '../../../../../common/tokens/tokenArray.js'; import { mapOutFalsy, n, rectToProps } from './utils.js'; import { localize } from '../../../../../../nls.js'; +import { IInlineEditsView } from './sideBySideDiff.js'; import { Range } from '../../../../../common/core/range.js'; import { LineRange } from '../../../../../common/core/lineRange.js'; import { InlineDecoration, InlineDecorationType } from '../../../../../common/viewModel.js'; @@ -35,7 +36,7 @@ export const transparentHoverBackground = registerColor( localize('inlineEdit.wordReplacementView.background', 'Background color for the inline edit word replacement view.') ); -export class WordReplacementView extends Disposable { +export class WordReplacementView extends Disposable implements IInlineEditsView { private readonly _start = this._editor.observePosition(constObservable(this._edit.range.getStartPosition()), this._store); private readonly _end = this._editor.observePosition(constObservable(this._edit.range.getEndPosition()), this._store); @@ -239,6 +240,10 @@ export class WordReplacementView extends Disposable { }) ]).keepUpdated(this._store); + readonly isHovered = derived(this, reader => { + return this._div.getIsHovered(this._store).read(reader); + }); + constructor( private readonly _editor: ObservableCodeEditor, /** Must be single-line in both sides */ @@ -257,7 +262,7 @@ export class WordReplacementView extends Disposable { } } -export class LineReplacementView extends Disposable { +export class LineReplacementView extends Disposable implements IInlineEditsView { private readonly _originalBubblesDecorationCollection = this._editor.editor.createDecorationsCollection(); private readonly _originalBubblesDecorationOptions: IModelDecorationOptions = { @@ -492,6 +497,10 @@ export class LineReplacementView extends Disposable { }) ]).keepUpdated(this._store); + readonly isHovered = derived(this, reader => { + return this._div.getIsHovered(this._store).read(reader); + }); + constructor( private readonly _editor: ObservableCodeEditor, private readonly _originalRange: LineRange, @@ -538,7 +547,7 @@ export interface Replacement { modifiedRange: Range; } -export class WordInsertView extends Disposable { +export class WordInsertView extends Disposable implements IInlineEditsView { private readonly _start = this._editor.observePosition(constObservable(this._edit.range.getStartPosition()), this._store); private readonly _layout = derived(this, reader => { @@ -630,6 +639,8 @@ export class WordInsertView extends Disposable { }) ]).keepUpdated(this._store); + readonly isHovered = constObservable(false); + constructor( private readonly _editor: ObservableCodeEditor, /** Must be single-line in both sides */ From b01852228b77ca6fb20fc39d4ba4fd88ec5c0c5d Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Thu, 23 Jan 2025 15:19:46 -0800 Subject: [PATCH 0828/3587] proper cancellation source per nb inline value controller (#238624) proper cancellation source per inline value controller --- .../notebookInlineVariables.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts index 9f26801fd323..9f180017354b 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts @@ -47,6 +47,8 @@ export class NotebookInlineVariablesController extends Disposable implements INo private cellDecorationIds = new Map(); private cellContentListeners = new ResourceMap(); + private currentCancellationTokenSource: CancellationTokenSource | null = null; + constructor( private readonly notebookEditor: INotebookEditor, @INotebookKernelService private readonly notebookKernelService: INotebookKernelService, @@ -69,6 +71,15 @@ export class NotebookInlineVariablesController extends Disposable implements INo } private async updateInlineVariables(event: ICellExecutionStateChangedEvent): Promise { + // Cancel any ongoing request + if (this.currentCancellationTokenSource) { + this.currentCancellationTokenSource.cancel(); + } + + // Create a new CancellationTokenSource for the new request + this.currentCancellationTokenSource = new CancellationTokenSource(); + const token = this.currentCancellationTokenSource.token; + if (this.debugService.state !== State.Inactive) { this._clearNotebookInlineDecorations(); return; @@ -93,7 +104,6 @@ export class NotebookInlineVariablesController extends Disposable implements INo this.clearCellInlineDecorations(cell); - const cts = new CancellationTokenSource(); const inlineDecorations: IModelDeltaDecoration[] = []; if (this.languageFeaturesService.inlineValuesProvider.has(model)) { @@ -110,7 +120,7 @@ export class NotebookInlineVariablesController extends Disposable implements INo const fullCellRange = new Range(1, 1, lastLine, lastColumn); - const promises = providers.flatMap(provider => Promise.resolve(provider.provideInlineValues(model, fullCellRange, ctx, cts.token)).then(async (result) => { + const promises = providers.flatMap(provider => Promise.resolve(provider.provideInlineValues(model, fullCellRange, ctx, token)).then(async (result) => { if (result) { let kernel: INotebookKernelMatchResult; @@ -120,7 +130,7 @@ export class NotebookInlineVariablesController extends Disposable implements INo return; // should not happen, a cell will be executed } kernel = this.notebookKernelService.getMatchingKernel(this.notebookEditor.textModel); - const variables = kernel.selected?.provideVariables(event.notebook, undefined, 'named', 0, cts.token); + const variables = kernel.selected?.provideVariables(event.notebook, undefined, 'named', 0, token); if (!variables) { return; } @@ -318,6 +328,9 @@ export class NotebookInlineVariablesController extends Disposable implements INo override dispose(): void { super.dispose(); this._clearNotebookInlineDecorations(); + if (this.currentCancellationTokenSource) { + this.currentCancellationTokenSource.cancel(); + } } } From 6fbae9a6a65bdcf5506f0b2c9ca5f95f41b29287 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:28:16 -0800 Subject: [PATCH 0829/3587] Rename keyMatchSize to keyMatchScore (#238625) --- .../preferences/browser/preferencesSearch.ts | 17 ++++++++++------- .../preferences/browser/settingsEditor2.ts | 2 +- .../preferences/browser/settingsTreeModels.ts | 2 +- .../services/preferences/common/preferences.ts | 4 ++-- .../preferences/common/preferencesModels.ts | 4 ++-- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index 463b9facdaf0..ba3ab00f1c09 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -99,7 +99,7 @@ export class LocalSearchProvider implements ISearchProvider { let orderedScore = LocalSearchProvider.START_SCORE; // Sort is not stable const settingMatcher = (setting: ISetting) => { - const { matches, matchType, keyMatchSize } = new SettingMatches(this._filter, setting, true, true, (filter, setting) => preferencesModel.findValueMatches(filter, setting), this.configurationService); + const { matches, matchType, keyMatchScore } = new SettingMatches(this._filter, setting, true, true, (filter, setting) => preferencesModel.findValueMatches(filter, setting), this.configurationService); const score = strings.equalsIgnoreCase(this._filter, setting.key) ? LocalSearchProvider.EXACT_MATCH_SCORE : orderedScore--; @@ -108,7 +108,7 @@ export class LocalSearchProvider implements ISearchProvider { { matches, matchType, - keyMatchSize, + keyMatchScore, score } : null; @@ -139,8 +139,11 @@ export class LocalSearchProvider implements ISearchProvider { export class SettingMatches { readonly matches: IRange[]; matchType: SettingMatchType = SettingMatchType.None; - /** A more precise match score for key matches. */ - keyMatchSize: number = 0; + /** + * A match score for key matches to allow comparing key matches against each other. + * Otherwise, all key matches are treated the same, and sorting is done by ToC order. + */ + keyMatchScore: number = 0; constructor( searchString: string, @@ -186,7 +189,7 @@ export class SettingMatches { } if (keyMatchingWords.size) { this.matchType |= SettingMatchType.KeyMatch; - this.keyMatchSize = keyMatchingWords.size; + this.keyMatchScore = keyMatchingWords.size; } // Also check if the user tried searching by id. @@ -407,7 +410,7 @@ class AiRelatedInformationSearchProvider implements IRemoteSearchProvider { setting: settingsRecord[pick], matches: [settingsRecord[pick].range], matchType: SettingMatchType.RemoteMatch, - keyMatchSize: 0, + keyMatchScore: 0, score: info.weight }); } @@ -503,7 +506,7 @@ class TfIdfSearchProvider implements IRemoteSearchProvider { setting: this._settingsRecord[pick], matches: [this._settingsRecord[pick].range], matchType: SettingMatchType.RemoteMatch, - keyMatchSize: 0, + keyMatchScore: 0, score: info.score }); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 5de358b38d51..de486320c041 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -1679,7 +1679,7 @@ export class SettingsEditor2 extends EditorPane { for (const g of this.defaultSettingsEditorModel.settingsGroups.slice(1)) { for (const sect of g.sections) { for (const setting of sect.settings) { - fullResult.filterMatches.push({ setting, matches: [], matchType: SettingMatchType.None, keyMatchSize: 0, score: 0 }); + fullResult.filterMatches.push({ setting, matches: [], matchType: SettingMatchType.None, keyMatchScore: 0, score: 0 }); } } } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index 646164d1c841..d8712dc5e91e 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -963,7 +963,7 @@ export class SearchResultModel extends SettingsTreeModel { } else if (a.matchType === SettingMatchType.KeyMatch) { // The match types are the same and are KeyMatch. // Sort by the number of words matched in the key. - return b.keyMatchSize - a.keyMatchSize; + return b.keyMatchScore - a.keyMatchScore; } else if (a.matchType === SettingMatchType.RemoteMatch) { // The match types are the same and are RemoteMatch. // Sort by score. diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 835993ab9029..f9cb32deed32 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -146,7 +146,7 @@ export interface ISettingMatch { setting: ISetting; matches: IRange[] | null; matchType: SettingMatchType; - keyMatchSize: number; + keyMatchScore: number; score: number; } @@ -186,7 +186,7 @@ export interface IPreferencesEditorModel { } export type IGroupFilter = (group: ISettingsGroup) => boolean | null; -export type ISettingMatcher = (setting: ISetting, group: ISettingsGroup) => { matches: IRange[]; matchType: SettingMatchType; keyMatchSize: number; score: number } | null; +export type ISettingMatcher = (setting: ISetting, group: ISettingsGroup) => { matches: IRange[]; matchType: SettingMatchType; keyMatchScore: number; score: number } | null; export interface ISettingsEditorModel extends IPreferencesEditorModel { readonly onDidChangeGroups: Event; diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index f6c0bc95c132..44f3e8035de4 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -71,7 +71,7 @@ abstract class AbstractSettingsModel extends EditorModel { setting, matches: settingMatchResult && settingMatchResult.matches, matchType: settingMatchResult?.matchType ?? SettingMatchType.None, - keyMatchSize: settingMatchResult?.keyMatchSize ?? 0, + keyMatchScore: settingMatchResult?.keyMatchScore ?? 0, score: settingMatchResult?.score ?? 0 }); } @@ -900,7 +900,7 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements setting: filteredMatch.setting, score: filteredMatch.score, matchType: filteredMatch.matchType, - keyMatchSize: filteredMatch.keyMatchSize, + keyMatchScore: filteredMatch.keyMatchScore, matches: filteredMatch.matches && filteredMatch.matches.map(match => { return new Range( match.startLineNumber - filteredMatch.setting.range.startLineNumber, From d4d168bfa66454cf08486edab19641ae62553011 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 23 Jan 2025 17:54:19 -0800 Subject: [PATCH 0830/3587] Persisted storage for replace widget --- .../base/browser/ui/findinput/replaceInput.ts | 7 +- src/vs/editor/common/config/editorOptions.ts | 23 ++++- .../contrib/find/browser/findController.ts | 5 +- .../editor/contrib/find/browser/findWidget.ts | 8 +- .../find/browser/replaceWidgetHistory.ts | 99 +++++++++++++++++++ .../contrib/find/notebookFindReplaceWidget.ts | 2 +- .../contrib/search/browser/searchWidget.ts | 2 +- 7 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 src/vs/editor/contrib/find/browser/replaceWidgetHistory.ts diff --git a/src/vs/base/browser/ui/findinput/replaceInput.ts b/src/vs/base/browser/ui/findinput/replaceInput.ts index 22a81e211c92..11c3c04a8cc7 100644 --- a/src/vs/base/browser/ui/findinput/replaceInput.ts +++ b/src/vs/base/browser/ui/findinput/replaceInput.ts @@ -17,6 +17,7 @@ import { KeyCode } from '../../../common/keyCodes.js'; import './findInput.css'; import * as nls from '../../../../nls.js'; import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js'; +import { IHistory } from '../../../common/history.js'; export interface IReplaceInputOptions { @@ -29,7 +30,7 @@ export interface IReplaceInputOptions { readonly flexibleMaxHeight?: number; readonly appendPreserveCaseLabel?: string; - readonly history?: string[]; + readonly history?: IHistory; readonly showHistoryHint?: () => boolean; readonly inputBoxStyles: IInputBoxStyles; readonly toggleStyles: IToggleStyles; @@ -94,7 +95,7 @@ export class ReplaceInput extends Widget { this.label = options.label || NLS_DEFAULT_LABEL; const appendPreserveCaseLabel = options.appendPreserveCaseLabel || ''; - const history = options.history || []; + const history = options.history || new Set([]); const flexibleHeight = !!options.flexibleHeight; const flexibleWidth = !!options.flexibleWidth; const flexibleMaxHeight = options.flexibleMaxHeight; @@ -108,7 +109,7 @@ export class ReplaceInput extends Widget { validationOptions: { validation: this.validation }, - history: new Set(history), + history, showHistoryHint: options.showHistoryHint, flexibleHeight, flexibleWidth, diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 60057bc930e4..b01d6ecd30d6 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -1670,7 +1670,12 @@ export interface IEditorFindOptions { * @internal * Controls how the find widget search history should be stored */ - history?: 'never' | 'workspace'; + findHistory?: 'never' | 'workspace'; + /** + * @internal + * Controls how the find widget search history should be stored + */ + replaceHistory?: 'never' | 'workspace'; } /** @@ -1688,7 +1693,8 @@ class EditorFind extends BaseEditorOption(input.history, this.defaultValue.history, ['never', 'workspace']), + findHistory: stringSet<'never' | 'workspace'>(input.findHistory, this.defaultValue.findHistory, ['never', 'workspace']), + replaceHistory: stringSet<'never' | 'workspace'>(input.replaceHistory, this.defaultValue.replaceHistory, ['never', 'workspace']), }; } } diff --git a/src/vs/editor/contrib/find/browser/findController.ts b/src/vs/editor/contrib/find/browser/findController.ts index 128d89dc571f..6b6102ea5ac9 100644 --- a/src/vs/editor/contrib/find/browser/findController.ts +++ b/src/vs/editor/contrib/find/browser/findController.ts @@ -33,6 +33,7 @@ import { IThemeService, themeColorFromId } from '../../../../platform/theme/comm import { Selection } from '../../../common/core/selection.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { FindWidgetSearchHistory } from './findWidgetSearchHistory.js'; +import { ReplaceWidgetHistory } from './replaceWidgetHistory.js'; const SEARCH_STRING_MAX_LENGTH = 524288; @@ -444,6 +445,7 @@ export class FindController extends CommonFindController implements IFindControl private _widget: FindWidget | null; private _findOptionsWidget: FindOptionsWidget | null; private _findWidgetSearchHistory: FindWidgetSearchHistory; + private _replaceWidgetHistory: ReplaceWidgetHistory; constructor( editor: ICodeEditor, @@ -460,6 +462,7 @@ export class FindController extends CommonFindController implements IFindControl this._widget = null; this._findOptionsWidget = null; this._findWidgetSearchHistory = FindWidgetSearchHistory.getOrCreate(_storageService); + this._replaceWidgetHistory = ReplaceWidgetHistory.getOrCreate(_storageService); } protected override async _start(opts: IFindStartOptions, newState?: INewFindReplaceState): Promise { @@ -511,7 +514,7 @@ export class FindController extends CommonFindController implements IFindControl } private _createFindWidget() { - this._widget = this._register(new FindWidget(this._editor, this, this._state, this._contextViewService, this._keybindingService, this._contextKeyService, this._themeService, this._storageService, this._notificationService, this._hoverService, this._findWidgetSearchHistory)); + this._widget = this._register(new FindWidget(this._editor, this, this._state, this._contextViewService, this._keybindingService, this._contextKeyService, this._themeService, this._storageService, this._notificationService, this._hoverService, this._findWidgetSearchHistory, this._replaceWidgetHistory)); this._findOptionsWidget = this._register(new FindOptionsWidget(this._editor, this._state, this._keybindingService)); } diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index 96786676a39c..a305114ac1e0 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -175,6 +175,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL notificationService: INotificationService, private readonly _hoverService: IHoverService, private readonly _findWidgetSearchHistory: IHistory | undefined, + private readonly _replaceWidgetHistory: IHistory | undefined, ) { super(); this._codeEditor = codeEditor; @@ -941,7 +942,8 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL const flexibleHeight = true; const flexibleWidth = true; // Find input - const findSearchHistoryConfig = this._codeEditor.getOption(EditorOption.find).history; + const findSearchHistoryConfig = this._codeEditor.getOption(EditorOption.find).findHistory; + const replaceHistoryConfig = this._codeEditor.getOption(EditorOption.find).replaceHistory; this._findInput = this._register(new ContextScopedFindInput(null, this._contextViewProvider, { width: FIND_INPUT_AREA_WIDTH, label: NLS_FIND_INPUT_LABEL, @@ -1112,13 +1114,13 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL label: NLS_REPLACE_INPUT_LABEL, placeholder: NLS_REPLACE_INPUT_PLACEHOLDER, appendPreserveCaseLabel: this._keybindingLabelFor(FIND_IDS.TogglePreserveCaseCommand), - history: [], + history: replaceHistoryConfig === 'workspace' ? this._replaceWidgetHistory : new Set([]), flexibleHeight, flexibleWidth, flexibleMaxHeight: 118, showHistoryHint: () => showHistoryKeybindingHint(this._keybindingService), inputBoxStyles: defaultInputBoxStyles, - toggleStyles: defaultToggleStyles + toggleStyles: defaultToggleStyles, }, this._contextKeyService, true)); this._replaceInput.setPreserveCase(!!this._state.preserveCase); this._register(this._replaceInput.onKeyDown((e) => this._onReplaceInputKeyDown(e))); diff --git a/src/vs/editor/contrib/find/browser/replaceWidgetHistory.ts b/src/vs/editor/contrib/find/browser/replaceWidgetHistory.ts new file mode 100644 index 000000000000..a570cc7b9e27 --- /dev/null +++ b/src/vs/editor/contrib/find/browser/replaceWidgetHistory.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../../../base/common/event.js'; +import { IHistory } from '../../../../base/common/history.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; + +export class ReplaceWidgetHistory implements IHistory { + public static readonly FIND_HISTORY_KEY = 'workbench.replace.history'; + private inMemoryValues: Set = new Set(); + public onDidChange?: Event; + private _onDidChangeEmitter: Emitter; + + private static _instance: ReplaceWidgetHistory | null = null; + + static getOrCreate( + storageService: IStorageService, + ): ReplaceWidgetHistory { + if (!ReplaceWidgetHistory._instance) { + ReplaceWidgetHistory._instance = new ReplaceWidgetHistory(storageService); + } + return ReplaceWidgetHistory._instance; + } + + constructor( + @IStorageService private readonly storageService: IStorageService, + ) { + this._onDidChangeEmitter = new Emitter(); + this.onDidChange = this._onDidChangeEmitter.event; + this.load(); + } + + delete(t: string): boolean { + const result = this.inMemoryValues.delete(t); + this.save(); + return result; + } + + add(t: string): this { + this.inMemoryValues.add(t); + this.save(); + return this; + } + + has(t: string): boolean { + return this.inMemoryValues.has(t); + } + + clear(): void { + this.inMemoryValues.clear(); + this.save(); + } + + forEach(callbackfn: (value: string, value2: string, set: Set) => void, thisArg?: any): void { + // fetch latest from storage + this.load(); + return this.inMemoryValues.forEach(callbackfn); + } + replace?(t: string[]): void { + this.inMemoryValues = new Set(t); + this.save(); + } + + load() { + let result: [] | undefined; + const raw = this.storageService.get( + ReplaceWidgetHistory.FIND_HISTORY_KEY, + StorageScope.WORKSPACE + ); + + if (raw) { + try { + result = JSON.parse(raw); + } catch (e) { + // Invalid data + } + } + + this.inMemoryValues = new Set(result || []); + } + + // Run saves async + save(): Promise { + const elements: string[] = []; + this.inMemoryValues.forEach(e => elements.push(e)); + return new Promise(resolve => { + this.storageService.store( + ReplaceWidgetHistory.FIND_HISTORY_KEY, + JSON.stringify(elements), + StorageScope.WORKSPACE, + StorageTarget.USER, + ); + this._onDidChangeEmitter.fire(elements); + resolve(); + }); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts index 8509900d4b07..74a3bc881b5c 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -566,7 +566,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._replaceInput = this._register(new ContextScopedReplaceInput(null, undefined, { label: NLS_REPLACE_INPUT_LABEL, placeholder: NLS_REPLACE_INPUT_PLACEHOLDER, - history: [], + history: new Set([]), inputBoxStyles: defaultInputBoxStyles, toggleStyles: defaultToggleStyles }, contextKeyService, false)); diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index 3b1bab8cf718..77b589490ec4 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -512,7 +512,7 @@ export class SearchWidget extends Widget { label: nls.localize('label.Replace', 'Replace: Type replace term and press Enter to preview'), placeholder: nls.localize('search.replace.placeHolder', "Replace"), appendPreserveCaseLabel: appendKeyBindingLabel('', this.keybindingService.lookupKeybinding(Constants.SearchCommandIds.TogglePreserveCaseId)), - history: options.replaceHistory, + history: new Set(options.replaceHistory), showHistoryHint: () => showHistoryKeybindingHint(this.keybindingService), flexibleHeight: true, flexibleMaxHeight: SearchWidget.INPUT_MAX_HEIGHT, From ac7533df77aa2f24d745bf7bc66bb2fb50bf87e5 Mon Sep 17 00:00:00 2001 From: henry Date: Thu, 23 Jan 2025 20:10:05 -0600 Subject: [PATCH 0831/3587] fixed lazy client --- .../typescript-language-features/src/lazyClientHost.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extensions/typescript-language-features/src/lazyClientHost.ts b/extensions/typescript-language-features/src/lazyClientHost.ts index be832b3e1697..1a99c2b46067 100644 --- a/extensions/typescript-language-features/src/lazyClientHost.ts +++ b/extensions/typescript-language-features/src/lazyClientHost.ts @@ -15,7 +15,7 @@ import { ActiveJsTsEditorTracker } from './ui/activeJsTsEditorTracker'; import ManagedFileContextManager from './ui/managedFileContext'; import { ServiceConfigurationProvider } from './configuration/configuration'; import * as fileSchemes from './configuration/fileSchemes'; -import { standardLanguageDescriptions } from './configuration/languageDescription'; +import { standardLanguageDescriptions, isJsConfigOrTsConfigFileName } from './configuration/languageDescription'; import { Lazy, lazy } from './utils/lazy'; import { Logger } from './logging/logger'; import { PluginManager } from './tsServer/plugins'; @@ -97,6 +97,10 @@ function isSupportedDocument( supportedLanguage: readonly string[], document: vscode.TextDocument ): boolean { + //Activate for JS/TS config files + if (isJsConfigOrTsConfigFileName(document.fileName)) { + return true; + } return supportedLanguage.indexOf(document.languageId) >= 0 && !fileSchemes.disabledSchemes.has(document.uri.scheme); } From 6551f0c830ef12f6e54d13f7f23992cedd576b76 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:47:56 -0800 Subject: [PATCH 0832/3587] Initialize shellType state after reconnect (#238588) --- src/vs/workbench/api/browser/mainThreadTerminalService.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 9482ade46611..90d5a2ec03fb 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -93,6 +93,9 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape for (const instance of this._terminalService.instances) { this._onTerminalOpened(instance); instance.processReady.then(() => this._onTerminalProcessIdReady(instance)); + if (instance.shellType) { + this._proxy.$acceptTerminalShellType(instance.instanceId, instance.shellType); + } } const activeInstance = this._terminalService.activeInstance; if (activeInstance) { From 235263308ba950c69cd6f46fe75fe5272309bcce Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 23 Jan 2025 21:12:14 -0600 Subject: [PATCH 0833/3587] adjust terminal suggest font, line height when they change (#238611) --- .../suggest/browser/terminalSuggestAddon.ts | 5 ++++- .../suggest/browser/simpleSuggestWidget.ts | 11 ++++++++--- .../browser/simpleSuggestWidgetRenderer.ts | 17 +++++++++++------ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 1fdfe2f4262c..a175d8f35d4f 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -401,7 +401,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest const fontInfo: ISimpleSuggestWidgetFontInfo = { fontFamily: font.fontFamily, fontSize: font.fontSize, - lineHeight: Math.ceil(1.5 * font.fontSize), + // In the editor's world, lineHeight is the pixels between the baselines of two lines of text + // In the terminal's world, lineHeight is the multiplier of the font size + // 1.5 is needed so that it's taller than a 16px icon + lineHeight: Math.ceil(c.lineHeight * font.fontSize * 1.5), fontWeight: c.fontWeight.toString(), letterSpacing: font.letterSpacing }; diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index bbb56eefaeb1..6092257e73d4 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -23,6 +23,7 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { canExpandCompletionItem, SimpleSuggestDetailsOverlay, SimpleSuggestDetailsWidget } from './simpleSuggestWidgetDetails.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { TerminalSettingId } from '../../../../platform/terminal/common/terminal.js'; const $ = dom.$; @@ -179,7 +180,7 @@ export class SimpleSuggestWidget extends Disposable { const applyIconStyle = () => this.element.domNode.classList.toggle('no-icons', !_configurationService.getValue('editor.suggest.showIcons')); applyIconStyle(); - const renderer = new SimpleSuggestWidgetItemRenderer(_getFontInfo); + const renderer = new SimpleSuggestWidgetItemRenderer(_getFontInfo, this._configurationService); this._register(renderer); this._listElement = dom.append(this.element.domNode, $('.tree')); this._list = this._register(new List('SuggestWidget', this._listElement, { @@ -246,6 +247,10 @@ export class SimpleSuggestWidget extends Disposable { if (e.affectsConfiguration('editor.suggest.showIcons')) { applyIconStyle(); } + if (this._completionModel && e.affectsConfiguration(TerminalSettingId.FontSize) || e.affectsConfiguration(TerminalSettingId.LineHeight) || e.affectsConfiguration(TerminalSettingId.FontFamily)) { + this._layout(undefined); + this._list.splice(0, this._list.length, this._completionModel!.items); + } if (_options.statusBarMenuId && _options.showStatusBarSettingId && e.affectsConfiguration(_options.showStatusBarSettingId)) { const showStatusBar: boolean = _configurationService.getValue(_options.showStatusBarSettingId); if (showStatusBar && !this._status) { @@ -764,9 +769,9 @@ export class SimpleSuggestWidget extends Disposable { private _getLayoutInfo() { const fontInfo = this._getFontInfo(); - const itemHeight = clamp(Math.ceil(fontInfo.lineHeight), 8, 1000); + const itemHeight = clamp(fontInfo.lineHeight, 8, 1000); const statusBarHeight = !this._options.statusBarMenuId || !this._options.showStatusBarSettingId || !this._configurationService.getValue(this._options.showStatusBarSettingId) || this._state === State.Empty || this._state === State.Loading ? 0 : itemHeight; - const borderWidth = 1; //this._details.widget.borderWidth; + const borderWidth = this._details.widget.borderWidth; const borderHeight = 2 * borderWidth; return { diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts index 83605e2156de..71129442dda4 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts @@ -12,6 +12,8 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { createMatches } from '../../../../base/common/filters.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { TerminalSettingId } from '../../../../platform/terminal/common/terminal.js'; export function getAriaId(index: number): string { return `simple-suggest-aria-id-${index}`; @@ -55,13 +57,16 @@ export class SimpleSuggestWidgetItemRenderer implements IListRenderer(); readonly onDidToggleDetails: Event = this._onDidToggleDetails.event; + private readonly _disposables = new DisposableStore(); + readonly templateId = 'suggestion'; - constructor(private readonly _getFontInfo: () => ISimpleSuggestWidgetFontInfo) { + constructor(private readonly _getFontInfo: () => ISimpleSuggestWidgetFontInfo, @IConfigurationService private readonly _configurationService: IConfigurationService) { } dispose(): void { this._onDidToggleDetails.dispose(); + this._disposables.dispose(); } renderTemplate(container: HTMLElement): ISimpleSuggestionTemplateData { @@ -111,11 +116,11 @@ export class SimpleSuggestWidgetItemRenderer implements IListRenderer { - // if (e.hasChanged(EditorOption.fontInfo) || e.hasChanged(EditorOption.suggestFontSize) || e.hasChanged(EditorOption.suggestLineHeight)) { - // configureFont(); - // } - // })); + this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(TerminalSettingId.FontSize) || e.affectsConfiguration(TerminalSettingId.FontFamily) || e.affectsConfiguration(TerminalSettingId.FontWeight) || e.affectsConfiguration(TerminalSettingId.LineHeight)) { + configureFont(); + } + })); return { root, left, right, icon, colorspan, iconLabel, iconContainer, parametersLabel, qualifierLabel, detailsLabel, disposables }; } From f5782f528629d4200a964cc652055a4da43a7eb1 Mon Sep 17 00:00:00 2001 From: sunnylost Date: Fri, 24 Jan 2025 11:12:35 +0800 Subject: [PATCH 0834/3587] fix(settings-editor): ensure the width of the key name does not shrink (#229919) --- .../contrib/preferences/browser/media/settingsWidgets.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css b/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css index 89ba7579904b..088783e156c6 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css @@ -72,6 +72,7 @@ display: inline-block; line-height: 24px; min-height: 24px; + flex: none; } /* Use monospace to display glob patterns in include/exclude widget */ From 4575cfbecacc9725f99ae3b8f1dbb3ec75902370 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 23 Jan 2025 21:47:17 -0800 Subject: [PATCH 0835/3587] Check api proposal for tool request ID (#238634) --- src/vs/workbench/api/common/extHostLanguageModelTools.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index 74c4d0aff4d7..c5d845256956 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -12,11 +12,9 @@ import { revive } from '../../../base/common/marshalling.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { IPreparedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; +import { isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js'; import * as typeConvert from './extHostTypeConverters.js'; -import { isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; - - export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape { /** A map of tools that were registered in this EH */ @@ -68,7 +66,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape parameters: options.input, tokenBudget: options.tokenizationOptions?.tokenBudget, context: options.toolInvocationToken as IToolInvocationContext | undefined, - chatRequestId: options.chatRequestId, + chatRequestId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatRequestId : undefined, }, token); return typeConvert.LanguageModelToolResult.to(result); } finally { From 0c595fdb49be4c9d1275c5ea26870d83a5955463 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 23 Jan 2025 21:47:50 -0800 Subject: [PATCH 0836/3587] Add dropdown for agent mode picker (#238633) * toggle blue * Use CheckboxActionViewItem * Try dropdown * Polish * Undo checkbox changes --- .../browser/menuEntryActionViewItem.ts | 3 +- .../browser/actions/chatExecuteActions.ts | 6 +- .../contrib/chat/browser/chatInputPart.ts | 73 ++++++++++++++++++- 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 517bd2f66def..f90896f91a3b 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -169,6 +169,7 @@ export interface IMenuEntryActionViewItemOptions { draggable?: boolean; keybinding?: string; hoverDelegate?: IHoverDelegate; + keybindingNotRenderedWithLabel?: boolean; } export class MenuEntryActionViewItem extends ActionViewItem { @@ -187,7 +188,7 @@ export class MenuEntryActionViewItem this.action.run() + }, + { + ...this.action, + id: 'normalMode', + label: localize('chat.normalMode', "Normal"), + class: undefined, + enabled: true, + checked: !this.action.checked, + run: () => this.action.run() + }, + ]; + } + + override async onClick(event: MouseEvent): Promise { + this._openContextMenu(); + } + + override render(container: HTMLElement): void { + super.render(container); + container.classList.add('chat-modelPicker-item'); + + // TODO@roblourens this should be a DropdownMenuActionViewItem, but we can't customize how it's rendered yet. + this._register(dom.addDisposableListener(container, dom.EventType.KEY_UP, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { + this._openContextMenu(); + } + })); + } + + protected override updateLabel(): void { + if (this.label) { + const state = this.agentStateActions.find(action => action.checked)?.label ?? ''; + dom.reset(this.label, dom.$('span.chat-model-label', undefined, state), ...renderLabelWithIcons(`$(chevron-down)`)); + } + } + + private _openContextMenu() { + this._contextMenuService.showContextMenu({ + getAnchor: () => this.element!, + getActions: () => this.agentStateActions + }); + } +} From 3eb1f2c914fb291cd9f843a25e63188305346f77 Mon Sep 17 00:00:00 2001 From: henry Date: Fri, 24 Jan 2025 00:41:28 -0600 Subject: [PATCH 0837/3587] correct conditions --- .../typescript-language-features/src/lazyClientHost.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/extensions/typescript-language-features/src/lazyClientHost.ts b/extensions/typescript-language-features/src/lazyClientHost.ts index 1a99c2b46067..6d2bb34604f1 100644 --- a/extensions/typescript-language-features/src/lazyClientHost.ts +++ b/extensions/typescript-language-features/src/lazyClientHost.ts @@ -97,10 +97,6 @@ function isSupportedDocument( supportedLanguage: readonly string[], document: vscode.TextDocument ): boolean { - //Activate for JS/TS config files - if (isJsConfigOrTsConfigFileName(document.fileName)) { - return true; - } - return supportedLanguage.indexOf(document.languageId) >= 0 + return (supportedLanguage.indexOf(document.languageId) >= 0 || isJsConfigOrTsConfigFileName(document.fileName)) && !fileSchemes.disabledSchemes.has(document.uri.scheme); } From 79d2a4c8263543b45902dfa40f19946834a82371 Mon Sep 17 00:00:00 2001 From: Robo Date: Fri, 24 Jan 2025 16:27:05 +0900 Subject: [PATCH 0838/3587] fix: corrupted vsce dotnet binaries in rpm package (#238636) --- resources/linux/rpm/code.spec.template | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/linux/rpm/code.spec.template b/resources/linux/rpm/code.spec.template index a73bc02c3e7f..5691bb6a956c 100644 --- a/resources/linux/rpm/code.spec.template +++ b/resources/linux/rpm/code.spec.template @@ -12,6 +12,9 @@ Requires: @@DEPENDENCIES@@ AutoReq: 0 %global __provides_exclude_from ^%{_datadir}/%{name}/.*\\.so.*$ +# Disable elf stripping, refer https://github.com/microsoft/vscode/issues/223455#issuecomment-2610001754 +%global __brp_strip %{nil} +%global __brp_strip_comment_note %{nil} %description Visual Studio Code is a new choice of tool that combines the simplicity of a code editor with what developers need for the core edit-build-debug cycle. See https://code.visualstudio.com/docs/setup/linux for installation instructions and FAQ. From d96ee375ea3cd64a5110e1873269775f8d2d7347 Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 24 Jan 2025 08:46:16 +0100 Subject: [PATCH 0839/3587] fix tests --- .../inlineChat/test/browser/inlineChatController.test.ts | 3 ++- .../inlineChat/test/browser/inlineChatSession.test.ts | 6 ++++++ test/unit/electron/renderer.js | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 06791cc94323..d8d8ce7467ce 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -68,7 +68,7 @@ import { IChatEditingService, IChatEditingSession } from '../../../chat/common/c import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { TextModelResolverService } from '../../../../services/textmodelResolver/common/textModelResolverService.js'; import { ChatInputBoxContentProvider } from '../../../chat/browser/chatEdinputInputContentProvider.js'; -import { IObservable, observableValue } from '../../../../../base/common/observable.js'; +import { constObservable, IObservable, observableValue } from '../../../../../base/common/observable.js'; import { ILanguageModelToolsService } from '../../../chat/common/languageModelToolsService.js'; import { MockLanguageModelToolsService } from '../../../chat/test/common/mockLanguageModelToolsService.js'; @@ -164,6 +164,7 @@ suite('InlineChatController', function () { [ICommandService, new SyncDescriptor(TestCommandService)], [IChatEditingService, new class extends mock() { override currentEditingSessionObs: IObservable = observableValue(this, null); + override editingSessionsObs: IObservable = constObservable([]); }], [IEditorProgressService, new class extends mock() { override show(total: unknown, delay?: unknown): IProgressRunner { diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts index 80b877eb95a8..cfa95c164672 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -60,6 +60,8 @@ import { ILanguageModelToolsService } from '../../../chat/common/languageModelTo import { MockLanguageModelToolsService } from '../../../chat/test/common/mockLanguageModelToolsService.js'; import { IChatRequestModel } from '../../../chat/common/chatModel.js'; import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; +import { IObservable, observableValue, constObservable } from '../../../../../base/common/observable.js'; +import { IChatEditingService, IChatEditingSession } from '../../../chat/common/chatEditingService.js'; suite('InlineChatSession', function () { @@ -103,6 +105,10 @@ suite('InlineChatSession', function () { }; } }], + [IChatEditingService, new class extends mock() { + override currentEditingSessionObs: IObservable = observableValue(this, null); + override editingSessionsObs: IObservable = constObservable([]); + }], [IChatAccessibilityService, new class extends mock() { override acceptResponse(response: IChatResponseViewModel | undefined, requestId: number): void { } override acceptRequest(): number { return -1; } diff --git a/test/unit/electron/renderer.js b/test/unit/electron/renderer.js index b93d91a78e94..033667df0aa2 100644 --- a/test/unit/electron/renderer.js +++ b/test/unit/electron/renderer.js @@ -294,10 +294,12 @@ async function loadTests(opts) { // should not have unexpected errors const errors = _unexpectedErrors.concat(_loaderErrors); if (errors.length) { + const msg = []; for (const error of errors) { console.error(`Error: Test run should not have unexpected errors:\n${error}`); + msg.push(String(error)) } - assert.ok(false, 'Error: Test run should not have unexpected errors.'); + assert.ok(false, `Error: Test run should not have unexpected errors:\n${msg.join('\n')}`); } }); From b17af073a57fd9afa731b5a669d1a920b158f0e0 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 24 Jan 2025 10:28:27 +0100 Subject: [PATCH 0840/3587] chat setup - cleanup (#238641) --- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 0192a3a0fd0c..e8081d9a3676 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -172,7 +172,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr }); } - override async run(accessor: ServicesAccessor, startSetup: boolean | undefined): Promise { + override async run(accessor: ServicesAccessor): Promise { const viewsService = accessor.get(IViewsService); const viewDescriptorService = accessor.get(IViewDescriptorService); const configurationService = accessor.get(IConfigurationService); @@ -183,10 +183,6 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr showCopilotView(viewsService, layoutService); ensureSideBarChatViewSize(viewDescriptorService, layoutService); - if (startSetup === true) { - controller.value.setup(); - } - configurationService.updateValue('chat.commandCenter.enabled', true); } } @@ -313,7 +309,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr }, handleURL: async url => { const params = new URLSearchParams(url.query); - this.telemetryService.publicLog2('workbenchActionExecuted', { id: TRIGGER_SETUP_COMMAND_ID, from: params.get('referrer') ?? 'url' }); + this.telemetryService.publicLog2('workbenchActionExecuted', { id: TRIGGER_SETUP_COMMAND_ID, from: 'url', detail: params.get('referrer') ?? undefined }); await this.commandService.executeCommand(TRIGGER_SETUP_COMMAND_ID); From 6655b2ceaaaa7c73c84b0f625af642b1fd3bbd4d Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 24 Jan 2025 10:44:53 +0100 Subject: [PATCH 0841/3587] take all profiles for populating trusted publishers (#238638) --- .../browser/extensions.contribution.ts | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index c420bb92b681..75f95e903ebe 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -78,6 +78,7 @@ import { ProgressLocation } from '../../../../platform/progress/common/progress. import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IConfigurationMigrationRegistry, Extensions as ConfigurationMigrationExtensions } from '../../../common/configuration.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService, InstantiationType.Eager /* Auto updates extensions */); @@ -1932,31 +1933,34 @@ class ExtensionStorageCleaner implements IWorkbenchContribution { class TrustedPublishersInitializer implements IWorkbenchContribution { constructor( @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, @IProductService productService: IProductService, @IStorageService storageService: IStorageService, ) { const trustedPublishersInitStatusKey = 'trusted-publishers-initialized'; if (!storageService.get(trustedPublishersInitStatusKey, StorageScope.APPLICATION)) { - extensionManagementService.getInstalled(ExtensionType.User) - .then(async extensions => { - const trustedPublishers = new Set(); - for (const extension of extensions) { - if (!extension.publisherId) { - continue; + for (const profile of userDataProfilesService.profiles) { + extensionManagementService.getInstalled(ExtensionType.User, profile.extensionsResource) + .then(async extensions => { + const trustedPublishers = new Set(); + for (const extension of extensions) { + if (!extension.publisherId) { + continue; + } + const publisher = extension.manifest.publisher.toLowerCase(); + if (productService.trustedExtensionPublishers?.includes(publisher) + || (extension.publisherDisplayName && productService.trustedExtensionPublishers?.includes(extension.publisherDisplayName.toLowerCase()))) { + continue; + } + trustedPublishers.add(publisher); } - const publisher = extension.manifest.publisher.toLowerCase(); - if (productService.trustedExtensionPublishers?.includes(publisher) - || (extension.publisherDisplayName && productService.trustedExtensionPublishers?.includes(extension.publisherDisplayName.toLowerCase()))) { - continue; + if (trustedPublishers.size) { + await allowedExtensionsService.trustPublishers(...trustedPublishers); } - trustedPublishers.add(publisher); - } - if (trustedPublishers.size) { - await allowedExtensionsService.trustPublishers(...trustedPublishers); - } - storageService.store(trustedPublishersInitStatusKey, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE); - }); + storageService.store(trustedPublishersInitStatusKey, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE); + }); + } } } } From e88b4e73cbd7bfef60472458018f02f2092173e1 Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Fri, 24 Jan 2025 11:33:32 +0100 Subject: [PATCH 0842/3587] Fix multiple issues with edits on the last line (#238647) * Fix multiple issues with edits on the last line * Fixes for rendering an insertion after the last line --- src/vs/editor/browser/observableCodeEditor.ts | 19 ++++++++++++++++--- .../contentWidgets/contentWidgets.ts | 4 ++-- src/vs/editor/common/core/textLength.ts | 2 +- .../view/inlineEdits/sideBySideDiff.ts | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts index ac1ab182fa44..7e9d3bde0c14 100644 --- a/src/vs/editor/browser/observableCodeEditor.ts +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -286,7 +286,14 @@ export class ObservableCodeEditor extends Disposable { start.read(reader); end.read(reader); const range = lineRange.read(reader); - const s = this.editor.getTopForLineNumber(range.startLineNumber) - this.scrollTop.read(reader); + const lineCount = this.model.read(reader)?.getLineCount(); + const s = ( + (typeof lineCount !== 'undefined' && range.startLineNumber > lineCount + ? this.editor.getBottomForLineNumber(lineCount) + : this.editor.getTopForLineNumber(range.startLineNumber) + ) + - this.scrollTop.read(reader) + ); const e = range.isEmpty ? s : (this.editor.getBottomForLineNumber(range.endLineNumberExclusive - 1) - this.scrollTop.read(reader)); return new OffsetRange(s, e); }); @@ -304,8 +311,14 @@ export class ObservableCodeEditor extends Disposable { }, getId: () => contentWidgetId, allowEditorOverflow: false, - afterRender(position, coordinate) { - result.set(coordinate ? new Point(coordinate.left, coordinate.top) : null, undefined); + afterRender: (position, coordinate) => { + const model = this._model.get(); + if (model && pos && pos.lineNumber > model.getLineCount()) { + // the position is after the last line + result.set(new Point(0, this.editor.getBottomForLineNumber(model.getLineCount()) - this.scrollTop.get()), undefined); + } else { + result.set(coordinate ? new Point(coordinate.left, coordinate.top) : null, undefined); + } }, }; this.editor.addContentWidget(w); diff --git a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index dcce94f49636..6ae696f90dd6 100644 --- a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -5,7 +5,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { FastDomNode, createFastDomNode } from '../../../../base/browser/fastDomNode.js'; -import { ContentWidgetPositionPreference, IContentWidget } from '../../editorBrowser.js'; +import { ContentWidgetPositionPreference, IContentWidget, IContentWidgetRenderedCoordinate } from '../../editorBrowser.js'; import { PartFingerprint, PartFingerprints, ViewPart } from '../../view/viewPart.js'; import { RenderingContext, RestrictedRenderingContext } from '../../view/renderingContext.js'; import { ViewContext } from '../../../common/viewModel/viewContext.js'; @@ -600,7 +600,7 @@ class PositionPair { ) { } } -class Coordinate { +class Coordinate implements IContentWidgetRenderedCoordinate { _coordinateBrand: void = undefined; constructor( diff --git a/src/vs/editor/common/core/textLength.ts b/src/vs/editor/common/core/textLength.ts index 76f3ee378bdd..a1f2d8e35c8e 100644 --- a/src/vs/editor/common/core/textLength.ts +++ b/src/vs/editor/common/core/textLength.ts @@ -115,7 +115,7 @@ export class TextLength { } public toLineRange(): LineRange { - return LineRange.ofLength(1, this.lineCount); + return LineRange.ofLength(1, this.lineCount + 1); } public addToPosition(position: Position): Position { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index 5ba16a1ef5d0..46f1cc5d282b 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -452,7 +452,7 @@ export class InlineEditsSideBySideDiff extends Disposable implements IInlineEdit } const selectionTop = this._originalVerticalStartPosition.read(reader) ?? this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); - const selectionBottom = this._originalVerticalEndPosition.read(reader) ?? this._editor.getTopForLineNumber(range.endLineNumberExclusive) - this._editorObs.scrollTop.read(reader); + const selectionBottom = this._originalVerticalEndPosition.read(reader) ?? this._editor.getBottomForLineNumber(range.endLineNumberExclusive - 1) - this._editorObs.scrollTop.read(reader); const codeLeft = editorLayout.contentLeft; From 88ce1c8de718d6ef5d350f5f8298d6cc462fbad1 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 24 Jan 2025 11:36:13 +0100 Subject: [PATCH 0843/3587] SCM - accessibility help dialog (#238645) * Initial stub for SCMAccessibilityHelp and SCMAccessibilityHelpContentProvider * Add content --- .../accessibility/browser/accessibleView.ts | 1 + .../browser/accessibilityConfiguration.ts | 1 + .../contrib/scm/browser/scm.contribution.ts | 4 + .../scm/browser/scmAccessibilityHelp.ts | 136 ++++++++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 src/vs/workbench/contrib/scm/browser/scmAccessibilityHelp.ts diff --git a/src/vs/platform/accessibility/browser/accessibleView.ts b/src/vs/platform/accessibility/browser/accessibleView.ts index da30ffe3a258..b438dedc42e3 100644 --- a/src/vs/platform/accessibility/browser/accessibleView.ts +++ b/src/vs/platform/accessibility/browser/accessibleView.ts @@ -35,6 +35,7 @@ export const enum AccessibleViewProviderId { ReplHelp = 'replHelp', RunAndDebug = 'runAndDebug', Walkthrough = 'walkthrough', + SourceControl = 'scm' } export const enum AccessibleViewType { diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index fc060c2832e6..3edbf376c5a1 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -63,6 +63,7 @@ export const enum AccessibilityVerbositySettingId { DiffEditorActive = 'accessibility.verbosity.diffEditorActive', Debug = 'accessibility.verbosity.debug', Walkthrough = 'accessibility.verbosity.walkthrough', + SourceControl = 'accessibility.verbosity.scm' } const baseVerbosityProperty: IConfigurationPropertySchema = { diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 7f8e9c71a099..c9811c5d89f1 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -42,6 +42,8 @@ import { QuickDiffModelService, IQuickDiffModelService } from './quickDiffModel. import { QuickDiffEditorController } from './quickDiffWidget.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; import { RemoteNameContext } from '../../../common/contextkeys.js'; +import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { SCMAccessibilityHelp } from './scmAccessibilityHelp.js'; ModesRegistry.registerLanguage({ id: 'scminput', @@ -605,3 +607,5 @@ registerSingleton(ISCMService, SCMService, InstantiationType.Delayed); registerSingleton(ISCMViewService, SCMViewService, InstantiationType.Delayed); registerSingleton(IQuickDiffService, QuickDiffService, InstantiationType.Delayed); registerSingleton(IQuickDiffModelService, QuickDiffModelService, InstantiationType.Delayed); + +AccessibleViewRegistry.register(new SCMAccessibilityHelp()); diff --git a/src/vs/workbench/contrib/scm/browser/scmAccessibilityHelp.ts b/src/vs/workbench/contrib/scm/browser/scmAccessibilityHelp.ts new file mode 100644 index 000000000000..73e17ce8c49c --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/scmAccessibilityHelp.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { localize } from '../../../../nls.js'; +import { AccessibleViewType, AccessibleContentProvider, IAccessibleViewContentProvider, AccessibleViewProviderId } from '../../../../platform/accessibility/browser/accessibleView.js'; +import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { FocusedViewContext, SidebarFocusContext } from '../../../common/contextkeys.js'; +import { IViewsService } from '../../../services/views/common/viewsService.js'; +import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; +import { HISTORY_VIEW_PANE_ID, ISCMViewService, REPOSITORIES_VIEW_PANE_ID, VIEW_PANE_ID } from '../common/scm.js'; + +export class SCMAccessibilityHelp implements IAccessibleViewImplentation { + readonly name = 'scm'; + readonly type = AccessibleViewType.Help; + readonly priority = 100; + readonly when = ContextKeyExpr.or( + ContextKeyExpr.and(ContextKeyExpr.equals('activeViewlet', 'workbench.view.scm'), SidebarFocusContext), + ContextKeyExpr.equals(FocusedViewContext.key, REPOSITORIES_VIEW_PANE_ID), + ContextKeyExpr.equals(FocusedViewContext.key, VIEW_PANE_ID), + ContextKeyExpr.equals(FocusedViewContext.key, HISTORY_VIEW_PANE_ID) + ); + + getProvider(accessor: ServicesAccessor): AccessibleContentProvider { + const commandService = accessor.get(ICommandService); + const scmViewService = accessor.get(ISCMViewService); + const viewsService = accessor.get(IViewsService); + + return new SCMAccessibilityHelpContentProvider(commandService, scmViewService, viewsService); + } +} + +class SCMAccessibilityHelpContentProvider extends Disposable implements IAccessibleViewContentProvider { + readonly id = AccessibleViewProviderId.SourceControl; + readonly verbositySettingKey = AccessibilityVerbositySettingId.SourceControl; + readonly options = { type: AccessibleViewType.Help }; + + private _focusedView: string | undefined; + + constructor( + @ICommandService private readonly _commandService: ICommandService, + @ISCMViewService private readonly _scmViewService: ISCMViewService, + @IViewsService private readonly _viewsService: IViewsService + ) { + super(); + this._focusedView = this._viewsService.getFocusedViewName(); + } + + onClose(): void { + switch (this._focusedView) { + case 'Source Control': + this._commandService.executeCommand('workbench.scm'); + break; + case 'Source Control Repositories': + this._commandService.executeCommand('workbench.scm.repositories'); + break; + case 'Source Control Graph': + this._commandService.executeCommand('workbench.scm.history'); + break; + default: + this._commandService.executeCommand('workbench.view.scm'); + } + } + + provideContent(): string { + const content: string[] = []; + + // Active Repository State + if (this._scmViewService.visibleRepositories.length > 1) { + const repositoryList = this._scmViewService.visibleRepositories.map(r => r.provider.name).join(', '); + content.push(localize('state-msg1', "Visible repositories: {0}", repositoryList)); + } + + const activeRepository = this._scmViewService.activeRepository.get(); + if (activeRepository) { + content.push(localize('state-msg2', "Repository: {0}", activeRepository.provider.name)); + + // History Item Reference + const currentHistoryItemRef = activeRepository.provider.historyProvider.get()?.historyItemRef.get(); + if (currentHistoryItemRef) { + content.push(localize('state-msg3', "History item reference: {0}", currentHistoryItemRef.name)); + } + + // Commit Message + if (activeRepository.input.visible && activeRepository.input.enabled && activeRepository.input.value !== '') { + content.push(localize('state-msg4', "Commit message: {0}", activeRepository.input.value)); + } + + // Action Button + const actionButton = activeRepository.provider.actionButton.get(); + if (actionButton) { + const label = actionButton.command.tooltip ?? actionButton.command.title; + const enablementLabel = actionButton.enabled ? localize('enabled', "enabled") : localize('disabled', "disabled"); + content.push(localize('state-msg5', "Action button: {0}, {1}", label, enablementLabel)); + } + + // Resource Groups + const resourceGroups: string[] = []; + for (const resourceGroup of activeRepository.provider.groups) { + resourceGroups.push(`${resourceGroup.label} (${resourceGroup.resources.length} resource(s))`); + } + + activeRepository.provider.groups.map(g => g.label).join(', '); + content.push(localize('state-msg6', "Resource groups: {0}", resourceGroups.join(', '))); + } + + // Source Control Repositories + content.push(localize('scm-repositories-msg1', "Use the \"Source Control: Focus on Source Control Repositories View\" command to open the Source Control Repositories view.")); + content.push(localize('scm-repositories-msg2', "The Source Control Repositories view lists all repositories from the workspace and is only shown when the workspace contains more than one repository.")); + content.push(localize('scm-repositories-msg3', "Once the Source Control Repositories view is opened you can:")); + content.push(localize('scm-repositories-msg4', " - Use the up/down arrow keys to navigate the list of repositories.")); + content.push(localize('scm-repositories-msg5', " - Use the Enter or Space keys to select a repository.")); + content.push(localize('scm-repositories-msg6', " - Use Shift + up/down keys to select multiple repositories.")); + + // Source Control + content.push(localize('scm-msg1', "Use the \"Source Control: Focus on Source Control View\" command to open the Source Control view.")); + content.push(localize('scm-msg2', "The Source Control view displays the resource groups and resources of the repository. If the workspace contains more than one repository it will list the resource groups and resources of the repositories selected in the Source Control Repositories view.")); + content.push(localize('scm-msg3', "Once the Source Control view is opened you can:")); + content.push(localize('scm-msg4', " - Use the up/down arrow keys to navigate the list of repositories, resource groups and resources.")); + content.push(localize('scm-msg5', " - Use the Space key to expand or collapse a resource group.")); + + // Source Control Graph + content.push(localize('scm-graph-msg1', "Use the \"Source Control: Focus on Source Control Graph View\" command to open the Source Control Graph view.")); + content.push(localize('scm-graph-msg2', "The Source Control Graph view displays a graph history items of the repository. If the workspace contains more than one repository it will list the history items of the active repository.")); + content.push(localize('scm-graph-msg3', "Once the Source Control Graph view is opened you can:")); + content.push(localize('scm-graph-msg4', " - Use the up/down arrow keys to navigate the list of history items.")); + content.push(localize('scm-graph-msg5', " - Use the Space key to open the history item details in the multi-file diff editor.")); + + return content.join('\n'); + } +} From faad1212c77d162e24514292888a99f5f9a6be64 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 24 Jan 2025 11:45:44 +0100 Subject: [PATCH 0844/3587] inline chat fixes (#238649) * workaround weird CSS padding * fix strings#rcut * tweak cancel and toggle keybindings --- src/vs/base/common/strings.ts | 19 +++++++------- src/vs/base/test/common/strings.test.ts | 18 +++++++++++++ .../browser/inlineChatController2.ts | 25 ++++++++++++------- .../inlineChat/browser/media/inlineChat.css | 2 +- 4 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index c4913e9bfa98..44dc32027ccb 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -786,21 +786,20 @@ export function rcut(text: string, n: number, suffix = ''): string { return trimmed; } - const re = /\b/g; - let lastWordBreak = trimmed.length; - - while (re.test(trimmed)) { - if (trimmed.length - re.lastIndex > n) { - lastWordBreak = re.lastIndex; + const parts = text.split(/\b/); + let result = ''; + for (const part of parts) { + if (result.length > 0 && result.length + part.length > n) { + break; } - re.lastIndex += 1; + result += part; } - if (lastWordBreak === trimmed.length) { - return trimmed; + if (result === trimmed) { + return result; } - return (trimmed.substring(0, lastWordBreak) + suffix).trimEnd(); + return result.trim().replace(/b$/, '') + suffix; } // Escape codes, compiled from https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ diff --git a/src/vs/base/test/common/strings.test.ts b/src/vs/base/test/common/strings.test.ts index b075808571ef..7c567df4b6f1 100644 --- a/src/vs/base/test/common/strings.test.ts +++ b/src/vs/base/test/common/strings.test.ts @@ -155,6 +155,24 @@ suite('Strings', () => { assert.strictEqual(strings.lcut('............a', 10, '…'), '............a'); }); + suite('rcut', () => { + test('basic truncation', () => { + assert.strictEqual(strings.rcut('foo bar', 0), 'foo'); + assert.strictEqual(strings.rcut('foo bar', 1), 'foo'); + assert.strictEqual(strings.rcut('foo bar', 4), 'foo'); + assert.strictEqual(strings.rcut('foo bar', 7), 'foo bar'); + assert.strictEqual(strings.rcut('test string 0.1.2.3', 3), 'test'); + }); + + test('truncation with suffix', () => { + assert.strictEqual(strings.rcut('foo bar', 0, '…'), 'foo…'); + assert.strictEqual(strings.rcut('foo bar', 1, '…'), 'foo…'); + assert.strictEqual(strings.rcut('foo bar', 4, '…'), 'foo…'); + assert.strictEqual(strings.rcut('foo bar', 7, '…'), 'foo bar'); + assert.strictEqual(strings.rcut('test string 0.1.2.3', 3, '…'), 'test…'); + }); + }); + test('escape', () => { assert.strictEqual(strings.escape(''), ''); assert.strictEqual(strings.escape('foo'), 'foo'); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts index b444ddebd81b..e8c36b2084d4 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts @@ -36,7 +36,7 @@ import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSess import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; -export const CTX_HAS_SESSION = new RawContextKey('inlineChatHasSession', undefined, localize('chat.hasInlineChatSession', "The current editor has an active inline chat session")); +export const CTX_HAS_SESSION = new RawContextKey('inlineChatHasSession', undefined, localize('chat.hasInlineChatSession', "The current editor has an active inline chat session")); export class InlineChatController2 implements IEditorContribution { @@ -103,6 +103,8 @@ export class InlineChatController2 implements IEditorContribution { this._editor ); + zone.domNode.classList.add('inline-chat-2'); + const overlay = ChatEditorOverlayController.get(_editor)!; const editorObs = observableCodeEditor(_editor); @@ -117,9 +119,16 @@ export class InlineChatController2 implements IEditorContribution { }); - this._store.add(autorun(r => { + this._store.add(autorunWithStore((r, store) => { const session = sessionObs.read(r); - ctxHasSession.set(Boolean(session)); + + if (!session) { + ctxHasSession.set(undefined); + } else { + const checkRequests = () => ctxHasSession.set(session.chatModel.getRequests().length === 0 ? 'empty' : 'active'); + store.add(session.chatModel.onDidChange(checkRequests)); + checkRequests(); + } })); const visibleSessionObs = observableValue(this, undefined); @@ -166,9 +175,8 @@ export class InlineChatController2 implements IEditorContribution { zone.widget.setChatModel(session.chatModel); if (!zone.position) { zone.show(session.initialPosition); - } else { - zone.reveal(zone.position); } + zone.reveal(zone.position!); zone.widget.focus(); session.editingSession.getEntry(session.uri)?.autoAcceptController.get()?.cancel(); } @@ -303,12 +311,11 @@ export class StopSessionAction2 extends AbstractInlineChatAction { id: 'inlineChat2.stop', title: localize2('stop', "Stop"), f1: true, - precondition: ContextKeyExpr.and( - CTX_HAS_SESSION, CTX_INLINE_CHAT_VISIBLE - ), + precondition: ContextKeyExpr.and(CTX_HAS_SESSION.isEqualTo('empty'), CTX_INLINE_CHAT_VISIBLE), keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyCode.Escape + primary: KeyCode.Escape, + secondary: [KeyMod.CtrlCmd | KeyCode.KeyI] }, }); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index 7ecb62c8cbcc..327ae54548d7 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -72,7 +72,7 @@ outline: none; } -.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part { +.monaco-workbench .zone-widget.inline-chat-widget:not(.inline-chat-2) .inline-chat .chat-widget .interactive-session .interactive-input-part { padding: 4px 0 0 0; } From 3ba865e2cf2ccf103b4bfe8706953bec0f33e0bf Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Fri, 24 Jan 2025 11:50:19 +0100 Subject: [PATCH 0845/3587] Rename the action (#238650) --- .../contrib/inlineCompletions/browser/controller/commands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts index 2fe48fc4eb9b..12fd45acf161 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts @@ -84,7 +84,7 @@ export class ExplicitTriggerInlineEditAction extends EditorAction { constructor() { super({ id: 'editor.action.inlineSuggest.triggerInlineEditExplicit', - label: nls.localize2('action.inlineSuggest.trigger.explicitInlineEdit', "Trigger Inline Edit"), + label: nls.localize2('action.inlineSuggest.trigger.explicitInlineEdit', "Trigger Next Edit Suggestion"), precondition: EditorContextKeys.writable, }); } From 5089552cb1239887ea0679b0da45706ed07b4c62 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 24 Jan 2025 11:53:49 +0100 Subject: [PATCH 0846/3587] fixes https://github.com/microsoft/vscode-copilot/issues/11860 (#238651) --- src/vs/workbench/contrib/chat/browser/chatEditorActions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts index 0c6b0f4dee73..48d0cd3fb885 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts @@ -131,7 +131,7 @@ abstract class AcceptDiscardAction extends Action2 { ? localize2('accept2', 'Accept') : localize2('discard2', 'Discard'), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey), + precondition: ContextKeyExpr.and(ctxHasEditorModification, hasUndecidedChatEditingResourceContextKey), icon: accept ? Codicon.check : Codicon.discard, @@ -209,7 +209,7 @@ class RejectHunkAction extends EditorAction2 { id: 'chatEditor.action.undoHunk', title: localize2('undo', 'Discard this Change'), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), + precondition: ContextKeyExpr.and(ctxHasEditorModification, ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), icon: Codicon.discard, f1: true, keybinding: { @@ -235,7 +235,7 @@ class AcceptHunkAction extends EditorAction2 { id: 'chatEditor.action.acceptHunk', title: localize2('acceptHunk', 'Accept this Change'), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), + precondition: ContextKeyExpr.and(ctxHasEditorModification, ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), icon: Codicon.check, f1: true, keybinding: { From 6e740ceccf6b0b18b966745ff9c38c75c76664b0 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 24 Jan 2025 11:58:53 +0100 Subject: [PATCH 0847/3587] Accessibility - renamed IAccessibleViewImplentation to IAccessibleViewImplementation (#238652) --- .../editor/contrib/hover/browser/hoverAccessibleViews.ts | 8 ++++---- .../browser/inlineCompletionsAccessibleView.ts | 4 ++-- .../accessibility/browser/accessibleViewRegistry.ts | 8 ++++---- .../parts/notifications/notificationAccessibleView.ts | 4 ++-- .../contrib/chat/browser/actions/chatAccessibilityHelp.ts | 8 ++++---- .../contrib/chat/browser/chatResponseAccessibleView.ts | 4 ++-- .../codeEditor/browser/diffEditorAccessibilityHelp.ts | 4 ++-- .../contrib/comments/browser/commentsAccessibility.ts | 4 ++-- .../contrib/comments/browser/commentsAccessibleView.ts | 6 +++--- .../contrib/debug/browser/replAccessibilityHelp.ts | 4 ++-- .../workbench/contrib/debug/browser/replAccessibleView.ts | 4 ++-- .../contrib/debug/browser/runAndDebugAccessibilityHelp.ts | 4 ++-- .../inlineChat/browser/inlineChatAccessibilityHelp.ts | 4 ++-- .../inlineChat/browser/inlineChatAccessibleView.ts | 4 ++-- .../contrib/notebook/browser/notebookAccessibilityHelp.ts | 4 ++-- .../contrib/notebook/browser/notebookAccessibleView.ts | 4 ++-- .../contrib/notebook/browser/replEditorAccessibleView.ts | 4 ++-- .../replNotebook/browser/replEditorAccessibilityHelp.ts | 6 +++--- .../workbench/contrib/scm/browser/scmAccessibilityHelp.ts | 4 ++-- .../chat/browser/terminalChatAccessibilityHelp.ts | 4 ++-- .../chat/browser/terminalChatAccessibleView.ts | 4 ++-- .../browser/gettingStartedAccessibleView.ts | 4 ++-- 22 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts index ffce8901c85b..0ffff8a229b7 100644 --- a/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts +++ b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts @@ -6,7 +6,7 @@ import { localize } from '../../../../nls.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; import { ContentHoverController } from './contentHoverController.js'; import { AccessibleViewType, AccessibleViewProviderId, AccessibleContentProvider, IAccessibleViewContentProvider, IAccessibleViewOptions } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; @@ -27,7 +27,7 @@ namespace HoverAccessibilityHelpNLS { export const decreaseVerbosity = localize('decreaseVerbosity', '- The focused hover part verbosity level can be decreased with the Decrease Hover Verbosity command.', ``); } -export class HoverAccessibleView implements IAccessibleViewImplentation { +export class HoverAccessibleView implements IAccessibleViewImplementation { public readonly type = AccessibleViewType.View; public readonly priority = 95; @@ -49,7 +49,7 @@ export class HoverAccessibleView implements IAccessibleViewImplentation { } } -export class HoverAccessibilityHelp implements IAccessibleViewImplentation { +export class HoverAccessibilityHelp implements IAccessibleViewImplementation { public readonly priority = 100; public readonly name = 'hover'; @@ -228,7 +228,7 @@ export class HoverAccessibleViewProvider extends BaseHoverAccessibleViewProvider } } -export class ExtHoverAccessibleView implements IAccessibleViewImplentation { +export class ExtHoverAccessibleView implements IAccessibleViewImplementation { public readonly type = AccessibleViewType.View; public readonly priority = 90; public readonly name = 'extension-hover'; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts index 20b6cc6abae1..0a4e864d2c67 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts @@ -9,13 +9,13 @@ import { ICodeEditorService } from '../../../browser/services/codeEditorService. import { InlineCompletionContextKeys } from './controller/inlineCompletionContextKeys.js'; import { InlineCompletionsController } from './controller/inlineCompletionsController.js'; import { AccessibleViewType, AccessibleViewProviderId, IAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { InlineCompletionsModel } from './model/inlineCompletionsModel.js'; -export class InlineCompletionsAccessibleView implements IAccessibleViewImplentation { +export class InlineCompletionsAccessibleView implements IAccessibleViewImplementation { readonly type = AccessibleViewType.View; readonly priority = 95; readonly name = 'inline-completions'; diff --git a/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts b/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts index fd643b97def3..2f98c85f893d 100644 --- a/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts +++ b/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts @@ -8,7 +8,7 @@ import { AccessibleViewType, AccessibleContentProvider, ExtensionContentProvider import { ContextKeyExpression } from '../../contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../instantiation/common/instantiation.js'; -export interface IAccessibleViewImplentation { +export interface IAccessibleViewImplementation { type: AccessibleViewType; priority: number; name: string; @@ -20,9 +20,9 @@ export interface IAccessibleViewImplentation { } export const AccessibleViewRegistry = new class AccessibleViewRegistry { - _implementations: IAccessibleViewImplentation[] = []; + _implementations: IAccessibleViewImplementation[] = []; - register(implementation: IAccessibleViewImplentation): IDisposable { + register(implementation: IAccessibleViewImplementation): IDisposable { this._implementations.push(implementation); return { dispose: () => { @@ -34,7 +34,7 @@ export const AccessibleViewRegistry = new class AccessibleViewRegistry { }; } - getImplementations(): IAccessibleViewImplentation[] { + getImplementations(): IAccessibleViewImplementation[] { return this._implementations; } }; diff --git a/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts b/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts index 0e206216e580..723c97a134c9 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts @@ -8,7 +8,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; import { IAccessibleViewService, AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { IAccessibilitySignalService, AccessibilitySignal } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; @@ -17,7 +17,7 @@ import { getNotificationFromContext } from './notificationsCommands.js'; import { NotificationFocusedContext } from '../../../common/contextkeys.js'; import { INotificationViewItem } from '../../../common/notifications.js'; -export class NotificationAccessibleView implements IAccessibleViewImplentation { +export class NotificationAccessibleView implements IAccessibleViewImplementation { readonly priority = 90; readonly name = 'notifications'; readonly when = NotificationFocusedContext; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index 098054522798..5ed43c1ba433 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -9,7 +9,7 @@ import { ICodeEditorService } from '../../../../../editor/browser/services/codeE import { AccessibleDiffViewerNext } from '../../../../../editor/browser/widget/diffEditor/commands.js'; import { localize } from '../../../../../nls.js'; import { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType } from '../../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { ActiveAuxiliaryContext } from '../../../../common/contextkeys.js'; @@ -19,7 +19,7 @@ import { ChatAgentLocation } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatWidgetService } from '../chat.js'; -export class PanelChatAccessibilityHelp implements IAccessibleViewImplentation { +export class PanelChatAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 107; readonly name = 'panelChat'; readonly type = AccessibleViewType.Help; @@ -30,7 +30,7 @@ export class PanelChatAccessibilityHelp implements IAccessibleViewImplentation { } } -export class QuickChatAccessibilityHelp implements IAccessibleViewImplentation { +export class QuickChatAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 107; readonly name = 'quickChat'; readonly type = AccessibleViewType.Help; @@ -41,7 +41,7 @@ export class QuickChatAccessibilityHelp implements IAccessibleViewImplentation { } } -export class EditsChatAccessibilityHelp implements IAccessibleViewImplentation { +export class EditsChatAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 119; readonly name = 'editsView'; readonly type = AccessibleViewType.Help; diff --git a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts index 75f75066e1bd..5c96d139e1a8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts @@ -7,14 +7,14 @@ import { renderMarkdownAsPlaintext } from '../../../../base/browser/markdownRend import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { isResponseVM } from '../common/chatViewModel.js'; import { ChatTreeItem, IChatWidget, IChatWidgetService } from './chat.js'; -export class ChatResponseAccessibleView implements IAccessibleViewImplentation { +export class ChatResponseAccessibleView implements IAccessibleViewImplementation { readonly priority = 100; readonly name = 'panelChat'; readonly type = AccessibleViewType.View; diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts index f73fd0bd85c4..f2262f6a4980 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts @@ -8,7 +8,7 @@ import { AccessibleDiffViewerNext, AccessibleDiffViewerPrev } from '../../../../ import { DiffEditorWidget } from '../../../../editor/browser/widget/diffEditor/diffEditorWidget.js'; import { localize } from '../../../../nls.js'; import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyEqualsExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; @@ -16,7 +16,7 @@ import { AccessibilityVerbositySettingId } from '../../accessibility/browser/acc import { getCommentCommandInfo } from '../../accessibility/browser/editorAccessibilityHelp.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -export class DiffEditorAccessibilityHelp implements IAccessibleViewImplentation { +export class DiffEditorAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 105; readonly name = 'diff-editor'; readonly when = ContextKeyEqualsExpr.create('isInDiffEditor', true); diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts index 4a52b4036ff3..89a885968f01 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts @@ -12,7 +12,7 @@ import { AccessibilityVerbositySettingId } from '../../accessibility/browser/acc import { CommentCommandId } from '../common/commentCommandIds.js'; import { ToggleTabFocusModeAction } from '../../../../editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode.js'; import { IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; @@ -44,7 +44,7 @@ export class CommentsAccessibilityHelpProvider extends Disposable implements IAc } } -export class CommentsAccessibilityHelp implements IAccessibleViewImplentation { +export class CommentsAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 110; readonly name = 'comments'; readonly type = AccessibleViewType.Help; diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts index 655618ac92c7..c18066cbe58e 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts @@ -7,7 +7,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { MarshalledId } from '../../../../base/common/marshallingIds.js'; import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { IMenuService } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; @@ -24,7 +24,7 @@ import { URI } from '../../../../base/common/uri.js'; import { CommentThread, Comment } from '../../../../editor/common/languages.js'; import { IRange } from '../../../../editor/common/core/range.js'; -export class CommentsAccessibleView extends Disposable implements IAccessibleViewImplentation { +export class CommentsAccessibleView extends Disposable implements IAccessibleViewImplementation { readonly priority = 90; readonly name = 'comment'; readonly when = CONTEXT_KEY_COMMENT_FOCUSED; @@ -50,7 +50,7 @@ export class CommentsAccessibleView extends Disposable implements IAccessibleVie } -export class CommentThreadAccessibleView extends Disposable implements IAccessibleViewImplentation { +export class CommentThreadAccessibleView extends Disposable implements IAccessibleViewImplementation { readonly priority = 85; readonly name = 'commentThread'; readonly when = CommentContextKeys.commentFocused; diff --git a/src/vs/workbench/contrib/debug/browser/replAccessibilityHelp.ts b/src/vs/workbench/contrib/debug/browser/replAccessibilityHelp.ts index 299374e3154b..3b994c5e514f 100644 --- a/src/vs/workbench/contrib/debug/browser/replAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/debug/browser/replAccessibilityHelp.ts @@ -5,7 +5,7 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { getReplView, Repl } from './repl.js'; @@ -13,7 +13,7 @@ import { IViewsService } from '../../../services/views/common/viewsService.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; import { localize } from '../../../../nls.js'; -export class ReplAccessibilityHelp implements IAccessibleViewImplentation { +export class ReplAccessibilityHelp implements IAccessibleViewImplementation { priority = 120; name = 'replHelp'; when = ContextKeyExpr.equals('focusedView', 'workbench.panel.repl.view'); diff --git a/src/vs/workbench/contrib/debug/browser/replAccessibleView.ts b/src/vs/workbench/contrib/debug/browser/replAccessibleView.ts index 43ec21a1cd3f..638dd70b513d 100644 --- a/src/vs/workbench/contrib/debug/browser/replAccessibleView.ts +++ b/src/vs/workbench/contrib/debug/browser/replAccessibleView.ts @@ -6,7 +6,7 @@ import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider, IAccessibleViewService } from '../../../../platform/accessibility/browser/accessibleView.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; import { IReplElement } from '../common/debug.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { getReplView, Repl } from './repl.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; @@ -15,7 +15,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { Position } from '../../../../editor/common/core/position.js'; -export class ReplAccessibleView implements IAccessibleViewImplentation { +export class ReplAccessibleView implements IAccessibleViewImplementation { priority = 70; name = 'debugConsole'; when = ContextKeyExpr.equals('focusedView', 'workbench.panel.repl.view'); diff --git a/src/vs/workbench/contrib/debug/browser/runAndDebugAccessibilityHelp.ts b/src/vs/workbench/contrib/debug/browser/runAndDebugAccessibilityHelp.ts index 90920dc95c03..fcdf564b93c0 100644 --- a/src/vs/workbench/contrib/debug/browser/runAndDebugAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/debug/browser/runAndDebugAccessibilityHelp.ts @@ -6,7 +6,7 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; @@ -17,7 +17,7 @@ import { AccessibilityHelpNLS } from '../../../../editor/common/standaloneString import { FocusedViewContext, SidebarFocusContext } from '../../../common/contextkeys.js'; import { BREAKPOINTS_VIEW_ID, CALLSTACK_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, VARIABLES_VIEW_ID, WATCH_VIEW_ID } from '../common/debug.js'; -export class RunAndDebugAccessibilityHelp implements IAccessibleViewImplentation { +export class RunAndDebugAccessibilityHelp implements IAccessibleViewImplementation { priority = 120; name = 'runAndDebugHelp'; when = ContextKeyExpr.or( diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts index 338697c4b7bd..bce9724997ca 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts @@ -6,13 +6,13 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { AccessibleViewType } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { getChatAccessibilityHelpProvider } from '../../chat/browser/actions/chatAccessibilityHelp.js'; import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; import { CTX_INLINE_CHAT_RESPONSE_FOCUSED } from '../common/inlineChat.js'; -export class InlineChatAccessibilityHelp implements IAccessibleViewImplentation { +export class InlineChatAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 106; readonly name = 'inlineChat'; readonly type = AccessibleViewType.Help; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts index b1d46e25c5a7..3f733e337486 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts @@ -9,12 +9,12 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { renderMarkdownAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; -export class InlineChatAccessibleView implements IAccessibleViewImplentation { +export class InlineChatAccessibleView implements IAccessibleViewImplementation { readonly priority = 100; readonly name = 'inlineChat'; readonly when = ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts index 9e2118bcb10b..45d8a96ba202 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { IS_COMPOSITE_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED } from '../common/notebookContextKeys.js'; import { localize } from '../../../../nls.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; @@ -14,7 +14,7 @@ import { IVisibleEditorPane } from '../../../common/editor.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -export class NotebookAccessibilityHelp implements IAccessibleViewImplentation { +export class NotebookAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 105; readonly name = 'notebook'; readonly when = ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, IS_COMPOSITE_NOTEBOOK.negate()); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts index fc8d932181a2..56e6d347e4e2 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; @@ -13,7 +13,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { InputFocusedContext } from '../../../../platform/contextkey/common/contextkeys.js'; import { getAllOutputsText } from './viewModel/cellOutputTextHelper.js'; -export class NotebookAccessibleView implements IAccessibleViewImplentation { +export class NotebookAccessibleView implements IAccessibleViewImplementation { readonly priority = 100; readonly name = 'notebook'; readonly type = AccessibleViewType.View; diff --git a/src/vs/workbench/contrib/notebook/browser/replEditorAccessibleView.ts b/src/vs/workbench/contrib/notebook/browser/replEditorAccessibleView.ts index 98184cdfe3aa..7c55c37b9019 100644 --- a/src/vs/workbench/contrib/notebook/browser/replEditorAccessibleView.ts +++ b/src/vs/workbench/contrib/notebook/browser/replEditorAccessibleView.ts @@ -5,7 +5,7 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { AccessibleViewType, AccessibleContentProvider, AccessibleViewProviderId } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; @@ -16,7 +16,7 @@ import { getAllOutputsText } from './viewModel/cellOutputTextHelper.js'; /** * The REPL input is already accessible, so we can show a view for the most recent execution output. */ -export class ReplEditorAccessibleView implements IAccessibleViewImplentation { +export class ReplEditorAccessibleView implements IAccessibleViewImplementation { readonly priority = 100; readonly name = 'replEditorInput'; readonly type = AccessibleViewType.View; diff --git a/src/vs/workbench/contrib/replNotebook/browser/replEditorAccessibilityHelp.ts b/src/vs/workbench/contrib/replNotebook/browser/replEditorAccessibilityHelp.ts index b88e9f6d3554..e8090a53fe52 100644 --- a/src/vs/workbench/contrib/replNotebook/browser/replEditorAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/replNotebook/browser/replEditorAccessibilityHelp.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { localize } from '../../../../nls.js'; import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; @@ -11,7 +11,7 @@ import { AccessibilityVerbositySettingId } from '../../accessibility/browser/acc import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { IS_COMPOSITE_NOTEBOOK, NOTEBOOK_CELL_LIST_FOCUSED } from '../../notebook/common/notebookContextKeys.js'; -export class ReplEditorInputAccessibilityHelp implements IAccessibleViewImplentation { +export class ReplEditorInputAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 105; readonly name = 'REPL Editor Input'; readonly when = ContextKeyExpr.and(IS_COMPOSITE_NOTEBOOK, NOTEBOOK_CELL_LIST_FOCUSED.negate()); @@ -33,7 +33,7 @@ function getAccessibilityInputHelpText(): string { ].join('\n'); } -export class ReplEditorHistoryAccessibilityHelp implements IAccessibleViewImplentation { +export class ReplEditorHistoryAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 105; readonly name = 'REPL Editor History'; readonly when = ContextKeyExpr.and(IS_COMPOSITE_NOTEBOOK, NOTEBOOK_CELL_LIST_FOCUSED); diff --git a/src/vs/workbench/contrib/scm/browser/scmAccessibilityHelp.ts b/src/vs/workbench/contrib/scm/browser/scmAccessibilityHelp.ts index 73e17ce8c49c..3126416fcfd1 100644 --- a/src/vs/workbench/contrib/scm/browser/scmAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/scm/browser/scmAccessibilityHelp.ts @@ -6,7 +6,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { localize } from '../../../../nls.js'; import { AccessibleViewType, AccessibleContentProvider, IAccessibleViewContentProvider, AccessibleViewProviderId } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; @@ -15,7 +15,7 @@ import { IViewsService } from '../../../services/views/common/viewsService.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; import { HISTORY_VIEW_PANE_ID, ISCMViewService, REPOSITORIES_VIEW_PANE_ID, VIEW_PANE_ID } from '../common/scm.js'; -export class SCMAccessibilityHelp implements IAccessibleViewImplentation { +export class SCMAccessibilityHelp implements IAccessibleViewImplementation { readonly name = 'scm'; readonly type = AccessibleViewType.Help; readonly priority = 100; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts index 5017c56364db..1b62a8d1b930 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -5,7 +5,7 @@ import { localize } from '../../../../../nls.js'; import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from '../../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; @@ -13,7 +13,7 @@ import { ITerminalService } from '../../../terminal/browser/terminal.js'; import { TerminalChatCommandId, TerminalChatContextKeys } from './terminalChat.js'; import { TerminalChatController } from './terminalChatController.js'; -export class TerminalChatAccessibilityHelp implements IAccessibleViewImplentation { +export class TerminalChatAccessibilityHelp implements IAccessibleViewImplementation { readonly priority = 110; readonly name = 'terminalChat'; readonly when = TerminalChatContextKeys.focused; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts index d864fcbd0bf3..734ac7e62a66 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts @@ -7,13 +7,13 @@ import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { ITerminalService } from '../../../terminal/browser/terminal.js'; import { TerminalChatController } from './terminalChatController.js'; -import { IAccessibleViewImplentation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { IMenuService, MenuItemAction } from '../../../../../platform/actions/common/actions.js'; import { MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatContextKeys } from './terminalChat.js'; import { IAction } from '../../../../../base/common/actions.js'; -export class TerminalInlineChatAccessibleView implements IAccessibleViewImplentation { +export class TerminalInlineChatAccessibleView implements IAccessibleViewImplementation { readonly priority = 105; readonly name = 'terminalInlineChat'; readonly type = AccessibleViewType.View; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedAccessibleView.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedAccessibleView.ts index b7de7bccf06e..e1d9858430ba 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedAccessibleView.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedAccessibleView.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { AccessibleViewType, AccessibleContentProvider, ExtensionContentProvider, IAccessibleViewContentProvider, AccessibleViewProviderId } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplentation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { GettingStartedPage, inWelcomeContext } from './gettingStarted.js'; @@ -22,7 +22,7 @@ import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { Codicon } from '../../../../base/common/codicons.js'; -export class GettingStartedAccessibleView implements IAccessibleViewImplentation { +export class GettingStartedAccessibleView implements IAccessibleViewImplementation { readonly type = AccessibleViewType.View; readonly priority = 110; readonly name = 'walkthroughs'; From 9c22a5c402e990bd7d5fa8af9a50dc88f3aa9a2f Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 24 Jan 2025 12:09:15 +0100 Subject: [PATCH 0848/3587] Add code overlay option for inline suggestions (#238654) overlay with view zones --- src/vs/editor/common/config/editorOptions.ts | 11 ++- .../browser/view/inlineEdits/view.ts | 18 +++-- .../view/inlineEdits/wordReplacementView.ts | 76 ++++++++++++++++--- src/vs/monaco.d.ts | 1 + 4 files changed, 89 insertions(+), 17 deletions(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index b01d6ecd30d6..faf966171669 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -4215,6 +4215,7 @@ export interface IInlineSuggestOptions { enabled?: boolean; useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; + useCodeOverlay?: 'never' | 'whenPossible' | 'moveCodeWhenPossible'; useGutterIndicator?: boolean; }; @@ -4249,6 +4250,7 @@ class InlineEditorSuggest extends BaseEditorOption s.edits.experimental.useMixedLinesDiff); private readonly _useInterleavedLinesDiff = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useInterleavedLinesDiff); + private readonly _useCodeOverlay = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useCodeOverlay); private _previousView: { id: string; view: ReturnType; userJumpedToIt: boolean } | undefined; @@ -226,13 +227,16 @@ export class InlineEditsView extends Disposable { return 'deletion'; } - const numOriginalLines = edit.originalLineRange.length; - const numModifiedLines = edit.modifiedLineRange.length; - const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); - if (allInnerChangesNotTooLong && isSingleInnerEdit && numOriginalLines === 1 && numModifiedLines === 1) { - return 'wordReplacements'; - } else if (numOriginalLines > 0 && numModifiedLines > 0) { - return 'lineReplacement'; + const useCodeOverlay = this._useCodeOverlay.read(reader); + if (useCodeOverlay !== 'never') { + const numOriginalLines = edit.originalLineRange.length; + const numModifiedLines = edit.modifiedLineRange.length; + const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); + if (allInnerChangesNotTooLong && isSingleInnerEdit && numOriginalLines === 1 && numModifiedLines === 1 && useCodeOverlay === 'whenPossible') { + return 'wordReplacements'; + } else if (numOriginalLines > 0 && numModifiedLines > 0) { + return 'lineReplacement'; + } } if ( diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index bfba2fe07d8b..2173dc88e31d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -25,6 +25,9 @@ import { Range } from '../../../../../common/core/range.js'; import { LineRange } from '../../../../../common/core/lineRange.js'; import { InlineDecoration, InlineDecorationType } from '../../../../../common/viewModel.js'; import { IModelDecorationOptions, TrackedRangeStickiness } from '../../../../../common/model.js'; +import { $ } from '../../../../../../base/browser/dom.js'; +import { observableValue } from '../../../../../../base/common/observableInternal/base.js'; +import { IViewZoneChangeAccessor } from '../../../../../browser/editorBrowser.js'; export const transparentHoverBackground = registerColor( 'inlineEdit.wordReplacementView.background', { @@ -334,6 +337,8 @@ export class LineReplacementView extends Disposable implements IInlineEditsView return { lines, requiredWidth: requiredWidth - 10 }; // TODO: Width is always too large, why? }); + private readonly _viewZoneInfo = observableValue<{ height: number; lineNumber: number } | undefined>('viewZoneInfo', undefined); + private readonly _layout = derived(this, reader => { const { requiredWidth } = this._modifiedLineElements.read(reader); @@ -360,32 +365,44 @@ export class LineReplacementView extends Disposable implements IInlineEditsView const bottomOfOriginalLines = this._editor.editor.getBottomForLineNumber(endLineNumber) - scrollTop; if (bottomOfOriginalLines <= 0) { + this._viewZoneInfo.set(undefined, undefined); return undefined; } const prefixTrimOffset = maxPrefixTrim * w; // Box Widget positioning - const originalLine = Rect.fromLeftTopWidthHeight( + const originalLinesOverlay = Rect.fromLeftTopWidthHeight( editorLeftOffset + prefixTrimOffset, topOfOriginalLines, maxLineWidth, bottomOfOriginalLines - topOfOriginalLines + PADDING ); - const modifiedLine = Rect.fromLeftTopWidthHeight( - originalLine.left, - originalLine.bottom + PADDING, - originalLine.width, + const modifiedLinesOverlay = Rect.fromLeftTopWidthHeight( + originalLinesOverlay.left, + originalLinesOverlay.bottom + PADDING, + originalLinesOverlay.width, this._modifiedRange.length * lineHeight ); - const background = Rect.hull([originalLine, modifiedLine]).withMargin(PADDING); + const background = Rect.hull([originalLinesOverlay, modifiedLinesOverlay]).withMargin(PADDING); - const lowerBackground = background.intersectVertical(new OffsetRange(originalLine.bottom, Number.MAX_SAFE_INTEGER)); + const lowerBackground = background.intersectVertical(new OffsetRange(originalLinesOverlay.bottom, Number.MAX_SAFE_INTEGER)); const lowerText = new Rect(lowerBackground.left + PADDING, lowerBackground.top + PADDING, lowerBackground.right, lowerBackground.bottom); + // Add ViewZone if needed + const shouldShowViewZone = this._editor.editor.getOption(EditorOption.inlineSuggest).edits.experimental.useCodeOverlay === 'moveCodeWhenPossible'; + if (shouldShowViewZone) { + const viewZoneHeight = lowerBackground.height + 2 * PADDING; + const viewZoneLineNumber = this._originalRange.endLineNumberExclusive; + const activeViewZone = this._viewZoneInfo.get(); + if (!activeViewZone || activeViewZone.lineNumber !== viewZoneLineNumber || activeViewZone.height !== viewZoneHeight) { + this._viewZoneInfo.set({ height: viewZoneHeight, lineNumber: viewZoneLineNumber }, undefined); + } + } + return { - originalLine, - modifiedLine, + originalLinesOverlay, + modifiedLinesOverlay, background, lowerBackground, lowerText, @@ -394,6 +411,46 @@ export class LineReplacementView extends Disposable implements IInlineEditsView }; }); + private _previousViewZoneInfo: { height: number; lineNumber: number; id: string } | undefined = undefined; + protected readonly _viewZone = derived(this, reader => { + const viewZoneInfo = this._viewZoneInfo.read(reader); + this._editor.editor.changeViewZones((changeAccessor) => { + this.removePreviousViewZone(changeAccessor); + if (!viewZoneInfo) { return; } + this.addViewZone(viewZoneInfo, changeAccessor); + }); + }).recomputeInitiallyAndOnChange(this._store); + + private removePreviousViewZone(changeAccessor: IViewZoneChangeAccessor) { + if (!this._previousViewZoneInfo) { + return; + } + + changeAccessor.removeZone(this._previousViewZoneInfo.id); + + const cursorLineNumber = this._editor.cursorLineNumber.get(); + if (cursorLineNumber !== null && cursorLineNumber >= this._previousViewZoneInfo.lineNumber) { + this._editor.editor.setScrollTop(this._editor.scrollTop.get() - this._previousViewZoneInfo.height); + } + + this._previousViewZoneInfo = undefined; + } + + private addViewZone(viewZoneInfo: { height: number; lineNumber: number }, changeAccessor: IViewZoneChangeAccessor) { + const activeViewZone = changeAccessor.addZone({ + afterLineNumber: viewZoneInfo.lineNumber - 1, + heightInPx: viewZoneInfo.height, // move computation to layout? + domNode: $('div'), + }); + + const cursorLineNumber = this._editor.cursorLineNumber.get(); + if (cursorLineNumber !== null && cursorLineNumber >= viewZoneInfo.lineNumber) { + this._editor.editor.setScrollTop(this._editor.scrollTop.get() + viewZoneInfo.height); + } + + this._previousViewZoneInfo = { height: viewZoneInfo.height, lineNumber: viewZoneInfo.lineNumber, id: activeViewZone }; + } + private readonly _div = n.div({ class: 'line-replacement', }, [ @@ -512,6 +569,7 @@ export class LineReplacementView extends Disposable implements IInlineEditsView super(); this._register(toDisposable(() => this._originalBubblesDecorationCollection.clear())); + this._register(toDisposable(() => this._editor.editor.changeViewZones(accessor => this.removePreviousViewZone(accessor)))); const originalBubbles = rangesToBubbleRanges(this._replacements.map(r => r.originalRange)); this._originalBubblesDecorationCollection.set(originalBubbles.map(r => ({ range: r, options: this._originalBubblesDecorationOptions }))); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index b80a2fc776b8..8101016bf89c 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4613,6 +4613,7 @@ declare namespace monaco.editor { enabled?: boolean; useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; + useCodeOverlay?: 'never' | 'whenPossible' | 'moveCodeWhenPossible'; useGutterIndicator?: boolean; }; }; From 7d470666ea547392188d5e27f9acff4cc51a7a2f Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Fri, 24 Jan 2025 12:33:53 +0100 Subject: [PATCH 0849/3587] Increase the weight of Escape (#238655) --- .../contrib/inlineCompletions/browser/controller/commands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts index 12fd45acf161..a95f0b594be9 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts @@ -271,7 +271,7 @@ export class HideInlineCompletion extends EditorAction { label: nls.localize2('action.inlineSuggest.hide', "Hide Inline Suggestion"), precondition: ContextKeyExpr.or(InlineCompletionContextKeys.inlineSuggestionVisible, InlineCompletionContextKeys.inlineEditVisible), kbOpts: { - weight: 100, + weight: KeybindingWeight.EditorContrib + 90, // same as hiding the suggest widget primary: KeyCode.Escape, }, menuOpts: [{ From a740888864bd630bfec1f3f7e3b3f65528e5006e Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Fri, 24 Jan 2025 12:36:22 +0100 Subject: [PATCH 0850/3587] Fix bad parsing resuming in tree sitter parser (#238642) * Fix bad parsing resuming in tree sitter parser * Don't reset parse when not initial parse * Don't set tokens when there aren't any * Remove logging note --- .../treeSitter/treeSitterParserService.ts | 36 ++++++++++------- .../test/common/model/tokenStore.test.ts | 29 ++++++++++++++ .../browser/treeSitterTokenizationFeature.ts | 39 ++++++++++--------- 3 files changed, 70 insertions(+), 34 deletions(-) diff --git a/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts index 5de94af33cf0..11175de6a0aa 100644 --- a/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts +++ b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts @@ -93,7 +93,7 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit } })); this._parseSessionDisposables.add(this.model.onDidChangeContent(e => this._onDidChangeContent(treeSitterTree, e))); - await this._onDidChangeContent(treeSitterTree, undefined); + this._onDidChangeContent(treeSitterTree, undefined); if (token.isCancellationRequested) { return; } @@ -122,8 +122,8 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit }); } - private async _onDidChangeContent(treeSitterTree: TreeSitterParseResult, change: IModelContentChangedEvent | undefined) { - return treeSitterTree.onDidChangeContent(this.model, change?.changes ?? []); + private _onDidChangeContent(treeSitterTree: TreeSitterParseResult, change: IModelContentChangedEvent | undefined) { + return treeSitterTree.onDidChangeContent(this.model, change); } } @@ -165,13 +165,11 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul this._isDisposed = true; this._onDidUpdate.dispose(); this._tree?.delete(); + this._lastFullyParsed?.delete(); + this._lastFullyParsedWithEdits?.delete(); this.parser?.delete(); } - get tree() { return this._tree; } - private set tree(newTree: Parser.Tree | undefined) { - this._tree?.delete(); - this._tree = newTree; - } + get tree() { return this._lastFullyParsed; } get isDisposed() { return this._isDisposed; } private findChangedNodes(newTree: Parser.Tree, oldTree: Parser.Tree, version: number): ChangedRange[] { @@ -321,13 +319,13 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul } private _onDidChangeContentQueue: LimitedQueue = new LimitedQueue(); - public async onDidChangeContent(model: ITextModel, changes: IModelContentChange[]): Promise { + public onDidChangeContent(model: ITextModel, changes: IModelContentChangedEvent | undefined): void { const version = model.getVersionId(); if (version === this._editVersion) { return; } - this._applyEdits(changes, version); + this._applyEdits(changes?.changes ?? [], version); this._onDidChangeContentQueue.queue(async () => { if (this.isDisposed) { @@ -341,7 +339,7 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul } const completed = await this._parseAndUpdateTree(model, version); - if (completed && (version === model.getVersionId())) { + if (completed) { if (!ranges) { ranges = [{ newRange: model.getFullModelRange(), oldRangeLength: model.getValueLength(), newRangeStartOffset: 0, newRangeEndOffset: model.getValueLength() }]; } @@ -375,12 +373,20 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul private async _parseAndUpdateTree(model: ITextModel, version: number): Promise { const tree = await this._parse(model); if (tree) { - this.tree = tree; + this._tree?.delete(); + this._tree = tree; + this._lastFullyParsed?.delete(); this._lastFullyParsed = tree.copy(); + this._lastFullyParsedWithEdits?.delete(); this._lastFullyParsedWithEdits = tree.copy(); this._versionId = version; + return tree; + } else if (!this._tree) { + // No tree means this is the inial parse and there were edits + // parse function doesn't handle this well and we can end up with an incorrect tree, so we reset + this.parser.reset(); } - return tree; + return undefined; } private _parse(model: ITextModel): Promise { @@ -401,7 +407,7 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul do { const timer = performance.now(); try { - newTree = this.parser.parse((index: number, position?: Parser.Point) => this._parseCallback(model, index), this.tree); + newTree = this.parser.parse((index: number, position?: Parser.Point) => this._parseCallback(model, index), this._tree); } catch (e) { // parsing can fail when the timeout is reached, will resume upon next loop } finally { @@ -409,7 +415,7 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul passes++; } - // Even if the model changes and edits are applied, the tree parsing will continue correctly after the await. + // So long as this isn't the initial parse, even if the model changes and edits are applied, the tree parsing will continue correctly after the await. await new Promise(resolve => setTimeout0(resolve)); } while (!model.isDisposed() && !this.isDisposed && !newTree && inProgressVersion === model.getVersionId()); diff --git a/src/vs/editor/test/common/model/tokenStore.test.ts b/src/vs/editor/test/common/model/tokenStore.test.ts index 7e3b3b52a424..328fff4be8ba 100644 --- a/src/vs/editor/test/common/model/tokenStore.test.ts +++ b/src/vs/editor/test/common/model/tokenStore.test.ts @@ -204,6 +204,35 @@ suite('TokenStore', () => { assert.strictEqual(root.children[2].length, 2); // Last 2 tokens }); + test('update deeply nested tree with a range of tokens that causes tokens to split', () => { + const store = new TokenStore(textModel); + store.buildStore([ + { startOffsetInclusive: 0, length: 3, token: 1 }, + { startOffsetInclusive: 3, length: 3, token: 2 }, + { startOffsetInclusive: 6, length: 4, token: 3 }, + { startOffsetInclusive: 10, length: 5, token: 4 }, + { startOffsetInclusive: 15, length: 4, token: 5 }, + { startOffsetInclusive: 19, length: 3, token: 6 }, + { startOffsetInclusive: 22, length: 5, token: 7 }, + { startOffsetInclusive: 27, length: 3, token: 8 } + ]); + + // Update token in the middle which causes tokens to split + store.update(8, [ + { startOffsetInclusive: 12, length: 4, token: 9 }, + { startOffsetInclusive: 16, length: 4, token: 10 } + ]); + + const root = store.root as any; + // Verify the structure remains balanced + assert.strictEqual(root.children.length, 2); + assert.strictEqual(root.children[0].children.length, 2); + + // Verify the lengths are updated correctly + assert.strictEqual(root.children[0].length, 12); + assert.strictEqual(root.children[1].length, 18); + }); + test('getTokensInRange returns tokens in middle of document', () => { const store = new TokenStore(textModel); store.buildStore([ diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts index 88c69a7028cb..f168a28f2897 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts @@ -115,6 +115,12 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi super(); this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => this.reset())); this._register(this._treeSitterService.onDidUpdateTree((e) => { + if (this._tokenizationStoreService.hasTokens(e.textModel)) { + // Mark the range for refresh immediately + for (const range of e.ranges) { + this._tokenizationStoreService.markForRefresh(e.textModel, range.newRange); + } + } if (e.versionId !== e.textModel.getVersionId()) { return; } @@ -125,10 +131,6 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi if (!this._tokenizationStoreService.hasTokens(e.textModel)) { this._firstTreeUpdate(e.textModel, e.versionId, ranges); } else { - // Mark the range for refresh immediately - for (const range of e.ranges) { - this._tokenizationStoreService.markForRefresh(e.textModel, range.newRange); - } this._handleTreeUpdate(e, ranges); } })); @@ -146,15 +148,14 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi } private _firstTreeUpdate(textModel: ITextModel, versionId: number, ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[]) { - if (versionId !== textModel.getVersionId()) { - return; - } const modelEndOffset = textModel.getValueLength(); const editorEndPosition = textModel.getPositionAt(modelEndOffset); const captures = this._getTreeAndCaptures(new Range(1, 1, editorEndPosition.lineNumber, editorEndPosition.column), textModel); + if (captures.captures.length === 0) { + return; + } // Make empty tokens to populate the store const tokens: TokenUpdate[] = this._createEmptyTokens(captures, modelEndOffset) ?? []; - this._tokenizationStoreService.setTokens(textModel, tokens); this._setViewPortTokens(textModel, versionId); } @@ -182,7 +183,7 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi oldRangeLength: newRangeEndOffset - newRangeStartOffset }; } - this._handleTreeUpdate({ ranges: rangeChanges, textModel, versionId }, ranges, 'viewport'); + this._handleTreeUpdate({ ranges: rangeChanges, textModel, versionId }, ranges); } /** @@ -232,7 +233,7 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi // Get the captures immediately while the text model is correct const captures = rangeChanges.map(range => this._getTreeAndCaptures(range.newRange, e.textModel)); // Don't block - this._updateTreeForRanges(e.textModel, rangeChanges, e.versionId, ranges, captures, note).then(() => { + this._updateTreeForRanges(e.textModel, rangeChanges, e.versionId, captures).then(() => { const tree = this._getTree(e.textModel); if (!e.textModel.isDisposed() && (tree?.versionId === e.textModel.getVersionId())) { this._refreshNeedsRefresh(e.textModel); @@ -241,11 +242,11 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi }); } - private async _updateTreeForRanges(textModel: ITextModel, rangeChanges: RangeChange[], versionId: number, ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[], captures: { tree: ITreeSitterParseResult | undefined; captures: QueryCapture[] }[], note?: string) { + private async _updateTreeForRanges(textModel: ITextModel, rangeChanges: RangeChange[], versionId: number, captures: { tree: ITreeSitterParseResult | undefined; captures: QueryCapture[] }[]) { let tokenUpdate: { oldRangeLength: number; newTokens: TokenUpdate[] } | undefined; for (let i = 0; i < rangeChanges.length; i++) { - if (versionId < textModel.getVersionId()) { + if (versionId !== textModel.getVersionId()) { // Our captures have become invalid and we need to re-capture break; } @@ -259,15 +260,15 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi tokenUpdate = { oldRangeLength: range.oldRangeLength, newTokens: [] }; } this._tokenizationStoreService.updateTokens(textModel, versionId, [tokenUpdate]); + this._onDidChangeTokens.fire({ + textModel: textModel, + changes: { + semanticTokensApplied: false, + ranges: [{ fromLineNumber: range.newRange.getStartPosition().lineNumber, toLineNumber: range.newRange.getEndPosition().lineNumber }] + } + }); await new Promise(resolve => setTimeout0(resolve)); } - this._onDidChangeTokens.fire({ - textModel: textModel, - changes: { - semanticTokensApplied: false, - ranges - } - }); } private _refreshNeedsRefresh(textModel: ITextModel) { From afdf18f8cd8b368db2d842c2c2e95b1e10fd1568 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:24:50 +0100 Subject: [PATCH 0851/3587] Choose line replacement or side-by-side view based on editor width (#238659) * pick linereplcement or side by side depending on editor width * do not enable overflow --- src/vs/editor/browser/observableCodeEditor.ts | 1 + .../view/inlineEdits/sideBySideDiff.ts | 26 +++++++++++++++++-- .../browser/view/inlineEdits/utils.ts | 2 +- .../browser/view/inlineEdits/view.ts | 21 ++++++++++++--- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts index 7e9d3bde0c14..e81a961a7f26 100644 --- a/src/vs/editor/browser/observableCodeEditor.ts +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -231,6 +231,7 @@ export class ObservableCodeEditor extends Disposable { public readonly layoutInfo = observableFromEvent(this.editor.onDidLayoutChange, () => this.editor.getLayoutInfo()); public readonly layoutInfoContentLeft = this.layoutInfo.map(l => l.contentLeft); public readonly layoutInfoDecorationsLeft = this.layoutInfo.map(l => l.decorationsLeft); + public readonly layoutInfoWidth = this.layoutInfo.map(l => l.width); public readonly contentWidth = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentWidth()); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index 46f1cc5d282b..6f1fed18b485 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -8,7 +8,7 @@ import { IAction } from '../../../../../../base/common/actions.js'; import { Color } from '../../../../../../base/common/color.js'; import { structuralEquals } from '../../../../../../base/common/equals.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { IObservable, autorun, constObservable, derived, derivedObservableWithCache, derivedOpts, observableFromEvent } from '../../../../../../base/common/observable.js'; +import { IObservable, IReader, autorun, constObservable, derived, derivedObservableWithCache, derivedOpts, observableFromEvent } from '../../../../../../base/common/observable.js'; import { MenuId, MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; @@ -111,7 +111,27 @@ export interface IInlineEditsView { isHovered: IObservable; } +const PADDING = 4; +const ENABLE_OVERFLOW = false; + export class InlineEditsSideBySideDiff extends Disposable implements IInlineEditsView { + + // This is an approximation and should be improved by using the real parameters used bellow + static fitsInsideViewport(editor: ICodeEditor, edit: InlineEditWithChanges, reader: IReader): boolean { + const editorObs = observableCodeEditor(editor); + const editorWidth = editorObs.layoutInfoWidth.read(reader); + const editorContentLeft = editorObs.layoutInfoContentLeft.read(reader); + const editorVerticalScrollBar = editor.getLayoutInfo().verticalScrollbarWidth; + const w = editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; + + const maxOriginalContent = maxContentWidthInRange(editorObs, edit.originalLineRange, undefined/* do not reconsider on each layout info change */); + const maxModifiedContent = edit.lineEdit.newLines.reduce((max, line) => Math.max(max, line.length * w), 0); + const endOfEditorPadding = 20; // padding after last line of editor + const editorsPadding = edit.modifiedLineRange.length <= edit.originalLineRange.length ? PADDING * 3 + endOfEditorPadding : 60 + endOfEditorPadding * 2; + + return maxOriginalContent + maxModifiedContent + editorsPadding < editorWidth - editorContentLeft - editorVerticalScrollBar; + } + private readonly _editorObs = observableCodeEditor(this._editor); constructor( @@ -471,7 +491,6 @@ export class InlineEditsSideBySideDiff extends Disposable implements IInlineEdit : new OffsetRange(60, 61); const clipped = dist === 0; - const PADDING = 4; const codeEditDist = editIsSameHeight ? PADDING : codeEditDistRange.clip(dist); // TODO: Is there a better way to specify the distance? @@ -527,6 +546,9 @@ export class InlineEditsSideBySideDiff extends Disposable implements IInlineEdit private readonly _stickyScrollHeight = this._stickyScrollController ? observableFromEvent(this._stickyScrollController.onDidChangeStickyScrollHeight, () => this._stickyScrollController!.stickyScrollWidgetHeight) : constObservable(0); private readonly _shouldOverflow = derived(reader => { + if (!ENABLE_OVERFLOW) { + return false; + } const range = this._edit.read(reader)?.originalLineRange; if (!range) { return false; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts index 142938e1ee2b..541e6e75cef6 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts @@ -25,7 +25,7 @@ import { Range } from '../../../../../common/core/range.js'; import { SingleTextEdit, TextEdit } from '../../../../../common/core/textEdit.js'; import { RangeMapping } from '../../../../../common/diff/rangeMapping.js'; -export function maxContentWidthInRange(editor: ObservableCodeEditor, range: LineRange, reader: IReader): number { +export function maxContentWidthInRange(editor: ObservableCodeEditor, range: LineRange, reader: IReader | undefined): number { editor.layoutInfo.read(reader); editor.value.read(reader); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index c1b0c3898246..b6feaca9daee 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -33,7 +33,12 @@ export class InlineEditsView extends Disposable { private readonly _useInterleavedLinesDiff = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useInterleavedLinesDiff); private readonly _useCodeOverlay = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useCodeOverlay); - private _previousView: { id: string; view: ReturnType; userJumpedToIt: boolean } | undefined; + private _previousView: { + id: string; + view: ReturnType; + userJumpedToIt: boolean; + editorWidth: number; + } | undefined; constructor( private readonly _editor: ICodeEditor, @@ -202,11 +207,18 @@ export class InlineEditsView extends Disposable { (this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible' && this._previousView?.view !== 'mixedLines') || (this._useInterleavedLinesDiff.read(reader) === 'afterJump' && this._previousView?.view !== 'interleavedLines') ); + const reconsiderViewEditorWidthChange = this._previousView?.editorWidth !== this._editor.getLayoutInfo().width && + ( + (this._previousView?.view === 'sideBySide' && this._useCodeOverlay.read(reader) !== 'never') || + (this._previousView?.view === 'lineReplacement') + ); - if (canUseCache && !reconsiderViewAfterJump) { + if (canUseCache && !reconsiderViewAfterJump && !reconsiderViewEditorWidthChange) { return this._previousView!.view; } + // Determine the view based on the edit / diff + if (edit.isCollapsed) { return 'collapsed'; } @@ -234,7 +246,7 @@ export class InlineEditsView extends Disposable { const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); if (allInnerChangesNotTooLong && isSingleInnerEdit && numOriginalLines === 1 && numModifiedLines === 1 && useCodeOverlay === 'whenPossible') { return 'wordReplacements'; - } else if (numOriginalLines > 0 && numModifiedLines > 0) { + } else if (numOriginalLines > 0 && numModifiedLines > 0 && !InlineEditsSideBySideDiff.fitsInsideViewport(this._editor, edit, reader)) { return 'lineReplacement'; } } @@ -256,7 +268,8 @@ export class InlineEditsView extends Disposable { private determineRenderState(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText) { const view = this.determineView(edit, reader, diff, newText); - this._previousView = { id: edit.inlineCompletion.id, view, userJumpedToIt: edit.userJumpedToIt }; + + this._previousView = { id: edit.inlineCompletion.id, view, userJumpedToIt: edit.userJumpedToIt, editorWidth: this._editor.getLayoutInfo().width }; switch (view) { case 'collapsed': return { kind: 'collapsed' as const }; From 27d92d9a8880db5e6d3dd6efbc016d1efd36eb9a Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Fri, 24 Jan 2025 14:33:06 +0100 Subject: [PATCH 0852/3587] Bumpt distro (#238664) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 18ef1aece3db..c66ab02b1ef0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.97.0", - "distro": "0f416a8c0a95ea8996009a16346e8b469601abee", + "distro": "1b4e04ea3f9e9e068fd300cf935b008187847029", "author": { "name": "Microsoft Corporation" }, @@ -237,4 +237,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} \ No newline at end of file +} From 3cdb982f06f2a331250550e0261f2be1498708af Mon Sep 17 00:00:00 2001 From: isidorn Date: Fri, 24 Jan 2025 15:41:51 +0100 Subject: [PATCH 0853/3587] add commithash to telemetry for alignment --- src/vs/workbench/api/common/extHostTelemetry.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/api/common/extHostTelemetry.ts b/src/vs/workbench/api/common/extHostTelemetry.ts index 09383009cd86..71e920b72df9 100644 --- a/src/vs/workbench/api/common/extHostTelemetry.ts +++ b/src/vs/workbench/api/common/extHostTelemetry.ts @@ -108,6 +108,7 @@ export class ExtHostTelemetry extends Disposable implements ExtHostTelemetryShap commonProperties['common.extversion'] = extension.version; commonProperties['common.vscodemachineid'] = this.initData.telemetryInfo.machineId; commonProperties['common.vscodesessionid'] = this.initData.telemetryInfo.sessionId; + commonProperties['common.vscodecommithash'] = this.initData.commit; commonProperties['common.sqmid'] = this.initData.telemetryInfo.sqmId; commonProperties['common.devDeviceId'] = this.initData.telemetryInfo.devDeviceId; commonProperties['common.vscodeversion'] = this.initData.version; From b9cbfa49d87c9dc1d1b1aa0dc0ee54664d504248 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 24 Jan 2025 16:05:13 +0100 Subject: [PATCH 0854/3587] GitHub - fix event handled (#238676) --- extensions/github/src/historyItemDetailsProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/github/src/historyItemDetailsProvider.ts b/extensions/github/src/historyItemDetailsProvider.ts index 00a67bcebcd7..b31126e2ada2 100644 --- a/extensions/github/src/historyItemDetailsProvider.ts +++ b/extensions/github/src/historyItemDetailsProvider.ts @@ -83,7 +83,7 @@ export class GitHubSourceControlHistoryItemDetailsProvider implements SourceCont private readonly _disposables = new DisposableStore(); constructor(private readonly _gitAPI: API, private readonly _logger: LogOutputChannel) { - this._disposables.add(this._gitAPI.onDidCloseRepository(this._onDidCloseRepository)); + this._disposables.add(this._gitAPI.onDidCloseRepository(repository => this._onDidCloseRepository(repository))); this._disposables.add(authentication.onDidChangeSessions(e => { if (e.provider.id === 'github') { From 1384466b77e21932d55390f52fad4930e7a71bb9 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 24 Jan 2025 16:09:20 +0100 Subject: [PATCH 0855/3587] Fix whitespace insertion rendered as deletion bug (#238677) whitespace insertion rendered as deletion bug fix --- .../contrib/inlineCompletions/browser/view/inlineEdits/view.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index b6feaca9daee..33f71f8d77a7 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -235,7 +235,7 @@ export class InlineEditsView extends Disposable { return 'ghostText'; } - if (inner.every(m => newText.getValueOfRange(m.modifiedRange).trim() === '')) { + if (inner.every(m => newText.getValueOfRange(m.modifiedRange).trim() === '' && edit.originalText.getValueOfRange(m.originalRange).trim() !== '')) { return 'deletion'; } From 90a24fbd32a8d18e43596b7597d68fe5967b110c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 24 Jan 2025 07:24:44 -0800 Subject: [PATCH 0856/3587] Editor GPU: Pass content width through to scroll bar Fixes #233790 --- src/vs/editor/browser/gpu/gpu.ts | 6 +++++- .../browser/gpu/renderStrategy/baseRenderStrategy.ts | 4 ++-- .../gpu/renderStrategy/fullFileRenderStrategy.ts | 10 +++++++--- .../gpu/renderStrategy/viewportRenderStrategy.ts | 9 ++++++--- src/vs/editor/browser/gpu/viewGpuContext.ts | 2 ++ src/vs/editor/browser/view.ts | 1 + .../browser/viewParts/viewLinesGpu/viewLinesGpu.ts | 11 +++++++++-- 7 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/vs/editor/browser/gpu/gpu.ts b/src/vs/editor/browser/gpu/gpu.ts index 7284d275c769..500b167e59a9 100644 --- a/src/vs/editor/browser/gpu/gpu.ts +++ b/src/vs/editor/browser/gpu/gpu.ts @@ -36,6 +36,10 @@ export interface IGpuRenderStrategy extends IDisposable { * Resets the render strategy, clearing all data and setting up for a new frame. */ reset(): void; - update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number; + update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): IGpuRenderStrategyUpdateResult; draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void; } + +export interface IGpuRenderStrategyUpdateResult { + localContentWidth: number; +} diff --git a/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts index b5d9fc895ccb..59c7a3e3a535 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts @@ -7,7 +7,7 @@ import { ViewEventHandler } from '../../../common/viewEventHandler.js'; import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; import type { ViewContext } from '../../../common/viewModel/viewContext.js'; import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions.js'; -import type { IGpuRenderStrategy } from '../gpu.js'; +import type { IGpuRenderStrategy, IGpuRenderStrategyUpdateResult } from '../gpu.js'; import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; import type { ViewGpuContext } from '../viewGpuContext.js'; @@ -31,6 +31,6 @@ export abstract class BaseRenderStrategy extends ViewEventHandler implements IGp } abstract reset(): void; - abstract update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number; + abstract update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): IGpuRenderStrategyUpdateResult; abstract draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void; } diff --git a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index 7706390f0eed..ec00a4e72d1b 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -16,7 +16,7 @@ import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions. import type { ITextureAtlasPageGlyph } from '../atlas/atlas.js'; import { createContentSegmenter, type IContentSegmenter } from '../contentSegmenter.js'; import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js'; -import { BindingId } from '../gpu.js'; +import { BindingId, type IGpuRenderStrategyUpdateResult } from '../gpu.js'; import { GPULifecycle } from '../gpuDisposable.js'; import { quadVertices } from '../gpuUtils.js'; import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; @@ -232,7 +232,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { this._finalRenderedLine = 0; } - update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number { + update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): IGpuRenderStrategyUpdateResult { // IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the // loop. This is done so we don't need to trust the JIT compiler to do this optimization to // avoid potential additional blocking time in garbage collector which is a common cause of @@ -272,6 +272,8 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { this._scrollInitialized = true; } + let localContentWidth = 0; + // Update cell data const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]); const lineIndexCount = FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell; @@ -466,6 +468,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { // Adjust the x pixel offset for the next character absoluteOffsetX += charWidth; + localContentWidth = Math.max(localContentWidth, absoluteOffsetX); } tokenStartIndex = tokenEndIndex; @@ -500,7 +503,8 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1; this._visibleObjectCount = visibleObjectCount; - return visibleObjectCount; + + return { localContentWidth }; } draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void { diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts index 71dea59e0f41..5f3d2a349566 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -16,7 +16,7 @@ import type { ViewContext } from '../../../common/viewModel/viewContext.js'; import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions.js'; import type { ITextureAtlasPageGlyph } from '../atlas/atlas.js'; import { createContentSegmenter, type IContentSegmenter } from '../contentSegmenter.js'; -import { BindingId } from '../gpu.js'; +import { BindingId, type IGpuRenderStrategyUpdateResult } from '../gpu.js'; import { GPULifecycle } from '../gpuDisposable.js'; import { quadVertices } from '../gpuUtils.js'; import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; @@ -184,7 +184,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { } } - update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number { + update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): IGpuRenderStrategyUpdateResult { // IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the // loop. This is done so we don't need to trust the JIT compiler to do this optimization to // avoid potential additional blocking time in garbage collector which is a common cause of @@ -224,6 +224,8 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { this._scrollInitialized = true; } + let localContentWidth = 0; + // Zero out cell buffer or rebuild if needed if (this._cellBindBufferLineCapacity < viewportData.endLineNumber - viewportData.startLineNumber + 1) { this._rebuildCellBuffer(viewportData.endLineNumber - viewportData.startLineNumber + 1); @@ -369,6 +371,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { // Adjust the x pixel offset for the next character absoluteOffsetX += charWidth; + localContentWidth = Math.max(localContentWidth, absoluteOffsetX); } tokenStartIndex = tokenEndIndex; @@ -394,7 +397,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1; this._visibleObjectCount = visibleObjectCount; - return visibleObjectCount; + return { localContentWidth }; } draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void { diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 181c468f12c5..3cf8238c6c60 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -33,6 +33,7 @@ export class ViewGpuContext extends Disposable { readonly maxGpuCols = ViewportRenderStrategy.maxSupportedColumns; readonly canvas: FastDomNode; + readonly scrollWidthElement: FastDomNode; readonly ctx: GPUCanvasContext; static device: Promise; @@ -87,6 +88,7 @@ export class ViewGpuContext extends Disposable { this.canvas = createFastDomNode(document.createElement('canvas')); this.canvas.setClassName('editorCanvas'); + this.scrollWidthElement = createFastDomNode(document.createElement('div')); // Adjust the canvas size to avoid drawing under the scroll bar this._register(Event.runAndSubscribe(configurationService.onDidChangeConfiguration, e => { diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 1e883bfcd2b2..e383a7c9b2cb 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -256,6 +256,7 @@ export class View extends ViewEventHandler { this._overflowGuardContainer.appendChild(this._scrollbar.getDomNode()); if (this._viewGpuContext) { this._overflowGuardContainer.appendChild(this._viewGpuContext.canvas); + this._linesContent.appendChild(this._viewGpuContext.scrollWidthElement); } this._overflowGuardContainer.appendChild(scrollDecoration.getDomNode()); this._overflowGuardContainer.appendChild(this._overlayWidgets.getDomNode()); diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index a6cabd559512..99312616d3d3 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -49,6 +49,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { private _initViewportData?: ViewportData[]; private _lastViewportData?: ViewportData; private _lastViewLineOptions?: ViewLineOptions; + private _maxLocalContentWidthSoFar = 0; private _device!: GPUDevice; private _renderPassDescriptor!: GPURenderPassDescriptor; @@ -429,7 +430,6 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { return true; } override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { return true; } override onFlushed(e: viewEvents.ViewFlushedEvent): boolean { return true; } - override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean { return true; } override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean { return true; } override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean { return true; } @@ -473,7 +473,14 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { const options = new ViewLineOptions(this._context.configuration, this._context.theme.type); - this._renderStrategy.value!.update(viewportData, options); + const { localContentWidth } = this._renderStrategy.value!.update(viewportData, options); + + // Track the largest local content width so far in this session and use it as the scroll + // width. This is how the DOM renderer works as well, so you may not be able to scroll to + // the right in a file with long lines until you scroll down. + this._maxLocalContentWidthSoFar = Math.max(this._maxLocalContentWidthSoFar, localContentWidth); + this._context.viewModel.viewLayout.setMaxLineWidth(this._maxLocalContentWidthSoFar); + this._viewGpuContext.scrollWidthElement.setWidth(this._context.viewLayout.getScrollWidth()); this._updateAtlasStorageBufferAndTexture(); From 7717b477c158be1cc542b5244cb655ad34260c60 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:02:09 +0100 Subject: [PATCH 0857/3587] Properly trim prefixes in inline edits (#238672) proper prefix trim --- src/vs/editor/common/config/editorOptions.ts | 2 +- .../browser/view/inlineEdits/deletionView.ts | 22 ++++++---- .../browser/view/inlineEdits/utils.ts | 18 +++++++++ .../browser/view/inlineEdits/view.ts | 40 ++++--------------- .../view/inlineEdits/wordReplacementView.ts | 40 ++++--------------- 5 files changed, 49 insertions(+), 73 deletions(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index faf966171669..189122c966dd 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -4250,7 +4250,7 @@ class InlineEditorSuggest extends BaseEditorOption, private readonly _uiState: IObservable<{ - edit: InlineEditWithChanges; - originalDisplayRange: LineRange; - widgetStartColumn: number; + originalRange: LineRange; + deletions: Range[]; } | undefined>, ) { super(); @@ -55,7 +54,7 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV private readonly _originalVerticalStartPosition = this._editorObs.observePosition(this._originalStartPosition, this._store).map(p => p?.y); private readonly _originalVerticalEndPosition = this._editorObs.observePosition(this._originalEndPosition, this._store).map(p => p?.y); - private readonly _originalDisplayRange = this._uiState.map(s => s?.originalDisplayRange); + private readonly _originalDisplayRange = this._uiState.map(s => s?.originalRange); private readonly _editorMaxContentWidthInRange = derived(this, reader => { const originalDisplayRange = this._originalDisplayRange.read(reader); if (!originalDisplayRange) { @@ -71,6 +70,14 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV }); }).map((v, r) => v.read(r)); + private readonly _maxPrefixTrim = derived(reader => { + const state = this._uiState.read(reader); + if (!state) { + return { prefixTrim: 0, prefixLeftOffset: 0 }; + } + return getPrefixTrim(state.deletions, state.originalRange, [], this._editor); + }); + private readonly _editorLayoutInfo = derived(this, (reader) => { const inlineEdit = this._edit.read(reader); if (!inlineEdit) { @@ -81,7 +88,6 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV return null; } - const w = this._editorObs.getOption(EditorOption.fontInfo).read(reader).typicalHalfwidthCharacterWidth; const editorLayout = this._editorObs.layoutInfo.read(reader); const horizontalScrollOffset = this._editorObs.scrollLeft.read(reader); @@ -91,7 +97,7 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV const selectionTop = this._originalVerticalStartPosition.read(reader) ?? this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); const selectionBottom = this._originalVerticalEndPosition.read(reader) ?? this._editor.getTopForLineNumber(range.endLineNumberExclusive) - this._editorObs.scrollTop.read(reader); - const codeLeft = editorLayout.contentLeft + state.widgetStartColumn * w; + const codeLeft = editorLayout.contentLeft + this._maxPrefixTrim.read(reader).prefixLeftOffset; if (left <= codeLeft) { return null; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts index 541e6e75cef6..6f855179b1ee 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts @@ -14,6 +14,7 @@ import { OS } from '../../../../../../base/common/platform.js'; import { getIndentationLength, splitLines } from '../../../../../../base/common/strings.js'; import { URI } from '../../../../../../base/common/uri.js'; import { MenuEntryActionViewItem } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { ObservableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { Point } from '../../../../../browser/point.js'; import { Rect } from '../../../../../browser/rect.js'; @@ -24,6 +25,7 @@ import { Position } from '../../../../../common/core/position.js'; import { Range } from '../../../../../common/core/range.js'; import { SingleTextEdit, TextEdit } from '../../../../../common/core/textEdit.js'; import { RangeMapping } from '../../../../../common/diff/rangeMapping.js'; +import { indentOfLine } from '../../../../../common/model/textModel.js'; export function maxContentWidthInRange(editor: ObservableCodeEditor, range: LineRange, reader: IReader | undefined): number { editor.layoutInfo.read(reader); @@ -66,6 +68,22 @@ export function getOffsetForPos(editor: ObservableCodeEditor, pos: Position, rea return lineContentWidth; } +export function getPrefixTrim(diffRanges: Range[], originalLinesRange: LineRange, modifiedLines: string[], editor: ICodeEditor): { prefixTrim: number; prefixLeftOffset: number } { + const textModel = editor.getModel(); + if (!textModel) { + return { prefixTrim: 0, prefixLeftOffset: 0 }; + } + + 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 prefixTrim = Math.min(...replacementStart, ...originalIndents, ...modifiedIndents); + + const prefixLeftOffset = editor.getOffsetForColumn(originalLinesRange.startLineNumber, prefixTrim + 1); + + return { prefixTrim, prefixLeftOffset }; +} + export class StatusBarViewItem extends MenuEntryActionViewItem { protected readonly _updateLabelListener = this._register(this._contextKeyService.onDidChangeContext(() => { this.updateLabel(); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 33f71f8d77a7..950247780fae 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -130,9 +130,8 @@ export class InlineEditsView extends Disposable { this._editor, this._edit, this._uiState.map(s => s && s.state?.kind === 'deletion' ? ({ - edit: s.edit, - originalDisplayRange: s.originalDisplayRange, - widgetStartColumn: s.state.widgetStartColumn, + originalRange: s.state.originalRange, + deletions: s.state.deletions, }) : undefined), )); @@ -244,7 +243,7 @@ export class InlineEditsView extends Disposable { const numOriginalLines = edit.originalLineRange.length; const numModifiedLines = edit.modifiedLineRange.length; const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); - if (allInnerChangesNotTooLong && isSingleInnerEdit && numOriginalLines === 1 && numModifiedLines === 1 && useCodeOverlay === 'whenPossible') { + if (allInnerChangesNotTooLong && isSingleInnerEdit && numOriginalLines === 1 && numModifiedLines === 1) { return 'wordReplacements'; } else if (numOriginalLines > 0 && numModifiedLines > 0 && !InlineEditsSideBySideDiff.fitsInsideViewport(this._editor, edit, reader)) { return 'lineReplacement'; @@ -282,9 +281,11 @@ export class InlineEditsView extends Disposable { const inner = diff.flatMap(d => d.innerChanges ?? []); if (view === 'deletion') { - const trimLength = getPrefixTrimLength(edit, inner, newText); - const widgetStartColumn = Math.min(trimLength, ...inner.map(m => m.originalRange.startLineNumber !== m.originalRange.endLineNumber ? 0 : m.originalRange.startColumn - 1)); - return { kind: 'deletion' as const, widgetStartColumn }; + return { + kind: 'deletion' as const, + originalRange: edit.originalLineRange, + deletions: inner.map(m => m.originalRange), + }; } const replacements = inner.map(m => new SingleTextEdit(m.originalRange, newText.getValueOfRange(m.modifiedRange))); @@ -354,28 +355,3 @@ function isSingleLineDeletion(diff: DetailedLineRangeMapping[]): boolean { return true; } } - -function getPrefixTrimLength(edit: InlineEditWithChanges, innerChanges: RangeMapping[], newText: StringText) { - if (innerChanges.some(m => m.originalRange.startLineNumber !== m.originalRange.endLineNumber)) { - return 0; - } - - let minTrimLength = Number.MAX_SAFE_INTEGER; - for (let i = 0; i < edit.originalLineRange.length; i++) { - const lineNumber = edit.originalLineRange.startLineNumber + i; - const originalLine = edit.originalText.getLineAt(lineNumber); - const editedLine = newText.getLineAt(lineNumber); - const trimLength = getLinePrefixTrimLength(originalLine, editedLine); - minTrimLength = Math.min(minTrimLength, trimLength); - } - - return Math.min(minTrimLength, ...innerChanges.map(m => m.originalRange.startColumn - 1)); - - function getLinePrefixTrimLength(originalLine: string, editedLine: string) { - let startTrim = 0; - while (originalLine[startTrim] === editedLine[startTrim] && (originalLine[startTrim] === ' ' || originalLine[startTrim] === '\t')) { - startTrim++; - } - return startTrim; - } -} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index 2173dc88e31d..91dddc5b9cd0 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -18,7 +18,7 @@ import { SingleTextEdit } from '../../../../../common/core/textEdit.js'; import { ILanguageService } from '../../../../../common/languages/language.js'; import { LineTokens } from '../../../../../common/tokens/lineTokens.js'; import { TokenArray } from '../../../../../common/tokens/tokenArray.js'; -import { mapOutFalsy, n, rectToProps } from './utils.js'; +import { getPrefixTrim, mapOutFalsy, n, rectToProps } from './utils.js'; import { localize } from '../../../../../../nls.js'; import { IInlineEditsView } from './sideBySideDiff.js'; import { Range } from '../../../../../common/core/range.js'; @@ -274,35 +274,13 @@ export class LineReplacementView extends Disposable implements IInlineEditsView stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; - private readonly maxPrefixTrim = derived(this, reader => { - const maxPrefixTrim = Math.max(...this._replacements.flatMap(r => [r.originalRange, r.modifiedRange]).map(r => r.isSingleLine() ? r.startColumn - 1 : 0)); - if (maxPrefixTrim === 0) { - return 0; - } - - const textModel = this._editor.editor.getModel()!; - - const getLineTrimColLength = (line: string) => { - let i = 0; - while (i < line.length && line[i] === ' ') { i++; } - return i; - }; - - // TODO: make sure this works for tabs - return Math.min( - maxPrefixTrim, - ...this._originalRange.mapToLineArray(line => getLineTrimColLength(textModel.getLineContent(line))), - ...this._modifiedLines.map(line => getLineTrimColLength(line)) - ); - }); + private readonly _maxPrefixTrim = getPrefixTrim(this._replacements.flatMap(r => [r.originalRange, r.modifiedRange]), this._originalRange, this._modifiedLines, this._editor.editor); private readonly _modifiedLineElements = derived(reader => { - - const maxPrefixTrim = this.maxPrefixTrim.read(reader); - const lines = []; let requiredWidth = 0; + const maxPrefixTrim = this._maxPrefixTrim.prefixTrim; const modifiedBubbles = rangesToBubbleRanges(this._replacements.map(r => r.modifiedRange)).map(r => new Range(r.startLineNumber, r.startColumn - maxPrefixTrim, r.endLineNumber, r.endColumn - maxPrefixTrim)); const textModel = this._editor.model.get()!; @@ -351,12 +329,12 @@ export class LineReplacementView extends Disposable implements IInlineEditsView const PADDING = 4; const editorModel = this._editor.editor.getModel()!; - const maxPrefixTrim = this.maxPrefixTrim.read(reader); + const { prefixTrim, prefixLeftOffset } = this._maxPrefixTrim; - // TODO, correctly count tabs + // TODO: correctly count tabs const originalLineContents: string[] = []; this._originalRange.forEach(line => originalLineContents.push(editorModel.getLineContent(line))); - const maxOriginalLineLength = Math.max(...originalLineContents.map(l => l.length)) - maxPrefixTrim; + const maxOriginalLineLength = Math.max(...originalLineContents.map(l => l.length)) - prefixTrim; const maxLineWidth = Math.max(maxOriginalLineLength * w, requiredWidth); const startLineNumber = this._originalRange.startLineNumber; @@ -369,11 +347,9 @@ export class LineReplacementView extends Disposable implements IInlineEditsView return undefined; } - const prefixTrimOffset = maxPrefixTrim * w; - // Box Widget positioning const originalLinesOverlay = Rect.fromLeftTopWidthHeight( - editorLeftOffset + prefixTrimOffset, + editorLeftOffset + prefixLeftOffset, topOfOriginalLines, maxLineWidth, bottomOfOriginalLines - topOfOriginalLines + PADDING @@ -407,7 +383,7 @@ export class LineReplacementView extends Disposable implements IInlineEditsView lowerBackground, lowerText, padding: PADDING, - minContentWidthRequired: prefixTrimOffset + maxLineWidth + PADDING * 2, + minContentWidthRequired: maxLineWidth + PADDING * 2, }; }); From 1e191dcb44b41747da6b5e6eca88e63f793d3a64 Mon Sep 17 00:00:00 2001 From: eleanorjboyd Date: Fri, 24 Jan 2025 08:10:15 -0800 Subject: [PATCH 0858/3587] update distro --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c66ab02b1ef0..a36ef397b37c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.97.0", - "distro": "1b4e04ea3f9e9e068fd300cf935b008187847029", + "distro": "a58700b63af9b1fc5d17e3530bf7e07c8e62efd2", "author": { "name": "Microsoft Corporation" }, From aaa070a22327ae2d90f14f6bd003c162ecd4c82d Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:31:27 +0100 Subject: [PATCH 0859/3587] Add support for multi-line insertions with Ghost Text (#238685) * multi line insertions with Ghost Text * :lipstick: --- src/vs/editor/common/config/editorOptions.ts | 8 +++ .../browser/view/inlineEdits/view.ts | 61 ++++++++++++++++++- src/vs/monaco.d.ts | 1 + 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 189122c966dd..a3486183a459 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -4216,6 +4216,7 @@ export interface IInlineSuggestOptions { useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; useCodeOverlay?: 'never' | 'whenPossible' | 'moveCodeWhenPossible'; + useMultiLineGhostText?: boolean; useGutterIndicator?: boolean; }; @@ -4251,6 +4252,7 @@ class InlineEditorSuggest extends BaseEditorOption s.edits.experimental.useMixedLinesDiff); private readonly _useInterleavedLinesDiff = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useInterleavedLinesDiff); private readonly _useCodeOverlay = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useCodeOverlay); + private readonly _useMultiLineGhostText = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useMultiLineGhostText); private _previousView: { id: string; @@ -135,10 +138,32 @@ export class InlineEditsView extends Disposable { }) : undefined), )); + protected readonly _insertion = this._register(this._instantiationService.createInstance(GhostTextView, + this._editor, + { + ghostText: derived(reader => { + const state = this._uiState.read(reader)?.state; + if (!state || state.kind !== 'insertion') { return undefined; } + + const textModel = this._editor.getModel()!; + + // Try to not insert on the same line where there is other content + if (state.column === 1 && state.lineNumber > 1 && textModel.getLineLength(state.lineNumber) !== 0 && state.text.endsWith('\n') && !state.text.startsWith('\n')) { + const endOfLineColumn = textModel.getLineLength(state.lineNumber - 1) + 1; + return new GhostText(state.lineNumber - 1, [new GhostTextPart(endOfLineColumn, '\n' + state.text.slice(0, -1), false)]); + } + + return new GhostText(state.lineNumber, [new GhostTextPart(state.column, state.text, false)]); + }), + minReservedLineCount: constObservable(0), + targetTextModel: this._model.map(v => v?.textModel), + } + )); + private readonly _inlineDiffViewState = derived(this, reader => { const e = this._uiState.read(reader); if (!e || !e.state) { return undefined; } - if (e.state.kind === 'wordReplacements' || e.state.kind === 'lineReplacement') { + if (e.state.kind === 'wordReplacements' || e.state.kind === 'lineReplacement' || e.state.kind === 'insertion') { return undefined; } return { @@ -238,6 +263,10 @@ export class InlineEditsView extends Disposable { return 'deletion'; } + if (isSingleMultiLineInsertion(diff) && this._useMultiLineGhostText.read(reader)) { + return 'insertion'; + } + const useCodeOverlay = this._useCodeOverlay.read(reader); if (useCodeOverlay !== 'never') { const numOriginalLines = edit.originalLineRange.length; @@ -288,6 +317,16 @@ export class InlineEditsView extends Disposable { }; } + if (view === 'insertion') { + const change = inner[0]; + return { + kind: 'insertion' as const, + lineNumber: change.originalRange.startLineNumber, + column: change.originalRange.startColumn, + text: newText.getValueOfRange(change.modifiedRange), + }; + } + const replacements = inner.map(m => new SingleTextEdit(m.originalRange, newText.getValueOfRange(m.modifiedRange))); if (replacements.length === 0) { return undefined; @@ -341,6 +380,24 @@ function isSingleLineInsertionAfterPosition(diff: DetailedLineRangeMapping[], po } } +function isSingleMultiLineInsertion(diff: DetailedLineRangeMapping[]) { + const inner = diff.flatMap(d => d.innerChanges ?? []); + if (inner.length !== 1) { + return false; + } + + const change = inner[0]; + if (!change.originalRange.isEmpty()) { + return false; + } + + if (change.modifiedRange.startLineNumber === change.modifiedRange.endLineNumber) { + return false; + } + + return true; +} + function isSingleLineDeletion(diff: DetailedLineRangeMapping[]): boolean { return diff.every(m => m.innerChanges!.every(r => isDeletion(r))); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 8101016bf89c..4cb664194035 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4614,6 +4614,7 @@ declare namespace monaco.editor { useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; useCodeOverlay?: 'never' | 'whenPossible' | 'moveCodeWhenPossible'; + useMultiLineGhostText?: boolean; useGutterIndicator?: boolean; }; }; From b94947f22a5eac2e384b02f11d05a78caae37a31 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:36:43 +0100 Subject: [PATCH 0860/3587] Remove unused color (#238690) remove dead code --- .../browser/view/inlineEdits/sideBySideDiff.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index 6f1fed18b485..db3fe4132518 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -13,7 +13,7 @@ import { MenuId, MenuItemAction } from '../../../../../../platform/actions/commo import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { diffInserted, diffRemoved, editorHoverBorder } from '../../../../../../platform/theme/common/colorRegistry.js'; -import { registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; +import { registerColor } from '../../../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; @@ -95,18 +95,6 @@ export const modifiedBorder = registerColor( localize('inlineEdit.modifiedBorder', 'Border color for the modified text in inline edits.') ); -export const acceptedDecorationBackgroundColor = registerColor( - 'inlineEdit.acceptedBackground', - { - light: transparent(modifiedChangedTextOverlayColor, 0.75), - dark: transparent(modifiedChangedTextOverlayColor, 0.75), - hcDark: modifiedChangedTextOverlayColor, - hcLight: modifiedChangedTextOverlayColor - }, - localize('inlineEdit.acceptedBackground', 'Background color for the accepted text after applying an inline edit.'), - true -); - export interface IInlineEditsView { isHovered: IObservable; } From c53aa1bf736c4f86cd255d23d9d4960aa6d3fbb0 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 24 Jan 2025 09:00:34 -0800 Subject: [PATCH 0861/3587] chat: invoke save participants only when accepting changes (#238628) - Don't trigger save participants in edits from the edit file tool. Fixes the bad undo stack we discussed this morning. - When modifications are accepted, if the file is not dirty, then trigger save participants. This lets you still get nice formatting. --- .../browser/chatEditing/chatEditingSession.ts | 61 +++++++++++++++---- .../contrib/chat/browser/tools/tools.ts | 6 +- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index f6a2e9daa54f..f55b942fd7c7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -4,14 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { ITask, Sequencer, timeout } from '../../../../../base/common/async.js'; +import { VSBuffer } from '../../../../../base/common/buffer.js'; import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { Emitter } from '../../../../../base/common/event.js'; -import { Disposable, dispose } from '../../../../../base/common/lifecycle.js'; +import { StringSHA1 } from '../../../../../base/common/hash.js'; +import { Disposable, DisposableMap, dispose } from '../../../../../base/common/lifecycle.js'; import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js'; import { autorun, derived, IObservable, IReader, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; +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'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { ITextModel } from '../../../../../editor/common/model.js'; @@ -19,29 +24,26 @@ import { IModelService } from '../../../../../editor/common/services/model.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { localize } from '../../../../../nls.js'; import { EditorActivation } from '../../../../../platform/editor/common/editor.js'; +import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IEditorCloseEvent } from '../../../../common/editor.js'; +import { IEditorCloseEvent, 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, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IModifiedFileEntry, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; -import { ChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; -import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; -import { isEqual, joinPath } from '../../../../../base/common/resources.js'; -import { StringSHA1 } from '../../../../../base/common/hash.js'; -import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; -import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { IOffsetEdit, ISingleOffsetEdit, OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; -import { ILogService } from '../../../../../platform/log/common/log.js'; import { IChatService } from '../../common/chatService.js'; -import { INotebookService } from '../../../notebook/common/notebookService.js'; +import { ChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; import { ChatEditingModifiedNotebookEntry } from './chatEditingModifiedNotebookEntry.js'; -import { isNotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js'; +import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; const STORAGE_CONTENTS_FOLDER = 'contents'; const STORAGE_STATE_FILE = 'state.json'; @@ -174,6 +176,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio @IEditorService private readonly _editorService: IEditorService, @IChatService private readonly _chatService: IChatService, @INotebookService private readonly _notebookService: INotebookService, + @ITextFileService private readonly _textFileService: ITextFileService, ) { super(); } @@ -193,6 +196,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(); })); @@ -225,6 +229,39 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return storage.storeState(state); } + private _triggerSaveParticipantsOnAccept() { + const im = this._register(new DisposableMap()); + const attachToEntry = (entry: ChatEditingModifiedFileEntry) => { + return autorunDelta(entry.state, ({ lastValue, newValue }) => { + if (newValue === WorkingSetEntryState.Accepted && lastValue === WorkingSetEntryState.Modified) { + // Don't save a file if there's still pending changes. If there's not (e.g. + // the agentic flow with autosave) then save again to trigger participants. + if (!this._textFileService.isDirty(entry.modifiedURI)) { + this._textFileService.save(entry.modifiedURI, { + reason: SaveReason.EXPLICIT, + force: true, + ignoreErrorHandler: true, + }).catch(() => { + // ignored + }); + } + } + }); + }; + + this._register(autorunIterableDelta( + reader => this._entriesObs.read(reader), + ({ addedValues, removedValues }) => { + for (const entry of addedValues) { + im.set(entry, attachToEntry(entry)); + } + for (const entry of removedValues) { + im.deleteAndDispose(entry); + } + } + )); + } + private _trackCurrentEditorsInWorkingSet(e?: IEditorCloseEvent) { const existingTransientEntries = new ResourceSet(); for (const file of this._workingSet.keys()) { diff --git a/src/vs/workbench/contrib/chat/browser/tools/tools.ts b/src/vs/workbench/contrib/chat/browser/tools/tools.ts index 29746370f035..259e9ecc71ea 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/tools.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/tools.ts @@ -12,6 +12,7 @@ import { localize } from '../../../../../nls.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; +import { SaveReason } from '../../../../common/editor.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { IChatEditingService } from '../../common/chatEditingService.js'; @@ -171,7 +172,10 @@ class EditTool implements IToolImpl { dispose.dispose(); }); - await this.textFileService.save(uri); + await this.textFileService.save(uri, { + reason: SaveReason.AUTO, + skipSaveParticipants: true, + }); return { content: [{ kind: 'text', value: 'The file was edited successfully' }] From c06e38cb493d6c1ac276c94bbdd774f92eca6b89 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Fri, 24 Jan 2025 09:01:06 -0800 Subject: [PATCH 0862/3587] Update distro --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a36ef397b37c..4ad642bd0140 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.97.0", - "distro": "a58700b63af9b1fc5d17e3530bf7e07c8e62efd2", + "distro": "69135fca1356098ff5746c9225339cdd52f21844", "author": { "name": "Microsoft Corporation" }, From ef61f9dc6f2ac0873638fceadb21d94bc10a1182 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 24 Jan 2025 09:29:22 -0800 Subject: [PATCH 0863/3587] Rename "Normal" to "Edit" (#238695) --- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index f135e364ac1f..d19b3cf34f02 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -1696,7 +1696,7 @@ class ToggleAgentCheckActionViewItem extends MenuEntryActionViewItem { { ...this.action, id: 'normalMode', - label: localize('chat.normalMode', "Normal"), + label: localize('chat.normalMode', "Edit"), class: undefined, enabled: true, checked: !this.action.checked, From 340c5738d6f500864d47e8a7f1b2788271d86cb4 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Fri, 24 Jan 2025 18:52:00 +0100 Subject: [PATCH 0864/3587] Fix range end position in piece tree buffer (#238658) * Fix range end position in piece tree buffer also fix refresh ranges in token guess * Simplify first tree sitter tokens * Compute newRange end for changes in tree sitter * Fix tests --- .../pieceTreeTextBuffer.ts | 18 +++------------ src/vs/editor/common/model/textModel.ts | 1 - src/vs/editor/common/model/tokenStore.ts | 22 +++++++++---------- .../model/treeSitterTokenStoreService.ts | 9 ++++---- .../treeSitter/treeSitterParserService.ts | 20 ++++++++--------- src/vs/editor/common/textModelEvents.ts | 5 ----- .../widget/observableCodeEditor.test.ts | 16 +++++++------- src/vs/monaco.d.ts | 4 ---- .../test/browser/extHostDocumentData.test.ts | 6 ----- .../extHostDocumentSaveParticipant.test.ts | 2 -- .../browser/treeSitterTokenizationFeature.ts | 21 +++++------------- 11 files changed, 42 insertions(+), 82 deletions(-) diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts index 1b812d383bc1..42d1995e3679 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts @@ -27,10 +27,6 @@ export interface IValidatedEditOperation { isAutoWhitespaceEdit: boolean; } -export interface IValidatedEditOperationWithReverseRange extends IValidatedEditOperation { - reverseRange: Range; -} - interface IReverseSingleEditOperation extends IValidEditOperation { sortIndex: number; } @@ -327,7 +323,7 @@ export class PieceTreeTextBuffer extends Disposable implements ITextBuffer { } // Delta encode operations - const reverseRanges = PieceTreeTextBuffer._getInverseEditRanges(operations); + const reverseRanges = (computeUndoEdits || recordTrimAutoWhitespace ? PieceTreeTextBuffer._getInverseEditRanges(operations) : []); const newTrimAutoWhitespaceCandidates: { lineNumber: number; oldContent: string }[] = []; if (recordTrimAutoWhitespace) { for (let i = 0; i < operations.length; i++) { @@ -377,19 +373,12 @@ export class PieceTreeTextBuffer extends Disposable implements ITextBuffer { } } - const operationsWithReverseRanges: IValidatedEditOperationWithReverseRange[] = new Array(operations.length); - for (let i = 0; i < operations.length; i++) { - operationsWithReverseRanges[i] = { - ...operations[i], - reverseRange: reverseRanges[i] - }; - } this._mightContainRTL = mightContainRTL; this._mightContainUnusualLineTerminators = mightContainUnusualLineTerminators; this._mightContainNonBasicASCII = mightContainNonBasicASCII; - const contentChanges = this._doApplyEdits(operationsWithReverseRanges); + const contentChanges = this._doApplyEdits(operations); let trimAutoWhitespaceLineNumbers: number[] | null = null; if (recordTrimAutoWhitespace && newTrimAutoWhitespaceCandidates.length > 0) { @@ -487,7 +476,7 @@ export class PieceTreeTextBuffer extends Disposable implements ITextBuffer { }; } - private _doApplyEdits(operations: IValidatedEditOperationWithReverseRange[]): IInternalModelContentChange[] { + private _doApplyEdits(operations: IValidatedEditOperation[]): IInternalModelContentChange[] { operations.sort(PieceTreeTextBuffer._sortOpsDescending); const contentChanges: IInternalModelContentChange[] = []; @@ -520,7 +509,6 @@ export class PieceTreeTextBuffer extends Disposable implements ITextBuffer { contentChanges.push({ range: contentChangeRange, rangeLength: op.rangeLength, - rangeEndPosition: op.reverseRange.getEndPosition(), text: op.text, rangeOffset: op.rangeOffset, forceMoveMarkers: op.forceMoveMarkers diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index cac9f3b882ed..e54c9ebd69da 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -460,7 +460,6 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati range: range, rangeOffset: rangeOffset, rangeLength: rangeLength, - rangeEndPosition: rangeEndPosition, text: text, }], eol: this._buffer.getEOL(), diff --git a/src/vs/editor/common/model/tokenStore.ts b/src/vs/editor/common/model/tokenStore.ts index 9ad6e415308c..862b300c1a43 100644 --- a/src/vs/editor/common/model/tokenStore.ts +++ b/src/vs/editor/common/model/tokenStore.ts @@ -210,15 +210,15 @@ export class TokenStore implements IDisposable { this._root = this.createFromUpdates(tokens, true); } - private createFromUpdates(tokens: TokenUpdate[], isNew?: boolean): Node { + private createFromUpdates(tokens: TokenUpdate[], needsRefresh?: boolean): Node { let newRoot: Node = { length: tokens[0].length, token: tokens[0].token, height: 0, - needsRefresh: isNew + needsRefresh }; for (let j = 1; j < tokens.length; j++) { - newRoot = append(newRoot, { length: tokens[j].length, token: tokens[j].token, height: 0, needsRefresh: isNew }); + newRoot = append(newRoot, { length: tokens[j].length, token: tokens[j].token, height: 0, needsRefresh }); } return newRoot; } @@ -227,22 +227,22 @@ export class TokenStore implements IDisposable { * * @param tokens tokens are in sequence in the document. */ - update(length: number, tokens: TokenUpdate[]) { + update(length: number, tokens: TokenUpdate[], needsRefresh?: boolean) { if (tokens.length === 0) { return; } - this.replace(length, tokens[0].startOffsetInclusive, tokens); + this.replace(length, tokens[0].startOffsetInclusive, tokens, needsRefresh); } delete(length: number, startOffset: number) { - this.replace(length, startOffset, []); + this.replace(length, startOffset, [], true); } /** * * @param tokens tokens are in sequence in the document. */ - private replace(length: number, updateOffsetStart: number, tokens: TokenUpdate[]) { + private replace(length: number, updateOffsetStart: number, tokens: TokenUpdate[], needsRefresh?: boolean) { const firstUnchangedOffsetAfterUpdate = updateOffsetStart + length; // Find the last unchanged node preceding the update const precedingNodes: Node[] = []; @@ -260,7 +260,7 @@ export class TokenStore implements IDisposable { continue; } else if (isLeaf(node.node) && (currentOffset < updateOffsetStart)) { // We have a partial preceding node - precedingNodes.push({ length: updateOffsetStart - currentOffset, token: node.node.token, height: 0 }); + precedingNodes.push({ length: updateOffsetStart - currentOffset, token: node.node.token, height: 0, needsRefresh: needsRefresh || node.node.needsRefresh }); // Node could also be postceeding, so don't continue } @@ -274,7 +274,7 @@ export class TokenStore implements IDisposable { continue; } else if (isLeaf(node.node) && (currentOffset + node.node.length >= firstUnchangedOffsetAfterUpdate)) { // we have a partial postceeding node - postcedingNodes.push({ length: currentOffset + node.node.length - firstUnchangedOffsetAfterUpdate, token: node.node.token, height: 0 }); + postcedingNodes.push({ length: currentOffset + node.node.length - firstUnchangedOffsetAfterUpdate, token: node.node.token, height: 0, needsRefresh: needsRefresh || node.node.needsRefresh }); continue; } @@ -290,7 +290,7 @@ export class TokenStore implements IDisposable { let allNodes: Node[]; if (tokens.length > 0) { - allNodes = precedingNodes.concat(this.createFromUpdates(tokens), postcedingNodes); + allNodes = precedingNodes.concat(this.createFromUpdates(tokens, needsRefresh), postcedingNodes); } else { allNodes = precedingNodes.concat(postcedingNodes); } @@ -445,7 +445,7 @@ export class TokenStore implements IDisposable { const indent = ' '.repeat(depth); if (isLeaf(node)) { - result.push(`${indent}Leaf(length: ${node.length}, token: ${node.token})\n`); + result.push(`${indent}Leaf(length: ${node.length}, token: ${node.token}, refresh: ${node.needsRefresh})\n`); } else { result.push(`${indent}List(length: ${node.length})\n`); // Push children in reverse order so they get processed left-to-right diff --git a/src/vs/editor/common/model/treeSitterTokenStoreService.ts b/src/vs/editor/common/model/treeSitterTokenStoreService.ts index 56302824adae..4295bab91ac8 100644 --- a/src/vs/editor/common/model/treeSitterTokenStoreService.ts +++ b/src/vs/editor/common/model/treeSitterTokenStoreService.ts @@ -48,17 +48,18 @@ class TreeSitterTokenizationStoreService implements ITreeSitterTokenizationStore storeInfo.guessVersion = e.versionId; for (const change of e.changes) { - storeInfo.store.markForRefresh(change.rangeOffset, change.rangeOffset + change.rangeLength); if (change.text.length > change.rangeLength) { const oldToken = storeInfo.store.getTokenAt(change.rangeOffset)!; // Insert. Just grow the token at this position to include the insert. const newToken = { startOffsetInclusive: oldToken.startOffsetInclusive, length: oldToken.length + change.text.length - change.rangeLength, token: oldToken.token }; - storeInfo.store.update(oldToken.length, [newToken]); + storeInfo.store.update(oldToken.length, [newToken], true); } else if (change.text.length < change.rangeLength) { // Delete. Delete the tokens at the corresponding range. const deletedCharCount = change.rangeLength - change.text.length; storeInfo.store.delete(deletedCharCount, change.rangeOffset); } + const refreshLength = change.rangeLength > change.text.length ? change.rangeLength : change.text.length; + storeInfo.store.markForRefresh(change.rangeOffset, change.rangeOffset + refreshLength); } })); disposables.add(model.onWillDispose(() => { @@ -105,8 +106,8 @@ class TreeSitterTokenizationStoreService implements ITreeSitterTokenizationStore existingTokens.accurateVersion = version; for (const update of updates) { - const lastToken = update.newTokens[update.newTokens.length - 1]; - const oldRangeLength = (existingTokens.guessVersion >= version) ? (lastToken.startOffsetInclusive + lastToken.length - update.newTokens[0].startOffsetInclusive) : update.oldRangeLength; + const lastToken = update.newTokens.length > 0 ? update.newTokens[update.newTokens.length - 1] : undefined; + const oldRangeLength = ((existingTokens.guessVersion >= version) && lastToken) ? (lastToken.startOffsetInclusive + lastToken.length - update.newTokens[0].startOffsetInclusive) : update.oldRangeLength; existingTokens.store.update(oldRangeLength, update.newTokens); } } diff --git a/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts index 11175de6a0aa..2d0ac90468ee 100644 --- a/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts +++ b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts @@ -24,6 +24,7 @@ import { PromiseResult } from '../../../../base/common/observable.js'; import { Range } from '../../core/range.js'; import { Position } from '../../core/position.js'; import { LimitedQueue } from '../../../../base/common/async.js'; +import { TextLength } from '../../core/textLength.js'; const EDITOR_TREESITTER_TELEMETRY = 'editor.experimental.treeSitterTelemetry'; const MODULE_LOCATION_SUBPATH = `@vscode/tree-sitter-wasm/wasm`; @@ -350,22 +351,19 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul private _applyEdits(changes: IModelContentChange[], version: number) { for (const change of changes) { - this._tree?.edit({ + const originalTextLength = TextLength.ofRange(Range.lift(change.range)); + const newTextLength = TextLength.ofText(change.text); + const summedTextLengths = change.text.length === 0 ? newTextLength : originalTextLength.add(newTextLength); + const edit = { startIndex: change.rangeOffset, oldEndIndex: change.rangeOffset + change.rangeLength, newEndIndex: change.rangeOffset + change.text.length, startPosition: { row: change.range.startLineNumber - 1, column: change.range.startColumn - 1 }, oldEndPosition: { row: change.range.endLineNumber - 1, column: change.range.endColumn - 1 }, - newEndPosition: { row: change.rangeEndPosition.lineNumber - 1, column: change.rangeEndPosition.column - 1 } - }); - this._lastFullyParsedWithEdits?.edit({ - startIndex: change.rangeOffset, - oldEndIndex: change.rangeOffset + change.rangeLength, - newEndIndex: change.rangeOffset + change.text.length, - startPosition: { row: change.range.startLineNumber - 1, column: change.range.startColumn - 1 }, - oldEndPosition: { row: change.range.endLineNumber - 1, column: change.range.endColumn - 1 }, - newEndPosition: { row: change.rangeEndPosition.lineNumber - 1, column: change.rangeEndPosition.column - 1 } - }); + newEndPosition: { row: change.range.startLineNumber + summedTextLengths.lineCount - 1, column: summedTextLengths.columnCount } + }; + this._tree?.edit(edit); + this._lastFullyParsedWithEdits?.edit(edit); } this._editVersion = version; } diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index 8cc19576d3ee..c35d0472106b 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Position } from './core/position.js'; import { IRange } from './core/range.js'; import { Selection } from './core/selection.js'; import { IModelDecoration, InjectedTextOptions } from './model.js'; @@ -46,10 +45,6 @@ export interface IModelContentChange { * The length of the range that got replaced. */ readonly rangeLength: number; - /** - * The new end position of the range that got replaced. - */ - readonly rangeEndPosition: Position; /** * The new text for the range. */ diff --git a/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts b/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts index 501d92ea85ca..7bb14f138713 100644 --- a/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts +++ b/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts @@ -82,9 +82,9 @@ suite("CodeEditorWidget", () => { assert.deepStrictEqual(log.getAndClearEntries(), [ 'handle change: editor.onDidType "abc"', - 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"rangeEndPosition":{"lineNumber":1,"column":2},"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', - 'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"rangeEndPosition":{"lineNumber":1,"column":3},"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}', - 'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"rangeEndPosition":{"lineNumber":1,"column":4},"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}', + 'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}', 'handle change: editor.selections {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', 'running derived: selection: [1,4 -> 1,4], value: 4', ]); @@ -96,9 +96,9 @@ suite("CodeEditorWidget", () => { assert.deepStrictEqual(log.getAndClearEntries(), [ 'handle change: editor.onDidType "abc"', - 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"rangeEndPosition":{"lineNumber":1,"column":2},"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', - 'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"rangeEndPosition":{"lineNumber":1,"column":3},"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}', - 'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"rangeEndPosition":{"lineNumber":1,"column":4},"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}', + 'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}', 'handle change: editor.selections {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', 'running derived: selection: [1,4 -> 1,4], value: 4', ]); @@ -134,7 +134,7 @@ suite("CodeEditorWidget", () => { ">>> before get", "<<< after get", 'handle change: editor.onDidType "a"', - 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"rangeEndPosition":{"lineNumber":1,"column":2},"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', 'handle change: editor.selections {"selection":"[1,2 -> 1,2]","modelVersionId":2,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', "running derived: selection: [1,2 -> 1,2], value: 2", ]); @@ -172,7 +172,7 @@ suite("CodeEditorWidget", () => { "running derived: selection: [1,2 -> 1,2], value: 2", "<<< after get", 'handle change: editor.onDidType "a"', - 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"rangeEndPosition":{"lineNumber":1,"column":2},"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', 'handle change: editor.selections {"selection":"[1,2 -> 1,2]","modelVersionId":2,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', "running derived: selection: [1,2 -> 1,2], value: 2", ]); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 4cb664194035..f64b9dda5d55 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2914,10 +2914,6 @@ declare namespace monaco.editor { * The length of the range that got replaced. */ readonly rangeLength: number; - /** - * The new end position of the range that got replaced. - */ - readonly rangeEndPosition: Position; /** * The new text for the range. */ diff --git a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts index 1c05433c1170..926b8fdc6d29 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts @@ -104,7 +104,6 @@ suite('ExtHostDocumentData', () => { range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, rangeOffset: undefined!, rangeLength: undefined!, - rangeEndPosition: undefined!, text: '\t ' }], eol: undefined!, @@ -164,7 +163,6 @@ suite('ExtHostDocumentData', () => { range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 }, rangeOffset: undefined!, rangeLength: undefined!, - rangeEndPosition: undefined!, text: '' }], eol: undefined!, @@ -185,7 +183,6 @@ suite('ExtHostDocumentData', () => { range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 }, rangeOffset: undefined!, rangeLength: undefined!, - rangeEndPosition: undefined!, text: 'is could be' }], eol: undefined!, @@ -206,7 +203,6 @@ suite('ExtHostDocumentData', () => { range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 }, rangeOffset: undefined!, rangeLength: undefined!, - rangeEndPosition: undefined!, text: 'is could be\na line with number' }], eol: undefined!, @@ -230,7 +226,6 @@ suite('ExtHostDocumentData', () => { range: { startLineNumber: 1, startColumn: 3, endLineNumber: 2, endColumn: 6 }, rangeOffset: undefined!, rangeLength: undefined!, - rangeEndPosition: undefined!, text: '' }], eol: undefined!, @@ -421,7 +416,6 @@ suite('ExtHostDocumentData updates line mapping', () => { range: range, rangeOffset: undefined!, rangeLength: undefined!, - rangeEndPosition: undefined!, text: text }], eol: eol!, diff --git a/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts index b5b3933c529a..965bb16223ed 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts @@ -294,7 +294,6 @@ suite('ExtHostDocumentSaveParticipant', () => { documents.$acceptModelChanged(resource, { changes: [{ range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, - rangeEndPosition: undefined!, rangeOffset: undefined!, rangeLength: undefined!, text: 'bar' @@ -330,7 +329,6 @@ suite('ExtHostDocumentSaveParticipant', () => { changes: [{ range, text, - rangeEndPosition: undefined!, rangeOffset: undefined!, rangeLength: undefined!, }], diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts index f168a28f2897..a1a3b86b8183 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts @@ -136,26 +136,17 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi })); } - private _createEmptyTokens(captures: { tree: ITreeSitterParseResult | undefined; captures: QueryCapture[] }, modelEndOffset: number) { + private _createEmptyTokens(textModel: ITextModel) { const languageId = this._languageIdCodec.encodeLanguageId(this._languageId); const emptyToken = this._emptyToken(languageId); - const capturedTokens = this._createTokensFromCaptures(captures.tree, captures.captures, 0, modelEndOffset); - if (!capturedTokens) { - return; - } - const emptyTokens: EndOffsetToken[] = capturedTokens.endOffsets.map(capture => ({ endOffset: capture.endOffset, metadata: emptyToken })); - return this._rangeTokensAsUpdates(0, emptyTokens); + const modelEndOffset = textModel.getValueLength(); + + const emptyTokens: TokenUpdate[] = [{ token: emptyToken, length: modelEndOffset, startOffsetInclusive: 0 }]; + return emptyTokens; } private _firstTreeUpdate(textModel: ITextModel, versionId: number, ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[]) { - const modelEndOffset = textModel.getValueLength(); - const editorEndPosition = textModel.getPositionAt(modelEndOffset); - const captures = this._getTreeAndCaptures(new Range(1, 1, editorEndPosition.lineNumber, editorEndPosition.column), textModel); - if (captures.captures.length === 0) { - return; - } - // Make empty tokens to populate the store - const tokens: TokenUpdate[] = this._createEmptyTokens(captures, modelEndOffset) ?? []; + const tokens: TokenUpdate[] = this._createEmptyTokens(textModel); this._tokenizationStoreService.setTokens(textModel, tokens); this._setViewPortTokens(textModel, versionId); } From 426019609e1ffa091de25bbfe70a8d8ecc47a838 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 24 Jan 2025 09:58:16 -0800 Subject: [PATCH 0865/3587] Sub-pixel offset rendering Part of #225428 --- src/vs/editor/browser/gpu/atlas/textureAtlas.ts | 7 ++++++- src/vs/editor/browser/gpu/raster/glyphRasterizer.ts | 5 ++++- .../gpu/renderStrategy/fullFileRenderStrategy.ts | 11 +++++------ .../gpu/renderStrategy/viewportRenderStrategy.ts | 11 +++++------ .../browser/viewParts/viewLinesGpu/viewLinesGpu.ts | 2 +- src/vs/editor/contrib/gpu/browser/gpuActions.ts | 2 +- .../test/browser/gpu/atlas/textureAtlas.test.ts | 4 ++-- 7 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts index 32987628de34..67c6942c505c 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts @@ -110,11 +110,16 @@ export class TextureAtlas extends Disposable { this._onDidDeleteGlyphs.fire(); } - getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly { + getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number, x: number): Readonly { // TODO: Encode font size and family into key // Ignore metadata that doesn't affect the glyph tokenMetadata &= ~(MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK); + // Add x offset for sub-pixel rendering to the unused portion or tokenMetadata. This + // converts the decimal part of the x to a range from 0 to 9, where 0 = 0.0px x offset, + // 9 = 0.9px x offset + tokenMetadata |= Math.floor((x % 1) * 10); + // Warm up common glyphs if (!this._warmedUpRasterizers.has(rasterizer.id)) { this._warmUpAtlas(rasterizer); diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 7c9c8d9a2863..49796167f04d 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -112,6 +112,9 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._ctx.save(); + const xSubPixelOffset = (tokenMetadata & 0b1111) / 10; + // console.log('xSubPixelOffset', xSubPixelOffset); + const bgId = TokenMetadata.getBackground(tokenMetadata); const bg = colorMap[bgId]; @@ -157,7 +160,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._ctx.globalAlpha = decorationStyleSet.opacity; } - this._ctx.fillText(chars, originX, originY); + this._ctx.fillText(chars, originX + xSubPixelOffset, originY); this._ctx.restore(); const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); diff --git a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index ec00a4e72d1b..71f6cc94228f 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -272,8 +272,6 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { this._scrollInitialized = true; } - let localContentWidth = 0; - // Update cell data const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]); const lineIndexCount = FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell; @@ -445,7 +443,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { } const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity); - glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId); + glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX); absoluteOffsetY = Math.round( // Top of layout box (includes line height) @@ -461,14 +459,13 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { ); cellIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell; - cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.round(absoluteOffsetX); + cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.floor(absoluteOffsetX); cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = absoluteOffsetY; cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex; cellBuffer[cellIndex + CellBufferInfo.TextureIndex] = glyph.pageIndex; // Adjust the x pixel offset for the next character absoluteOffsetX += charWidth; - localContentWidth = Math.max(localContentWidth, absoluteOffsetX); } tokenStartIndex = tokenEndIndex; @@ -504,7 +501,9 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { this._visibleObjectCount = visibleObjectCount; - return { localContentWidth }; + return { + localContentWidth: absoluteOffsetX + }; } draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void { diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts index 5f3d2a349566..82b7e75fd3b4 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -224,8 +224,6 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { this._scrollInitialized = true; } - let localContentWidth = 0; - // Zero out cell buffer or rebuild if needed if (this._cellBindBufferLineCapacity < viewportData.endLineNumber - viewportData.startLineNumber + 1) { this._rebuildCellBuffer(viewportData.endLineNumber - viewportData.startLineNumber + 1); @@ -348,7 +346,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { } const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity); - glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId); + glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX); absoluteOffsetY = Math.round( // Top of layout box (includes line height) @@ -364,14 +362,13 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { ); cellIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell; - cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.round(absoluteOffsetX); + cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.floor(absoluteOffsetX); cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = absoluteOffsetY; cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex; cellBuffer[cellIndex + CellBufferInfo.TextureIndex] = glyph.pageIndex; // Adjust the x pixel offset for the next character absoluteOffsetX += charWidth; - localContentWidth = Math.max(localContentWidth, absoluteOffsetX); } tokenStartIndex = tokenEndIndex; @@ -397,7 +394,9 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1; this._visibleObjectCount = visibleObjectCount; - return { localContentWidth }; + return { + localContentWidth: Math.ceil(absoluteOffsetX / dpr) + }; } draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void { diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index 99312616d3d3..f7485a2f2b7b 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -478,7 +478,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { // Track the largest local content width so far in this session and use it as the scroll // width. This is how the DOM renderer works as well, so you may not be able to scroll to // the right in a file with long lines until you scroll down. - this._maxLocalContentWidthSoFar = Math.max(this._maxLocalContentWidthSoFar, localContentWidth); + this._maxLocalContentWidthSoFar = Math.max(this._maxLocalContentWidthSoFar, localContentWidth / this._viewGpuContext.devicePixelRatio.get()); this._context.viewModel.viewLayout.setMaxLineWidth(this._maxLocalContentWidthSoFar); this._viewGpuContext.scrollWidthElement.setWidth(this._context.viewLayout.getScrollWidth()); diff --git a/src/vs/editor/contrib/gpu/browser/gpuActions.ts b/src/vs/editor/contrib/gpu/browser/gpuActions.ts index 58adf582bdd7..a1a3afe9a5bb 100644 --- a/src/vs/editor/contrib/gpu/browser/gpuActions.ts +++ b/src/vs/editor/contrib/gpu/browser/gpuActions.ts @@ -118,7 +118,7 @@ class DebugEditorGpuRendererAction extends EditorAction { } const tokenMetadata = 0; const charMetadata = 0; - const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, tokenMetadata, charMetadata); + const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, tokenMetadata, charMetadata, 0); if (!rasterizedGlyph) { return; } diff --git a/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts index 816fa17cf811..2b0e55aab2c1 100644 --- a/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts @@ -17,13 +17,13 @@ const blackInt = 0x000000FF; const nullCharMetadata = 0x0; let lastUniqueGlyph: string | undefined; -function getUniqueGlyphId(): [chars: string, tokenMetadata: number, charMetadata: number] { +function getUniqueGlyphId(): [chars: string, tokenMetadata: number, charMetadata: number, x: number] { if (!lastUniqueGlyph) { lastUniqueGlyph = 'a'; } else { lastUniqueGlyph = String.fromCharCode(lastUniqueGlyph.charCodeAt(0) + 1); } - return [lastUniqueGlyph, blackInt, nullCharMetadata]; + return [lastUniqueGlyph, blackInt, nullCharMetadata, 0]; } class TestGlyphRasterizer implements IGlyphRasterizer { From 720f2eb1fb2bbb28bb87d037a09295ea666ae86c Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 24 Jan 2025 09:59:58 -0800 Subject: [PATCH 0866/3587] Implement past tense invocation message and tooltip for tool calls (#238635) * Implement past tense invocation message and tooltip for tool calls * Fix * Clean up --- .../api/common/extHostLanguageModelTools.ts | 14 +++++++----- .../chatToolInvocationPart.ts | 22 +++++++++++++++---- .../chat/browser/languageModelToolsService.ts | 2 +- .../contrib/chat/browser/media/chat.css | 21 +++++++++++------- .../chatProgressTypes/chatToolInvocation.ts | 4 ++++ .../contrib/chat/common/chatService.ts | 4 ++++ .../chat/common/languageModelToolsService.ts | 2 ++ ...scode.proposed.chatParticipantPrivate.d.ts | 5 +++++ 8 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index c5d845256956..d07093176f2f 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -12,7 +12,7 @@ import { revive } from '../../../base/common/marshalling.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { IPreparedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; -import { isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; +import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js'; import * as typeConvert from './extHostTypeConverters.js'; @@ -136,16 +136,18 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape return undefined; } + if (result.pastTenseMessage || result.tooltip) { + checkProposedApiEnabled(item.extension, 'chatParticipantPrivate'); + } + return { confirmationMessages: result.confirmationMessages ? { title: result.confirmationMessages.title, message: typeof result.confirmationMessages.message === 'string' ? result.confirmationMessages.message : typeConvert.MarkdownString.from(result.confirmationMessages.message), } : undefined, - invocationMessage: typeof result.invocationMessage === 'string' ? - result.invocationMessage : - (result.invocationMessage ? - typeConvert.MarkdownString.from(result.invocationMessage) : - undefined), + invocationMessage: typeConvert.MarkdownString.fromStrict(result.invocationMessage), + pastTenseMessage: typeConvert.MarkdownString.fromStrict(result.pastTenseMessage), + tooltip: result.tooltip ? typeConvert.MarkdownString.fromStrict(result.tooltip) : undefined, }; } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts index 2d065692bdc6..ee51460fad83 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts @@ -6,10 +6,11 @@ import * as dom from '../../../../../base/browser/dom.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter, Relay } from '../../../../../base/common/event.js'; -import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; import { localize } from '../../../../../nls.js'; +import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IChatProgressMessage, IChatToolInvocation, IChatToolInvocationSerialized } from '../../common/chatService.js'; import { IChatRendererContent } from '../../common/chatViewModel.js'; @@ -75,6 +76,7 @@ class ChatToolInvocationSubPart extends Disposable { context: IChatContentPartRenderContext, renderer: MarkdownRenderer, @IInstantiationService instantiationService: IInstantiationService, + @IHoverService hoverService: IHoverService, ) { super(); @@ -93,9 +95,17 @@ class ChatToolInvocationSubPart extends Disposable { this._onDidChangeHeight.input = confirmWidget.onDidChangeHeight; toolInvocation.confirmed.p.then(() => this._onNeedsRerender.fire()); } else { - const content = typeof toolInvocation.invocationMessage === 'string' ? - new MarkdownString().appendText(toolInvocation.invocationMessage + '…') : - new MarkdownString(toolInvocation.invocationMessage.value + '…'); + let content: IMarkdownString; + if (toolInvocation.isComplete && toolInvocation.isConfirmed !== false && toolInvocation.pastTenseMessage) { + content = typeof toolInvocation.pastTenseMessage === 'string' ? + new MarkdownString().appendText(toolInvocation.pastTenseMessage) : + toolInvocation.pastTenseMessage; + } else { + content = typeof toolInvocation.invocationMessage === 'string' ? + new MarkdownString().appendText(toolInvocation.invocationMessage + '…') : + new MarkdownString(toolInvocation.invocationMessage.value + '…'); + } + const progressMessage: IChatProgressMessage = { kind: 'progressMessage', content @@ -105,6 +115,10 @@ class ChatToolInvocationSubPart extends Disposable { toolInvocation.isComplete ? Codicon.check : undefined; const progressPart = this._register(instantiationService.createInstance(ChatProgressContentPart, progressMessage, renderer, context, undefined, true, iconOverride)); + if (toolInvocation.tooltip) { + this._register(hoverService.setupDelayedHover(progressPart.domNode, { content: toolInvocation.tooltip, additionalClasses: ['chat-tool-hover'] })); + } + this.domNode = progressPart.domNode; } diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index d53c0db6859a..9e3d66aeca9d 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -187,7 +187,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo const defaultMessage = localize('toolInvocationMessage', "Using {0}", `"${tool.data.displayName}"`); const invocationMessage = prepared?.invocationMessage ?? defaultMessage; if (tool.data.id !== 'vscode_editFile') { - toolInvocation = new ChatToolInvocation(invocationMessage, prepared?.confirmationMessages); + toolInvocation = new ChatToolInvocation(invocationMessage, prepared?.pastTenseMessage, prepared?.tooltip, prepared?.confirmationMessages); model.acceptResponseProgress(request, toolInvocation); if (prepared?.confirmationMessages) { const userConfirmed = await toolInvocation.confirmed.p; diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index d315ab07737c..d35fcc065f51 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -392,14 +392,15 @@ have to be updated for changes to the rules above, or to support more deeply nes max-width: 100%; } -.interactive-item-container .monaco-tokenized-source, -.interactive-item-container code { - font-family: var(--monaco-monospace-font); - font-size: 12px; - color: var(--vscode-textPreformat-foreground); - background-color: var(--vscode-textPreformat-background); - padding: 1px 3px; - border-radius: 4px; +.chat-tool-hover, .interactive-item-container { + .monaco-tokenized-source, code { + font-family: var(--monaco-monospace-font); + font-size: 12px; + color: var(--vscode-textPreformat-foreground); + background-color: var(--vscode-textPreformat-background); + padding: 1px 3px; + border-radius: 4px; + } } .interactive-item-container.interactive-item-compact { @@ -1447,6 +1448,10 @@ have to be updated for changes to the rules above, or to support more deeply nes padding: 4px 8px; } +.interactive-item-container .chat-confirmation-widget .rendered-markdown [data-code] { + margin-bottom: 8px; +} + .interactive-item-container .chat-command-button .monaco-button .codicon { margin-left: 0; margin-top: 1px; diff --git a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts index 76e572c91df5..2b216ac49bce 100644 --- a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts +++ b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts @@ -38,6 +38,8 @@ export class ChatToolInvocation implements IChatToolInvocation { constructor( public readonly invocationMessage: string | IMarkdownString, + public readonly pastTenseMessage: string | IMarkdownString | undefined, + public readonly tooltip: string | IMarkdownString | undefined, private _confirmationMessages: IToolConfirmationMessages | undefined) { if (!_confirmationMessages) { // No confirmation needed @@ -63,6 +65,8 @@ export class ChatToolInvocation implements IChatToolInvocation { return { kind: 'toolInvocationSerialized', invocationMessage: this.invocationMessage, + pastTenseMessage: this.pastTenseMessage, + tooltip: this.tooltip, isConfirmed: this._isConfirmed ?? false, isComplete: this._isComplete, }; diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index e6974becb638..4251fe543566 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -203,6 +203,8 @@ export interface IChatToolInvocation { /** A 3-way: undefined=don't know yet. */ isConfirmed: boolean | undefined; invocationMessage: string | IMarkdownString; + pastTenseMessage: string | IMarkdownString | undefined; + tooltip: string | IMarkdownString | undefined; isComplete: boolean; isCompleteDeferred: DeferredPromise; @@ -214,6 +216,8 @@ export interface IChatToolInvocation { */ export interface IChatToolInvocationSerialized { invocationMessage: string | IMarkdownString; + pastTenseMessage: string | IMarkdownString | undefined; + tooltip: string | IMarkdownString | undefined; isConfirmed: boolean; isComplete: boolean; kind: 'toolInvocationSerialized'; diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index bc80d7c62866..2e48c18bf08f 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -66,6 +66,8 @@ export interface IToolConfirmationMessages { export interface IPreparedToolInvocation { invocationMessage?: string | IMarkdownString; + pastTenseMessage?: string | IMarkdownString; + tooltip?: string | IMarkdownString; confirmationMessages?: IToolConfirmationMessages; } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index 4f08ef3a4a47..0aff2d2091b1 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -122,4 +122,9 @@ declare module 'vscode' { export interface LanguageModelToolInvocationOptions { chatRequestId?: string; } + + export interface PreparedToolInvocation { + pastTenseMessage?: string | MarkdownString; + tooltip?: string | MarkdownString; + } } From a577b1e6801023ace39cc5b5bbf42dced9dbbd5a Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Fri, 24 Jan 2025 10:06:49 -0800 Subject: [PATCH 0867/3587] Replace history labeling fix --- src/vs/editor/common/config/editorOptions.ts | 16 ++++++++-------- src/vs/editor/contrib/find/browser/findWidget.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index a3486183a459..fac74c6535f4 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -1670,10 +1670,10 @@ export interface IEditorFindOptions { * @internal * Controls how the find widget search history should be stored */ - findHistory?: 'never' | 'workspace'; + history?: 'never' | 'workspace'; /** * @internal - * Controls how the find widget search history should be stored + * Controls how the replace widget search history should be stored */ replaceHistory?: 'never' | 'workspace'; } @@ -1693,7 +1693,7 @@ class EditorFind extends BaseEditorOption(input.findHistory, this.defaultValue.findHistory, ['never', 'workspace']), + history: stringSet<'never' | 'workspace'>(input.history, this.defaultValue.history, ['never', 'workspace']), replaceHistory: stringSet<'never' | 'workspace'>(input.replaceHistory, this.defaultValue.replaceHistory, ['never', 'workspace']), }; } diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index a305114ac1e0..e60addcd0846 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -942,7 +942,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL const flexibleHeight = true; const flexibleWidth = true; // Find input - const findSearchHistoryConfig = this._codeEditor.getOption(EditorOption.find).findHistory; + const findSearchHistoryConfig = this._codeEditor.getOption(EditorOption.find).history; const replaceHistoryConfig = this._codeEditor.getOption(EditorOption.find).replaceHistory; this._findInput = this._register(new ContextScopedFindInput(null, this._contextViewProvider, { width: FIND_INPUT_AREA_WIDTH, From ba781f99db1d93f5422c4b499f473bbd0a75be08 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 24 Jan 2025 10:53:48 -0800 Subject: [PATCH 0868/3587] fix: remove disposed untitled files from working set (#238704) --- .../browser/chatEditing/chatEditingSession.ts | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index f55b942fd7c7..00f9558143bc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -8,8 +8,9 @@ import { VSBuffer } from '../../../../../base/common/buffer.js'; import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { Emitter } from '../../../../../base/common/event.js'; import { StringSHA1 } from '../../../../../base/common/hash.js'; -import { Disposable, DisposableMap, dispose } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore, dispose } from '../../../../../base/common/lifecycle.js'; import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js'; +import { Schemas } from '../../../../../base/common/network.js'; import { autorun, derived, IObservable, IReader, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; import { autorunDelta, autorunIterableDelta } from '../../../../../base/common/observableInternal/autorun.js'; import { isEqual, joinPath } from '../../../../../base/common/resources.js'; @@ -614,6 +615,36 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._sequencer.queue(() => this._resolve()); } + private _trackUntitledWorkingSetEntry(resource: URI) { + if (resource.scheme !== Schemas.untitled) { + return; + } + const untitled = this._textFileService.untitled.get(resource); + if (!untitled) { // Shouldn't happen + return; + } + + // Track this file until + // 1. it is removed from the working set + // 2. it is closed + // 3. we are disposed + const store = new DisposableStore(); + store.add(this.onDidChange(e => { + if (e === ChatEditingSessionChangeType.WorkingSet && !this._workingSet.get(resource)) { + // The user has removed the file from the working set + store.dispose(); + } + })); + store.add(this._editorService.onDidCloseEditor((e) => { + if (isEqual(e.editor.resource, resource)) { + this._workingSet.delete(resource); + store.dispose(); + this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); + } + })); + this._store.add(store); + } + addFileToWorkingSet(resource: URI, description?: string, proposedState?: WorkingSetEntryState.Suggested): void { const state = this._workingSet.get(resource); if (proposedState === WorkingSetEntryState.Suggested) { @@ -621,9 +652,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return; } 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) { this._workingSet.set(resource, { description, state: WorkingSetEntryState.Attached }); + this._trackUntitledWorkingSetEntry(resource); this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); } } From 5331ff2fdb008239c4eadf1e0e5e0cefb2026996 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 24 Jan 2025 20:14:42 +0100 Subject: [PATCH 0869/3587] chat - offer entry to enable more models if Copilot Free user --- .../contrib/chat/browser/chatInputPart.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index d19b3cf34f02..ee5a492fb635 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -17,7 +17,7 @@ import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../../../.. import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { ProgressBar } from '../../../../base/browser/ui/progressbar/progressbar.js'; -import { IAction } from '../../../../base/common/actions.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'; @@ -1601,7 +1601,8 @@ class ModelPickerActionViewItem extends MenuEntryActionViewItem { @IThemeService themeService: IThemeService, @IContextMenuService contextMenuService: IContextMenuService, @ILanguageModelsService private readonly _languageModelsService: ILanguageModelsService, - @IAccessibilityService _accessibilityService: IAccessibilityService + @IAccessibilityService _accessibilityService: IAccessibilityService, + @ICommandService private readonly _commandService: ICommandService ) { super(action, options, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, _accessibilityService); @@ -1660,7 +1661,16 @@ class ModelPickerActionViewItem extends MenuEntryActionViewItem { models.sort((a, b) => a.model.name.localeCompare(b.model.name)); this._contextMenuService.showContextMenu({ getAnchor: () => this.element!, - getActions: () => models.map(entry => setLanguageModelAction(entry.id, entry.model)), + getActions: () => { + const actions = models.map(entry => setLanguageModelAction(entry.id, entry.model)); + + if (this._contextKeyService.getContextKeyValue(ChatContextKeys.Setup.limited.key) === true) { + actions.push(new Separator()); + actions.push(toAction({ id: 'moreModels', label: localize('chat.moreModels', "Enable More Models..."), run: () => this._commandService.executeCommand('workbench.action.chat.upgradePlan') })); + } + + return actions; + }, }); } } From 42ac4fb2034fd89c1b4d85eb506f1a8f538747d0 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Fri, 24 Jan 2025 12:29:13 -0700 Subject: [PATCH 0870/3587] Add more info to chat install entitlement (#238710) --- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index e8081d9a3676..3751be60c74e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -324,6 +324,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr //#region Chat Setup Request Service type EntitlementClassification = { + tid: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight'; comment: 'The anonymized analytics id returned by the service'; endpoint: 'GoogleAnalyticsId' }; entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating the chat entitlement state' }; quotaChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat completions available to the user' }; quotaCompletions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat completions available to the user' }; @@ -334,6 +335,7 @@ type EntitlementClassification = { type EntitlementEvent = { entitlement: ChatEntitlement; + tid: string; quotaChat: number | undefined; quotaCompletions: number | undefined; quotaResetDate: string | undefined; @@ -344,6 +346,7 @@ interface IEntitlementsResponse { readonly assigned_date: string; readonly can_signup_for_limited: boolean; readonly chat_enabled: boolean; + readonly analytics_tracking_id: string; readonly limited_user_quotas?: { readonly chat: number; readonly completions: number; @@ -570,6 +573,7 @@ class ChatSetupRequests extends Disposable { this.logService.trace(`[chat setup] entitlement: resolved to ${entitlements.entitlement}, quotas: ${JSON.stringify(entitlements.quotas)}`); this.telemetryService.publicLog2('chatInstallEntitlement', { entitlement: entitlements.entitlement, + tid: entitlementsResponse.analytics_tracking_id, quotaChat: entitlementsResponse.limited_user_quotas?.chat, quotaCompletions: entitlementsResponse.limited_user_quotas?.completions, quotaResetDate: entitlementsResponse.limited_user_reset_date From eb0da63435b7c35cb8a9be57b40e3768a18da5d6 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 24 Jan 2025 11:38:31 -0800 Subject: [PATCH 0871/3587] tools: allow followup message with implicit cancel on tool confirmation (#238712) - Make 'requestInProgress' false while waiting for a tool confirmation - Cancel pending requests when sending a new request via the chat input --- .../browser/actions/chatExecuteActions.ts | 5 +++- .../chatToolInvocationPart.ts | 4 ++- .../contrib/chat/browser/chatWidget.ts | 2 ++ .../contrib/chat/common/chatModel.ts | 25 ++++++++++++++++++- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index acfe75254a38..4eb88121e97f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -432,7 +432,10 @@ export class CancelAction extends Action2 { icon: Codicon.stopCircle, menu: { id: MenuId.ChatExecute, - when: ContextKeyExpr.or(ChatContextKeys.requestInProgress, ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey)), + when: ContextKeyExpr.or( + ChatContextKeys.requestInProgress, + ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey) + ), order: 4, group: 'navigation', }, diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts index ee51460fad83..a863c4f11005 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts @@ -93,7 +93,9 @@ class ChatToolInvocationSubPart extends Disposable { toolInvocation.confirmed.complete(button.data); })); this._onDidChangeHeight.input = confirmWidget.onDidChangeHeight; - toolInvocation.confirmed.p.then(() => this._onNeedsRerender.fire()); + toolInvocation.confirmed.p.then(() => { + this._onNeedsRerender.fire(); + }); } else { let content: IMarkdownString; if (toolInvocation.isComplete && toolInvocation.isConfirmed !== false && toolInvocation.pastTenseMessage) { diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index ed820a0e0797..2d6cdbdb67d4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1098,6 +1098,8 @@ export class ChatWidget extends Disposable implements IChatWidget { currentEditingSession?.remove(WorkingSetEntryRemovalReason.User, ...unconfirmedSuggestions); } + this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionId); + const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, { userSelectedModelId: this.inputPart.currentLanguageModel, location: this.location, diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index d79d8c7cd906..151875b93d8a 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -208,6 +208,7 @@ export interface IChatResponseModel { readonly response: IResponse; readonly isComplete: boolean; readonly isCanceled: boolean; + readonly isPendingConfirmation: boolean; readonly shouldBeRemovedOnSend: boolean; readonly isCompleteAddedRequest: boolean; /** A stale response is one that has been persisted and rehydrated, so e.g. Commands that have their arguments stored in the EH are gone. */ @@ -399,6 +400,14 @@ export class Response extends Disposable implements IResponse { this._updateRepr(false); }); + } else if (progress.kind === 'toolInvocation') { + if (progress.confirmationMessages) { + progress.confirmed.p.then(() => { + this._updateRepr(false); + }); + } + this._responseParts.push(progress); + this._updateRepr(quiet); } else { this._responseParts.push(progress); this._updateRepr(quiet); @@ -595,6 +604,12 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this._isStale; } + public get isPendingConfirmation() { + return this._response.value.some(part => + part.kind === 'toolInvocation' && part.isConfirmed === undefined + || part.kind === 'confirmation' && part.isUsed === false); + } + constructor( _response: IMarkdownString | ReadonlyArray, private _session: ChatModel, @@ -979,7 +994,15 @@ export class ChatModel extends Disposable implements IChatModel { get requestInProgress(): boolean { const lastRequest = this.lastRequest; - return !!lastRequest?.response && !lastRequest.response.isComplete; + if (!lastRequest?.response) { + return false; + } + + if (lastRequest.response.isPendingConfirmation) { + return false; + } + + return !lastRequest.response.isComplete; } get hasRequests(): boolean { From 093de79c9c95ae52eafc557fbd00ff652befc478 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 24 Jan 2025 11:47:49 -0800 Subject: [PATCH 0872/3587] chat: disable readonly files for stable for now (#238715) We're still not confident that this direction is the way to go. --- .../contrib/chat/browser/chatEditing/chatEditingService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts index fa18d8628e0d..8e14897c2f57 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts @@ -26,6 +26,7 @@ import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { bindContextKey } from '../../../../../platform/observable/common/platformObservableUtils.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; @@ -104,6 +105,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic @IStorageService storageService: IStorageService, @ILogService logService: ILogService, @IExtensionService extensionService: IExtensionService, + @IProductService productService: IProductService, ) { super(); this._applyingChatEditsFailedContextKey = applyingChatEditsFailedContextKey.bindTo(contextKeyService); @@ -161,7 +163,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic // todo@connor4312: temporary until chatReadonlyPromptReference proposal is finalized const readonlyEnabledContextKey = chatEditingAgentSupportsReadonlyReferencesContextKey.bindTo(contextKeyService); const setReadonlyFilesEnabled = () => { - const enabled = extensionService.extensions.some(e => e.enabledApiProposals?.includes('chatReadonlyPromptReference')); + const enabled = productService.quality !== 'stable' && extensionService.extensions.some(e => e.enabledApiProposals?.includes('chatReadonlyPromptReference')); readonlyEnabledContextKey.set(enabled); }; setReadonlyFilesEnabled(); From a1c6c50fd913ec8d7a7d03bfb4a3e03113453eb4 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 24 Jan 2025 13:56:35 -0600 Subject: [PATCH 0873/3587] use editor font info for simple completion widget (#238702) use editor font info --- .../suggest/browser/terminalSuggestAddon.ts | 32 +------------- .../terminalSuggestAddon.integrationTest.ts | 8 +--- .../suggest/browser/simpleSuggestWidget.ts | 44 +++++++++++++++---- .../browser/simpleSuggestWidgetRenderer.ts | 3 +- 4 files changed, 41 insertions(+), 46 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index a175d8f35d4f..4a28688b8930 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -19,16 +19,14 @@ import { TerminalCapability, type ITerminalCapabilityStore } from '../../../../. import type { IPromptInputModel, IPromptInputModelState } from '../../../../../platform/terminal/common/capabilities/commandDetection/promptInputModel.js'; import { getListStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { activeContrastBorder } from '../../../../../platform/theme/common/colorRegistry.js'; -import { ITerminalConfigurationService } from '../../../terminal/browser/terminal.js'; import type { IXtermCore } from '../../../terminal/browser/xterm-private.js'; import { TerminalStorageKeys } from '../../../terminal/common/terminalStorageKeys.js'; import { terminalSuggestConfigSection, TerminalSuggestSettingId, type ITerminalSuggestConfiguration } from '../common/terminalSuggestConfiguration.js'; import { SimpleCompletionItem } from '../../../../services/suggest/browser/simpleCompletionItem.js'; import { LineContext, SimpleCompletionModel } from '../../../../services/suggest/browser/simpleCompletionModel.js'; import { ISimpleSelectedSuggestion, SimpleSuggestWidget } from '../../../../services/suggest/browser/simpleSuggestWidget.js'; -import type { ISimpleSuggestWidgetFontInfo } from '../../../../services/suggest/browser/simpleSuggestWidgetRenderer.js'; import { ITerminalCompletionService, TerminalCompletionItemKind } from './terminalCompletionService.js'; -import { TerminalSettingId, TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; +import { TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; @@ -80,8 +78,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest readonly onAcceptedCompletion = this._onAcceptedCompletion.event; private readonly _onDidReceiveCompletions = this._register(new Emitter()); readonly onDidReceiveCompletions = this._onDidReceiveCompletions.event; - private readonly _onDidFontConfigurationChange = this._register(new Emitter()); - readonly onDidFontConfigurationChange = this._onDidFontConfigurationChange.event; private _kindToIconMap = new Map([ [TerminalCompletionItemKind.File, Codicon.file], @@ -100,7 +96,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest @ITerminalCompletionService private readonly _terminalCompletionService: ITerminalCompletionService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService, @IExtensionService private readonly _extensionService: IExtensionService ) { super(); @@ -395,36 +390,13 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest }); } - private _getFontInfo(): ISimpleSuggestWidgetFontInfo { - const c = this._terminalConfigurationService.config; - const font = this._terminalConfigurationService.getFont(dom.getActiveWindow()); - const fontInfo: ISimpleSuggestWidgetFontInfo = { - fontFamily: font.fontFamily, - fontSize: font.fontSize, - // In the editor's world, lineHeight is the pixels between the baselines of two lines of text - // In the terminal's world, lineHeight is the multiplier of the font size - // 1.5 is needed so that it's taller than a 16px icon - lineHeight: Math.ceil(c.lineHeight * font.fontSize * 1.5), - fontWeight: c.fontWeight.toString(), - letterSpacing: font.letterSpacing - }; - return fontInfo; - } + private _ensureSuggestWidget(terminal: Terminal): SimpleSuggestWidget { if (!this._suggestWidget) { - - this._register(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TerminalSettingId.FontFamily) || e.affectsConfiguration(TerminalSettingId.FontSize) || e.affectsConfiguration(TerminalSettingId.LineHeight) || e.affectsConfiguration(TerminalSettingId.FontFamily) || e.affectsConfiguration('editor.fontSize') || e.affectsConfiguration('editor.fontFamily')) { - this._onDidFontConfigurationChange.fire(); - } - } - )); this._suggestWidget = this._register(this._instantiationService.createInstance( SimpleSuggestWidget, this._container!, this._instantiationService.createInstance(PersistedWidgetSize), - this._getFontInfo.bind(this), - this.onDidFontConfigurationChange, { statusBarMenuId: MenuId.MenubarTerminalSuggestStatusMenu, showStatusBarSettingId: TerminalSuggestSettingId.ShowStatusBar diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts index bdfbb2661f9f..ab229b5be42a 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts @@ -95,11 +95,6 @@ suite('Terminal Contrib Suggest Recordings', () => { setup(async () => { const terminalConfig = { - fontFamily: 'monospace', - fontSize: 12, - fontWeight: 'normal', - letterSpacing: 0, - lineHeight: 1, integrated: { suggest: { enabled: true, @@ -121,7 +116,8 @@ suite('Terminal Contrib Suggest Recordings', () => { const instantiationService = workbenchInstantiationService({ configurationService: () => new TestConfigurationService({ files: { autoSave: false }, - terminal: terminalConfig + terminal: terminalConfig, + editor: { fontSize: 14, fontFamily: 'Arial', lineHeight: 12, fontWeight: 'bold' } }) }, store); const terminalConfigurationService = instantiationService.get(ITerminalConfigurationService) as TestTerminalConfigurationService; diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index 6092257e73d4..b1ed529e0523 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -23,7 +23,6 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { canExpandCompletionItem, SimpleSuggestDetailsOverlay, SimpleSuggestDetailsWidget } from './simpleSuggestWidgetDetails.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { TerminalSettingId } from '../../../../platform/terminal/common/terminal.js'; const $ = dom.$; @@ -106,6 +105,8 @@ export class SimpleSuggestWidget extends Disposable { readonly onDidFocus: Event = this._onDidFocus.event; private readonly _onDidBlurDetails = this._register(new Emitter()); readonly onDidBlurDetails = this._onDidBlurDetails.event; + private readonly _onDidFontConfigurationChange = this._register(new Emitter()); + readonly onDidFontConfigurationChange = this._onDidFontConfigurationChange.event; get list(): List { return this._list; } @@ -114,8 +115,6 @@ export class SimpleSuggestWidget extends Disposable { constructor( private readonly _container: HTMLElement, private readonly _persistedSize: IPersistedWidgetSizeDelegate, - private readonly _getFontInfo: () => ISimpleSuggestWidgetFontInfo, - private readonly _onDidFontConfigurationChange: Event, private readonly _options: IWorkbenchSuggestWidgetOptions, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -180,7 +179,7 @@ export class SimpleSuggestWidget extends Disposable { const applyIconStyle = () => this.element.domNode.classList.toggle('no-icons', !_configurationService.getValue('editor.suggest.showIcons')); applyIconStyle(); - const renderer = new SimpleSuggestWidgetItemRenderer(_getFontInfo, this._configurationService); + const renderer = new SimpleSuggestWidgetItemRenderer(this._getFontInfo.bind(this), this._configurationService); this._register(renderer); this._listElement = dom.append(this.element.domNode, $('.tree')); this._list = this._register(new List('SuggestWidget', this._listElement, { @@ -229,7 +228,7 @@ export class SimpleSuggestWidget extends Disposable { this._messageElement = dom.append(this.element.domNode, dom.$('.message')); - const details: SimpleSuggestDetailsWidget = this._register(instantiationService.createInstance(SimpleSuggestDetailsWidget, this._getFontInfo, this._onDidFontConfigurationChange)); + const details: SimpleSuggestDetailsWidget = this._register(instantiationService.createInstance(SimpleSuggestDetailsWidget, this._getFontInfo.bind(this), this.onDidFontConfigurationChange)); this._register(details.onDidClose(() => this.toggleDetails())); this._details = this._register(new SimpleSuggestDetailsOverlay(details, this._listElement)); this._register(dom.addDisposableListener(this._details.widget.domNode, 'blur', (e) => this._onDidBlurDetails.fire(e))); @@ -247,9 +246,13 @@ export class SimpleSuggestWidget extends Disposable { if (e.affectsConfiguration('editor.suggest.showIcons')) { applyIconStyle(); } - if (this._completionModel && e.affectsConfiguration(TerminalSettingId.FontSize) || e.affectsConfiguration(TerminalSettingId.LineHeight) || e.affectsConfiguration(TerminalSettingId.FontFamily)) { - this._layout(undefined); - this._list.splice(0, this._list.length, this._completionModel!.items); + if (this._completionModel && ( + e.affectsConfiguration('editor.fontSize') || + e.affectsConfiguration('editor.lineHeight') || + e.affectsConfiguration('editor.fontWeight') || + e.affectsConfiguration('editor.fontFamily'))) { + this._list.splice(0, this._completionModel.items.length, this._completionModel!.items); + this._onDidFontConfigurationChange.fire(); } if (_options.statusBarMenuId && _options.showStatusBarSettingId && e.affectsConfiguration(_options.showStatusBarSettingId)) { const showStatusBar: boolean = _configurationService.getValue(_options.showStatusBarSettingId); @@ -767,6 +770,31 @@ export class SimpleSuggestWidget extends Disposable { } } + private _getFontInfo(): ISimpleSuggestWidgetFontInfo { + let lineHeight: number = this._configurationService.getValue('editor.lineHeight'); + const fontSize: number = this._configurationService.getValue('editor.fontSize'); + const fontFamily: string = this._configurationService.getValue('editor.fontFamily'); + const fontWeight: string = this._configurationService.getValue('editor.fontWeight'); + const letterSpacing: number = this._configurationService.getValue('editor.letterSpacing'); + + if (lineHeight <= 1 && fontSize < 16) { + // Scale so icon shows by default + lineHeight = Math.ceil(fontSize * 1.5); + } else if (lineHeight <= 8) { + lineHeight = fontSize * lineHeight; + } + + const fontInfo = { + fontSize, + lineHeight, + fontWeight: fontWeight.toString(), + letterSpacing, + fontFamily + }; + + return fontInfo; + } + private _getLayoutInfo() { const fontInfo = this._getFontInfo(); const itemHeight = clamp(fontInfo.lineHeight, 8, 1000); diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts index 71129442dda4..a12d4e5440aa 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetRenderer.ts @@ -13,7 +13,6 @@ import { createMatches } from '../../../../base/common/filters.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { TerminalSettingId } from '../../../../platform/terminal/common/terminal.js'; export function getAriaId(index: number): string { return `simple-suggest-aria-id-${index}`; @@ -117,7 +116,7 @@ export class SimpleSuggestWidgetItemRenderer implements IListRenderer { - if (e.affectsConfiguration(TerminalSettingId.FontSize) || e.affectsConfiguration(TerminalSettingId.FontFamily) || e.affectsConfiguration(TerminalSettingId.FontWeight) || e.affectsConfiguration(TerminalSettingId.LineHeight)) { + if (e.affectsConfiguration('editor.fontSize') || e.affectsConfiguration('editor.fontFamily') || e.affectsConfiguration('editor.lineHeight') || e.affectsConfiguration('editor.fontWeight')) { configureFont(); } })); From baf5cf7c70f92d1a25791b179106e70ed79adc24 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 24 Jan 2025 20:58:38 +0100 Subject: [PATCH 0874/3587] splash - include more parts with override support (#238691) --- .../electron-sandbox/workbench/workbench.ts | 174 +++++++++++------- .../electron-main/nativeHostMainService.ts | 4 +- src/vs/platform/theme/common/themeService.ts | 7 + .../theme/electron-main/themeMainService.ts | 84 ++++++++- .../windows/electron-main/windowImpl.ts | 2 +- .../platform/windows/electron-main/windows.ts | 2 +- .../contrib/splash/browser/partsSplash.ts | 1 + .../splash/browser/splash.contribution.ts | 2 + .../electron-sandbox/splash.contribution.ts | 2 + 9 files changed, 197 insertions(+), 81 deletions(-) diff --git a/src/vs/code/electron-sandbox/workbench/workbench.ts b/src/vs/code/electron-sandbox/workbench/workbench.ts index 833f8118d7ab..47c3d28a6fcd 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.ts +++ b/src/vs/code/electron-sandbox/workbench/workbench.ts @@ -102,63 +102,67 @@ } // ensure there is enough space - layoutInfo.sideBarWidth = Math.min(layoutInfo.sideBarWidth, window.innerWidth - (layoutInfo.activityBarWidth + layoutInfo.editorPartMinWidth)); + layoutInfo.auxiliarySideBarWidth = Math.min(layoutInfo.auxiliarySideBarWidth, window.innerWidth - (layoutInfo.activityBarWidth + layoutInfo.editorPartMinWidth + layoutInfo.sideBarWidth)); + layoutInfo.sideBarWidth = Math.min(layoutInfo.sideBarWidth, window.innerWidth - (layoutInfo.activityBarWidth + layoutInfo.editorPartMinWidth + layoutInfo.auxiliarySideBarWidth)); // part: title - const titleDiv = document.createElement('div'); - titleDiv.style.position = 'absolute'; - titleDiv.style.width = '100%'; - titleDiv.style.height = `${layoutInfo.titleBarHeight}px`; - titleDiv.style.left = '0'; - titleDiv.style.top = '0'; - titleDiv.style.backgroundColor = `${colorInfo.titleBarBackground}`; - (titleDiv.style as any)['-webkit-app-region'] = 'drag'; - splash.appendChild(titleDiv); - - if (colorInfo.titleBarBorder && layoutInfo.titleBarHeight > 0) { - const titleBorder = document.createElement('div'); - titleBorder.style.position = 'absolute'; - titleBorder.style.width = '100%'; - titleBorder.style.height = '1px'; - titleBorder.style.left = '0'; - titleBorder.style.bottom = '0'; - titleBorder.style.borderBottom = `1px solid ${colorInfo.titleBarBorder}`; - titleDiv.appendChild(titleBorder); + if (layoutInfo.titleBarHeight > 0) { + const titleDiv = document.createElement('div'); + titleDiv.style.position = 'absolute'; + titleDiv.style.width = '100%'; + titleDiv.style.height = `${layoutInfo.titleBarHeight}px`; + titleDiv.style.left = '0'; + titleDiv.style.top = '0'; + titleDiv.style.backgroundColor = `${colorInfo.titleBarBackground}`; + (titleDiv.style as any)['-webkit-app-region'] = 'drag'; + splash.appendChild(titleDiv); + + if (colorInfo.titleBarBorder) { + const titleBorder = document.createElement('div'); + titleBorder.style.position = 'absolute'; + titleBorder.style.width = '100%'; + titleBorder.style.height = '1px'; + titleBorder.style.left = '0'; + titleBorder.style.bottom = '0'; + titleBorder.style.borderBottom = `1px solid ${colorInfo.titleBarBorder}`; + titleDiv.appendChild(titleBorder); + } } // part: activity bar - const activityDiv = document.createElement('div'); - activityDiv.style.position = 'absolute'; - activityDiv.style.width = `${layoutInfo.activityBarWidth}px`; - activityDiv.style.height = `calc(100% - ${layoutInfo.titleBarHeight + layoutInfo.statusBarHeight}px)`; - activityDiv.style.top = `${layoutInfo.titleBarHeight}px`; - if (layoutInfo.sideBarSide === 'left') { - activityDiv.style.left = '0'; - } else { - activityDiv.style.right = '0'; - } - activityDiv.style.backgroundColor = `${colorInfo.activityBarBackground}`; - splash.appendChild(activityDiv); - - if (colorInfo.activityBarBorder && layoutInfo.activityBarWidth > 0) { - const activityBorderDiv = document.createElement('div'); - activityBorderDiv.style.position = 'absolute'; - activityBorderDiv.style.width = '1px'; - activityBorderDiv.style.height = '100%'; - activityBorderDiv.style.top = '0'; + if (layoutInfo.activityBarWidth > 0) { + const activityDiv = document.createElement('div'); + activityDiv.style.position = 'absolute'; + activityDiv.style.width = `${layoutInfo.activityBarWidth}px`; + activityDiv.style.height = `calc(100% - ${layoutInfo.titleBarHeight + layoutInfo.statusBarHeight}px)`; + activityDiv.style.top = `${layoutInfo.titleBarHeight}px`; if (layoutInfo.sideBarSide === 'left') { - activityBorderDiv.style.right = '0'; - activityBorderDiv.style.borderRight = `1px solid ${colorInfo.activityBarBorder}`; + activityDiv.style.left = '0'; } else { - activityBorderDiv.style.left = '0'; - activityBorderDiv.style.borderLeft = `1px solid ${colorInfo.activityBarBorder}`; + activityDiv.style.right = '0'; + } + activityDiv.style.backgroundColor = `${colorInfo.activityBarBackground}`; + splash.appendChild(activityDiv); + + if (colorInfo.activityBarBorder) { + const activityBorderDiv = document.createElement('div'); + activityBorderDiv.style.position = 'absolute'; + activityBorderDiv.style.width = '1px'; + activityBorderDiv.style.height = '100%'; + activityBorderDiv.style.top = '0'; + if (layoutInfo.sideBarSide === 'left') { + activityBorderDiv.style.right = '0'; + activityBorderDiv.style.borderRight = `1px solid ${colorInfo.activityBarBorder}`; + } else { + activityBorderDiv.style.left = '0'; + activityBorderDiv.style.borderLeft = `1px solid ${colorInfo.activityBarBorder}`; + } + activityDiv.appendChild(activityBorderDiv); } - activityDiv.appendChild(activityBorderDiv); } // part: side bar (only when opening workspace/folder) - // folder or workspace -> status bar color, sidebar - if (configuration.workspace) { + if (configuration.workspace && layoutInfo.sideBarWidth > 0) { const sideDiv = document.createElement('div'); sideDiv.style.position = 'absolute'; sideDiv.style.width = `${layoutInfo.sideBarWidth}px`; @@ -172,7 +176,7 @@ sideDiv.style.backgroundColor = `${colorInfo.sideBarBackground}`; splash.appendChild(sideDiv); - if (colorInfo.sideBarBorder && layoutInfo.sideBarWidth > 0) { + if (colorInfo.sideBarBorder) { const sideBorderDiv = document.createElement('div'); sideBorderDiv.style.position = 'absolute'; sideBorderDiv.style.width = '1px'; @@ -189,28 +193,62 @@ } } - // part: statusbar - const statusDiv = document.createElement('div'); - statusDiv.style.position = 'absolute'; - statusDiv.style.width = '100%'; - statusDiv.style.height = `${layoutInfo.statusBarHeight}px`; - statusDiv.style.bottom = '0'; - statusDiv.style.left = '0'; - if (configuration.workspace && colorInfo.statusBarBackground) { - statusDiv.style.backgroundColor = colorInfo.statusBarBackground; - } else if (!configuration.workspace && colorInfo.statusBarNoFolderBackground) { - statusDiv.style.backgroundColor = colorInfo.statusBarNoFolderBackground; + // part: auxiliary sidebar + if (layoutInfo.auxiliarySideBarWidth > 0) { + const auxSideDiv = document.createElement('div'); + auxSideDiv.style.position = 'absolute'; + auxSideDiv.style.width = `${layoutInfo.auxiliarySideBarWidth}px`; + auxSideDiv.style.height = `calc(100% - ${layoutInfo.titleBarHeight + layoutInfo.statusBarHeight}px)`; + auxSideDiv.style.top = `${layoutInfo.titleBarHeight}px`; + if (layoutInfo.sideBarSide === 'left') { + auxSideDiv.style.right = '0'; + } else { + auxSideDiv.style.left = '0'; + } + auxSideDiv.style.backgroundColor = `${colorInfo.sideBarBackground}`; + splash.appendChild(auxSideDiv); + + if (colorInfo.sideBarBorder) { + const auxSideBorderDiv = document.createElement('div'); + auxSideBorderDiv.style.position = 'absolute'; + auxSideBorderDiv.style.width = '1px'; + auxSideBorderDiv.style.height = '100%'; + auxSideBorderDiv.style.top = '0'; + if (layoutInfo.sideBarSide === 'left') { + auxSideBorderDiv.style.left = '0'; + auxSideBorderDiv.style.borderLeft = `1px solid ${colorInfo.sideBarBorder}`; + } else { + auxSideBorderDiv.style.right = '0'; + auxSideBorderDiv.style.borderRight = `1px solid ${colorInfo.sideBarBorder}`; + } + auxSideDiv.appendChild(auxSideBorderDiv); + } } - splash.appendChild(statusDiv); - - if (colorInfo.statusBarBorder && layoutInfo.statusBarHeight > 0) { - const statusBorderDiv = document.createElement('div'); - statusBorderDiv.style.position = 'absolute'; - statusBorderDiv.style.width = '100%'; - statusBorderDiv.style.height = '1px'; - statusBorderDiv.style.top = '0'; - statusBorderDiv.style.borderTop = `1px solid ${colorInfo.statusBarBorder}`; - statusDiv.appendChild(statusBorderDiv); + + // part: statusbar + if (layoutInfo.statusBarHeight > 0) { + const statusDiv = document.createElement('div'); + statusDiv.style.position = 'absolute'; + statusDiv.style.width = '100%'; + statusDiv.style.height = `${layoutInfo.statusBarHeight}px`; + statusDiv.style.bottom = '0'; + statusDiv.style.left = '0'; + if (configuration.workspace && colorInfo.statusBarBackground) { + statusDiv.style.backgroundColor = colorInfo.statusBarBackground; + } else if (!configuration.workspace && colorInfo.statusBarNoFolderBackground) { + statusDiv.style.backgroundColor = colorInfo.statusBarNoFolderBackground; + } + splash.appendChild(statusDiv); + + if (colorInfo.statusBarBorder) { + const statusBorderDiv = document.createElement('div'); + statusBorderDiv.style.position = 'absolute'; + statusBorderDiv.style.width = '100%'; + statusBorderDiv.style.height = '1px'; + statusBorderDiv.style.top = '0'; + statusBorderDiv.style.borderTop = `1px solid ${colorInfo.statusBarBorder}`; + statusDiv.appendChild(statusBorderDiv); + } } window.document.body.appendChild(splash); diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index b272b5836187..56263e7fdadd 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -323,7 +323,9 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } async saveWindowSplash(windowId: number | undefined, splash: IPartsSplash): Promise { - this.themeMainService.saveWindowSplash(windowId, splash); + const window = this.codeWindowById(windowId); + + this.themeMainService.saveWindowSplash(windowId, window?.openedWorkspace, splash); } async overrideDefaultTitlebarStyle(windowId: number | undefined, style: 'custom' | undefined): Promise { diff --git a/src/vs/platform/theme/common/themeService.ts b/src/vs/platform/theme/common/themeService.ts index bca5b3b3b3ea..7911e0221da9 100644 --- a/src/vs/platform/theme/common/themeService.ts +++ b/src/vs/platform/theme/common/themeService.ts @@ -230,8 +230,15 @@ export interface IPartsSplash { titleBarHeight: number; activityBarWidth: number; sideBarWidth: number; + auxiliarySideBarWidth: number; statusBarHeight: number; windowBorder: boolean; windowBorderRadius: string | undefined; } | undefined; } + +export interface IPartsSplashWorkspaceOverride { + layoutInfo: { + auxiliarySideBarWidth: [number, string[] /* workspace identifier the override applies to */]; + }; +} diff --git a/src/vs/platform/theme/electron-main/themeMainService.ts b/src/vs/platform/theme/electron-main/themeMainService.ts index 3cc4fb246e13..90d2be8dc27b 100644 --- a/src/vs/platform/theme/electron-main/themeMainService.ts +++ b/src/vs/platform/theme/electron-main/themeMainService.ts @@ -10,9 +10,11 @@ import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.j import { IConfigurationService } from '../../configuration/common/configuration.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; import { IStateService } from '../../state/node/state.js'; -import { IPartsSplash } from '../common/themeService.js'; +import { IPartsSplash, IPartsSplashWorkspaceOverride } from '../common/themeService.js'; import { IColorScheme } from '../../window/common/window.js'; import { ThemeTypeSelector } from '../common/theme.js'; +import { IBaseWorkspaceIdentifier } from '../../workspace/common/workspace.js'; +import { coalesce } from '../../../base/common/arrays.js'; // These default colors match our default themes // editor background color ("Dark Modern", etc...) @@ -23,7 +25,9 @@ const DEFAULT_BG_HC_LIGHT = '#FFFFFF'; const THEME_STORAGE_KEY = 'theme'; const THEME_BG_STORAGE_KEY = 'themeBackground'; -const THEME_WINDOW_SPLASH = 'windowSplash'; + +const THEME_WINDOW_SPLASH_KEY = 'windowSplash'; +const THEME_WINDOW_SPLASH_WORKSPACE_OVERRIDE_KEY = 'windowSplashWorkspaceOverride'; namespace ThemeSettings { export const DETECT_COLOR_SCHEME = 'window.autoDetectColorScheme'; @@ -41,8 +45,8 @@ export interface IThemeMainService { getBackgroundColor(): string; - saveWindowSplash(windowId: number | undefined, splash: IPartsSplash): void; - getWindowSplash(): IPartsSplash | undefined; + saveWindowSplash(windowId: number | undefined, workspace: IBaseWorkspaceIdentifier | undefined, splash: IPartsSplash): void; + getWindowSplash(workspace: IBaseWorkspaceIdentifier | undefined): IPartsSplash | undefined; getColorScheme(): IColorScheme; } @@ -163,14 +167,18 @@ export class ThemeMainService extends Disposable implements IThemeMainService { } } - saveWindowSplash(windowId: number | undefined, splash: IPartsSplash): void { + saveWindowSplash(windowId: number | undefined, workspace: IBaseWorkspaceIdentifier | undefined, splash: IPartsSplash): void { + + // Update override as needed + const splashOverride = this.updateWindowSplashOverride(workspace, splash); // Update in storage - this.stateService.setItems([ + this.stateService.setItems(coalesce([ { key: THEME_STORAGE_KEY, data: splash.baseTheme }, { key: THEME_BG_STORAGE_KEY, data: splash.colorInfo.background }, - { key: THEME_WINDOW_SPLASH, data: splash } - ]); + { key: THEME_WINDOW_SPLASH_KEY, data: splash }, + splashOverride ? { key: THEME_WINDOW_SPLASH_WORKSPACE_OVERRIDE_KEY, data: splashOverride } : undefined + ])); // Update in opened windows if (typeof windowId === 'number') { @@ -181,6 +189,35 @@ export class ThemeMainService extends Disposable implements IThemeMainService { this.updateSystemColorTheme(); } + private updateWindowSplashOverride(workspace: IBaseWorkspaceIdentifier | undefined, splash: IPartsSplash): IPartsSplashWorkspaceOverride | undefined { + let splashOverride: IPartsSplashWorkspaceOverride | undefined = undefined; + let changed = false; + if (workspace) { + splashOverride = { ...this.getWindowSplashOverride() }; // make a copy for modifications + + const [auxiliarySideBarWidth, workspaceIds] = splashOverride.layoutInfo.auxiliarySideBarWidth; + if (splash.layoutInfo?.auxiliarySideBarWidth) { + if (auxiliarySideBarWidth !== splash.layoutInfo.auxiliarySideBarWidth) { + splashOverride.layoutInfo.auxiliarySideBarWidth[0] = splash.layoutInfo.auxiliarySideBarWidth; + changed = true; + } + + if (!workspaceIds.includes(workspace.id)) { + workspaceIds.push(workspace.id); + changed = true; + } + } else { + const index = workspaceIds.indexOf(workspace.id); + if (index > -1) { + workspaceIds.splice(index, 1); + changed = true; + } + } + } + + return changed ? splashOverride : undefined; + } + private updateBackgroundColor(windowId: number, splash: IPartsSplash): void { for (const window of electron.BrowserWindow.getAllWindows()) { if (window.id === windowId) { @@ -190,7 +227,34 @@ export class ThemeMainService extends Disposable implements IThemeMainService { } } - getWindowSplash(): IPartsSplash | undefined { - return this.stateService.getItem(THEME_WINDOW_SPLASH); + getWindowSplash(workspace: IBaseWorkspaceIdentifier | undefined): IPartsSplash | undefined { + const partSplash = this.stateService.getItem(THEME_WINDOW_SPLASH_KEY); + if (!partSplash?.layoutInfo) { + return partSplash; // return early: overrides currently only apply to layout info + } + + // Apply workspace specific overrides + let auxiliarySideBarWidthOverride: number | undefined; + if (workspace) { + const [auxiliarySideBarWidth, workspaceIds] = this.getWindowSplashOverride().layoutInfo.auxiliarySideBarWidth; + if (workspaceIds.includes(workspace.id)) { + auxiliarySideBarWidthOverride = auxiliarySideBarWidth; + } + } + + return { + ...partSplash, + layoutInfo: { + ...partSplash.layoutInfo, + // Only apply an auxiliary bar width when we have a workspace specific + // override. Auxiliary bar is not visible by default unless explicitly + // opened in a workspace. + auxiliarySideBarWidth: typeof auxiliarySideBarWidthOverride === 'number' ? auxiliarySideBarWidthOverride : 0 + } + }; + } + + private getWindowSplashOverride(): IPartsSplashWorkspaceOverride { + return this.stateService.getItem(THEME_WINDOW_SPLASH_WORKSPACE_OVERRIDE_KEY, { layoutInfo: { auxiliarySideBarWidth: [0, []] } }); } } diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 6664f2b502c9..3c8efe30dd68 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -1066,7 +1066,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { } configuration.fullscreen = this.isFullScreen; configuration.maximized = this._win.isMaximized(); - configuration.partsSplash = this.themeMainService.getWindowSplash(); + configuration.partsSplash = this.themeMainService.getWindowSplash(configuration.workspace); configuration.zoomLevel = this.getZoomLevel(); configuration.isCustomZoomLevel = typeof this.customZoomLevel === 'number'; if (configuration.isCustomZoomLevel && configuration.partsSplash) { diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 6d14080654d4..b4c024a39f21 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -194,7 +194,7 @@ export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowSt // to use on initialization, but prefer to keep things // simple as it is temporary and not noticeable - const titleBarColor = themeMainService.getWindowSplash()?.colorInfo.titleBarBackground ?? themeMainService.getBackgroundColor(); + const titleBarColor = themeMainService.getWindowSplash(undefined)?.colorInfo.titleBarBackground ?? themeMainService.getBackgroundColor(); const symbolColor = Color.fromHex(titleBarColor).isDarker() ? '#FFFFFF' : '#000000'; options.titleBarOverlay = { diff --git a/src/vs/workbench/contrib/splash/browser/partsSplash.ts b/src/vs/workbench/contrib/splash/browser/partsSplash.ts index d410339e5ada..935a7432d718 100644 --- a/src/vs/workbench/contrib/splash/browser/partsSplash.ts +++ b/src/vs/workbench/contrib/splash/browser/partsSplash.ts @@ -95,6 +95,7 @@ export class PartsSplash { titleBarHeight: this._layoutService.isVisible(Parts.TITLEBAR_PART, mainWindow) ? dom.getTotalHeight(assertIsDefined(this._layoutService.getContainer(mainWindow, Parts.TITLEBAR_PART))) : 0, activityBarWidth: this._layoutService.isVisible(Parts.ACTIVITYBAR_PART) ? dom.getTotalWidth(assertIsDefined(this._layoutService.getContainer(mainWindow, Parts.ACTIVITYBAR_PART))) : 0, sideBarWidth: this._layoutService.isVisible(Parts.SIDEBAR_PART) ? dom.getTotalWidth(assertIsDefined(this._layoutService.getContainer(mainWindow, Parts.SIDEBAR_PART))) : 0, + auxiliarySideBarWidth: this._layoutService.isVisible(Parts.AUXILIARYBAR_PART) ? dom.getTotalWidth(assertIsDefined(this._layoutService.getContainer(mainWindow, Parts.AUXILIARYBAR_PART))) : 0, statusBarHeight: this._layoutService.isVisible(Parts.STATUSBAR_PART, mainWindow) ? dom.getTotalHeight(assertIsDefined(this._layoutService.getContainer(mainWindow, Parts.STATUSBAR_PART))) : 0, windowBorder: this._layoutService.hasMainWindowBorder(), windowBorderRadius: this._layoutService.getMainWindowBorderRadius() diff --git a/src/vs/workbench/contrib/splash/browser/splash.contribution.ts b/src/vs/workbench/contrib/splash/browser/splash.contribution.ts index e690968bb847..e9c46b3355af 100644 --- a/src/vs/workbench/contrib/splash/browser/splash.contribution.ts +++ b/src/vs/workbench/contrib/splash/browser/splash.contribution.ts @@ -10,12 +10,14 @@ import { PartsSplash } from './partsSplash.js'; import { IPartsSplash } from '../../../../platform/theme/common/themeService.js'; registerSingleton(ISplashStorageService, class SplashStorageService implements ISplashStorageService { + _serviceBrand: undefined; async saveWindowSplash(splash: IPartsSplash): Promise { const raw = JSON.stringify(splash); localStorage.setItem('monaco-parts-splash', raw); } + }, InstantiationType.Delayed); registerWorkbenchContribution2( diff --git a/src/vs/workbench/contrib/splash/electron-sandbox/splash.contribution.ts b/src/vs/workbench/contrib/splash/electron-sandbox/splash.contribution.ts index 7081bd0c6c2b..503f15712b4e 100644 --- a/src/vs/workbench/contrib/splash/electron-sandbox/splash.contribution.ts +++ b/src/vs/workbench/contrib/splash/electron-sandbox/splash.contribution.ts @@ -11,7 +11,9 @@ import { PartsSplash } from '../browser/partsSplash.js'; import { IPartsSplash } from '../../../../platform/theme/common/themeService.js'; class SplashStorageService implements ISplashStorageService { + _serviceBrand: undefined; + readonly saveWindowSplash: (splash: IPartsSplash) => Promise; constructor(@INativeHostService nativeHostService: INativeHostService) { From a9e5a0eb66bb38a6f13d0b14e0937a872841162a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 24 Jan 2025 10:07:56 -0800 Subject: [PATCH 0875/3587] Warm up all sub-pixel offsets --- .../editor/browser/gpu/atlas/textureAtlas.ts | 36 +++++++++++-------- .../browser/gpu/raster/glyphRasterizer.ts | 7 ++-- .../renderStrategy/viewportRenderStrategy.ts | 2 +- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts index 67c6942c505c..c0ed5fdd06e9 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts @@ -172,27 +172,33 @@ export class TextureAtlas extends Disposable { // Warm up using roughly the larger glyphs first to help optimize atlas allocation // A-Z for (let code = CharCode.A; code <= CharCode.Z; code++) { - taskQueue.enqueue(() => { - for (const fgColor of colorMap.keys()) { - this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0); - } - }); + for (const fgColor of colorMap.keys()) { + taskQueue.enqueue(() => { + for (let x = 0; x < 1; x += 0.1) { + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0, x); + } + }); + } } // a-z for (let code = CharCode.a; code <= CharCode.z; code++) { - taskQueue.enqueue(() => { - for (const fgColor of colorMap.keys()) { - this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0); - } - }); + for (const fgColor of colorMap.keys()) { + taskQueue.enqueue(() => { + for (let x = 0; x < 1; x += 0.1) { + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0, x); + } + }); + } } // Remaining ascii for (let code = CharCode.ExclamationMark; code <= CharCode.Tilde; code++) { - taskQueue.enqueue(() => { - for (const fgColor of colorMap.keys()) { - this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0); - } - }); + for (const fgColor of colorMap.keys()) { + taskQueue.enqueue(() => { + for (let x = 0; x < 1; x += 0.1) { + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0, x); + } + }); + } } } } diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 49796167f04d..d06871cc24db 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -112,8 +112,9 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._ctx.save(); - const xSubPixelOffset = (tokenMetadata & 0b1111) / 10; - // console.log('xSubPixelOffset', xSubPixelOffset); + // The sub-pixel x offset is the fractional part of the x pixel coordinate of the cell, this + // is used to improve the spacing between rendered characters. + const xSubPixelXOffset = (tokenMetadata & 0b1111) / 10; const bgId = TokenMetadata.getBackground(tokenMetadata); const bg = colorMap[bgId]; @@ -160,7 +161,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._ctx.globalAlpha = decorationStyleSet.opacity; } - this._ctx.fillText(chars, originX + xSubPixelOffset, originY); + this._ctx.fillText(chars, originX + xSubPixelXOffset, originY); this._ctx.restore(); const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts index 82b7e75fd3b4..b71aaff05bc6 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -395,7 +395,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { this._visibleObjectCount = visibleObjectCount; return { - localContentWidth: Math.ceil(absoluteOffsetX / dpr) + localContentWidth: absoluteOffsetX }; } From 00d093ea29f4cd773212361438d7400cb56943a6 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 24 Jan 2025 21:38:37 +0100 Subject: [PATCH 0876/3587] Git - fix autostash to work with staged changes (#238719) --- extensions/git/src/repository.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 20fb28e8e587..3f7ccb765f3e 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -2530,7 +2530,9 @@ export class Repository implements Disposable { private async maybeAutoStash(runOperation: () => Promise): Promise { const config = workspace.getConfiguration('git', Uri.file(this.root)); const shouldAutoStash = config.get('autoStash') - && this.workingTreeGroup.resourceStates.some(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED); + && (this.indexGroup.resourceStates.length > 0 + || this.workingTreeGroup.resourceStates.some( + r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED)); if (!shouldAutoStash) { return await runOperation(); From 07f4d67ebb0b7f237bc98563376aa4882debf055 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Fri, 24 Jan 2025 13:24:29 -0800 Subject: [PATCH 0877/3587] debt: remove unused filter logging (#238722) --- .../preferences/browser/settingsEditor2.ts | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index de486320c041..ae9176fc4994 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -182,7 +182,6 @@ export class SettingsEditor2 extends EditorPane { private tocTreeContainer!: HTMLElement; private tocTree!: TOCTree; - private delayedFilterLogging: Delayer; private searchDelayer: Delayer; private searchInProgress: CancellationTokenSource | null = null; @@ -252,7 +251,6 @@ export class SettingsEditor2 extends EditorPane { @IUserDataProfileService userDataProfileService: IUserDataProfileService, ) { super(SettingsEditor2.ID, group, telemetryService, themeService, storageService); - this.delayedFilterLogging = new Delayer(1000); this.searchDelayer = new Delayer(300); this.viewState = { settingsTarget: ConfigurationTarget.USER_LOCAL }; @@ -1572,12 +1570,7 @@ export class SettingsEditor2 extends EditorPane { const query = this.searchWidget.getValue().trim(); this.viewState.query = query; - this.delayedFilterLogging.cancel(); await this.triggerSearch(query.replace(/\u203A/g, ' ')); - - if (query && this.searchResultModel) { - this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(this.searchResultModel)); - } } private parseSettingFromJSON(query: string): string | null { @@ -1688,44 +1681,6 @@ export class SettingsEditor2 extends EditorPane { return filterModel; } - private reportFilteringUsed(searchResultModel: SearchResultModel | null): void { - if (!searchResultModel) { - return; - } - - type SettingsEditorFilterEvent = { - 'counts.nlpResult': number | undefined; - 'counts.filterResult': number | undefined; - 'counts.uniqueResultsCount': number | undefined; - }; - type SettingsEditorFilterClassification = { - 'counts.nlpResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; 'comment': 'The number of matches found by the remote search provider, if applicable.' }; - 'counts.filterResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; 'comment': 'The number of matches found by the local search provider, if applicable.' }; - 'counts.uniqueResultsCount': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; 'comment': 'The number of unique matches over both search providers, if applicable.' }; - owner: 'rzhao271'; - comment: 'Tracks the performance of the built-in search providers'; - }; - // Count unique results - const counts: { nlpResult?: number; filterResult?: number } = {}; - const rawResults = searchResultModel.getRawResults(); - const filterResult = rawResults[SearchResultIdx.Local]; - if (filterResult) { - counts['filterResult'] = filterResult.filterMatches.length; - } - const nlpResult = rawResults[SearchResultIdx.Remote]; - if (nlpResult) { - counts['nlpResult'] = nlpResult.filterMatches.length; - } - - const uniqueResults = searchResultModel.getUniqueResults(); - const data = { - 'counts.nlpResult': counts['nlpResult'], - 'counts.filterResult': counts['filterResult'], - 'counts.uniqueResultsCount': uniqueResults?.filterMatches.length - }; - this.telemetryService.publicLog2('settingsEditor.filter', data); - } - private async triggerFilterPreferences(query: string): Promise { if (this.searchInProgress) { this.searchInProgress.cancel(); From 3faebfba41ce4e9d4b8c20132cd057275aa5dec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arda=20Ayd=C4=B1n?= Date: Sat, 25 Jan 2025 00:32:01 +0300 Subject: [PATCH 0878/3587] Change `Create New Terminal` to focus accordingly to the terminal location (#237404) * Change `Create New Terminal` codes to focus accordingly to the profile's location * Remove showPanel in favor of focusActiveTerminal in "Create New Terminal (In Active Workspace)" Co-authored-by: Megan Rogge --------- Co-authored-by: Megan Rogge --- src/vs/workbench/contrib/terminal/browser/terminalActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 9218eff9f83b..7b967628a7c4 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -317,8 +317,8 @@ export function registerTerminalActions() { return; } c.service.setActiveInstance(instance); + await focusActiveTerminal(instance, c); } - await c.groupService.showPanel(true); } }); From 7cb0aa04f2f871fa3e5c69c3517f2a0a9bb8b3fd Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 24 Jan 2025 16:14:08 -0600 Subject: [PATCH 0879/3587] set `top` on resize so widget is positioned correctly (#238723) fix #235080 --- .../workbench/services/suggest/browser/simpleSuggestWidget.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index b1ed529e0523..b2996c57db79 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -760,7 +760,9 @@ export class SimpleSuggestWidget extends Disposable { this._listElement.style.width = `${width}px`; this.element.layout(height, width); - + if (this._cursorPosition) { + this.element.domNode.style.top = `${this._cursorPosition.top - height}px`; + } this._positionDetails(); } From eefe48435177499a9e92a28d45ddb30c64324e03 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 24 Jan 2025 16:24:25 -0600 Subject: [PATCH 0880/3587] Add `~/` completion (#238727) part of #234352 --- .../src/terminalSuggestMain.ts | 21 +++++++++++++++---- .../browser/terminalCompletionService.ts | 19 +++++++++++++++++ .../browser/terminalCompletionService.test.ts | 14 +++++++++++++ ...e.proposed.terminalCompletionProvider.d.ts | 4 ++++ 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index b0511a31985d..6824fbb2e242 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -135,10 +135,18 @@ export async function activate(context: vscode.ExtensionContext) { const commands = [...commandsInPath.completionResources, ...builtinCommands]; const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition); - + const pathSeparator = isWindows ? '\\' : '/'; const result = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, terminal.shellIntegration?.cwd, token); + if (terminal.shellIntegration?.env) { + const homeDirCompletion = result.items.find(i => i.label === '~'); + if (homeDirCompletion && terminal.shellIntegration.env.HOME) { + homeDirCompletion.documentation = getFriendlyResourcePath(vscode.Uri.file(terminal.shellIntegration.env.HOME), pathSeparator, vscode.TerminalCompletionItemKind.Folder); + homeDirCompletion.kind = vscode.TerminalCompletionItemKind.Folder; + } + } + if (result.cwd && (result.filesRequested || result.foldersRequested)) { - return new vscode.TerminalCompletionList(result.items, { filesRequested: result.filesRequested, foldersRequested: result.foldersRequested, cwd: result.cwd, pathSeparator: isWindows ? '\\' : '/' }); + return new vscode.TerminalCompletionList(result.items, { filesRequested: result.filesRequested, foldersRequested: result.foldersRequested, cwd: result.cwd, pathSeparator: isWindows ? '\\' : '/', env: terminal.shellIntegration?.env }); } return result.items; } @@ -269,7 +277,7 @@ async function getCommandsInPath(env: { [key: string]: string | undefined } = pr const fileResource = vscode.Uri.file(path); const files = await vscode.workspace.fs.readDirectory(fileResource); for (const [file, fileType] of files) { - const formattedPath = getFriendlyFilePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); + const formattedPath = getFriendlyResourcePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath, cachedWindowsExecutableExtensions)) { executables.add({ label: file, detail: formattedPath }); labels.add(file); @@ -539,12 +547,17 @@ function getFirstCommand(commandLine: string): string | undefined { return firstCommand; } -function getFriendlyFilePath(uri: vscode.Uri, pathSeparator: string): string { +function getFriendlyResourcePath(uri: vscode.Uri, pathSeparator: string, kind?: vscode.TerminalCompletionItemKind): string { let path = uri.fsPath; // Ensure drive is capitalized on Windows if (pathSeparator === '\\' && path.match(/^[a-zA-Z]:\\/)) { path = `${path[0].toUpperCase()}:${path.slice(2)}`; } + if (kind === vscode.TerminalCompletionItemKind.Folder) { + if (!path.endsWith(pathSeparator)) { + path += pathSeparator; + } + } return path; } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 669c035acd40..fde380bd77b6 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -65,6 +65,7 @@ export interface TerminalResourceRequestConfig { cwd?: URI; pathSeparator: string; shouldNormalizePrefix?: boolean; + env?: { [key: string]: string | null | undefined }; } @@ -246,6 +247,24 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo const lastWordFolderHasDotPrefix = lastWordFolder.match(/^\.\.?[\\\/]/); + const lastWordFolderHasTildePrefix = lastWordFolder.match(/^~[\\\/]/); + if (lastWordFolderHasTildePrefix) { + // Handle specially + const resolvedFolder = resourceRequestConfig.env?.HOME ? URI.file(resourceRequestConfig.env.HOME) : undefined; + if (resolvedFolder) { + resourceCompletions.push({ + label: lastWordFolder, + provider, + kind: TerminalCompletionItemKind.Folder, + isDirectory: true, + isFile: false, + detail: getFriendlyPath(resolvedFolder, resourceRequestConfig.pathSeparator), + replacementIndex: cursorPosition - lastWord.length, + replacementLength: lastWord.length + }); + return resourceCompletions; + } + } // Add current directory. This should be shown at the top because it will be an exact match // and therefore highlight the detail, plus it improves the experience when runOnEnter is // used. diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index 679710de5fc2..7cdac7c3d674 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -162,6 +162,20 @@ suite('TerminalCompletionService', () => { { label: './../', detail: '/' }, ], { replacementIndex: 3, replacementLength: 3 }); }); + test('cd ~/| should return home folder completions', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///test/folder1'),// Updated to reflect home directory + foldersRequested: true, + pathSeparator, + shouldNormalizePrefix: true, + env: { HOME: '/test/' } + }; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ~/', 5, provider); + + assertCompletions(result, [ + { label: '~/', detail: '/test/' }, + ], { replacementIndex: 3, replacementLength: 2 }); + }); }); suite('resolveResources should handle file and folder completion requests correctly', () => { diff --git a/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts b/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts index 16b4809790b5..90e922f9304e 100644 --- a/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts @@ -128,5 +128,9 @@ declare module 'vscode' { * The path separator to use when constructing paths. */ pathSeparator: string; + /** + * Environment variables to use when constructing paths. + */ + env?: { [key: string]: string | null | undefined }; } } From 19e5c5853e0875575295fdf6d5c06034aa514e93 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 24 Jan 2025 16:26:51 -0600 Subject: [PATCH 0881/3587] cleanup suggest settings (#238709) --- extensions/terminal-suggest/README.md | 2 +- .../browser/terminal.suggest.contribution.ts | 13 ++++--------- .../suggest/browser/terminalCompletionService.ts | 5 ++--- .../suggest/browser/terminalSuggestAddon.ts | 3 +-- .../common/terminalSuggestConfiguration.ts | 15 +++------------ .../terminalSuggestAddon.integrationTest.ts | 3 +-- 6 files changed, 12 insertions(+), 29 deletions(-) diff --git a/extensions/terminal-suggest/README.md b/extensions/terminal-suggest/README.md index adaffc410ac4..cd1c1f4afa52 100644 --- a/extensions/terminal-suggest/README.md +++ b/extensions/terminal-suggest/README.md @@ -1,6 +1,6 @@ # Terminal Suggestions -**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. To enable the completions from this extension, set `terminal.integrated.suggest.enabled` and `terminal.integrated.suggest.enableExtensionCompletions` to `true`. +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. To enable the completions from this extension, set `terminal.integrated.suggest.enabled` to `true`. ## Features diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 442c19efa34b..7be1f75e05b0 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -65,20 +65,15 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo })); this._terminalSuggestWidgetVisibleContextKey = TerminalContextKeys.suggestWidgetVisible.bindTo(this._contextKeyService); this.add(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TerminalSuggestSettingId.EnableExtensionCompletions) || e.affectsConfiguration(TerminalSuggestSettingId.Enabled)) { - const extensionCompletionsEnabled = this._configurationService.getValue(terminalSuggestConfigSection).enableExtensionCompletions; + if (e.affectsConfiguration(TerminalSuggestSettingId.Enabled)) { const completionsEnabled = this._configurationService.getValue(terminalSuggestConfigSection).enabled; - if (!extensionCompletionsEnabled || !completionsEnabled) { - this._addon.clear(); - } if (!completionsEnabled) { + this._addon.clear(); this._pwshAddon.clear(); } const xtermRaw = this._ctx.instance.xterm?.raw; - if (!!xtermRaw && extensionCompletionsEnabled || completionsEnabled) { - if (xtermRaw) { - this._loadAddons(xtermRaw); - } + if (!!xtermRaw && completionsEnabled) { + this._loadAddons(xtermRaw); } } })); diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index fde380bd77b6..c7f4c3130698 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -14,7 +14,7 @@ import { IFileService } from '../../../../../platform/files/common/files.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; import { ISimpleCompletion } from '../../../../services/suggest/browser/simpleCompletionItem.js'; -import { ITerminalSuggestConfiguration, terminalSuggestConfigSection, TerminalSuggestSettingId } from '../common/terminalSuggestConfiguration.js'; +import { TerminalSuggestSettingId } from '../common/terminalSuggestConfiguration.js'; export const ITerminalCompletionService = createDecorator('terminalCompletionService'); @@ -131,7 +131,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return undefined; } - const extensionCompletionsEnabled = this._configurationService.getValue(terminalSuggestConfigSection).enableExtensionCompletions; let providers; if (triggerCharacter) { const providersToRequest: ITerminalCompletionProvider[] = []; @@ -151,7 +150,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo providers = [...this._providers.values()].flatMap(providerMap => [...providerMap.values()]); } - if (!extensionCompletionsEnabled || skipExtensionCompletions) { + if (skipExtensionCompletions) { providers = providers.filter(p => p.isBuiltin); return this._collectCompletions(providers, shellType, promptValue, cursorPosition, token); } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 4a28688b8930..efc1efa72e5a 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -156,8 +156,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest doNotRequestExtensionCompletions = true; } - const enableExtensionCompletions = this._configurationService.getValue(terminalSuggestConfigSection).enableExtensionCompletions; - if (enableExtensionCompletions && !doNotRequestExtensionCompletions) { + if (!doNotRequestExtensionCompletions) { await this._extensionService.activateByEvent('onTerminalCompletionsRequested'); } this._currentPromptInputState = { diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts index c126651bc712..21e0450d2224 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts @@ -14,7 +14,6 @@ export const enum TerminalSuggestSettingId { SuggestOnTriggerCharacters = 'terminal.integrated.suggest.suggestOnTriggerCharacters', RunOnEnter = 'terminal.integrated.suggest.runOnEnter', BuiltinCompletions = 'terminal.integrated.suggest.builtinCompletions', - EnableExtensionCompletions = 'terminal.integrated.suggest.enableExtensionCompletions', WindowsExecutableExtensions = 'terminal.integrated.suggest.windowsExecutableExtensions', Providers = 'terminal.integrated.suggest.providers', ShowStatusBar = 'terminal.integrated.suggest.showStatusBar', @@ -54,20 +53,19 @@ export interface ITerminalSuggestConfiguration { 'terminal-suggest': boolean; 'pwsh-shell-integration': boolean; }; - enableExtensionCompletions: boolean; } export const terminalSuggestConfiguration: IStringDictionary = { [TerminalSuggestSettingId.Enabled]: { restricted: true, - markdownDescription: localize('suggest.enabled', "Enables experimental terminal Intellisense suggestions for supported shells ({0}) when {1} is set to {2}.\n\nIf shell integration is installed manually, {3} needs to be set to {4} before calling the shell integration script. \n\nFor extension provided completions, {5} will also need to be set.", 'PowerShell v7+, zsh, bash, fish', `\`#${TerminalSettingId.ShellIntegrationEnabled}#\``, '`true`', '`VSCODE_SUGGEST`', '`1`', `\`#${TerminalSuggestSettingId.EnableExtensionCompletions}#\``), + markdownDescription: localize('suggest.enabled', "Enables experimental terminal Intellisense suggestions for supported shells ({0}) when {1} is set to {2}.\n\nIf shell integration is installed manually, {3} needs to be set to {4} before calling the shell integration script.", 'PowerShell v7+, zsh, bash, fish', `\`#${TerminalSettingId.ShellIntegrationEnabled}#\``, '`true`', '`VSCODE_SUGGEST`', '`1`'), type: 'boolean', default: false, tags: ['experimental'], }, [TerminalSuggestSettingId.Providers]: { restricted: true, - markdownDescription: localize('suggest.providers', "Controls which providers are enabled for terminal suggestions. Also be aware of the {0}-setting which controls if extensions are able to provide suggestions.", `\`#${TerminalSuggestSettingId.EnableExtensionCompletions}#\``), + markdownDescription: localize('suggest.providers', "Controls which providers are enabled for terminal suggestions."), type: 'object', properties: {}, default: { @@ -120,16 +118,9 @@ export const terminalSuggestConfiguration: IStringDictionary `- ${extension}`).join('\n'), ), type: 'object', diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts index ab229b5be42a..b3ee142148d2 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts @@ -109,7 +109,6 @@ suite('Terminal Contrib Suggest Recordings', () => { 'terminal-suggest': true, 'pwsh-shell-integration': true, }, - enableExtensionCompletions: false } satisfies ITerminalSuggestConfiguration } }; @@ -126,7 +125,7 @@ suite('Terminal Contrib Suggest Recordings', () => { instantiationService.stub(ITerminalCompletionService, store.add(completionService)); const shellIntegrationAddon = store.add(new ShellIntegrationAddon('', true, undefined, new NullLogService)); pwshCompletionProvider = store.add(instantiationService.createInstance(PwshCompletionProviderAddon, new Set(parseCompletionsFromShell(testRawPwshCompletions, -1, -1)), shellIntegrationAddon.capabilities)); - store.add(completionService.registerTerminalCompletionProvider('builtin-pwsh', 'pwsh', pwshCompletionProvider)); + store.add(completionService.registerTerminalCompletionProvider('builtin-pwsh', PwshCompletionProviderAddon.ID, pwshCompletionProvider)); const TerminalCtor = (await importAMDNodeModule('@xterm/xterm', 'lib/xterm.js')).Terminal; xterm = store.add(new TerminalCtor({ allowProposedApi: true })); capabilities = shellIntegrationAddon.capabilities; From e8780960526dd8e2313efc1725b1161ca2a41159 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 24 Jan 2025 23:45:59 +0100 Subject: [PATCH 0882/3587] fix #238693 (#238726) --- .../abstractExtensionManagementService.ts | 14 ----- .../common/allowedExtensionsService.ts | 50 +++++------------- .../common/extensionManagement.ts | 9 ++-- .../common/allowedExtensionsService.test.ts | 2 +- .../browser/extensions.contribution.ts | 18 ++----- .../extensions/browser/extensionsActions.ts | 10 ++-- .../browser/extensionsWorkbenchService.ts | 2 +- .../extensionRecommendationsService.test.ts | 1 + .../extensionsActions.test.ts | 2 + .../electron-sandbox/extensionsViews.test.ts | 1 + .../extensionsWorkbenchService.test.ts | 2 + .../browser/extensionEnablementService.ts | 2 +- .../common/extensionManagement.ts | 3 ++ .../common/extensionManagementService.ts | 51 +++++++++++++++++-- .../extensionManagementService.ts | 3 ++ .../extensionEnablementService.test.ts | 4 +- .../test/browser/workbenchTestServices.ts | 2 + 17 files changed, 92 insertions(+), 84 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index dde862d66563..4bef58907b79 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -196,20 +196,6 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio results.push(...await this.installExtensions(installableExtensions)); } - const untrustedPublishers = new Set(); - for (const result of results) { - if (result.local && result.source && !URI.isUri(result.source) && !this.allowedExtensionsService.isTrusted(result.source)) { - untrustedPublishers.add(result.source.publisher); - } - } - if (untrustedPublishers.size) { - try { - await this.allowedExtensionsService.trustPublishers(...untrustedPublishers); - } catch (error) { - this.logService.error('Error while trusting publishers', getErrorMessage(error), ...untrustedPublishers); - } - } - return results; } diff --git a/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts b/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts index b47dbd467230..fe483a5a408d 100644 --- a/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts +++ b/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts @@ -6,12 +6,11 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { URI } from '../../../base/common/uri.js'; import * as nls from '../../../nls.js'; -import { IGalleryExtension, AllowedExtensionsConfigKey, IAllowedExtensionsService, TrustedPublishersConfigKey } from './extensionManagement.js'; +import { IGalleryExtension, AllowedExtensionsConfigKey, IAllowedExtensionsService, AllowedExtensionsConfigValueType } from './extensionManagement.js'; import { ExtensionType, IExtension, TargetPlatform } from '../../extensions/common/extensions.js'; import { IProductService } from '../../product/common/productService.js'; import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; -import { IStringDictionary } from '../../../base/common/collections.js'; import { isBoolean, isObject, isUndefined } from '../../../base/common/types.js'; import { Emitter } from '../../../base/common/event.js'; @@ -26,18 +25,18 @@ function isIExtension(extension: any): extension is IExtension { const VersionRegex = /^(?\d+\.\d+\.\d+(-.*)?)(@(?.+))?$/; -type AllowedExtensionsConfigValueType = IStringDictionary; - export class AllowedExtensionsService extends Disposable implements IAllowedExtensionsService { _serviceBrand: undefined; - private allowedExtensions: AllowedExtensionsConfigValueType | undefined; private readonly publisherOrgs: string[]; - private readonly trustedPublishers: readonly string[]; + private _allowedExtensionsConfigValue: AllowedExtensionsConfigValueType | undefined; + get allowedExtensionsConfigValue(): AllowedExtensionsConfigValueType | undefined { + return this._allowedExtensionsConfigValue; + } private _onDidChangeAllowedExtensions = this._register(new Emitter()); - readonly onDidChangeAllowedExtensions = this._onDidChangeAllowedExtensions.event; + readonly onDidChangeAllowedExtensionsConfigValue = this._onDidChangeAllowedExtensions.event; constructor( @IProductService productService: IProductService, @@ -45,11 +44,10 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte ) { super(); this.publisherOrgs = productService.extensionPublisherOrgs?.map(p => p.toLowerCase()) ?? []; - this.allowedExtensions = this.getAllowedExtensionsValue(); - this.trustedPublishers = productService.trustedExtensionPublishers ?? []; + this._allowedExtensionsConfigValue = this.getAllowedExtensionsValue(); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AllowedExtensionsConfigKey)) { - this.allowedExtensions = this.getAllowedExtensionsValue(); + this._allowedExtensionsConfigValue = this.getAllowedExtensionsValue(); this._onDidChangeAllowedExtensions.fire(); } })); @@ -68,7 +66,7 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte } isAllowed(extension: IGalleryExtension | IExtension | { id: string; publisherDisplayName: string | undefined; version?: string; prerelease?: boolean; targetPlatform?: TargetPlatform }): true | IMarkdownString { - if (!this.allowedExtensions) { + if (!this._allowedExtensionsConfigValue) { return true; } @@ -98,7 +96,7 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte } const settingsCommandLink = URI.parse(`command:workbench.action.openSettings?${encodeURIComponent(JSON.stringify({ query: `@id:${AllowedExtensionsConfigKey}` }))}`).toString(); - const extensionValue = this.allowedExtensions[id]; + const extensionValue = this._allowedExtensionsConfigValue[id]; const extensionReason = new MarkdownString(nls.localize('specific extension not allowed', "it is not in the [allowed list]({0})", settingsCommandLink)); if (!isUndefined(extensionValue)) { if (isBoolean(extensionValue)) { @@ -127,7 +125,7 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte } const publisherKey = publisherDisplayName && this.publisherOrgs.includes(publisherDisplayName) ? publisherDisplayName : publisher; - const publisherValue = this.allowedExtensions[publisherKey]; + const publisherValue = this._allowedExtensionsConfigValue[publisherKey]; if (!isUndefined(publisherValue)) { if (isBoolean(publisherValue)) { return publisherValue ? true : new MarkdownString(nls.localize('publisher not allowed', "the extensions from this publisher are not in the [allowed list]({1})", publisherKey, settingsCommandLink)); @@ -138,34 +136,10 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte return true; } - if (this.allowedExtensions['*'] === true) { + if (this._allowedExtensionsConfigValue['*'] === true) { return true; } return extensionReason; } - - isTrusted(extension: IGalleryExtension): boolean { - const publisher = extension.publisher.toLowerCase(); - if (this.trustedPublishers.includes(publisher) || this.trustedPublishers.includes(extension.publisherDisplayName.toLowerCase())) { - return true; - } - - // Check if the extension is allowed by publisher or extension id - if (this.allowedExtensions && (this.allowedExtensions[publisher] || this.allowedExtensions[extension.identifier.id.toLowerCase()])) { - return true; - } - - const trustedPublishers = (this.configurationService.getValue(TrustedPublishersConfigKey) ?? []).map(p => p.toLowerCase()); - return trustedPublishers.includes(publisher); - } - - async trustPublishers(...publishers: string[]): Promise { - const trustedPublishers = (this.configurationService.getValue(TrustedPublishersConfigKey) ?? []).map(p => p.toLowerCase()); - publishers = publishers.map(p => p.toLowerCase()).filter(p => !trustedPublishers.includes(p)); - if (publishers.length) { - trustedPublishers.push(...publishers); - await this.configurationService.updateValue(TrustedPublishersConfigKey, trustedPublishers); - } - } } diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 1381ea594c37..c186beb87737 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -628,17 +628,17 @@ export interface IExtensionTipsService { getOtherExecutableBasedTips(): Promise; } +export type AllowedExtensionsConfigValueType = IStringDictionary; + export const IAllowedExtensionsService = createDecorator('IAllowedExtensionsService'); export interface IAllowedExtensionsService { readonly _serviceBrand: undefined; - readonly onDidChangeAllowedExtensions: Event; + readonly allowedExtensionsConfigValue: AllowedExtensionsConfigValueType | undefined; + readonly onDidChangeAllowedExtensionsConfigValue: Event; isAllowed(extension: IGalleryExtension | IExtension): true | IMarkdownString; isAllowed(extension: { id: string; publisherDisplayName: string | undefined; version?: string; prerelease?: boolean; targetPlatform?: TargetPlatform }): true | IMarkdownString; - - isTrusted(extension: IGalleryExtension): boolean; - trustPublishers(...publishers: string[]): Promise; } export async function computeSize(location: URI, fileService: IFileService): Promise { @@ -662,4 +662,3 @@ export const ExtensionsLocalizedLabel = localize2('extensions', "Extensions"); export const PreferencesLocalizedLabel = localize2('preferences', 'Preferences'); export const UseUnpkgResourceApiConfigKey = 'extensions.gallery.useUnpkgResourceApi'; export const AllowedExtensionsConfigKey = 'extensions.allowed'; -export const TrustedPublishersConfigKey = 'extensions.trustedPublishers'; diff --git a/src/vs/platform/extensionManagement/test/common/allowedExtensionsService.test.ts b/src/vs/platform/extensionManagement/test/common/allowedExtensionsService.test.ts index 72491b401109..3bcca170ffc7 100644 --- a/src/vs/platform/extensionManagement/test/common/allowedExtensionsService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/allowedExtensionsService.test.ts @@ -196,7 +196,7 @@ suite('AllowedExtensionsService', () => { test('should trigger change event when allowed list change', async () => { configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { '*': false }); const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); - const promise = Event.toPromise(testObject.onDidChangeAllowedExtensions); + const promise = Event.toPromise(testObject.onDidChangeAllowedExtensionsConfigValue); configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true, affectedKeys: new Set([AllowedExtensionsConfigKey]), change: { keys: [], overrides: [] }, source: ConfigurationTarget.USER }); await promise; }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 75f95e903ebe..e65cd1903cde 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -8,7 +8,7 @@ import { KeyMod, KeyCode } from '../../../../base/common/keyCodes.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuRegistry, MenuId, registerAction2, Action2, IMenuItem, IAction2Options } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApiConfigKey, AllowedExtensionsConfigKey, TrustedPublishersConfigKey, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApiConfigKey, AllowedExtensionsConfigKey, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; @@ -272,16 +272,6 @@ Registry.as(ConfigurationExtensions.Configuration) scope: ConfigurationScope.APPLICATION, tags: ['onExp', 'usesOnlineServices'] }, - [TrustedPublishersConfigKey]: { - type: 'array', - markdownDescription: localize('extensions.trustedPublishers', "Specify a list of trusted publishers. Extensions from these publishers will be allowed to install without consent from the user."), - scope: ConfigurationScope.APPLICATION, - items: { - type: 'string', - pattern: '^[a-z0-9A-Z][a-z0-9-A-Z]*$', - patternErrorMessage: localize('extensions.trustedPublishers.patternError', "Publisher names must only contain letters and numbers."), - } - }, [AllowedExtensionsConfigKey]: { // Note: Type is set only to object because to support policies generation during build time, where single type is expected. type: 'object', @@ -1932,13 +1922,13 @@ class ExtensionStorageCleaner implements IWorkbenchContribution { class TrustedPublishersInitializer implements IWorkbenchContribution { constructor( - @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IWorkbenchExtensionManagementService extensionManagementService: IWorkbenchExtensionManagementService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, @IProductService productService: IProductService, @IStorageService storageService: IStorageService, ) { - const trustedPublishersInitStatusKey = 'trusted-publishers-initialized'; + const trustedPublishersInitStatusKey = 'trusted-publishers-migration'; if (!storageService.get(trustedPublishersInitStatusKey, StorageScope.APPLICATION)) { for (const profile of userDataProfilesService.profiles) { extensionManagementService.getInstalled(ExtensionType.User, profile.extensionsResource) @@ -1956,7 +1946,7 @@ class TrustedPublishersInitializer implements IWorkbenchContribution { trustedPublishers.add(publisher); } if (trustedPublishers.size) { - await allowedExtensionsService.trustPublishers(...trustedPublishers); + extensionManagementService.trustPublishers(...trustedPublishers); } storageService.store(trustedPublishersInitStatusKey, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE); }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 052e14050eb7..83bf9531cc50 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -417,7 +417,7 @@ export class InstallAction extends ExtensionAction { this.hideOnDisabled = false; this.options = { isMachineScoped: false, ...options }; this.update(); - this._register(allowedExtensionsService.onDidChangeAllowedExtensions(() => this.update())); + this._register(allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => this.update())); this._register(this.labelService.onDidChangeFormatters(() => this.updateLabel(), this)); } @@ -1024,7 +1024,7 @@ export class ToggleAutoUpdateForExtensionAction extends ExtensionAction { this.update(); } })); - this._register(allowedExtensionsService.onDidChangeAllowedExtensions(e => this.update())); + this._register(allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(e => this.update())); this.update(); } @@ -1458,7 +1458,7 @@ export class TogglePreReleaseExtensionAction extends ExtensionAction { @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, ) { super(TogglePreReleaseExtensionAction.ID, TogglePreReleaseExtensionAction.LABEL, TogglePreReleaseExtensionAction.DisabledClass); - this._register(allowedExtensionsService.onDidChangeAllowedExtensions(() => this.update())); + this._register(allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => this.update())); this.update(); } @@ -1534,7 +1534,7 @@ export class InstallAnotherVersionAction extends ExtensionAction { @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, ) { super(InstallAnotherVersionAction.ID, InstallAnotherVersionAction.LABEL, ExtensionAction.LABEL_ACTION_CLASS); - this._register(allowedExtensionsService.onDidChangeAllowedExtensions(() => this.update())); + this._register(allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => this.update())); this.extension = extension; this.update(); } @@ -2521,7 +2521,7 @@ export class ExtensionStatusAction extends ExtensionAction { this._register(this.labelService.onDidChangeFormatters(() => this.update(), this)); this._register(this.extensionService.onDidChangeExtensions(() => this.update())); this._register(this.extensionFeaturesManagementService.onDidChangeAccessData(() => this.update())); - this._register(allowedExtensionsService.onDidChangeAllowedExtensions(() => this.update())); + this._register(allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => this.update())); this.update(); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index e68c51bcfa4e..a7afc648e877 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1101,7 +1101,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } })); - this._register(this.allowedExtensionsService.onDidChangeAllowedExtensions(() => { + this._register(this.allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => { if (this.isAutoCheckUpdatesEnabled()) { this.checkForUpdates(); } diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts index bff7df509c71..a380a7142001 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts @@ -228,6 +228,7 @@ suite('ExtensionRecommendationsService Test', () => { onDidUninstallExtension: Event.None, onDidUpdateExtensionMetadata: Event.None, onDidChangeProfile: Event.None, + onProfileAwareDidInstallExtensions: Event.None, async getInstalled() { return []; }, async canInstall() { return true; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [], publisherMapping: {} }; }, diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index dca3201e44d9..bce84045b3ee 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -101,6 +101,7 @@ function setupTest(disposables: Pick) { onDidUninstallExtension: didUninstallEvent.event as any, onDidUpdateExtensionMetadata: Event.None, onDidChangeProfile: Event.None, + onProfileAwareDidInstallExtensions: Event.None, async getInstalled() { return []; }, async getInstalledWorkspaceExtensions() { return []; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [], publisherMapping: {} }; }, @@ -2642,6 +2643,7 @@ function createExtensionManagementService(installed: ILocalExtension[] = []): IP onDidUninstallExtension: Event.None, onDidChangeProfile: Event.None, onDidUpdateExtensionMetadata: Event.None, + onProfileAwareDidInstallExtensions: Event.None, getInstalled: () => Promise.resolve(installed), canInstall: async (extension: IGalleryExtension) => { return true; }, installFromGallery: (extension: IGalleryExtension) => Promise.reject(new Error('not supported')), diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts index 19268a3cecd7..3752be94aaa1 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts @@ -101,6 +101,7 @@ suite('ExtensionsViews Tests', () => { onDidUninstallExtension: Event.None, onDidUpdateExtensionMetadata: Event.None, onDidChangeProfile: Event.None, + onProfileAwareDidInstallExtensions: Event.None, async getInstalled() { return []; }, async getInstalledWorkspaceExtensions() { return []; }, async canInstall() { return true; }, diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index c49ca4dfa186..9c8f2b5c983a 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -102,6 +102,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { onDidUninstallExtension: didUninstallEvent.event as any, onDidUpdateExtensionMetadata: Event.None, onDidChangeProfile: Event.None, + onProfileAwareDidInstallExtensions: Event.None, async getInstalled() { return []; }, async getInstalledWorkspaceExtensions() { return []; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [], publisherMapping: {} }; }, @@ -1706,6 +1707,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { onDidUninstallExtension: Event.None, onDidChangeProfile: Event.None, onDidUpdateExtensionMetadata: Event.None, + onProfileAwareDidInstallExtensions: Event.None, getInstalled: () => Promise.resolve(installed), installFromGallery: (extension: IGalleryExtension) => Promise.reject(new Error('not supported')), updateMetadata: async (local: Mutable, metadata: Partial, profileLocation: URI) => { diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 0a909924e607..2ad63cc370b8 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -80,7 +80,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench }); this._register(this.globalExtensionEnablementService.onDidChangeEnablement(({ extensions, source }) => this._onDidChangeGloballyDisabledExtensions(extensions, source))); - this._register(allowedExtensionsService.onDidChangeAllowedExtensions(() => this._onDidChangeExtensions([], [], false))); + this._register(allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => this._onDidChangeExtensions([], [], false))); // delay notification for extensions disabled until workbench restored if (this.allUserExtensionsDisabled) { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 1deee908d89b..927712e5b4b0 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -92,6 +92,9 @@ export interface IWorkbenchExtensionManagementService extends IProfileAwareExten updateFromGallery(gallery: IGalleryExtension, extension: ILocalExtension, installOptions?: InstallOptions): Promise; updateMetadata(local: ILocalExtension, metadata: Partial): Promise; + + isPublisherTrusted(extension: IGalleryExtension): boolean; + trustPublishers(...publishers: string[]): void; } export const enum EnablementState { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 37e772e97e34..276b50caf0e6 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -11,7 +11,7 @@ import { ExtensionInstallSource, DidUpdateExtensionMetadata, UninstallExtensionInfo, - IAllowedExtensionsService + IAllowedExtensionsService, } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IResourceExtension, IWorkbenchExtensionManagementService, IWorkbenchInstallOptions, UninstallExtensionOnServerEvent } from './extensionManagement.js'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from '../../../../platform/extensions/common/extensions.js'; @@ -49,6 +49,8 @@ import { joinPath } from '../../../../base/common/resources.js'; import { verifiedPublisherIcon } from './extensionsIcons.js'; import { Codicon } from '../../../../base/common/codicons.js'; +const TrustedPublishersStorageKey = 'extensions.trustedPublishers'; + function isGalleryExtension(extension: IResourceExtension | IGalleryExtension): extension is IGalleryExtension { return extension.type === 'gallery'; } @@ -57,6 +59,8 @@ export class ExtensionManagementService extends Disposable implements IWorkbench declare readonly _serviceBrand: undefined; + private readonly defaultTrustedPublishers: readonly string[]; + private readonly _onInstallExtension = this._register(new Emitter()); readonly onInstallExtension: Event; @@ -104,10 +108,12 @@ export class ExtensionManagementService extends Disposable implements IWorkbench @IInstantiationService private readonly instantiationService: IInstantiationService, @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, + @IStorageService private readonly storageService: IStorageService, @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); + this.defaultTrustedPublishers = productService.trustedExtensionPublishers ?? []; this.workspaceExtensionManagementService = this._register(this.instantiationService.createInstance(WorkspaceExtensionsManagementService)); this.onDidEnableExtensions = this.workspaceExtensionManagementService.onDidChangeInvalidExtensions; @@ -165,6 +171,18 @@ export class ExtensionManagementService extends Disposable implements IWorkbench this._register(onDidProfileAwareUpdateExtensionMetadaEventMultiplexer.add(server.extensionManagementService.onProfileAwareDidUpdateExtensionMetadata)); this._register(onDidChangeProfileEventMultiplexer.add(Event.map(server.extensionManagementService.onDidChangeProfile, e => ({ ...e, server })))); } + + this._register(this.onProfileAwareDidInstallExtensions(results => { + const untrustedPublishers = new Set(); + for (const result of results) { + if (result.local && result.source && !URI.isUri(result.source) && !this.isPublisherTrusted(result.source)) { + untrustedPublishers.add(result.source.publisher); + } + } + if (untrustedPublishers.size) { + this.trustPublishers(...untrustedPublishers); + } + })); } async getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise { @@ -772,7 +790,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } private async checkForTrustedPublisher(extension: IGalleryExtension): Promise { - if (this.allowedExtensionsService.isTrusted(extension)) { + if (this.isPublisherTrusted(extension)) { return; } @@ -789,9 +807,9 @@ export class ExtensionManagementService extends Disposable implements IWorkbench const installButton: IPromptButton = { label: localize({ key: 'trust and install', comment: ['&& denotes a mnemonic'] }, "Trust Publisher & &&Install"), - run: async () => { + run: () => { this.telemetryService.publicLog2('extensions:trustPublisher', { action: 'trust', extensionId: extension.identifier.id }); - await this.allowedExtensionsService.trustPublishers(extension.publisher); + this.trustPublishers(extension.publisher); } }; @@ -965,6 +983,31 @@ export class ExtensionManagementService extends Disposable implements IWorkbench registerParticipant() { throw new Error('Not Supported'); } installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise { throw new Error('Not Supported'); } + + isPublisherTrusted(extension: IGalleryExtension): boolean { + const publisher = extension.publisher.toLowerCase(); + if (this.defaultTrustedPublishers.includes(publisher) || this.defaultTrustedPublishers.includes(extension.publisherDisplayName.toLowerCase())) { + return true; + } + + // Check if the extension is allowed by publisher or extension id + if (this.allowedExtensionsService.allowedExtensionsConfigValue && this.allowedExtensionsService.isAllowed(extension)) { + return true; + } + + const trustedPublishers = this.storageService.getObject(TrustedPublishersStorageKey, StorageScope.APPLICATION, []).map(p => p.toLowerCase()); + this.logService.debug('Trusted publishers', trustedPublishers); + return trustedPublishers.includes(publisher); + } + + trustPublishers(...publishers: string[]): void { + const trustedPublishers = this.storageService.getObject(TrustedPublishersStorageKey, StorageScope.APPLICATION, []).map(p => p.toLowerCase()); + publishers = publishers.map(p => p.toLowerCase()).filter(p => !trustedPublishers.includes(p)); + if (publishers.length) { + trustedPublishers.push(...publishers); + this.storageService.store(TrustedPublishersStorageKey, trustedPublishers, StorageScope.APPLICATION, StorageTarget.USER); + } + } } class WorkspaceExtensionsManagementService extends Disposable { diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts index 403336b5f20f..eeae819a0837 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts @@ -26,6 +26,7 @@ import { IUserDataProfileService } from '../../userDataProfile/common/userDataPr import { IExtensionsScannerService } from '../../../../platform/extensionManagement/common/extensionsScannerService.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; +import { IStorageService } from '../../../../platform/storage/common/storage.js'; export class ExtensionManagementService extends BaseExtensionManagementService { @@ -47,6 +48,7 @@ export class ExtensionManagementService extends BaseExtensionManagementService { @IInstantiationService instantiationService: IInstantiationService, @IExtensionsScannerService extensionsScannerService: IExtensionsScannerService, @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, + @IStorageService storageService: IStorageService, @ITelemetryService telemetryService: ITelemetryService, ) { super( @@ -66,6 +68,7 @@ export class ExtensionManagementService extends BaseExtensionManagementService { instantiationService, extensionsScannerService, allowedExtensionsService, + storageService, telemetryService ); } diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index d5a9d45a0a06..f7c0ff78feb9 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -8,7 +8,7 @@ import { IExtensionManagementService, DidUninstallExtensionEvent, ILocalExtensio import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, ExtensionInstallLocation, IProfileAwareExtensionManagementService, DidChangeProfileEvent } from '../../common/extensionManagement.js'; import { ExtensionEnablementService } from '../../browser/extensionEnablementService.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { Emitter } from '../../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; import { IWorkspace, IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkbenchEnvironmentService } from '../../../environment/common/environmentService.js'; import { IStorageService, InMemoryStorageService } from '../../../../../platform/storage/common/storage.js'; @@ -72,6 +72,7 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { onDidUninstallExtension: disposables.add(new Emitter()).event, onDidChangeProfile: disposables.add(new Emitter()).event, onDidUpdateExtensionMetadata: disposables.add(new Emitter()).event, + onProfileAwareDidInstallExtensions: Event.None, }, }, null, null)); const extensionManagementService = disposables.add(instantiationService.createInstance(ExtensionManagementService)); @@ -147,6 +148,7 @@ suite('ExtensionEnablementService Test', () => { onDidInstallExtensions: didInstallEvent.event, onDidUninstallExtension: didUninstallEvent.event, onDidChangeProfile: didChangeProfileExtensionsEvent.event, + onProfileAwareDidInstallExtensions: Event.None, getInstalled: () => Promise.resolve(installed) }, }, null, null)); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 90709b3205db..2fe4a1635747 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -2248,6 +2248,8 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens getExtensions(): Promise { throw new Error('Method not implemented.'); } resetPinnedStateForAllUserExtensions(pinned: boolean): Promise { throw new Error('Method not implemented.'); } getInstallableServers(extension: IGalleryExtension): Promise { throw new Error('Method not implemented.'); } + isPublisherTrusted(extension: IGalleryExtension): boolean { return false; } + trustPublishers(...publishers: string[]): void { } } export class TestUserDataProfileService implements IUserDataProfileService { From 2817b9b0600d7e1d14808b70847c5f47610a0048 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:48:36 -0800 Subject: [PATCH 0883/3587] debt: reduce telemetry logging (#238729) --- .../contrib/preferences/browser/settingsEditor2.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index ae9176fc4994..101d7f4e5a2f 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -1157,6 +1157,13 @@ export class SettingsEditor2 extends EditorPane { this.refreshTOCTree(); } this.renderTree(key, isManualReset); + this.pendingSettingUpdate = null; + + // Only log 1% of modification events to reduce the volume of data + if (Math.random() >= 0.01) { + return; + } + const reportModifiedProps = { key, query, @@ -1166,8 +1173,6 @@ export class SettingsEditor2 extends EditorPane { isReset: typeof value === 'undefined', settingsTarget: this.settingsTargetsWidget.settingsTarget as SettingsTarget }; - - this.pendingSettingUpdate = null; return this.reportModifiedSetting(reportModifiedProps); }); } From ebd1049af4e920ccda722ef6d8d5c4872cb25001 Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Fri, 24 Jan 2025 15:06:03 -0800 Subject: [PATCH 0884/3587] Fix edge cases for nb inline values (#238730) break, lastCol vs 0, cts token per cell --- .../notebookInlineVariables.ts | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts index 9f180017354b..8c53b1cf1112 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts @@ -47,7 +47,7 @@ export class NotebookInlineVariablesController extends Disposable implements INo private cellDecorationIds = new Map(); private cellContentListeners = new ResourceMap(); - private currentCancellationTokenSource: CancellationTokenSource | null = null; + private currentCancellationTokenSources = new ResourceMap(); constructor( private readonly notebookEditor: INotebookEditor, @@ -71,32 +71,34 @@ export class NotebookInlineVariablesController extends Disposable implements INo } private async updateInlineVariables(event: ICellExecutionStateChangedEvent): Promise { - // Cancel any ongoing request - if (this.currentCancellationTokenSource) { - this.currentCancellationTokenSource.cancel(); + if (event.changed) { // undefined -> execution was completed, so return on all else. no code should execute until we know it's an execution completion + return; } - // Create a new CancellationTokenSource for the new request - this.currentCancellationTokenSource = new CancellationTokenSource(); - const token = this.currentCancellationTokenSource.token; - - if (this.debugService.state !== State.Inactive) { - this._clearNotebookInlineDecorations(); + const cell = this.notebookEditor.getCellByHandle(event.cellHandle); + if (!cell) { return; } - if (!this.notebookEditor.textModel?.uri || !isEqual(this.notebookEditor.textModel.uri, event.notebook)) { - return; + // Cancel any ongoing request in this cell + const existingSource = this.currentCancellationTokenSources.get(cell.uri); + if (existingSource) { + existingSource.cancel(); } - if (event.changed) { // undefined -> execution was completed, so return on all else + // Create a new CancellationTokenSource for the new request per cell + this.currentCancellationTokenSources.set(cell.uri, new CancellationTokenSource()); + const token = this.currentCancellationTokenSources.get(cell.uri)!.token; + + if (this.debugService.state !== State.Inactive) { + this._clearNotebookInlineDecorations(); return; } - const cell = this.notebookEditor.getCellByHandle(event.cellHandle); - if (!cell) { + if (!this.notebookEditor.textModel?.uri || !isEqual(this.notebookEditor.textModel.uri, event.notebook)) { return; } + const model = await cell.resolveTextModel(); if (!model) { return; @@ -112,7 +114,7 @@ export class NotebookInlineVariablesController extends Disposable implements INo const lastColumn = model.getLineMaxColumn(lastLine); const ctx: InlineValueContext = { frameId: 0, // ignored, we won't have a stack from since not in a debug session - stoppedLocation: new Range(lastLine + 1, 0, lastLine + 1, 0) // executing cell by cell, so "stopped" location would just be the end of document + stoppedLocation: new Range(lastLine, lastColumn, lastLine, lastColumn) // executing cell by cell, so "stopped" location would just be the end of document }; const providers = this.languageFeaturesService.inlineValuesProvider.ordered(model).reverse(); @@ -155,6 +157,7 @@ export class NotebookInlineVariablesController extends Disposable implements INo continue; } text = format('{0} = {1}', name, value); + break; } case 'expression': { continue; // no active debug session, so evaluate would break @@ -328,9 +331,8 @@ export class NotebookInlineVariablesController extends Disposable implements INo override dispose(): void { super.dispose(); this._clearNotebookInlineDecorations(); - if (this.currentCancellationTokenSource) { - this.currentCancellationTokenSource.cancel(); - } + this.currentCancellationTokenSources.forEach(source => source.cancel()); + this.currentCancellationTokenSources.clear(); } } From 6befccc0307824cb09158b3891314c2bba4eaac2 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Fri, 24 Jan 2025 15:16:55 -0800 Subject: [PATCH 0885/3587] feat: comprehensive key match scoring system (#238592) Also adds a new preview setting that is off by default to prevent more regressions. --- .../preferences/browser/preferencesSearch.ts | 54 +++++++++++++++---- .../common/preferencesContribution.ts | 7 +++ 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index ba3ab00f1c09..375b01219419 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -7,7 +7,7 @@ import { ISettingsEditorModel, ISetting, ISettingsGroup, ISearchResult, IGroupFi import { IRange } from '../../../../editor/common/core/range.js'; import { distinct } from '../../../../base/common/arrays.js'; import * as strings from '../../../../base/common/strings.js'; -import { IMatch, matchesContiguousSubString, matchesWords } from '../../../../base/common/filters.js'; +import { IMatch, matchesContiguousSubString, matchesSubString, matchesWords } from '../../../../base/common/filters.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { IPreferencesSearchService, IRemoteSearchProvider, ISearchProvider, IWorkbenchSettingsConfiguration } from '../common/preferences.js'; @@ -138,6 +138,8 @@ export class LocalSearchProvider implements ISearchProvider { export class SettingMatches { readonly matches: IRange[]; + /** Whether to use the new key matching search algorithm that calculates more weights for each result */ + useNewKeyMatchingSearch: boolean = false; matchType: SettingMatchType = SettingMatchType.None; /** * A match score for key matches to allow comparing key matches against each other. @@ -153,6 +155,7 @@ export class SettingMatches { valuesMatcher: (filter: string, setting: ISetting) => IRange[], @IConfigurationService private readonly configurationService: IConfigurationService ) { + this.useNewKeyMatchingSearch = this.configurationService.getValue('workbench.settings.useWeightedKeySearch') === true; this.matches = distinct(this._findMatchesInSetting(searchString, setting), (match) => `${match.startLineNumber}_${match.startColumn}_${match.endLineNumber}_${match.endColumn}_`); } @@ -176,27 +179,49 @@ export class SettingMatches { const keyMatchingWords: Map = new Map(); const valueMatchingWords: Map = new Map(); - const queryWords = new Set(searchString.split(' ')); - // Key search const settingKeyAsWords: string = this._keyToLabel(setting.key); + const queryWords = new Set(searchString.split(' ')); for (const word of queryWords) { // Check if the key contains the word. - const keyMatches = matchesWords(word, settingKeyAsWords, false); + // Force contiguous matching iff we're using the new algorithm. + const keyMatches = matchesWords(word, settingKeyAsWords, this.useNewKeyMatchingSearch); if (keyMatches?.length) { keyMatchingWords.set(word, keyMatches.map(match => this.toKeyRange(setting, match))); } } - if (keyMatchingWords.size) { - this.matchType |= SettingMatchType.KeyMatch; - this.keyMatchScore = keyMatchingWords.size; + if (this.useNewKeyMatchingSearch) { + if (keyMatchingWords.size === queryWords.size) { + // All words in the query matched with something in the setting key. + this.matchType |= SettingMatchType.KeyMatch; + // Score based on how many words matched out of the entire key, penalizing longer setting names. + const settingKeyAsWordsCount = settingKeyAsWords.split(' ').length; + this.keyMatchScore = (keyMatchingWords.size / settingKeyAsWordsCount) + (1 / setting.key.length); + } + const keyMatches = matchesSubString(searchString, settingKeyAsWords); + if (keyMatches?.length) { + // Handles cases such as "editor formonpast" with missing letters. + keyMatchingWords.set(searchString, keyMatches.map(match => this.toKeyRange(setting, match))); + this.matchType |= SettingMatchType.KeyMatch; + this.keyMatchScore = keyMatchingWords.size; + } + } else { + // Fall back to the old algorithm. + if (keyMatchingWords.size) { + this.matchType |= SettingMatchType.KeyMatch; + this.keyMatchScore = keyMatchingWords.size; + } } - - // Also check if the user tried searching by id. const keyIdMatches = matchesContiguousSubString(searchString, setting.key); if (keyIdMatches?.length) { + // Handles cases such as "editor.formatonpaste" where the user tries searching for the ID. keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match))); - this.matchType |= SettingMatchType.KeyIdMatch; + if (this.useNewKeyMatchingSearch) { + this.matchType |= SettingMatchType.KeyMatch; + this.keyMatchScore = Math.max(this.keyMatchScore, searchString.length / setting.key.length); + } else { + this.matchType |= SettingMatchType.KeyIdMatch; + } } // Check if the match was for a language tag group setting such as [markdown]. @@ -208,7 +233,14 @@ export class SettingMatches { return [...keyRanges]; } - // Search the description if there were no key matches. + // New algorithm only: exit early if the key already matched. + if (this.useNewKeyMatchingSearch && (this.matchType & SettingMatchType.KeyMatch)) { + const keyRanges = keyMatchingWords.size ? + Array.from(keyMatchingWords.values()).flat() : []; + return [...keyRanges]; + } + + // Description search if (this.searchDescription && this.matchType !== SettingMatchType.None) { for (const word of queryWords) { // Search the description lines. diff --git a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts index a020d871b14e..2e03281aa959 100644 --- a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts @@ -126,5 +126,12 @@ registry.registerConfiguration({ 'default': 'filter', 'scope': ConfigurationScope.WINDOW }, + 'workbench.settings.useWeightedKeySearch': { + 'type': 'boolean', + 'default': false, + 'description': nls.localize('useWeightedKeySearch', "Controls whether to use a new weight calculation algorithm to order certain search results in the Settings editor. The only search results that will be affected are those where the search query has been determined to match the setting key, and the weights will be calculated in a way that places settings with more matched words and shorter names to the top of the search results."), + 'scope': ConfigurationScope.WINDOW, + 'tags': ['preview'] + } } }); From a1fad3f3f9bac33384df5c1acda6ddbe6fe41833 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Fri, 24 Jan 2025 15:42:03 -0800 Subject: [PATCH 0886/3587] Avoid fs check on notebook cell uri (#238732) --- .../contrib/chat/browser/chatEditing/chatEditingService.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts index 8e14897c2f57..266dcfbf9efd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts @@ -13,6 +13,7 @@ import { Iterable } from '../../../../../base/common/iterator.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { LinkedList } from '../../../../../base/common/linkedList.js'; import { ResourceMap } from '../../../../../base/common/map.js'; +import { Schemas } from '../../../../../base/common/network.js'; import { derived, IObservable, observableValue, observableValueOpts, runOnChange, ValueWithChangeEventFromObservable } from '../../../../../base/common/observable.js'; import { compare } from '../../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; @@ -35,6 +36,7 @@ import { IEditorService } from '../../../../services/editor/common/editorService import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from '../../../multiDiffEditor/browser/multiDiffSourceResolverService.js'; +import { CellUri } from '../../../notebook/common/notebookCommon.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { applyingChatEditsContextKey, applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingAgentSupportsReadonlyReferencesContextKey, chatEditingMaxFileAssignmentName, chatEditingResourceContextKey, ChatEditingSessionState, decidedChatEditingResourceContextKey, defaultChatEditingMaxFileLimit, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, IChatEditingSessionStream, IChatRelatedFile, IChatRelatedFilesProvider, IModifiedFileEntry, inChatEditingSessionContextKey, WorkingSetEntryState } from '../../common/chatEditingService.js'; @@ -316,9 +318,10 @@ export class ChatEditingService extends Disposable implements IChatEditingServic if (part.kind === 'codeblockUri' || part.kind === 'textEditGroup') { // ensure editor is open asap if (!editedFilesExist.get(part.uri)) { - editedFilesExist.set(part.uri, this._fileService.exists(part.uri).then((e) => { + const uri = part.uri.scheme === Schemas.vscodeNotebookCell ? CellUri.parse(part.uri)?.notebook ?? part.uri : part.uri; + editedFilesExist.set(part.uri, this._fileService.exists(uri).then((e) => { if (e) { - this._editorService.openEditor({ resource: part.uri, options: { inactive: true, preserveFocus: true, pinned: true } }); + this._editorService.openEditor({ resource: uri, options: { inactive: true, preserveFocus: true, pinned: true } }); } return e; })); From 08564649c2397b818dbf259b88cdabe9e7df8b6c Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 24 Jan 2025 15:44:20 -0800 Subject: [PATCH 0887/3587] Fix counting chat word loading rate during tool calls (#238731) The idea was to ignore long pauses, but it wasn't ignoring text edits streaming into the model --- .../contrib/chat/common/chatViewModel.ts | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 5345af9ac65f..d2a00d5954ba 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -577,18 +577,23 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi const now = Date.now(); const wordCount = countWords(_model.response.getMarkdown()); - const timeDiff = Math.min(now - this._contentUpdateTimings.lastUpdateTime, 1000); - const newTotalTime = Math.max(this._contentUpdateTimings.totalTime + timeDiff, 250); - const impliedWordLoadRate = this._contentUpdateTimings.lastWordCount / (newTotalTime / 1000); - this.trace('onDidChange', `Update- got ${this._contentUpdateTimings.lastWordCount} words over last ${newTotalTime}ms = ${impliedWordLoadRate} words/s. ${wordCount} words are now available.`); - this._contentUpdateTimings = { - totalTime: this._contentUpdateTimings.totalTime !== 0 || this.response.value.some(v => v.kind === 'markdownContent') ? - newTotalTime : - this._contentUpdateTimings.totalTime, - lastUpdateTime: now, - impliedWordLoadRate, - lastWordCount: wordCount - }; + if (wordCount > 0 && wordCount === this._contentUpdateTimings.lastWordCount) { + this.trace('onDidChange', `Update- no new words`); + } else { + const timeDiff = Math.min(now - this._contentUpdateTimings.lastUpdateTime, 1000); + const newTotalTime = Math.max(this._contentUpdateTimings.totalTime + timeDiff, 250); + const impliedWordLoadRate = this._contentUpdateTimings.lastWordCount / (newTotalTime / 1000); + this.trace('onDidChange', `Update- got ${this._contentUpdateTimings.lastWordCount} words over last ${newTotalTime}ms = ${impliedWordLoadRate} words/s. ${wordCount} words are now available.`); + this._contentUpdateTimings = { + totalTime: this._contentUpdateTimings.totalTime !== 0 || this.response.value.some(v => v.kind === 'markdownContent') ? + newTotalTime : + this._contentUpdateTimings.totalTime, + lastUpdateTime: now, + impliedWordLoadRate, + lastWordCount: wordCount + }; + } + } else { this.logService.warn('ChatResponseViewModel#onDidChange: got model update but contentUpdateTimings is not initialized'); } From e5b5ec415bcb1bcdaaccb4fa19fadc970862273c Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Fri, 24 Jan 2025 15:53:05 -0800 Subject: [PATCH 0888/3587] Prevent file watching for notebook cells (#238733) No file watching for notebook cell --- .../chat/browser/chatEditing/chatEditingModifiedFileEntry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index e030552e8366..1dedc199e6e0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -197,7 +197,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie this._register(this.doc.onDidChangeContent(e => this._mirrorEdits(e))); - if (this.modifiedURI.scheme !== Schemas.untitled) { + if (this.modifiedURI.scheme !== Schemas.untitled && this.modifiedURI.scheme !== Schemas.vscodeNotebookCell) { this._register(this._fileService.watch(this.modifiedURI)); this._register(this._fileService.onDidFilesChange(e => { if (e.affects(this.modifiedURI) && kind === ChatEditKind.Created && e.gotDeleted()) { From be7d0e0bdd2f04e252ca43987f448a4caeaa0e9b Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Fri, 24 Jan 2025 16:29:36 -0800 Subject: [PATCH 0889/3587] Adds notebook markup font family support (#238734) * edits did this heyo (nb markup font family support) * setting description includes workbench fallback --- .../notebook/browser/notebook.contribution.ts | 6 +++++ .../notebook/browser/notebookOptions.ts | 23 +++++++++++++++---- .../view/renderers/backLayerWebView.ts | 3 +++ .../contrib/notebook/common/notebookCommon.ts | 1 + 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 0c7358aeeb32..1a0305147033 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -1256,5 +1256,11 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: false }, + [NotebookSetting.markupFontFamily]: { + markdownDescription: nls.localize('notebook.markupFontFamily', "Controls the font family of rendered markup in notebooks. When left blank, this will fall back to the default workbench font family."), + type: 'string', + default: '', + tags: ['notebookLayout'] + }, } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts b/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts index 634b9cdb5bb1..159928cf81b2 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts @@ -53,6 +53,7 @@ export interface NotebookDisplayOptions { // TODO @Yoyokrazy rename to a more ge 'editor.tabSize': number; 'editor.insertSpaces': boolean; }> | undefined; + markupFontFamily: string; } export interface NotebookLayoutConfiguration { @@ -108,6 +109,7 @@ export interface NotebookOptionsChangeEvent { readonly outputLinkifyFilePaths?: boolean; readonly minimalError?: boolean; readonly readonly?: boolean; + readonly markupFontFamily?: boolean; } const defaultConfigConstants = Object.freeze({ @@ -213,6 +215,7 @@ export class NotebookOptions extends Disposable { const outputLineLimit = this.configurationService.getValue(NotebookSetting.textOutputLineLimit) ?? 30; const linkifyFilePaths = this.configurationService.getValue(NotebookSetting.LinkifyOutputFilePaths) ?? true; const minimalErrors = this.configurationService.getValue(NotebookSetting.minimalErrorRendering); + const markupFontFamily = this.configurationService.getValue(NotebookSetting.markupFontFamily); const editorTopPadding = this._computeEditorTopPadding(); @@ -259,7 +262,8 @@ export class NotebookOptions extends Disposable { outputWordWrap: outputWordWrap, outputLineLimit: outputLineLimit, outputLinkifyFilePaths: linkifyFilePaths, - outputMinimalError: minimalErrors + outputMinimalError: minimalErrors, + markupFontFamily }; this._register(this.configurationService.onDidChangeConfiguration(e => { @@ -426,6 +430,7 @@ export class NotebookOptions extends Disposable { const outputWordWrap = e.affectsConfiguration(NotebookSetting.outputWordWrap); const outputLinkifyFilePaths = e.affectsConfiguration(NotebookSetting.LinkifyOutputFilePaths); const minimalError = e.affectsConfiguration(NotebookSetting.minimalErrorRendering); + const markupFontFamily = e.affectsConfiguration(NotebookSetting.markupFontFamily); if ( !cellStatusBarVisibility @@ -454,7 +459,8 @@ export class NotebookOptions extends Disposable { && !outputScrolling && !outputWordWrap && !outputLinkifyFilePaths - && !minimalError) { + && !minimalError + && !markupFontFamily) { return; } @@ -569,6 +575,10 @@ export class NotebookOptions extends Disposable { configuration.outputMinimalError = this.configurationService.getValue(NotebookSetting.minimalErrorRendering); } + if (markupFontFamily) { + configuration.markupFontFamily = this.configurationService.getValue(NotebookSetting.markupFontFamily); + } + this._layoutConfiguration = Object.freeze(configuration); // trigger event @@ -599,7 +609,8 @@ export class NotebookOptions extends Disposable { outputScrolling, outputWordWrap, outputLinkifyFilePaths, - minimalError + minimalError, + markupFontFamily }); } @@ -814,7 +825,8 @@ export class NotebookOptions extends Disposable { outputWordWrap: this._layoutConfiguration.outputWordWrap, outputLineLimit: this._layoutConfiguration.outputLineLimit, outputLinkifyFilePaths: this._layoutConfiguration.outputLinkifyFilePaths, - minimalError: this._layoutConfiguration.outputMinimalError + minimalError: this._layoutConfiguration.outputMinimalError, + markupFontFamily: this._layoutConfiguration.markupFontFamily }; } @@ -838,7 +850,8 @@ export class NotebookOptions extends Disposable { outputWordWrap: this._layoutConfiguration.outputWordWrap, outputLineLimit: this._layoutConfiguration.outputLineLimit, outputLinkifyFilePaths: false, - minimalError: false + minimalError: false, + markupFontFamily: this._layoutConfiguration.markupFontFamily }; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 8eb293b41bac..966a7e699173 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -121,6 +121,7 @@ interface BacklayerWebviewOptions { readonly outputLineLimit: number; readonly outputLinkifyFilePaths: boolean; readonly minimalError: boolean; + readonly markupFontFamily: string; } @@ -281,6 +282,7 @@ export class BackLayerWebView extends Themable { key: 'notebook.error.rendererFallbacksExhausted', comment: ['$0 is a placeholder for the mime type'] }, "Could not render content for '$0'"), + 'notebook-markup-font-family': this.options.markupFontFamily, }; } @@ -370,6 +372,7 @@ export class BackLayerWebView extends Themable { font-size: var(--notebook-markup-font-size); line-height: var(--notebook-markdown-line-height); color: var(--theme-ui-foreground); + font-family: var(--notebook-markup-font-family); } #container div.preview.draggable { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index e52053da2d71..6fd9ec8fa9ac 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -1034,6 +1034,7 @@ export const NotebookSetting = { cellFailureDiagnostics: 'notebook.cellFailureDiagnostics', outputBackupSizeLimit: 'notebook.backup.sizeLimit', multiCursor: 'notebook.multiCursor.enabled', + markupFontFamily: 'notebook.markupFontFamily', } as const; export const enum CellStatusbarAlignment { From a9ce0b55568c34935065bb253eea321f2eefb648 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 24 Jan 2025 16:46:50 -0800 Subject: [PATCH 0890/3587] Include an ability to change the default client id (#238736) Include an ability to change the client id So our migration is easy to test --- .../microsoft-authentication/package.json | 17 ++++++++++++++ .../microsoft-authentication/package.nls.json | 3 +++ .../src/common/scopeData.ts | 23 +++++++++++++++---- .../microsoft-authentication/src/extension.ts | 9 ++++++-- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 928ffa688899..4170a7787cfa 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -115,6 +115,23 @@ "tags": [ "onExP" ] + }, + "microsoft-authentication.clientIdVersion": { + "type": "string", + "default": "v1", + "enum": [ + "v2", + "v1" + ], + "enumDescriptions": [ + "%microsoft-authentication.clientIdVersion.enumDescriptions.v2%", + "%microsoft-authentication.clientIdVersion.enumDescriptions.v1%" + ], + "markdownDescription": "%microsoft-authentication.clientIdVersion.description%", + "tags": [ + "onExP", + "experimental" + ] } } } diff --git a/extensions/microsoft-authentication/package.nls.json b/extensions/microsoft-authentication/package.nls.json index c8e0189c08f9..ece95ac75c30 100644 --- a/extensions/microsoft-authentication/package.nls.json +++ b/extensions/microsoft-authentication/package.nls.json @@ -12,6 +12,9 @@ }, "microsoft-authentication.implementation.enumDescriptions.msal": "Use the Microsoft Authentication Library (MSAL) to sign in with a Microsoft account.", "microsoft-authentication.implementation.enumDescriptions.classic": "(deprecated) Use the classic authentication flow to sign in with a Microsoft account.", + "microsoft-authentication.clientIdVersion.description": "The version of the Microsoft Account client ID to use for signing in with a Microsoft account. Only change this if you have been asked to. The default is `v1`.", + "microsoft-authentication.clientIdVersion.enumDescriptions.v1": "Use the v1 Microsoft Account client ID to sign in with a Microsoft account.", + "microsoft-authentication.clientIdVersion.enumDescriptions.v2": "Use the v2 Microsoft Account client ID to sign in with a Microsoft account.", "microsoft-sovereign-cloud.environment.description": { "message": "The Sovereign Cloud to use for authentication. If you select `custom`, you must also set the `#microsoft-sovereign-cloud.customEnvironment#` setting.", "comment": [ diff --git a/extensions/microsoft-authentication/src/common/scopeData.ts b/extensions/microsoft-authentication/src/common/scopeData.ts index 4432abfed435..a43f2c431dd4 100644 --- a/extensions/microsoft-authentication/src/common/scopeData.ts +++ b/extensions/microsoft-authentication/src/common/scopeData.ts @@ -3,14 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const DEFAULT_CLIENT_ID = 'aebc6443-996d-45c2-90f0-388ff96faa56'; -const DEFAULT_TENANT = 'organizations'; +import { workspace } from 'vscode'; + +const DEFAULT_CLIENT_ID_V1 = 'aebc6443-996d-45c2-90f0-388ff96faa56'; +const DEFAULT_TENANT_V1 = 'organizations'; +const DEFAULT_CLIENT_ID_V2 = 'c27c220f-ce2f-4904-927d-333864217eeb'; +const DEFAULT_TENANT_V2 = 'common'; const OIDC_SCOPES = ['openid', 'email', 'profile', 'offline_access']; const GRAPH_TACK_ON_SCOPE = 'User.Read'; export class ScopeData { + private readonly _defaultClientId: string; + private readonly _defaultTenant: string; + /** * The full list of scopes including: * * the original scopes passed to the constructor @@ -40,6 +47,14 @@ export class ScopeData { readonly tenant: string; constructor(readonly originalScopes: readonly string[] = []) { + if (workspace.getConfiguration('microsoft-authentication').get<'v1' | 'v2'>('clientIdVersion') === 'v2') { + this._defaultClientId = DEFAULT_CLIENT_ID_V2; + this._defaultTenant = DEFAULT_TENANT_V2; + } else { + this._defaultClientId = DEFAULT_CLIENT_ID_V1; + this._defaultTenant = DEFAULT_TENANT_V1; + } + const modifiedScopes = [...originalScopes]; modifiedScopes.sort(); this.allScopes = modifiedScopes; @@ -55,7 +70,7 @@ export class ScopeData { return current.split('VSCODE_CLIENT_ID:')[1]; } return prev; - }, undefined) ?? DEFAULT_CLIENT_ID; + }, undefined) ?? this._defaultClientId; } private getTenantId(scopes: string[]) { @@ -64,7 +79,7 @@ export class ScopeData { return current.split('VSCODE_TENANT:')[1]; } return prev; - }, undefined) ?? DEFAULT_TENANT; + }, undefined) ?? this._defaultTenant; } private getScopesToSend(scopes: string[]) { diff --git a/extensions/microsoft-authentication/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts index c11f10876416..bd32a82290a6 100644 --- a/extensions/microsoft-authentication/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -38,8 +38,8 @@ function shouldUseMsal(expService: IExperimentationService): boolean { // If no setting or experiment value is found, default to true return true; } -let useMsal: boolean | undefined; +let useMsal: boolean | undefined; export async function activate(context: ExtensionContext) { const mainTelemetryReporter = new MicrosoftAuthenticationTelemetryReporter(context.extension.packageJSON.aiKey); const expService = await createExperimentationService( @@ -48,9 +48,14 @@ export async function activate(context: ExtensionContext) { env.uriScheme !== 'vscode', // isPreRelease ); useMsal = shouldUseMsal(expService); + const clientIdVersion = workspace.getConfiguration('microsoft-authentication').get<'v1' | 'v2'>('clientIdVersion', 'v1'); context.subscriptions.push(workspace.onDidChangeConfiguration(async e => { - if (!e.affectsConfiguration('microsoft-authentication.implementation') || useMsal === shouldUseMsal(expService)) { + if (!e.affectsConfiguration('microsoft-authentication')) { + return; + } + + if (useMsal === shouldUseMsal(expService) && clientIdVersion === workspace.getConfiguration('microsoft-authentication').get<'v1' | 'v2'>('clientIdVersion', 'v1')) { return; } From 9b2684c55e6a7c2f65a09b90ecf8441ad49f8e20 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Fri, 24 Jan 2025 23:10:32 -0800 Subject: [PATCH 0891/3587] unify prompt files configuration setting (#238741) [prompts]: unify the configuration setting --- .../chatInstructionAttachmentsModel.ts | 4 +- .../chatInstructionsFileLocator.ts | 44 +---- .../browser/contrib/chatDynamicVariables.ts | 4 +- .../chat/common/promptSyntax/config.ts | 167 ++++++++++++++++++ .../promptSyntax/parsers/basePromptParser.ts | 25 --- 5 files changed, 173 insertions(+), 71 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/config.ts diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts index 79a0a0d7c71c..0bfa66a79f0d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts @@ -7,10 +7,10 @@ import { URI } from '../../../../../base/common/uri.js'; import { Emitter } from '../../../../../base/common/event.js'; import { IChatRequestVariableEntry } from '../../common/chatModel.js'; import { ChatInstructionsFileLocator } from './chatInstructionsFileLocator.js'; +import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; import { IPromptFileReference } from '../../common/promptSyntax/parsers/types.js'; import { ChatInstructionsAttachmentModel } from './chatInstructionsAttachment.js'; import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; -import { BasePromptParser } from '../../common/promptSyntax/parsers/basePromptParser.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; @@ -229,6 +229,6 @@ export class ChatInstructionAttachmentsModel extends Disposable { * Checks if the prompt instructions feature is enabled in the user settings. */ public get featureEnabled(): boolean { - return BasePromptParser.promptSnippetsEnabled(this.configService); + return PromptFilesConfig.enabled(this.configService); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts index a0305c9598bc..a1e0b717f5cf 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts @@ -6,20 +6,11 @@ import { URI } from '../../../../../base/common/uri.js'; import { dirname, extUri } from '../../../../../base/common/resources.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; +import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js'; import { PROMP_SNIPPET_FILE_EXTENSION } from '../../common/promptSyntax/contentProviders/promptContentsProviderBase.js'; -/** - * Configuration setting name for the prompt instructions source folder paths. - */ -const PROMPT_FILES_LOCATION_SETTING_NAME = 'chat.experimental.prompt-files.location'; - -/** - * Default prompt instructions source folder paths. - */ -const PROMPT_FILES_DEFAULT_LOCATION = ['.copilot/prompts']; - /** * Class to locate prompt instructions files. */ @@ -66,7 +57,7 @@ export class ChatInstructionsFileLocator { return []; } - const sourceLocations = this.getSourceLocationsConfigValue(); + const sourceLocations = PromptFilesConfig.sourceLocations(this.configService); const result = []; // otherwise for each folder provided in the configuration, create @@ -93,37 +84,6 @@ export class ChatInstructionsFileLocator { return result; } - /** - * Get the configuation value for the prompt instructions source locations. - * Defaults to {@linkcode PROMPT_FILES_DEFAULT_LOCATION} if the value is not set. - * - * @returns List of prompt instructions source locations that were provided in - * user settings. - */ - private getSourceLocationsConfigValue(): readonly string[] { - const value = this.configService.getValue(PROMPT_FILES_LOCATION_SETTING_NAME); - - if (value === undefined || value === null) { - return PROMPT_FILES_DEFAULT_LOCATION; - } - - if (typeof value === 'string') { - return [value]; - } - - // if not a string nor an array, return an empty array - if (!Array.isArray(value)) { - return []; - } - - // filter out non-string values from the list - const result = value.filter((item) => { - return typeof item === 'string'; - }); - - return result; - } - /** * Finds all existent prompt instruction files in the provided locations. * diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 8418d0591f51..e25bcbb6c824 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -26,7 +26,7 @@ import { IChatRequestVariableValue, IChatVariablesService, IDynamicVariable } fr import { ISymbolQuickPickItem } from '../../../search/browser/symbolsQuickAccess.js'; import { ChatFileReference } from './chatDynamicVariables/chatFileReference.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { BasePromptParser } from '../../common/promptSyntax/parsers/basePromptParser.js'; +import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; export const dynamicVariableDecorationType = 'chat-dynamic-variable'; @@ -130,7 +130,7 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC addReference(ref: IDynamicVariable): void { // use `ChatFileReference` for file references and `IDynamicVariable` for other variables - const promptSnippetsEnabled = BasePromptParser.promptSnippetsEnabled(this.configService); + const promptSnippetsEnabled = PromptFilesConfig.enabled(this.configService); const variable = (ref.id === 'vscode.file' && promptSnippetsEnabled) ? this.instantiationService.createInstance(ChatFileReference, ref) : ref; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts new file mode 100644 index 000000000000..6cd78fbe7803 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../../platform/configuration/common/configuration.js'; + +/** + * Configuration helper for the `prompt files` feature. + * @see {@link PROMPT_FILES_CONFIG_KEY} and {@link PROMPT_FILES_DEFAULT_LOCATION} + * + * ### Functions + * + * - {@link getValue} allows to current read configuration value + * - {@link enabled} allows to check if the feature is enabled + * - {@link sourceLocations} gets the source folder locations for prompt files + * + * ### Configuration Examples + * + * Enable the feature, using defaults for prompt files source folder locations + * (see {@link PROMPT_FILES_DEFAULT_LOCATION}): + * ```json + * { + * "chat.experimental.prompt-files": true, + * } + * ``` + * + * Enable the feature specifying a single prompt files source folder location: + * ```json + * { + * "chat.experimental.prompt-files": '.copilot/prompts', + * } + * ``` + * + * Enable the feature specifying multiple prompt files source folder location: + * ```json + * { + * "chat.experimental.prompt-files": [ + * '.copilot/prompts', + * '.github/prompts', + * '/Users/legomushroom/repos/prompts', + * ], + * } + * ``` + * + * 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 folder locations + * fallback to {@link PROMPT_FILES_DEFAULT_LOCATION} + * - `false`: feature is disabled + * - `string`: + * - values that can be mapped to `boolean`(`"true"`, `"FALSE", "TrUe"`, etc.) + * are treated as `boolean` above + * - any other `non-empty` string value is treated as a single prompt files source folder path + * - `array`: + * - `string` items in the array are treated as prompt files source folder paths + * - all `non-string` items in the array are `ignored` + * - if the resulting array is empty, the feature is considered `enabled`, prompt files source + * folder locations fallback to defaults (see {@linkcode PROMPT_FILES_DEFAULT_LOCATION}) + * + * ### File Paths Resolution + * + * We resolve only `*.prompt.md` files inside the resulting source folder locations 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 snippets`, `prompt instructions`, etc.). + */ + const PROMPT_FILES_CONFIG_KEY: string = 'chat.experimental.prompt-files'; + + /** + * Default prompt instructions source folder paths. + */ + const PROMPT_FILES_DEFAULT_LOCATION = ['.copilot/prompts', '.github/prompts']; + + /** + * Get value of the `prompt files` configuration setting. + */ + export const getValue = ( + configService: IConfigurationService, + ): string | readonly string[] | boolean | undefined => { + const value = configService.getValue(PROMPT_FILES_CONFIG_KEY); + + if (value === undefined || value === null) { + return undefined; + } + + if (typeof value === 'string') { + const cleanValue = value.trim().toLowerCase(); + if (cleanValue === 'true') { + return true; + } + + if (cleanValue === 'false') { + return false; + } + + if (!cleanValue) { + return undefined; + } + + return value; + } + + if (typeof value === 'boolean') { + return value; + } + + if (Array.isArray(value)) { + return value.filter((item) => { + return typeof item === 'string'; + }); + } + + return undefined; + }; + + /** + * Checks if feature is enabled. + */ + export const enabled = ( + configService: IConfigurationService, + ): boolean => { + const value = getValue(configService); + + return value !== undefined && value !== false; + }; + + /** + * Gets the source folder locations for prompt files. + * Defaults to {@link PROMPT_FILES_DEFAULT_LOCATION}. + */ + export const sourceLocations = ( + configService: IConfigurationService, + ): readonly string[] => { + const value = getValue(configService); + + if (value === undefined) { + return PROMPT_FILES_DEFAULT_LOCATION; + } + + if (typeof value === 'string') { + return [value]; + } + + if (Array.isArray(value)) { + if (value.length !== 0) { + return value; + } + + return PROMPT_FILES_DEFAULT_LOCATION; + } + + return PROMPT_FILES_DEFAULT_LOCATION; + }; +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index 4c7faf561b3a..c7d3d2a689a5 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -41,11 +41,6 @@ const errorMessages = { */ export type TErrorCondition = FileOpenFailed | RecursiveReference | NonPromptSnippetFile; -/** - * Configuration key for the prompt snippets feature. - */ -const PROMPT_SNIPPETS_CONFIG_KEY: string = 'chat.experimental.prompt-snippets'; - /** * Base prompt parser class that provides a common interface for all * prompt parsers that are responsible for parsing chat prompt syntax. @@ -349,26 +344,6 @@ export abstract class BasePromptParser extend return URI.joinPath(this.uri, '..'); } - /** - * Check if the prompt snippets feature is enabled. - * @see {@link PROMPT_SNIPPETS_CONFIG_KEY} - */ - public static promptSnippetsEnabled( - configService: IConfigurationService, - ): boolean { - const value = configService.getValue(PROMPT_SNIPPETS_CONFIG_KEY); - - if (!value) { - return false; - } - - if (typeof value === 'string') { - return value.trim().toLowerCase() === 'true'; - } - - return !!value; - } - /** * Get a list of immediate child references of the prompt. */ From 42c9c3eaa5ea43daf0b4ddab72340a5780fb329b Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 25 Jan 2025 13:50:45 -0800 Subject: [PATCH 0892/3587] Add toolCalling capability to models (#238737) * Add toolCalling capability to models And filter out models that don't support it from the agent * Fix build --- .../contrib/chat/browser/chatInputPart.ts | 96 ++++++++++++------- .../contrib/chat/common/chatAgents.ts | 5 + .../contrib/chat/common/languageModels.ts | 11 ++- .../chat/test/common/voiceChatService.test.ts | 1 + .../vscode.proposed.chatProvider.d.ts | 1 + 5 files changed, 73 insertions(+), 41 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index ee5a492fb635..7aa61f4e42b1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -88,7 +88,7 @@ import { IChatFollowup } from '../common/chatService.js'; import { IChatVariablesService } from '../common/chatVariables.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; import { ChatInputHistoryMaxEntries, IChatHistoryEntry, IChatInputState, IChatWidgetHistoryService } from '../common/chatWidgetHistoryService.js'; -import { ILanguageModelChatMetadata, ILanguageModelsService } from '../common/languageModels.js'; +import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js'; import { CancelAction, ChatModelPickerActionId, ChatSubmitAction, ChatSubmitSecondaryAgentAction, IChatExecuteActionContext, ToggleAgentModeActionId } from './actions/chatExecuteActions.js'; import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js'; import { InstructionAttachmentsWidget } from './attachments/instructionsAttachment/instructionAttachments.js'; @@ -280,10 +280,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private promptInstructionsAttached: IContextKey; private readonly _waitForPersistedLanguageModel = this._register(new MutableDisposable()); - private _onDidChangeCurrentLanguageModel = this._register(new Emitter()); - private _currentLanguageModel: string | undefined; + private _onDidChangeCurrentLanguageModel = this._register(new Emitter()); + private _currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined; get currentLanguageModel() { - return this._currentLanguageModel; + return this._currentLanguageModel?.identifier; } private cachedDimensions: dom.Dimension | undefined; @@ -358,6 +358,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge @IStorageService private readonly storageService: IStorageService, @ILabelService private readonly labelService: ILabelService, @IChatVariablesService private readonly variableService: IChatVariablesService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, ) { super(); @@ -415,7 +416,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (persistedSelection) { const model = this.languageModelsService.lookupLanguageModel(persistedSelection); if (model) { - this._currentLanguageModel = persistedSelection; + this._currentLanguageModel = { metadata: model, identifier: persistedSelection }; this._onDidChangeCurrentLanguageModel.fire(this._currentLanguageModel); } else { this._waitForPersistedLanguageModel.value = this.languageModelsService.onDidChangeLanguageModels(e => { @@ -424,31 +425,57 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._waitForPersistedLanguageModel.clear(); if (persistedModel.metadata.isUserSelectable) { - this._currentLanguageModel = persistedSelection; + this._currentLanguageModel = { metadata: persistedModel.metadata, identifier: persistedSelection }; this._onDidChangeCurrentLanguageModel.fire(this._currentLanguageModel!); } } }); } } + + this._register(this.chatAgentService.onDidChangeToolsAgentModeEnabled(() => { + if (this._currentLanguageModel && !this.modelSupportedForDefaultAgent(this._currentLanguageModel)) { + this.setCurrentLanguageModelToDefault(); + } + })); } private supportsVision(): boolean { - const model = this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel) : undefined; - return model?.capabilities?.vision ?? false; + return this._currentLanguageModel?.metadata.capabilities?.vision ?? false; + } + + private modelSupportedForDefaultAgent(model: ILanguageModelChatMetadataAndIdentifier): boolean { + // Probably this logic could live in configuration on the agent, or somewhere else, if it gets more complex + if (this.chatAgentService.getDefaultAgent(this.location)?.isToolsAgent) { + // Filter out models that don't support tool calling, and models that don't support enough context to have a good experience with the tools agent + return !!model.metadata.capabilities?.toolCalling && model.metadata.maxInputTokens > 40000; + } + + return true; + } + + private getModels(): ILanguageModelChatMetadataAndIdentifier[] { + const models = this.languageModelsService.getLanguageModelIds() + .map(modelId => ({ identifier: modelId, metadata: this.languageModelsService.lookupLanguageModel(modelId)! })) + .filter(entry => entry.metadata?.isUserSelectable && this.modelSupportedForDefaultAgent(entry)); + models.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)); + + return models; } private setCurrentLanguageModelToDefault() { - const defaultLanguageModel = this.languageModelsService.getLanguageModelIds().find(id => this.languageModelsService.lookupLanguageModel(id)?.isDefault); + const defaultLanguageModelId = this.languageModelsService.getLanguageModelIds().find(id => this.languageModelsService.lookupLanguageModel(id)?.isDefault); const hasUserSelectableLanguageModels = this.languageModelsService.getLanguageModelIds().find(id => { const model = this.languageModelsService.lookupLanguageModel(id); return model?.isUserSelectable && !model.isDefault; }); - this._currentLanguageModel = hasUserSelectableLanguageModels ? defaultLanguageModel : undefined; + this._currentLanguageModel = hasUserSelectableLanguageModels && defaultLanguageModelId ? + { metadata: this.languageModelsService.lookupLanguageModel(defaultLanguageModelId)!, identifier: defaultLanguageModelId } : + undefined; } - private setCurrentLanguageModelByUser(modelId: string) { - this._currentLanguageModel = modelId; + private setCurrentLanguageModelByUser(model: ILanguageModelChatMetadataAndIdentifier) { + this._currentLanguageModel = model; // The user changed the language model, so we don't wait for the persisted option to be registered this._waitForPersistedLanguageModel.clear(); @@ -456,7 +483,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.layout(this.cachedDimensions.height, this.cachedDimensions.width); } - this.storageService.store(this.getSelectedModelStorageKey(), modelId, StorageScope.APPLICATION, StorageTarget.USER); + this.storageService.store(this.getSelectedModelStorageKey(), model.identifier, StorageScope.APPLICATION, StorageTarget.USER); } private loadHistory(): HistoryNavigator2 { @@ -787,10 +814,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (this._currentLanguageModel) { const itemDelegate: ModelPickerDelegate = { onDidChangeModel: this._onDidChangeCurrentLanguageModel.event, - setModel: (modelId: string) => { - this.setCurrentLanguageModelByUser(modelId); + setModel: (model: ILanguageModelChatMetadataAndIdentifier) => { + this.setCurrentLanguageModelByUser(model); this.renderAttachedContext(); - } + }, + getModels: () => this.getModels() }; return this.instantiationService.createInstance(ModelPickerActionViewItem, action, this._currentLanguageModel, itemDelegate, { hoverDelegate: options.hoverDelegate, keybinding: options.keybinding ?? undefined }); } @@ -1585,14 +1613,15 @@ class ChatSubmitDropdownActionItem extends DropdownWithPrimaryActionViewItem { } interface ModelPickerDelegate { - onDidChangeModel: Event; - setModel(selectedModelId: string): void; + onDidChangeModel: Event; + setModel(selectedModelId: ILanguageModelChatMetadataAndIdentifier): void; + getModels(): ILanguageModelChatMetadataAndIdentifier[]; } class ModelPickerActionViewItem extends MenuEntryActionViewItem { constructor( action: MenuItemAction, - private currentLanguageModel: string, + private currentLanguageModel: ILanguageModelChatMetadataAndIdentifier, private readonly delegate: ModelPickerDelegate, options: IMenuEntryActionViewItemOptions, @IKeybindingService keybindingService: IKeybindingService, @@ -1600,9 +1629,8 @@ class ModelPickerActionViewItem extends MenuEntryActionViewItem { @IContextKeyService contextKeyService: IContextKeyService, @IThemeService themeService: IThemeService, @IContextMenuService contextMenuService: IContextMenuService, - @ILanguageModelsService private readonly _languageModelsService: ILanguageModelsService, @IAccessibilityService _accessibilityService: IAccessibilityService, - @ICommandService private readonly _commandService: ICommandService + @ICommandService private readonly _commandService: ICommandService, ) { super(action, options, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, _accessibilityService); @@ -1630,39 +1658,33 @@ class ModelPickerActionViewItem extends MenuEntryActionViewItem { } protected override updateLabel(): void { - if (this.label) { - const model = this._languageModelsService.lookupLanguageModel(this.currentLanguageModel); - if (model) { - dom.reset(this.label, dom.$('span.chat-model-label', undefined, model.name), ...renderLabelWithIcons(`$(chevron-down)`)); - } + if (this.label && this.currentLanguageModel) { + dom.reset(this.label, dom.$('span.chat-model-label', undefined, this.currentLanguageModel.metadata.name), ...renderLabelWithIcons(`$(chevron-down)`)); } } private _openContextMenu() { - const setLanguageModelAction = (id: string, modelMetadata: ILanguageModelChatMetadata): IAction => { + const setLanguageModelAction = (entry: ILanguageModelChatMetadataAndIdentifier): IAction => { return { - id, - label: modelMetadata.name, + id: entry.identifier, + label: entry.metadata.name, tooltip: '', class: undefined, enabled: true, - checked: id === this.currentLanguageModel, + checked: entry.identifier === this.currentLanguageModel.identifier, run: () => { - this.currentLanguageModel = id; + this.currentLanguageModel = entry; this.updateLabel(); - this.delegate.setModel(id); + this.delegate.setModel(entry); } }; }; - const models = this._languageModelsService.getLanguageModelIds() - .map(modelId => ({ id: modelId, model: this._languageModelsService.lookupLanguageModel(modelId)! })) - .filter(entry => entry.model?.isUserSelectable); - models.sort((a, b) => a.model.name.localeCompare(b.model.name)); + const models: ILanguageModelChatMetadataAndIdentifier[] = this.delegate.getModels(); this._contextMenuService.showContextMenu({ getAnchor: () => this.element!, getActions: () => { - const actions = models.map(entry => setLanguageModelAction(entry.id, entry.model)); + const actions = models.map(entry => setLanguageModelAction(entry)); if (this._contextKeyService.getContextKeyValue(ChatContextKeys.Setup.limited.key) === true) { actions.push(new Separator()); diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 8045fc3d091d..8427b8014b79 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -206,6 +206,7 @@ export interface IChatAgentService { * undefined when an agent was removed */ readonly onDidChangeAgents: Event; + readonly onDidChangeToolsAgentModeEnabled: Event; readonly toolsAgentModeEnabled: boolean; toggleToolsAgentMode(): void; registerAgent(id: string, data: IChatAgentData): IDisposable; @@ -252,6 +253,9 @@ export class ChatAgentService extends Disposable implements IChatAgentService { private readonly _onDidChangeAgents = new Emitter(); readonly onDidChangeAgents: Event = this._onDidChangeAgents.event; + private readonly _onDidChangeToolsAgentModeEnabled = new Emitter(); + readonly onDidChangeToolsAgentModeEnabled: Event = this._onDidChangeToolsAgentModeEnabled.event; + private readonly _agentsContextKeys = new Set(); private readonly _hasDefaultAgent: IContextKey; private readonly _defaultAgentRegistered: IContextKey; @@ -423,6 +427,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService { toggleToolsAgentMode(): void { this._agentModeContextKey.set(!this._agentModeContextKey.get()); + this._onDidChangeToolsAgentModeEnabled.fire(); this._onDidChangeAgents.fire(this.getDefaultAgent(ChatAgentLocation.EditingSession)); } diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 3a64fc746da0..8424946dac5c 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -88,6 +88,7 @@ export interface ILanguageModelChatMetadata { }; readonly capabilities?: { readonly vision?: boolean; + readonly toolCalling?: boolean; }; } @@ -114,11 +115,13 @@ export interface ILanguageModelChatSelector { export const ILanguageModelsService = createDecorator('ILanguageModelsService'); +export interface ILanguageModelChatMetadataAndIdentifier { + metadata: ILanguageModelChatMetadata; + identifier: string; +} + export interface ILanguageModelsChangeEvent { - added?: { - identifier: string; - metadata: ILanguageModelChatMetadata; - }[]; + added?: ILanguageModelChatMetadataAndIdentifier[]; removed?: string[]; } diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts index f84b3ba640b9..f86b6d782c4e 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts @@ -66,6 +66,7 @@ suite('VoiceChat', () => { class TestChatAgentService implements IChatAgentService { _serviceBrand: undefined; readonly onDidChangeAgents = Event.None; + readonly onDidChangeToolsAgentModeEnabled = Event.None; registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); } invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index d00a42a22afb..68dda301e7dd 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -70,6 +70,7 @@ declare module 'vscode' { readonly isUserSelectable?: boolean; readonly capabilities?: { readonly vision?: boolean; + readonly toolCalling?: boolean; }; } From 35c64c274dbb325ad00a0202f71b8dc04d6ec2cf Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Sat, 25 Jan 2025 23:58:41 -0800 Subject: [PATCH 0893/3587] [config]: keep the old config setting name (#238785) --- .../contrib/chat/common/promptSyntax/config.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts index 6cd78fbe7803..2bf39eb41841 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts @@ -6,7 +6,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; /** - * Configuration helper for the `prompt files` feature. + * Configuration helper for the `prompt snippets` feature. * @see {@link PROMPT_FILES_CONFIG_KEY} and {@link PROMPT_FILES_DEFAULT_LOCATION} * * ### Functions @@ -21,21 +21,21 @@ import { IConfigurationService } from '../../../../../platform/configuration/com * (see {@link PROMPT_FILES_DEFAULT_LOCATION}): * ```json * { - * "chat.experimental.prompt-files": true, + * "chat.experimental.prompt-snippets": true, * } * ``` * - * Enable the feature specifying a single prompt files source folder location: + * Enable the feature, specifying a single prompt files source folder location: * ```json * { - * "chat.experimental.prompt-files": '.copilot/prompts', + * "chat.experimental.prompt-snippets": '.copilot/prompts', * } * ``` * - * Enable the feature specifying multiple prompt files source folder location: + * Enable the feature, specifying multiple prompt files source folder location: * ```json * { - * "chat.experimental.prompt-files": [ + * "chat.experimental.prompt-snippets": [ * '.copilot/prompts', * '.github/prompts', * '/Users/legomushroom/repos/prompts', @@ -74,10 +74,10 @@ import { IConfigurationService } from '../../../../../platform/configuration/com */ export namespace PromptFilesConfig { /** - * Configuration key for the `prompt files` feature (also + * Configuration key for the `prompt snippets` feature (also * known as `prompt snippets`, `prompt instructions`, etc.). */ - const PROMPT_FILES_CONFIG_KEY: string = 'chat.experimental.prompt-files'; + const PROMPT_FILES_CONFIG_KEY: string = 'chat.experimental.prompt-snippets'; /** * Default prompt instructions source folder paths. @@ -85,7 +85,7 @@ export namespace PromptFilesConfig { const PROMPT_FILES_DEFAULT_LOCATION = ['.copilot/prompts', '.github/prompts']; /** - * Get value of the `prompt files` configuration setting. + * Get value of the `prompt snippets` configuration setting. */ export const getValue = ( configService: IConfigurationService, From 11215282830a001c5c629ca3bb0ffae27ce792aa Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Sun, 26 Jan 2025 09:41:47 +0100 Subject: [PATCH 0894/3587] Use insert diff background color in side by side view to align with others (#238698) use same background color in side by side view --- .../view/inlineEdits/sideBySideDiff.ts | 55 +++++++++++++------ .../view/inlineEdits/wordReplacementView.ts | 3 +- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index db3fe4132518..60ada9c8e7f7 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -12,8 +12,8 @@ import { IObservable, IReader, autorun, constObservable, derived, derivedObserva import { MenuId, MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { diffInserted, diffRemoved, editorHoverBorder } from '../../../../../../platform/theme/common/colorRegistry.js'; -import { registerColor } from '../../../../../../platform/theme/common/colorUtils.js'; +import { diffInserted, diffInsertedLine, diffRemoved, editorHoverBorder } from '../../../../../../platform/theme/common/colorRegistry.js'; +import { registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; @@ -62,7 +62,12 @@ export const originalChangedTextOverlayColor = registerColor( export const modifiedChangedLineBackgroundColor = registerColor( 'inlineEdit.modifiedChangedLineBackground', - Color.transparent, + { + light: transparent(diffInsertedLine, 0.5), + dark: transparent(diffInsertedLine, 0.5), + hcDark: diffInsertedLine, + hcLight: diffInsertedLine + }, localize('inlineEdit.modifiedChangedLineBackground', 'Background color for the changed lines in the modified text of inline edits.'), true ); @@ -462,6 +467,7 @@ export class InlineEditsSideBySideDiff extends Disposable implements IInlineEdit const selectionTop = this._originalVerticalStartPosition.read(reader) ?? this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); const selectionBottom = this._originalVerticalEndPosition.read(reader) ?? this._editor.getBottomForLineNumber(range.endLineNumberExclusive - 1) - this._editorObs.scrollTop.read(reader); + // TODO: const { prefixLeftOffset } = getPrefixTrim(inlineEdit.edit.edits.map(e => e.range), inlineEdit.originalLineRange, [], this._editor); const codeLeft = editorLayout.contentLeft; let code1 = new Point(left, selectionTop); @@ -553,24 +559,25 @@ export class InlineEditsSideBySideDiff extends Disposable implements IInlineEdit return true; }); - private readonly _extendedModifiedPath = derived(reader => { const layoutInfo = this._previewEditorLayoutInfo.read(reader); if (!layoutInfo) { return undefined; } - const extendedModifiedPathBuilder = createRectangle( - { topLeft: layoutInfo.editStart1, width: layoutInfo.edit1.x - layoutInfo.editStart1.x, height: layoutInfo.editStart2.y - layoutInfo.editStart1.y }, - 0, - { topLeft: 0, bottomLeft: 0, topRight: layoutInfo.borderRadius, bottomRight: layoutInfo.borderRadius }, - { hideLeft: true } - ); + const path = new PathBuilder() + .moveTo(layoutInfo.code2) + .lineTo(layoutInfo.code1) + .lineTo(layoutInfo.editStart1) + .lineTo(layoutInfo.edit1.deltaX(-layoutInfo.borderRadius)) + .curveTo(layoutInfo.edit1, layoutInfo.edit1.deltaY(layoutInfo.borderRadius)) + .lineTo(layoutInfo.edit2.deltaY(-layoutInfo.borderRadius)) + .curveTo(layoutInfo.edit2, layoutInfo.edit2.deltaX(-layoutInfo.borderRadius)) + .lineTo(layoutInfo.editStart2); if (layoutInfo.editStart2.y !== layoutInfo.code2.y) { - extendedModifiedPathBuilder.moveTo(layoutInfo.editStart2); - extendedModifiedPathBuilder.curveTo2(layoutInfo.editStart2.deltaX(-20), layoutInfo.code2.deltaX(20), layoutInfo.code2.deltaX(0)); + path.curveTo2(layoutInfo.editStart2.deltaX(-20), layoutInfo.code2.deltaX(20), layoutInfo.code2.deltaX(0)); } - extendedModifiedPathBuilder.lineTo(layoutInfo.code2).moveTo(layoutInfo.code1).lineTo(layoutInfo.editStart1); - return extendedModifiedPathBuilder.build(); + path.lineTo(layoutInfo.code2); + return path.build(); }); private readonly _originalBackgroundColor = observableFromEvent(this, this._themeService.onDidColorThemeChange, () => { @@ -606,7 +613,7 @@ export class InlineEditsSideBySideDiff extends Disposable implements IInlineEdit }), ]).keepUpdated(this._store); - private readonly _foregroundBackgroundSvg = n.svg({ + private readonly _modifiedBackgroundSvg = n.svg({ transform: 'translate(-0.5 -0.5)', style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, }, [ @@ -615,6 +622,20 @@ export class InlineEditsSideBySideDiff extends Disposable implements IInlineEdit d: this._extendedModifiedPath, style: { fill: 'var(--vscode-editor-background, transparent)', + strokeWidth: '0px', + } + }), + ]).keepUpdated(this._store); + + private readonly _foregroundBackgroundSvg = n.svg({ + transform: 'translate(-0.5 -0.5)', + style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, + }, [ + n.svgElem('path', { + class: 'extendedModifiedBackgroundCoverUp', + d: this._extendedModifiedPath, + style: { + fill: 'var(--vscode-inlineEdit-modifiedChangedLineBackground, transparent)', strokeWidth: '1px', } }), @@ -693,7 +714,7 @@ export class InlineEditsSideBySideDiff extends Disposable implements IInlineEdit }, }, [ this._backgroundSvg, - derived(this, reader => this._shouldOverflow.read(reader) ? [] : [this._foregroundBackgroundSvg, this._editorContainer, this._foregroundSvg, this._middleBorderWithShadow]), + derived(this, reader => this._shouldOverflow.read(reader) ? [] : [this._modifiedBackgroundSvg, this._foregroundBackgroundSvg, this._editorContainer, this._foregroundSvg, this._middleBorderWithShadow]), ]).keepUpdated(this._store); private readonly _overflowView = n.div({ @@ -704,6 +725,6 @@ export class InlineEditsSideBySideDiff extends Disposable implements IInlineEdit display: this._display, }, }, [ - derived(this, reader => this._shouldOverflow.read(reader) ? [this._foregroundBackgroundSvg, this._editorContainer, this._foregroundSvg, this._middleBorderWithShadow] : []), + derived(this, reader => this._shouldOverflow.read(reader) ? [this._modifiedBackgroundSvg, this._foregroundBackgroundSvg, this._editorContainer, this._foregroundSvg, this._middleBorderWithShadow] : []), ]).keepUpdated(this._store); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index 91dddc5b9cd0..7b8bbd60ac3c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -495,8 +495,7 @@ export class LineReplacementView extends Disposable implements IInlineEditsView left: 0, width: '100%', height: '100%', - background: 'var(--vscode-diffEditor-insertedLineBackground)', - opacity: '0.5', + background: 'var(--vscode-inlineEdit-modifiedChangedLineBackground)', }, }) ]), From 06070b11db005b36017fdc5b9936dc99b42a9ca7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 26 Jan 2025 10:29:21 +0100 Subject: [PATCH 0895/3587] Allow to determine target URI from untitled editors being saved to disk (fix #238705) (#238790) --- .../browser/browserTextFileService.ts | 4 +- .../textfile/browser/textFileService.ts | 9 ++++- .../electron-sandbox/nativeTextFileService.ts | 4 +- .../common/untitledTextEditorService.ts | 40 +++++++++++++++++-- .../common/fileWorkingCopyManager.ts | 7 +++- .../common/untitledFileWorkingCopyManager.ts | 32 +++++++++++++++ .../untitledFileWorkingCopyManager.test.ts | 14 +++++++ .../test/browser/workbenchTestServices.ts | 4 +- 8 files changed, 101 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/services/textfile/browser/browserTextFileService.ts b/src/vs/workbench/services/textfile/browser/browserTextFileService.ts index d84171e43544..902e171d399e 100644 --- a/src/vs/workbench/services/textfile/browser/browserTextFileService.ts +++ b/src/vs/workbench/services/textfile/browser/browserTextFileService.ts @@ -19,7 +19,7 @@ import { IElevatedFileService } from '../../files/common/elevatedFileService.js' import { IFilesConfigurationService } from '../../filesConfiguration/common/filesConfigurationService.js'; import { ILifecycleService } from '../../lifecycle/common/lifecycle.js'; import { IPathService } from '../../path/common/pathService.js'; -import { IUntitledTextEditorService } from '../../untitled/common/untitledTextEditorService.js'; +import { IUntitledTextEditorModelManager, IUntitledTextEditorService } from '../../untitled/common/untitledTextEditorService.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IWorkingCopyFileService } from '../../workingCopy/common/workingCopyFileService.js'; import { IDecorationsService } from '../../decorations/common/decorations.js'; @@ -28,7 +28,7 @@ export class BrowserTextFileService extends AbstractTextFileService { constructor( @IFileService fileService: IFileService, - @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService, + @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorModelManager, @ILifecycleService lifecycleService: ILifecycleService, @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index fe8816efc16d..9ba24786bc5c 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -57,7 +57,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex constructor( @IFileService protected readonly fileService: IFileService, - @IUntitledTextEditorService private untitledTextEditorService: IUntitledTextEditorService, + @IUntitledTextEditorService private untitledTextEditorService: IUntitledTextEditorModelManager, @ILifecycleService protected readonly lifecycleService: ILifecycleService, @IInstantiationService protected readonly instantiationService: IInstantiationService, @IModelService private readonly modelService: IModelService, @@ -161,7 +161,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex this._register(this.decorationsService.registerDecorationsProvider(provider)); } - //#endregin + //#endregion //#region text file read / write / create @@ -451,6 +451,11 @@ export abstract class AbstractTextFileService extends Disposable implements ITex this.logService.error(error); } + // Events + if (source.scheme === Schemas.untitled) { + this.untitled.notifyDidSave(source, target); + } + return target; } diff --git a/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts index 7acdc4d22d5f..913208b2221c 100644 --- a/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts @@ -10,7 +10,7 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta import { URI } from '../../../../base/common/uri.js'; import { IFileService, IFileReadLimits } from '../../../../platform/files/common/files.js'; import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js'; -import { IUntitledTextEditorService } from '../../untitled/common/untitledTextEditorService.js'; +import { IUntitledTextEditorModelManager, IUntitledTextEditorService } from '../../untitled/common/untitledTextEditorService.js'; import { ILifecycleService } from '../../lifecycle/common/lifecycle.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IModelService } from '../../../../editor/common/services/model.js'; @@ -33,7 +33,7 @@ export class NativeTextFileService extends AbstractTextFileService { constructor( @IFileService fileService: IFileService, - @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService, + @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorModelManager, @ILifecycleService lifecycleService: ILifecycleService, @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts index 5b53a994a374..53932b615d85 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts @@ -61,7 +61,29 @@ export interface INewUntitledTextEditorWithAssociatedResourceOptions extends INe type IInternalUntitledTextEditorOptions = IExistingUntitledTextEditorOptions & INewUntitledTextEditorWithAssociatedResourceOptions; -export interface IUntitledTextEditorModelManager { +export interface IUntitledTextEditorModelSaveEvent { + + /** + * The source untitled file that was saved. It is disposed at this point. + */ + readonly source: URI; + + /** + * The target file the untitled was saved to. Is never untitled. + */ + readonly target: URI; +} + +export interface IUntitledTextEditorService { + + readonly _serviceBrand: undefined; + + /** + * An event for when an untitled editor model was saved to disk. + * At the point the event fires, the untitled editor model is + * disposed. + */ + readonly onDidSave: Event; /** * Events for when untitled text editors change (e.g. getting dirty, saved or reverted). @@ -131,17 +153,23 @@ export interface IUntitledTextEditorModelManager { canDispose(model: IUntitledTextEditorModel): true | Promise; } -export interface IUntitledTextEditorService extends IUntitledTextEditorModelManager { +export interface IUntitledTextEditorModelManager extends IUntitledTextEditorService { - readonly _serviceBrand: undefined; + /** + * Internal method: triggers the onDidSave event. + */ + notifyDidSave(source: URI, target: URI): void; } -export class UntitledTextEditorService extends Disposable implements IUntitledTextEditorService { +export class UntitledTextEditorService extends Disposable implements IUntitledTextEditorModelManager { declare readonly _serviceBrand: undefined; private static readonly UNTITLED_WITHOUT_ASSOCIATED_RESOURCE_REGEX = /Untitled-\d+/; + private readonly _onDidSave = this._register(new Emitter()); + readonly onDidSave = this._onDidSave.event; + private readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; @@ -311,6 +339,10 @@ export class UntitledTextEditorService extends Disposable implements IUntitledTe return true; } + + notifyDidSave(source: URI, target: URI): void { + this._onDidSave.fire({ source, target }); + } } registerSingleton(IUntitledTextEditorService, UntitledTextEditorService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index 80ad8dadfce6..017d52d081e8 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -277,7 +277,7 @@ export class FileWorkingCopyManager extends IBaseFileWorkingCopyManager> { + /** + * An event for when an untitled file working copy was saved. + * At the point the event fires, the untitled file working copy is + * disposed. + */ + readonly onDidSave: Event; + /** * An event for when a untitled file working copy changed it's dirty state. */ @@ -57,6 +77,11 @@ export interface IUntitledFileWorkingCopyManager>; + + /** + * Internal method: triggers the onDidSave event. + */ + notifyDidSave(source: URI, target: URI): void; } export interface INewUntitledFileWorkingCopyOptions { @@ -105,6 +130,9 @@ export class UntitledFileWorkingCopyManager()); + readonly onDidSave = this._onDidSave.event; + private readonly _onDidChangeDirty = this._register(new Emitter>()); readonly onDidChangeDirty = this._onDidChangeDirty.event; @@ -269,4 +297,8 @@ export class UntitledFileWorkingCopyManager { }); test('save - without associated resource', async () => { + let savedEvent: { source: URI; target: URI } | undefined = undefined; + disposables.add(manager.untitled.onDidSave(e => { + savedEvent = e; + })); + const workingCopy = await manager.untitled.resolve(); workingCopy.model?.updateContents('Simple Save'); @@ -255,11 +260,18 @@ suite('UntitledFileWorkingCopyManager', () => { assert.ok(result); assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); + assert.strictEqual(savedEvent!.source.toString(), workingCopy.resource.toString()); + assert.strictEqual(savedEvent!.target.toString(), URI.file('simple/file.txt').toString()); workingCopy.dispose(); }); test('save - with associated resource', async () => { + let savedEvent: { source: URI; target: URI } | undefined = undefined; + disposables.add(manager.untitled.onDidSave(e => { + savedEvent = e; + })); + const workingCopy = await manager.untitled.resolve({ associatedResource: { path: '/some/associated.txt' } }); workingCopy.model?.updateContents('Simple Save with associated resource'); @@ -269,6 +281,8 @@ suite('UntitledFileWorkingCopyManager', () => { assert.ok(result); assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); + assert.strictEqual(savedEvent!.source.toString(), workingCopy.resource.toString()); + assert.strictEqual(savedEvent!.target.toString(), URI.file('/some/associated.txt').toString()); workingCopy.dispose(); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 2fe4a1635747..8c5965ec8f4b 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -19,7 +19,7 @@ import { IWorkbenchLayoutService, PanelAlignment, Parts, Position as PartPositio import { TextModelResolverService } from '../../services/textmodelResolver/common/textModelResolverService.js'; import { ITextModelService } from '../../../editor/common/services/resolverService.js'; import { IEditorOptions, IResourceEditorInput, IResourceEditorInputIdentifier, ITextResourceEditorInput, ITextEditorOptions } from '../../../platform/editor/common/editor.js'; -import { IUntitledTextEditorService, UntitledTextEditorService } from '../../services/untitled/common/untitledTextEditorService.js'; +import { IUntitledTextEditorModelManager, IUntitledTextEditorService, UntitledTextEditorService } from '../../services/untitled/common/untitledTextEditorService.js'; import { IWorkspaceContextService, IWorkspaceIdentifier } from '../../../platform/workspace/common/workspace.js'; import { ILifecycleService, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent, BeforeShutdownErrorEvent, InternalBeforeShutdownEvent, IWillShutdownEventJoiner } from '../../services/lifecycle/common/lifecycle.js'; import { ServiceCollection } from '../../../platform/instantiation/common/serviceCollection.js'; @@ -414,7 +414,7 @@ export class TestTextFileService extends BrowserTextFileService { constructor( @IFileService fileService: IFileService, - @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService, + @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorModelManager, @ILifecycleService lifecycleService: ILifecycleService, @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, From c01176f7b243ff642a878d02be702d8a33c5af9d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 26 Jan 2025 10:29:51 +0100 Subject: [PATCH 0896/3587] layout - drop `grid.size` state (#238787) This key was never updated after the initial startup and thus would always be at the default window size. With this change, we rely entierly on the computed size of the window using DOM APIs right from the beginning. --- src/vs/workbench/browser/layout.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 9ec9e4a45599..1d362b33fb53 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -620,8 +620,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } private initLayoutState(lifecycleService: ILifecycleService, fileService: IFileService): void { - this.stateModel = new LayoutStateModel(this.storageService, this.configurationService, this.contextService, this.parent); - this.stateModel.load(); + this._mainContainerDimension = getClientArea(this.parent); + + this.stateModel = new LayoutStateModel(this.storageService, this.configurationService, this.contextService); + this.stateModel.load(this._mainContainerDimension); // Both editor and panel should not be hidden on startup if (this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN) && this.stateModel.getRuntimeValue(LayoutStateKeys.EDITOR_HIDDEN)) { @@ -2373,7 +2375,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } private createGridDescriptor(): ISerializedGrid { - const { width, height } = this.stateModel.getInitializationValue(LayoutStateKeys.GRID_SIZE); + const { width, height } = this._mainContainerDimension!; const sideBarSize = this.stateModel.getInitializationValue(LayoutStateKeys.SIDEBAR_SIZE); const auxiliaryBarPartSize = this.stateModel.getInitializationValue(LayoutStateKeys.AUXILIARYBAR_SIZE); const panelSize = this.stateModel.getInitializationValue(LayoutStateKeys.PANEL_SIZE); @@ -2579,7 +2581,6 @@ const LayoutStateKeys = { }), // Part Sizing - GRID_SIZE: new InitializationStateKey('grid.size', StorageScope.PROFILE, StorageTarget.MACHINE, { width: 800, height: 600 }), SIDEBAR_SIZE: new InitializationStateKey('sideBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200), AUXILIARYBAR_SIZE: new InitializationStateKey('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200), PANEL_SIZE: new InitializationStateKey('panel.size', StorageScope.PROFILE, StorageTarget.MACHINE, 300), @@ -2632,8 +2633,7 @@ class LayoutStateModel extends Disposable { constructor( private readonly storageService: IStorageService, private readonly configurationService: IConfigurationService, - private readonly contextService: IWorkspaceContextService, - private readonly container: HTMLElement + private readonly contextService: IWorkspaceContextService ) { super(); @@ -2669,7 +2669,7 @@ class LayoutStateModel extends Disposable { } } - load(): void { + load(mainContainerDimension: IDimension): void { let key: keyof typeof LayoutStateKeys; // Load stored values for all keys @@ -2688,12 +2688,10 @@ class LayoutStateModel extends Disposable { this.stateCache.set(LayoutStateKeys.SIDEBAR_POSITON.name, positionFromString(this.configurationService.getValue(LegacyWorkbenchLayoutSettings.SIDEBAR_POSITION) ?? 'left')); // Set dynamic defaults: part sizing and side bar visibility - const workbenchDimensions = getClientArea(this.container); LayoutStateKeys.PANEL_POSITION.defaultValue = positionFromString(this.configurationService.getValue(WorkbenchLayoutSettings.PANEL_POSITION) ?? 'bottom'); - LayoutStateKeys.GRID_SIZE.defaultValue = { height: workbenchDimensions.height, width: workbenchDimensions.width }; - LayoutStateKeys.SIDEBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); - LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); - LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? isHorizontal(LayoutStateKeys.PANEL_POSITION.defaultValue)) ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; + LayoutStateKeys.SIDEBAR_SIZE.defaultValue = Math.min(300, mainContainerDimension.width / 4); + LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, mainContainerDimension.width / 4); + LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? isHorizontal(LayoutStateKeys.PANEL_POSITION.defaultValue)) ? mainContainerDimension.height / 3 : mainContainerDimension.width / 4; LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY; // Apply all defaults From d21929c2a09aa3247450ef6eea057cbb1928b73b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 26 Jan 2025 18:00:22 +0100 Subject: [PATCH 0897/3587] status - always enable completions entry (#237428) (#238799) --- .../inlineCompletionLanguageStatusBarContribution.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts b/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts index b62de00c9dc2..f563593e70a7 100644 --- a/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts +++ b/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts @@ -9,8 +9,6 @@ import { autorunWithStore } from '../../../../base/common/observable.js'; import Severity from '../../../../base/common/severity.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { ILanguageStatusService } from '../../../services/languageStatus/common/languageStatusService.js'; export class InlineCompletionLanguageStatusBarContribution extends Disposable { @@ -18,22 +16,15 @@ export class InlineCompletionLanguageStatusBarContribution extends Disposable { public static Id = 'vs.editor.contrib.inlineCompletionLanguageStatusBarContribution'; - // TODO always enable this! - private readonly _inlineCompletionInlineEdits = observableConfigValue('editor.inlineSuggest.experimentalInlineEditsEnabled', false, this._configurationService); - constructor( private readonly _editor: ICodeEditor, @ILanguageStatusService private readonly _languageStatusService: ILanguageStatusService, - @IConfigurationService private readonly _configurationService: IConfigurationService, ) { super(); const c = InlineCompletionsController.get(this._editor); this._register(autorunWithStore((reader, store) => { - // TODO always enable this feature! - if (!this._inlineCompletionInlineEdits.read(reader)) { return; } - const model = c?.model.read(reader); if (!model) { return; } From 6ae3d1f854c43624872554788b1d98aaab135cfa Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Sun, 26 Jan 2025 19:01:35 -0800 Subject: [PATCH 0898/3587] fix: don't duplicate prompt instructions in chat variables context for edits (#238814) --- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 2d6cdbdb67d4..35f303086fbb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1055,6 +1055,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } // add prompt instruction references to the attached context, if enabled + const promptInstructionUris = new ResourceSet(promptInstructions.chatAttachments.map((v) => v.value) as URI[]); if (instructionsEnabled) { editingSessionAttachedContext .push(...promptInstructions.chatAttachments); @@ -1073,7 +1074,7 @@ export class ChatWidget extends Disposable implements IChatWidget { for (const variable of request.variableData.variables) { if (URI.isUri(variable.value) && variable.isFile && maximumFileEntries > 0) { const uri = variable.value; - if (!uniqueWorkingSetEntries.has(uri)) { + if (!uniqueWorkingSetEntries.has(uri) && !promptInstructionUris.has(uri)) { editingSessionAttachedContext.push(variable); uniqueWorkingSetEntries.add(variable.value); maximumFileEntries -= 1; From 2246a5fddfe024c027e5aba8bf0633c0c06bed9f Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Sun, 26 Jan 2025 20:31:26 -0800 Subject: [PATCH 0899/3587] chat: add a pause button for agent mode (#238735) * chat: add a pause button for agent mode There is a new `ChatParticipant.onDidChangePauseState` event that allows the editor to signal requests should be paused or unpaused. In agentic mode, a "pause" button is visible in the editor that surfaces to this event. It's up to the LM to respect it. When paused, actions go to their "idle" state, so new messages can be written which implicitly cancel the previous request, following PR earlier today. ![](https://memes.peet.io/img/25-01-b61af958-1390-4143-9e09-cef99b2edb86.png) This solves Kai's request of wanting to pause and see what the model is doing before continuing, as well as my desire to be able to interrupt and hint to the model when it gets off track, although that might ideally evolve to some other 'hint' vs a cancelled and new request. PR for copilot coming later this evening. * allow taking input when paused --- .../api/browser/mainThreadChatAgents2.ts | 3 + .../workbench/api/common/extHost.protocol.ts | 1 + .../api/common/extHostChatAgents2.ts | 36 +++++++++++ .../browser/actions/chatExecuteActions.ts | 59 ++++++++++++++++--- src/vs/workbench/contrib/chat/browser/chat.ts | 1 + .../contrib/chat/browser/chatWidget.ts | 14 ++++- .../contrib/chat/browser/media/chat.css | 4 ++ .../contrib/chat/common/chatAgents.ts | 17 ++++++ .../contrib/chat/common/chatContextKeys.ts | 2 + .../contrib/chat/common/chatModel.ts | 41 +++++++++++++ .../contrib/chat/common/chatViewModel.ts | 7 ++- .../chat/test/common/voiceChatService.test.ts | 1 + ...ode.proposed.chatParticipantAdditions.d.ts | 11 ++++ 13 files changed, 186 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index cf4513383754..eb2d7e2a9fd6 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -161,6 +161,9 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._pendingProgress.delete(request.requestId); } }, + setRequestPaused: (requestId, isPaused) => { + this._proxy.$setRequestPaused(handle, requestId, isPaused); + }, provideFollowups: async (request, result, history, token): Promise => { if (!this._agents.get(handle)?.hasFollowups) { return []; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 1c1d6a921988..5072aac5a208 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1328,6 +1328,7 @@ export type IChatAgentHistoryEntryDto = { export interface ExtHostChatAgentsShape2 { $invokeAgent(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; + $setRequestPaused(handle: number, requestId: string, isPaused: boolean): void; $provideFollowups(request: Dto, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; $acceptFeedback(handle: number, result: IChatAgentResult, voteAction: IChatVoteAction): void; $acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 45b7fd7fd0b4..4bf84b803aab 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -296,6 +296,11 @@ class ChatAgentResponseStream { } } +interface InFlightChatRequest { + requestId: string; + extRequest: vscode.ChatRequest; +} + export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsShape2 { private static _idPool = 0; @@ -312,6 +317,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS private readonly _sessionDisposables: DisposableMap = this._register(new DisposableMap()); private readonly _completionDisposables: DisposableMap = this._register(new DisposableMap()); + private readonly _inFlightRequests = new Set(); + constructor( mainContext: IMainContext, private readonly _logService: ILogService, @@ -443,6 +450,20 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return model; } + async $setRequestPaused(handle: number, requestId: string, isPaused: boolean) { + const agent = this._agents.get(handle); + if (!agent) { + return; + } + + const inFlight = Iterable.find(this._inFlightRequests, r => r.requestId === requestId); + if (!inFlight) { + return; + } + + agent.setChatRequestPauseState({ request: inFlight.extRequest, isPaused }); + } + async $invokeAgent(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { @@ -450,6 +471,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } let stream: ChatAgentResponseStream | undefined; + let inFlightRequest: InFlightChatRequest | undefined; try { const { request, location, history } = await this._createRequest(requestDto, context, agent.extension); @@ -465,6 +487,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS const model = await this.getModelForRequest(request, agent.extension); const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(agent.extension, 'chatReadonlyPromptReference')); + inFlightRequest = { requestId: requestDto.requestId, extRequest }; + this._inFlightRequests.add(inFlightRequest); const task = agent.invoke( extRequest, @@ -507,6 +531,9 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return { errorDetails: { message: toErrorMessage(e), responseIsIncomplete: true, isQuotaExceeded } }; } finally { + if (inFlightRequest) { + this._inFlightRequests.delete(inFlightRequest); + } stream?.close(); } } @@ -688,6 +715,7 @@ class ExtHostChatAgent { private _titleProvider?: vscode.ChatTitleProvider | undefined; private _requester: vscode.ChatRequesterInformation | undefined; private _supportsSlowReferences: boolean | undefined; + private _pauseStateEmitter = new Emitter(); constructor( public readonly extension: IExtensionDescription, @@ -705,6 +733,10 @@ class ExtHostChatAgent { this._onDidPerformAction.fire(event); } + setChatRequestPauseState(pauseState: vscode.ChatParticipantPauseStateEvent) { + this._pauseStateEmitter.fire(pauseState); + } + async invokeCompletionProvider(query: string, token: CancellationToken): Promise { if (!this._agentVariableProvider) { return []; @@ -910,6 +942,10 @@ class ExtHostChatAgent { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); return that._titleProvider; }, + get onDidChangePauseState() { + checkProposedApiEnabled(that.extension, 'chatParticipantAdditions'); + return that._pauseStateEmitter.event; + }, onDidPerformAction: !isProposedApiEnabled(this.extension, 'chatParticipantAdditions') ? undefined! : this._onDidPerformAction.event diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 4eb88121e97f..9e979f0038eb 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -75,7 +75,10 @@ export class ChatSubmitAction extends SubmitAction { { id: MenuId.ChatExecute, order: 4, - when: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession)), + when: ContextKeyExpr.and( + ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), + ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession), + ), group: 'navigation', }, ] @@ -125,6 +128,45 @@ export class ToggleAgentModeAction extends Action2 { } } +export const ToggleRequestPausedActionId = 'workbench.action.chat.toggleRequestPausd'; +export class ToggleRequestPausedAction extends Action2 { + static readonly ID = ToggleRequestPausedActionId; + + constructor() { + super({ + id: ToggleRequestPausedAction.ID, + title: localize2('interactive.toggleRequestPausd.label', "Toggle Request Paused"), + category: CHAT_CATEGORY, + icon: Codicon.debugPause, + toggled: { + condition: ChatContextKeys.isRequestPaused, + icon: Codicon.play, + tooltip: localize('requestIsPaused', "Resume Request"), + }, + tooltip: localize('requestNotPaused', "Pause Request"), + menu: [ + { + id: MenuId.ChatExecute, + order: 3.5, + when: ContextKeyExpr.and( + ChatContextKeys.canRequestBePaused, + ChatContextKeys.Editing.agentMode, + ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), + ), + group: 'navigation', + }, + ] + }); + } + + override run(accessor: ServicesAccessor, ...args: any[]): void { + const context: IChatExecuteActionContext | undefined = args[0]; + const widgetService = accessor.get(IChatWidgetService); + const widget = context?.widget ?? widgetService.lastFocusedWidget; + widget?.togglePaused(); + } +} + export class ChatEditingSessionSubmitAction extends SubmitAction { static readonly ID = 'workbench.action.edits.submit'; @@ -153,13 +195,13 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { { id: MenuId.ChatExecuteSecondary, group: 'group_1', - when: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey.toNegated()), + when: ContextKeyExpr.and(ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey.toNegated()), order: 1 }, { id: MenuId.ChatExecute, order: 4, - when: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey.toNegated()), + when: ContextKeyExpr.and(ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey.toNegated()), group: 'navigation', }, ] @@ -175,7 +217,7 @@ class SubmitWithoutDispatchingAction extends Action2 { // if the input has prompt instructions attached, allow submitting requests even // without text present - having instructions is enough context for a request ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), - ChatContextKeys.requestInProgress.negate(), + ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), ContextKeyExpr.and(ContextKeyExpr.or( ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ChatContextKeys.location.isEqualTo(ChatAgentLocation.Editor), @@ -241,7 +283,7 @@ export class ChatSubmitSecondaryAgentAction extends Action2 { // without text present - having instructions is enough context for a request ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), ChatContextKeys.inputHasAgent.negate(), - ChatContextKeys.requestInProgress.negate(), + ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), ); super({ @@ -292,7 +334,7 @@ class SendToChatEditingAction extends Action2 { // without text present - having instructions is enough context for a request ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), ChatContextKeys.inputHasAgent.negate(), - ChatContextKeys.requestInProgress.negate(), + ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), ); super({ @@ -384,7 +426,7 @@ class SendToNewChatAction extends Action2 { // if the input has prompt instructions attached, allow submitting requests even // without text present - having instructions is enough context for a request ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), - ChatContextKeys.requestInProgress.negate(), + ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), ); super({ @@ -433,7 +475,7 @@ export class CancelAction extends Action2 { menu: { id: MenuId.ChatExecute, when: ContextKeyExpr.or( - ChatContextKeys.requestInProgress, + ContextKeyExpr.and(ChatContextKeys.isRequestPaused.negate(), ChatContextKeys.requestInProgress), ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey) ), order: 4, @@ -478,4 +520,5 @@ export function registerChatExecuteActions() { registerAction2(ChatSubmitSecondaryAgentAction); registerAction2(SendToChatEditingAction); registerAction2(ToggleAgentModeAction); + registerAction2(ToggleRequestPausedAction); } diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index bc18d5eeee3b..ed82ef634284 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -246,6 +246,7 @@ export interface IChatWidget { getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined; clear(): void; getViewState(): IChatViewState; + togglePaused(): void; } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 35f303086fbb..f60446e7b9c5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -38,7 +38,7 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService, IChatWelcomeMessageContent, isChatWelcomeMessageContent } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IChatEditingService, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../common/chatEditingService.js'; -import { IChatModel, IChatRequestVariableEntry, IChatResponseModel } from '../common/chatModel.js'; +import { ChatPauseState, IChatModel, IChatRequestVariableEntry, IChatResponseModel } from '../common/chatModel.js'; import { ChatRequestAgentPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, formatChatQuestion } from '../common/chatParserTypes.js'; import { ChatRequestParser } from '../common/chatRequestParser.js'; import { IChatFollowup, IChatLocationData, IChatSendRequestOptions, IChatService } from '../common/chatService.js'; @@ -151,6 +151,8 @@ export class ChatWidget extends Disposable implements IChatWidget { private bodyDimension: dom.Dimension | undefined; private visibleChangeCount = 0; private requestInProgress: IContextKey; + private isRequestPaused: IContextKey; + private canRequestBePaused: IContextKey; private agentInInput: IContextKey; private _visible = false; @@ -248,6 +250,8 @@ export class ChatWidget extends Disposable implements IChatWidget { ChatContextKeys.inQuickChat.bindTo(contextKeyService).set(isQuickChat(this)); this.agentInInput = ChatContextKeys.inputHasAgent.bindTo(contextKeyService); this.requestInProgress = ChatContextKeys.requestInProgress.bindTo(contextKeyService); + this.isRequestPaused = ChatContextKeys.isRequestPaused.bindTo(contextKeyService); + this.canRequestBePaused = ChatContextKeys.canRequestBePaused.bindTo(contextKeyService); this._codeBlockModelCollection = this._register(instantiationService.createInstance(CodeBlockModelCollection)); @@ -857,6 +861,10 @@ export class ChatWidget extends Disposable implements IChatWidget { this.container.style.setProperty('--vscode-chat-list-background', this.themeService.getColorTheme().getColor(this.styles.listBackground)?.toString() ?? ''); } + togglePaused() { + this.viewModel?.model.toggleLastRequestPaused(); + } + setModel(model: IChatModel, viewState: IChatViewState): void { if (!this.container) { throw new Error('Call render() before setModel()'); @@ -876,6 +884,8 @@ export class ChatWidget extends Disposable implements IChatWidget { } this.requestInProgress.set(this.viewModel.requestInProgress); + this.isRequestPaused.set(this.viewModel.requestPausibility === ChatPauseState.Paused); + this.canRequestBePaused.set(this.viewModel.requestPausibility !== ChatPauseState.NotPausable); this.onDidChangeItems(); if (events.some(e => e?.kind === 'addRequest') && this.visible) { @@ -997,7 +1007,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } private async _acceptInput(query: { query: string } | { prefix: string } | undefined, options?: IChatAcceptInputOptions): Promise { - if (this.viewModel?.requestInProgress) { + if (this.viewModel?.requestInProgress && this.viewModel.requestPausibility !== ChatPauseState.Paused) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index d35fcc065f51..d931944021ce 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -804,6 +804,10 @@ have to be updated for changes to the rules above, or to support more deeply nes } } +.chat-execute-toolbar .codicon.codicon-debug-pause { + color: var(--vscode-icon-foreground) !important; +} + .interactive-session .chat-input-toolbars .chat-modelPicker-item .action-label { height: 16px; padding: 3px 0px 3px 6px; diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 8427b8014b79..1422b04c05f6 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -95,6 +95,7 @@ export function isChatWelcomeMessageContent(obj: any): obj is IChatWelcomeMessag export interface IChatAgentImplementation { invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; + setRequestPaused?(requestId: string, isPaused: boolean): void; provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; provideWelcomeMessage?(token: CancellationToken): ProviderResult; provideChatTitle?: (history: IChatAgentHistoryEntry[], token: CancellationToken) => Promise; @@ -218,6 +219,7 @@ export interface IChatAgentService { detectAgentOrCommand(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation }, token: CancellationToken): Promise<{ agent: IChatAgentData; command?: IChatAgentCommand } | undefined>; hasChatParticipantDetectionProviders(): boolean; invokeAgent(agent: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; + setRequestPaused(agent: string, requestId: string, isPaused: boolean): void; getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; getChatTitle(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; getAgent(id: string, includeDisabled?: boolean): IChatAgentData | undefined; @@ -501,6 +503,15 @@ export class ChatAgentService extends Disposable implements IChatAgentService { return await data.impl.invoke(request, progress, history, token); } + setRequestPaused(id: string, requestId: string, isPaused: boolean) { + const data = this._agents.get(id); + if (!data?.impl) { + throw new Error(`No activated agent with id "${id}"`); + } + + data.impl.setRequestPaused?.(requestId, isPaused); + } + async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._agents.get(id); if (!data?.impl) { @@ -608,6 +619,12 @@ export class MergedChatAgent implements IChatAgent { return this.impl.invoke(request, progress, history, token); } + setRequestPaused(requestId: string, isPaused: boolean): void { + if (this.impl.setRequestPaused) { + this.impl.setRequestPaused(requestId, isPaused); + } + } + async provideFollowups(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { if (this.impl.provideFollowups) { return this.impl.provideFollowups(request, result, history, token); diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 3108cd8be7ba..7723d67602f1 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -16,6 +16,8 @@ export namespace ChatContextKeys { export const responseIsFiltered = new RawContextKey('chatSessionResponseFiltered', false, { type: 'boolean', description: localize('chatResponseFiltered', "True when the chat response was filtered out by the server.") }); export const responseHasError = new RawContextKey('chatSessionResponseError', false, { type: 'boolean', description: localize('chatResponseErrored', "True when the chat response resulted in an error.") }); export const requestInProgress = new RawContextKey('chatSessionRequestInProgress', false, { type: 'boolean', description: localize('interactiveSessionRequestInProgress', "True when the current request is still in progress.") }); + export const isRequestPaused = new RawContextKey('chatRequestIsPaused', false, { type: 'boolean', description: localize('chatRequestIsPaused', "True when the current request is paused.") }); + export const canRequestBePaused = new RawContextKey('chatCanRequestBePaused', false, { type: 'boolean', description: localize('chatCanRequestBePaused', "True when the current request can be paused.") }); export const isResponse = new RawContextKey('chatResponse', false, { type: 'boolean', description: localize('chatResponse', "The chat item is a response.") }); export const isRequest = new RawContextKey('chatRequest', false, { type: 'boolean', description: localize('chatRequest', "The chat item is a request") }); diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 151875b93d8a..406896357199 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -132,6 +132,7 @@ export interface IChatRequestModel { readonly attachedContext?: IChatRequestVariableEntry[]; readonly workingSet?: URI[]; readonly isCompleteAddedRequest: boolean; + readonly paused: boolean; readonly response?: IChatResponseModel; shouldBeRemovedOnSend: boolean; } @@ -270,6 +271,15 @@ export class ChatRequestModel implements IChatRequestModel { return this._workingSet; } + private _paused = false; + public get paused(): boolean { + return this._paused; + } + + public set paused(paused: boolean) { + this._paused = paused; + } + constructor( private _session: ChatModel, public readonly message: IParsedChatRequest, @@ -722,6 +732,12 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel } } +export const enum ChatPauseState { + NotPausable, + Paused, + Unpaused, +} + export interface IChatModel { readonly onDidDispose: Event; readonly onDidChange: Event; @@ -732,7 +748,9 @@ export interface IChatModel { readonly welcomeMessage: IChatWelcomeMessageContent | undefined; readonly sampleQuestions: IChatFollowup[] | undefined; readonly requestInProgress: boolean; + readonly requestPausibility: ChatPauseState; readonly inputPlaceholder?: string; + toggleLastRequestPaused(paused?: boolean): void; disableRequests(requestIds: ReadonlyArray): void; getRequests(): IChatRequestModel[]; toExport(): IExportableChatData; @@ -1005,6 +1023,15 @@ export class ChatModel extends Disposable implements IChatModel { return !lastRequest.response.isComplete; } + get requestPausibility(): ChatPauseState { + const lastRequest = this.lastRequest; + if (!lastRequest?.response?.agent || lastRequest.response.isComplete || lastRequest.response.isPendingConfirmation) { + return ChatPauseState.NotPausable; + } + + return lastRequest.paused ? ChatPauseState.Paused : ChatPauseState.Unpaused; + } + get hasRequests(): boolean { return this._requests.length > 0; } @@ -1171,6 +1198,15 @@ export class ChatModel extends Disposable implements IChatModel { }; } + toggleLastRequestPaused(isPaused?: boolean) { + if (this.requestPausibility !== ChatPauseState.NotPausable && this.lastRequest?.response?.agent) { + const pausedValue = isPaused ?? !this.lastRequest.paused; + this.lastRequest.paused = pausedValue; + this.chatAgentService.setRequestPaused(this.lastRequest.response.agent.id, this.lastRequest.id, pausedValue); + this._onDidChange.fire({ kind: 'changedRequest', request: this.lastRequest }); + } + } + startInitialize(): void { if (this.initState !== ChatModelInitState.Created) { throw new Error(`ChatModel is in the wrong state for startInitialize: ${ChatModelInitState[this.initState]}`); @@ -1252,6 +1288,11 @@ export class ChatModel extends Disposable implements IChatModel { this._customTitle = title; } + setRequestPaused(request: ChatRequestModel, isPaused: boolean) { + request.paused = isPaused; + this._onDidChange.fire({ kind: 'changedRequest', request }); + } + updateRequest(request: ChatRequestModel, variableData: IChatRequestVariableData) { request.variableData = variableData; this._onDidChange.fire({ kind: 'changedRequest', request }); diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index d2a00d5954ba..6f58726d1679 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -13,7 +13,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { annotateVulnerabilitiesInText } from './annotations.js'; import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentResult } from './chatAgents.js'; -import { ChatModelInitState, IChatModel, IChatProgressRenderableResponseContent, IChatRequestModel, IChatRequestVariableEntry, IChatResponseModel, IChatTextEditGroup, IResponse } from './chatModel.js'; +import { ChatModelInitState, ChatPauseState, IChatModel, IChatProgressRenderableResponseContent, IChatRequestModel, IChatRequestVariableEntry, IChatResponseModel, IChatTextEditGroup, IResponse } from './chatModel.js'; import { IParsedChatRequest } from './chatParserTypes.js'; import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatCodeCitation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatTask, IChatUsedContext } from './chatService.js'; import { countWords } from './chatWordCounter.js'; @@ -53,6 +53,7 @@ export interface IChatViewModel { readonly onDidDisposeModel: Event; readonly onDidChange: Event; readonly requestInProgress: boolean; + readonly requestPausibility: ChatPauseState; readonly inputPlaceholder?: string; getItems(): (IChatRequestViewModel | IChatResponseViewModel)[]; setInputPlaceholder(text: string): void; @@ -228,6 +229,10 @@ export class ChatViewModel extends Disposable implements IChatViewModel { return this._model.requestInProgress; } + get requestPausibility(): ChatPauseState { + return this._model.requestPausibility; + } + get initState() { return this._model.initState; } diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts index f86b6d782c4e..0005bca091e8 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts @@ -70,6 +70,7 @@ suite('VoiceChat', () => { registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); } invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } + setRequestPaused(agent: string, requestId: string, isPaused: boolean): void { throw new Error('not implemented'); } getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } getActivatedAgents(): IChatAgent[] { return agents; } getAgents(): IChatAgent[] { return agents; } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 1be58f20bfbb..374ff087cc17 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -240,6 +240,17 @@ declare module 'vscode' { * Provide a set of variables that can only be used with this participant. */ participantVariableProvider?: { provider: ChatParticipantCompletionItemProvider; triggerCharacters: string[] }; + + /** + * Event that fires when a request is paused or unpaused. + * Chat requests are initialy unpaused in the {@link requestHandler}. + */ + onDidChangePauseState: Event; + } + + export interface ChatParticipantPauseStateEvent { + request: ChatRequest; + isPaused: boolean; } export interface ChatParticipantCompletionItemProvider { From a1fc8c144985285527fcceb7adfa5f66b6bb5399 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Sun, 26 Jan 2025 20:33:09 -0800 Subject: [PATCH 0900/3587] fix: track untitled editor save event in working set (#238816) --- .../chat/browser/chatEditing/chatEditingSession.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 00f9558143bc..358cb06f206d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -635,6 +635,15 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio store.dispose(); } })); + store.add(this._textFileService.untitled.onDidSave(e => { + const existing = this._workingSet.get(resource); + if (isEqual(e.source, resource) && existing) { + this._workingSet.delete(resource); + this._workingSet.set(e.target, existing); + store.dispose(); + this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); + } + })); store.add(this._editorService.onDidCloseEditor((e) => { if (isEqual(e.editor.resource, resource)) { this._workingSet.delete(resource); From 28927e2a05677601642d396cf10ecf9fe5497198 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 27 Jan 2025 07:44:25 +0100 Subject: [PATCH 0901/3587] :up: `@playwright/test@1.50.0` (#238671) --- package-lock.json | 46 +++++++++++++++------------------------------- package.json | 2 +- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ef04f1a0c5c..e1adc3a25994 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "yazl": "^2.4.3" }, "devDependencies": { - "@playwright/test": "^1.46.1", + "@playwright/test": "^1.50.0", "@stylistic/eslint-plugin-ts": "^2.8.0", "@types/cookie": "^0.3.3", "@types/debug": "^4.1.5", @@ -2071,12 +2071,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.46.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz", - "integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0.tgz", + "integrity": "sha512-ZGNXbt+d65EGjBORQHuYKj+XhCewlwpnSd/EDuLPZGSiEWmgOJB5RmMCCYGy5aMfTs9wx61RivfDKi8H/hcMvw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright": "1.46.1" + "playwright": "1.50.0" }, "bin": { "playwright": "cli.js" @@ -3155,25 +3156,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@vscode/test-web/node_modules/playwright": { - "version": "1.47.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz", - "integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.47.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, "node_modules/@vscode/tree-sitter-wasm": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.5.tgz", @@ -13838,12 +13820,13 @@ } }, "node_modules/playwright": { - "version": "1.46.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz", - "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0.tgz", + "integrity": "sha512-+GinGfGTrd2IfX1TA4N2gNmeIksSb+IAe589ZH+FlmpV3MYTx6+buChGIuDLQwrGNCw2lWibqV50fU510N7S+w==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.46.1" + "playwright-core": "1.50.0" }, "bin": { "playwright": "cli.js" @@ -13869,10 +13852,11 @@ } }, "node_modules/playwright/node_modules/playwright-core": { - "version": "1.46.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz", - "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0.tgz", + "integrity": "sha512-CXkSSlr4JaZs2tZHI40DsZUN/NIwgaUPsyLuOAaIZp2CyF2sN5MM5NJsyB188lFSSozFxQ5fPT4qM+f0tH/6wQ==", "dev": true, + "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, diff --git a/package.json b/package.json index 4ad642bd0140..2c28df7f5e36 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "yazl": "^2.4.3" }, "devDependencies": { - "@playwright/test": "^1.46.1", + "@playwright/test": "^1.50.0", "@stylistic/eslint-plugin-ts": "^2.8.0", "@types/cookie": "^0.3.3", "@types/debug": "^4.1.5", From 0d9cdc335d866e93acc8d07e7d606ff015e84e40 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 27 Jan 2025 08:53:52 +0100 Subject: [PATCH 0902/3587] chat - make quota indicator top level (#237428) (#238820) --- .../contrib/chat/browser/chatQuotasService.ts | 28 ++----------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts b/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts index 6e2ab0a5b340..0da5b5a3cfa5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts @@ -52,7 +52,7 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService private readonly chatQuotaExceededContextKey = ChatContextKeys.chatQuotaExceeded.bindTo(this.contextKeyService); private readonly completionsQuotaExceededContextKey = ChatContextKeys.completionsQuotaExceeded.bindTo(this.contextKeyService); - private ExtensionQuotaContextKeys = { // TODO@bpasero move into product.json or turn into core keys + private ExtensionQuotaContextKeys = { chatQuotaExceeded: 'github.copilot.chat.quotaExceeded', completionsQuotaExceeded: 'github.copilot.completions.quotaExceeded', }; @@ -176,8 +176,6 @@ export class ChatQuotasStatusBarEntry extends Disposable implements IWorkbenchCo static readonly ID = 'chat.quotasStatusBarEntry'; - private static readonly COPILOT_STATUS_ID = 'GitHub.copilot.status'; // TODO@bpasero unify into 1 core indicator - private readonly entry = this._register(new DisposableStore()); constructor( @@ -187,11 +185,6 @@ export class ChatQuotasStatusBarEntry extends Disposable implements IWorkbenchCo super(); this._register(Event.runAndSubscribe(this.chatQuotasService.onDidChangeQuotas, () => this.updateStatusbarEntry())); - this._register(this.statusbarService.onDidChangeEntryVisibility(e => { - if (e.id === ChatQuotasStatusBarEntry.COPILOT_STATUS_ID) { - this.updateStatusbarEntry(); - } - })); } private updateStatusbarEntry(): void { @@ -208,30 +201,15 @@ export class ChatQuotasStatusBarEntry extends Disposable implements IWorkbenchCo text = localize('chatAndCompletionsQuotaExceededStatus', "Copilot limit reached"); } - const isCopilotStatusVisible = this.statusbarService.isEntryVisible(ChatQuotasStatusBarEntry.COPILOT_STATUS_ID); - if (!isCopilotStatusVisible) { - text = `$(copilot-warning) ${text}`; - } - this.entry.add(this.statusbarService.addEntry({ name: localize('indicator', "Copilot Limit Indicator"), - text, + text: `$(copilot-warning) ${text}`, ariaLabel: text, command: OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, showInAllWindows: true, kind: 'prominent', tooltip: quotaToButtonMessage({ chatQuotaExceeded, completionsQuotaExceeded }) - }, ChatQuotasStatusBarEntry.ID, StatusbarAlignment.RIGHT, { - id: ChatQuotasStatusBarEntry.COPILOT_STATUS_ID, - alignment: StatusbarAlignment.RIGHT, - compact: isCopilotStatusVisible - })); - - this.entry.add(this.statusbarService.overrideEntry(ChatQuotasStatusBarEntry.COPILOT_STATUS_ID, { - text: '$(copilot-warning)', - ariaLabel: text, - kind: 'prominent' - })); + }, ChatQuotasStatusBarEntry.ID, StatusbarAlignment.RIGHT, 1)); } } } From 5e38f282a2836e2808fbc7fa0304ed630c3ebc2a Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 27 Jan 2025 10:35:39 +0100 Subject: [PATCH 0903/3587] edits fixes (#238826) one inline chat session per file, fix revealing for 1+n requests --- .../chat/browser/chatEditorController.ts | 16 +++++++++++++--- .../browser/inlineChatController2.ts | 18 ++++++++++++++---- .../browser/inlineChatSessionService.ts | 2 +- .../browser/inlineChatSessionServiceImpl.ts | 15 +++++++-------- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index c9137403d01c..88cb0f857c55 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -36,6 +36,7 @@ import { basename, isEqual } from '../../../../base/common/resources.js'; import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; import { EditorsOrder, IEditorIdentifier, isDiffEditorInput } from '../../../common/editor.js'; import { ChatEditorOverlayController } from './chatEditorOverlay.js'; +import { IChatService } from '../common/chatService.js'; export const ctxIsGlobalEditingSession = new RawContextKey('chat.isGlobalEditingSession', undefined, localize('chat.ctxEditSessionIsGlobal', "The current editor is part of the global edit session")); export const ctxHasEditorModification = new RawContextKey('chat.hasEditorModifications', undefined, localize('chat.hasEditorModifications', "The current editor contains chat modifications")); @@ -82,6 +83,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IEditorService private readonly _editorService: IEditorService, @IContextKeyService contextKeyService: IContextKeyService, + @IChatService chatService: IChatService, ) { super(); @@ -116,8 +118,10 @@ export class ChatEditorController extends Disposable implements IEditorContribut ? entries.findIndex(e => isEqual(e.modifiedURI, model.uri)) : -1; - if (idx >= 0) { - return { session, entry: entries[idx], entries, idx }; + const chatModel = chatService.getSession(session.chatSessionId); + + if (idx >= 0 && chatModel) { + return { session, chatModel, entry: entries[idx], entries, idx }; } } @@ -143,7 +147,13 @@ export class ChatEditorController extends Disposable implements IEditorContribut return; } - const { session, entries, idx, entry } = currentEditorEntry; + const { session, chatModel, entries, idx, entry } = currentEditorEntry; + + store.add(chatModel.onDidChange(e => { + if (e.kind === 'addRequest') { + didReval = false; + } + })); this._ctxIsGlobalEditsSession.set(session.isGlobalEditingSession); this._ctxReviewModelEnabled.set(entry.reviewMode.read(r)); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts index e8c36b2084d4..1e925be08a2d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts @@ -51,6 +51,8 @@ export class InlineChatController2 implements IEditorContribution { private readonly _showWidgetOverrideObs = observableValue(this, false); + private readonly _isActiveController = observableValue(this, false); + constructor( private readonly _editor: ICodeEditor, @@ -114,7 +116,7 @@ export class InlineChatController2 implements IEditorContribution { const sessionObs = derived(r => { sessionsSignal.read(r); const model = editorObs.model.read(r); - const value = model && inlineChatSessions.getSession2(_editor, model.uri); + const value = model && inlineChatSessions.getSession2(model.uri); return value ?? undefined; }); @@ -124,6 +126,7 @@ export class InlineChatController2 implements IEditorContribution { if (!session) { ctxHasSession.set(undefined); + this._isActiveController.set(false, undefined); } else { const checkRequests = () => ctxHasSession.set(session.chatModel.getRequests().length === 0 ? 'empty' : 'active'); store.add(session.chatModel.onDidChange(checkRequests)); @@ -136,8 +139,9 @@ export class InlineChatController2 implements IEditorContribution { this._store.add(autorunWithStore((r, store) => { const session = sessionObs.read(r); + const isActive = this._isActiveController.read(r); - if (!session) { + if (!session || !isActive) { visibleSessionObs.set(undefined, undefined); return; } @@ -214,6 +218,10 @@ export class InlineChatController2 implements IEditorContribution { const value = this._showWidgetOverrideObs.get(); this._showWidgetOverrideObs.set(!value, undefined); } + + markActiveController() { + this._isActiveController.set(true, undefined); + } } export class StartSessionAction2 extends EditorAction2 { @@ -251,6 +259,7 @@ export class StartSessionAction2 extends EditorAction2 { } const textModel = editor.getModel(); await inlineChatSessions.createSession2(editor, textModel.uri, CancellationToken.None); + InlineChatController2.get(editor)?.markActiveController(); } } @@ -326,7 +335,7 @@ export class StopSessionAction2 extends AbstractInlineChatAction { return; } const textModel = editor.getModel(); - inlineChatSessions.getSession2(editor, textModel.uri)?.dispose(); + inlineChatSessions.getSession2(textModel.uri)?.dispose(); } } @@ -356,8 +365,9 @@ class RevealWidget extends AbstractInlineChatAction { }); } - runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController2, editor: ICodeEditor, ...args: any[]): void { + runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController2, _editor: ICodeEditor): void { ctrl.toggleWidgetUntilNextRequest(); + ctrl.markActiveController(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts index 93be522ac5be..a6e06ad4b360 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts @@ -64,6 +64,6 @@ export interface IInlineChatSessionService { createSession2(editor: ICodeEditor, uri: URI, token: CancellationToken): Promise; - getSession2(editor: ICodeEditor, uri: URI): IInlineChatSession2 | undefined; + getSession2(uri: URI): IInlineChatSession2 | undefined; onDidChangeSessions: Event; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 767d074b27e5..b502148e9a4a 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -33,6 +33,7 @@ import { ITextFileService } from '../../../services/textfile/common/textfiles.js import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; import { assertType } from '../../../../base/common/types.js'; import { autorun } from '../../../../base/common/observable.js'; +import { ResourceMap } from '../../../../base/common/map.js'; type SessionData = { @@ -318,7 +319,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { // ---- NEW - private readonly _sessions2 = new Map(); + private readonly _sessions2 = new ResourceMap(); private readonly _onDidChangeSessions = this._store.add(new Emitter()); readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; @@ -328,8 +329,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { assertType(editor.hasModel()); - const key = this._key(editor, uri); - if (this._sessions2.has(key)) { + if (this._sessions2.has(uri)) { throw new Error('Session already exists'); } @@ -343,7 +343,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const store = new DisposableStore(); store.add(toDisposable(() => { editingSession.reject(); - this._sessions2.delete(key); + this._sessions2.delete(uri); this._onDidChangeSessions.fire(this); })); store.add(editingSession); @@ -365,14 +365,13 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { editingSession, dispose: store.dispose.bind(store) }; - this._sessions2.set(key, result); + this._sessions2.set(uri, result); this._onDidChangeSessions.fire(this); return result; } - getSession2(editor: ICodeEditor, uri: URI): IInlineChatSession2 | undefined { - const key = this._key(editor, uri); - return this._sessions2.get(key); + getSession2(uri: URI): IInlineChatSession2 | undefined { + return this._sessions2.get(uri); } } From ca0f0d44fbdff9eef8669274048c6a7a77e47339 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 27 Jan 2025 11:10:43 +0100 Subject: [PATCH 0904/3587] Clicking on an action in Language Status Indicator causes pins to disappear (fix #238718) (#238788) --- .../languageStatus/browser/languageStatus.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.ts index b04c5c16c5ab..142d518e68be 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.ts @@ -102,6 +102,8 @@ class LanguageStatus { private _dedicatedEntries = new Map(); private readonly _renderDisposables = new DisposableStore(); + private readonly _combinedEntryTooltip = document.createElement('div'); + constructor( @ILanguageStatusService private readonly _languageStatusService: ILanguageStatusService, @IStatusbarService private readonly _statusBarService: IStatusbarService, @@ -207,10 +209,9 @@ class LanguageStatus { let isOneBusy = false; const ariaLabels: string[] = []; - const element = document.createElement('div'); for (const status of model.combined) { const isPinned = model.dedicated.includes(status); - element.appendChild(this._renderStatus(status, showSeverity, isPinned, this._renderDisposables)); + this._renderStatus(this._combinedEntryTooltip, status, showSeverity, isPinned, this._renderDisposables); ariaLabels.push(LanguageStatus._accessibilityInformation(status).label); isOneBusy = isOneBusy || (!isPinned && status.busy); // unpinned items contribute to the busy-indicator of the composite status item } @@ -218,7 +219,7 @@ class LanguageStatus { const props: IStatusbarEntry = { name: localize('langStatus.name', "Editor Language Status"), ariaLabel: localize('langStatus.aria', "Editor Language Status: {0}", ariaLabels.join(', next: ')), - tooltip: element, + tooltip: this._combinedEntryTooltip, command: ShowTooltipCommand, text: isOneBusy ? '$(loading~spin)' : text, }; @@ -256,7 +257,7 @@ class LanguageStatus { const hoverTarget = targetWindow.document.querySelector('.monaco-workbench .context-view'); if (dom.isHTMLElement(hoverTarget)) { const observer = new MutationObserver(() => { - if (targetWindow.document.contains(element)) { + if (targetWindow.document.contains(this._combinedEntryTooltip)) { this._interactionCounter.increment(); observer.disconnect(); } @@ -284,11 +285,14 @@ class LanguageStatus { this._dedicatedEntries = newDedicatedEntries; } - private _renderStatus(status: ILanguageStatus, showSeverity: boolean, isPinned: boolean, store: DisposableStore): HTMLElement { + private _renderStatus(container: HTMLElement, status: ILanguageStatus, showSeverity: boolean, isPinned: boolean, store: DisposableStore): HTMLElement { const parent = document.createElement('div'); parent.classList.add('hover-language-status'); + container.appendChild(parent); + store.add(toDisposable(() => parent.remove())); + const severity = document.createElement('div'); severity.classList.add('severity', `sev${status.severity}`); severity.classList.toggle('show', showSeverity); From d8817d4a226e60310f37660ab1b06d1ac8f23484 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:29:54 +0100 Subject: [PATCH 0905/3587] Update ghost text handling for multi-line rendering (#238827) dont shift cursor with ghost text multi line rendering --- .../browser/view/ghostText/ghostTextView.ts | 53 +++++++++++++------ .../view/inlineEdits/inlineDiffView.ts | 8 +-- .../browser/view/inlineEdits/view.ts | 16 +++--- 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts index 01df05929393..ce5dfc8c9941 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts @@ -9,7 +9,7 @@ import { Disposable, toDisposable } from '../../../../../../base/common/lifecycl import { IObservable, autorun, derived, observableSignalFromEvent, observableValue } from '../../../../../../base/common/observable.js'; import * as strings from '../../../../../../base/common/strings.js'; import { applyFontInfo } from '../../../../../browser/config/domFontInfo.js'; -import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; +import { ICodeEditor, IViewZoneChangeAccessor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { EditorFontLigatures, EditorOption, IComputedEditorOptions } from '../../../../../common/config/editorOptions.js'; import { OffsetEdit, SingleOffsetEdit } from '../../../../../common/core/offsetEdit.js'; @@ -215,8 +215,8 @@ function computeGhostTextViewData(ghostText: GhostText | GhostTextReplacement, t } export class AdditionalLinesWidget extends Disposable { - private _viewZoneId: string | undefined = undefined; - public get viewZoneId(): string | undefined { return this._viewZoneId; } + private _viewZoneInfo: { viewZoneId: string; heightInLines: number; lineNumber: number } | undefined; + public get viewZoneId(): string | undefined { return this._viewZoneInfo?.viewZoneId; } private readonly editorOptionsChanged = observableSignalFromEvent('editorOptionChanged', Event.filter( this.editor.onDidChangeConfiguration, @@ -260,10 +260,7 @@ export class AdditionalLinesWidget extends Disposable { private clear(): void { this.editor.changeViewZones((changeAccessor) => { - if (this._viewZoneId) { - changeAccessor.removeZone(this._viewZoneId); - this._viewZoneId = undefined; - } + this.removeActiveViewZone(changeAccessor); }); } @@ -276,25 +273,47 @@ export class AdditionalLinesWidget extends Disposable { const { tabSize } = textModel.getOptions(); this.editor.changeViewZones((changeAccessor) => { - if (this._viewZoneId) { - changeAccessor.removeZone(this._viewZoneId); - this._viewZoneId = undefined; - } + this.removeActiveViewZone(changeAccessor); const heightInLines = Math.max(additionalLines.length, minReservedLineCount); if (heightInLines > 0) { const domNode = document.createElement('div'); renderLines(domNode, tabSize, additionalLines, this.editor.getOptions()); - this._viewZoneId = changeAccessor.addZone({ - afterLineNumber: lineNumber, - heightInLines: heightInLines, - domNode, - afterColumnAffinity: PositionAffinity.Right - }); + this.addViewZone(changeAccessor, lineNumber, heightInLines, domNode); } }); } + + private addViewZone(changeAccessor: IViewZoneChangeAccessor, afterLineNumber: number, heightInLines: number, domNode: HTMLElement): void { + const id = changeAccessor.addZone({ + afterLineNumber: afterLineNumber, + heightInLines: heightInLines, + domNode, + afterColumnAffinity: PositionAffinity.Right + }); + + this.keepCursorStable(afterLineNumber, heightInLines); + + this._viewZoneInfo = { viewZoneId: id, heightInLines, lineNumber: afterLineNumber }; + } + + private removeActiveViewZone(changeAccessor: IViewZoneChangeAccessor): void { + if (this._viewZoneInfo) { + changeAccessor.removeZone(this._viewZoneInfo.viewZoneId); + + this.keepCursorStable(this._viewZoneInfo.lineNumber, -this._viewZoneInfo.heightInLines); + + this._viewZoneInfo = undefined; + } + } + + private keepCursorStable(lineNumber: number, heightInLines: number): void { + const cursorLineNumber = this.editor.getSelection()?.getStartPosition()?.lineNumber; + if (cursorLineNumber !== undefined && lineNumber < cursorLineNumber) { + this.editor.setScrollTop(this.editor.getScrollTop() + heightInLines * this.editor.getOption(EditorOption.lineHeight)); + } + } } export interface LineData { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts index d6c99f67fe18..22f7a866254c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts @@ -24,7 +24,7 @@ import { classNames } from './utils.js'; export interface IOriginalEditorInlineDiffViewState { diff: DetailedLineRangeMapping[]; modifiedText: AbstractText; - mode: 'mixedLines' | 'ghostText' | 'interleavedLines' | 'sideBySide' | 'deletion'; + mode: 'mixedLines' | 'insertionInline' | 'interleavedLines' | 'sideBySide' | 'deletion'; modifiedCodeEditor: ICodeEditor; } @@ -114,7 +114,7 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE if (!diff) { return undefined; } const modified = diff.modifiedText; - const showInline = diff.mode === 'mixedLines' || diff.mode === 'ghostText'; + const showInline = diff.mode === 'mixedLines' || diff.mode === 'insertionInline'; const showEmptyDecorations = true; @@ -195,7 +195,7 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE shouldFillLineOnLineBreak: false, className: classNames( 'inlineCompletions-char-delete', - i.originalRange.isSingleLine() && diff.mode === 'ghostText' && 'single-line-inline', + i.originalRange.isSingleLine() && diff.mode === 'insertionInline' && 'single-line-inline', i.originalRange.isEmpty() && 'empty', ((i.originalRange.isEmpty() || diff.mode === 'deletion' && replacedText === '\n') && showEmptyDecorations && !useInlineDiff) && 'diff-range-empty' ), @@ -222,7 +222,7 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE content: insertedText, inlineClassName: classNames( 'inlineCompletions-char-insert', - i.modifiedRange.isSingleLine() && diff.mode === 'ghostText' && 'single-line-inline' + i.modifiedRange.isSingleLine() && diff.mode === 'insertionInline' && 'single-line-inline' ), }, zIndex: 2, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index bb2537f13a1d..3338ce7fb65d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -143,11 +143,11 @@ export class InlineEditsView extends Disposable { { ghostText: derived(reader => { const state = this._uiState.read(reader)?.state; - if (!state || state.kind !== 'insertion') { return undefined; } + if (!state || state.kind !== 'insertionMultiLine') { return undefined; } const textModel = this._editor.getModel()!; - // Try to not insert on the same line where there is other content + // Try to not insert on the same line where there is other content afterwards if (state.column === 1 && state.lineNumber > 1 && textModel.getLineLength(state.lineNumber) !== 0 && state.text.endsWith('\n') && !state.text.startsWith('\n')) { const endOfLineColumn = textModel.getLineLength(state.lineNumber - 1) + 1; return new GhostText(state.lineNumber - 1, [new GhostTextPart(endOfLineColumn, '\n' + state.text.slice(0, -1), false)]); @@ -163,7 +163,7 @@ export class InlineEditsView extends Disposable { private readonly _inlineDiffViewState = derived(this, reader => { const e = this._uiState.read(reader); if (!e || !e.state) { return undefined; } - if (e.state.kind === 'wordReplacements' || e.state.kind === 'lineReplacement' || e.state.kind === 'insertion') { + if (e.state.kind === 'wordReplacements' || e.state.kind === 'lineReplacement' || e.state.kind === 'insertionMultiLine') { return undefined; } return { @@ -256,7 +256,7 @@ export class InlineEditsView extends Disposable { || isSingleLineDeletion(diff) ) ) { - return 'ghostText'; + return 'insertionInline'; } if (inner.every(m => newText.getValueOfRange(m.modifiedRange).trim() === '' && edit.originalText.getValueOfRange(m.originalRange).trim() !== '')) { @@ -264,7 +264,7 @@ export class InlineEditsView extends Disposable { } if (isSingleMultiLineInsertion(diff) && this._useMultiLineGhostText.read(reader)) { - return 'insertion'; + return 'insertionMultiLine'; } const useCodeOverlay = this._useCodeOverlay.read(reader); @@ -301,7 +301,7 @@ export class InlineEditsView extends Disposable { switch (view) { case 'collapsed': return { kind: 'collapsed' as const }; - case 'ghostText': return { kind: 'ghostText' as const }; + case 'insertionInline': return { kind: 'insertionInline' as const }; case 'mixedLines': return { kind: 'mixedLines' as const }; case 'interleavedLines': return { kind: 'interleavedLines' as const }; case 'sideBySide': return { kind: 'sideBySide' as const }; @@ -317,10 +317,10 @@ export class InlineEditsView extends Disposable { }; } - if (view === 'insertion') { + if (view === 'insertionMultiLine') { const change = inner[0]; return { - kind: 'insertion' as const, + kind: 'insertionMultiLine' as const, lineNumber: change.originalRange.startLineNumber, column: change.originalRange.startColumn, text: newText.getValueOfRange(change.modifiedRange), From f521d86ad1fbfb5c1b2978e1a96c775f49e3fd79 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 27 Jan 2025 04:35:26 -0600 Subject: [PATCH 0906/3587] add kb to toggle diff for edits (#238717) fix #238145 --- src/vs/workbench/contrib/chat/browser/chatEditorActions.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts index 48d0cd3fb885..15e0a659f959 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts @@ -265,8 +265,13 @@ class OpenDiffAction extends EditorAction2 { condition: EditorContextKeys.inDiffEditor, icon: Codicon.goToFile, }, - precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), + precondition: ContextKeyExpr.and(ctxHasEditorModification, ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), icon: Codicon.diffSingle, + keybinding: { + when: EditorContextKeys.focus, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F7, + }, menu: [{ id: MenuId.ChatEditingEditorHunk, order: 10 From f0c1f4ddd62f6531f407f91aef0adf9e0aed5aec Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 27 Jan 2025 11:46:05 +0100 Subject: [PATCH 0907/3587] Fix tree sitter freeze and overwriting of scopes (#238829) Fixes microsoft/vscode-internalbacklog#5307 --- .../treeSitter/treeSitterParserService.ts | 32 +++++++++++++++---- .../browser/treeSitterTokenizationFeature.ts | 4 +-- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts index 2d0ac90468ee..a7fd4bb49bd7 100644 --- a/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts +++ b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts @@ -236,22 +236,29 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul oldFindPrev.resetTo(oldCursor); const startingNode = newCursor.currentNode; do { - if (newFindPrev.currentNode.previousSibling) { + if (newFindPrev.currentNode.previousSibling && ((newFindPrev.currentNode.endIndex - newFindPrev.currentNode.startIndex) !== 0)) { newFindPrev.gotoPreviousSibling(); oldFindPrev.gotoPreviousSibling(); } else { - newFindPrev.gotoParent(); - oldFindPrev.gotoParent(); + while (!newFindPrev.currentNode.previousSibling && newFindPrev.currentNode.parent) { + newFindPrev.gotoParent(); + oldFindPrev.gotoParent(); + } + newFindPrev.gotoPreviousSibling(); + oldFindPrev.gotoPreviousSibling(); } + } while ((newFindPrev.currentNode.endIndex > startingNode.startIndex) + && (newFindPrev.currentNode.parent || newFindPrev.currentNode.previousSibling) + + && (newFindPrev.currentNode.id !== startingNode.id)); - } while (newFindPrev.currentNode.startIndex === startingNode.startIndex && (newFindPrev.currentNode.parent || newFindPrev.currentNode.previousSibling) && (newFindPrev.currentNode.id !== startingNode.id)); - if ((newFindPrev.currentNode.id !== startingNode.id) && newFindPrev.currentNode.endIndex < startingNode.startIndex) { + if ((newFindPrev.currentNode.id !== startingNode.id) && newFindPrev.currentNode.endIndex <= startingNode.startIndex) { return { old: oldFindPrev.currentNode, new: newFindPrev.currentNode }; } else { return undefined; } }; - + const childrenToVisit: Map = new Map(); do { if (newCursor.currentNode.hasChanges) { // Check if only one of the children has changes. @@ -267,6 +274,12 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul return c.hasChanges; }); if (changedChildren.length >= 1) { + if (changedChildren.length > 1) { + // We need to visit each child eventually + for (let i = 1; i < changedChildren.length; i++) { + childrenToVisit.set(changedChildren[i].id, { new: changedChildren[i], old: oldCursor.currentNode.children[i] }); + } + } next = gotoNthChild(indexChangedChildren[0]); } else if (changedChildren.length === 0) { const newNode = newCursor.currentNode; @@ -286,6 +299,13 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul } else { next = nextSiblingOrParentSibling(); } + if (!next && childrenToVisit.size > 0) { + const nextChildId = childrenToVisit.keys().next().value?.valueOf()!; + const nextChild = childrenToVisit.get(nextChildId)!; + childrenToVisit.delete(nextChildId); + newCursor.reset(nextChild.new); + oldCursor.reset(nextChild.old); + } } while (next); if (changedRanges.length === 0 && newTree.rootNode.hasChanges) { diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts index a1a3b86b8183..28f1a2f73e22 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts @@ -454,9 +454,9 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi if (previousPreviousTokenEndOffset !== intermediateTokenOffset) { endOffsetsAndScopes[tokenIndex - 1] = { endOffset: intermediateTokenOffset, scopes: endOffsetsAndScopes[tokenIndex - 1].scopes }; addCurrentTokenToArray(); - originalPreviousTokenScopes = endOffsetsAndScopes[tokenIndex - 2].scopes; + originalPreviousTokenScopes = [...endOffsetsAndScopes[tokenIndex - 2].scopes]; } else { - originalPreviousTokenScopes = endOffsetsAndScopes[tokenIndex - 1].scopes; + originalPreviousTokenScopes = [...endOffsetsAndScopes[tokenIndex - 1].scopes]; endOffsetsAndScopes[tokenIndex - 1] = { endOffset: lineRelativeOffset, scopes: [capture.name] }; } From 9c6630b8172511bd685b44842699912057b0cead Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 27 Jan 2025 12:03:06 +0100 Subject: [PATCH 0908/3587] Fix parsing a log entry with empty lines (#238832) --- .../output/common/outputChannelModel.ts | 8 +- .../test/browser/outputChannelModel.test.ts | 144 ++++++++++++++++++ 2 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 src/vs/workbench/contrib/output/test/browser/outputChannelModel.test.ts diff --git a/src/vs/workbench/contrib/output/common/outputChannelModel.ts b/src/vs/workbench/contrib/output/common/outputChannelModel.ts index a650e8e297c0..9b1fd05fb695 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModel.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModel.ts @@ -28,7 +28,7 @@ import { binarySearch, sortedDiff } from '../../../../base/common/arrays.js'; const LOG_ENTRY_REGEX = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s(\[(info|trace|debug|error|warning)\])\s(\[(.*?)\])?/; -function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntry | null { +export function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntry | null { const lineContent = model.getLineContent(lineNumber); const match = LOG_ENTRY_REGEX.exec(lineContent); if (match) { @@ -40,9 +40,11 @@ function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntry | nul const startLine = lineNumber; let endLine = lineNumber; - while (endLine < model.getLineCount()) { + const lineCount = model.getLineCount(); + while (endLine < lineCount) { const nextLineContent = model.getLineContent(endLine + 1); - if (model.getLineFirstNonWhitespaceColumn(endLine + 1) === 0 || LOG_ENTRY_REGEX.test(nextLineContent)) { + const isLastLine = endLine + 1 === lineCount && nextLineContent === ''; // Last line will be always empty + if (LOG_ENTRY_REGEX.test(nextLineContent) || isLastLine) { break; } endLine++; diff --git a/src/vs/workbench/contrib/output/test/browser/outputChannelModel.test.ts b/src/vs/workbench/contrib/output/test/browser/outputChannelModel.test.ts new file mode 100644 index 000000000000..94e98d079695 --- /dev/null +++ b/src/vs/workbench/contrib/output/test/browser/outputChannelModel.test.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { parseLogEntryAt } from '../../common/outputChannelModel.js'; +import { TextModel } from '../../../../../editor/common/model/textModel.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { LogLevel } from '../../../../../platform/log/common/log.js'; +import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; + +suite('Logs Parsing', () => { + + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + + setup(() => { + instantiationService = disposables.add(workbenchInstantiationService({}, disposables)); + }); + + test('should parse log entry with all components', () => { + const text = '2023-10-15 14:30:45.123 [info] [Git] Initializing repository'; + const model = createModel(text); + const entry = parseLogEntryAt(model, 1); + + assert.strictEqual(entry?.timestamp, new Date('2023-10-15 14:30:45.123').getTime()); + assert.strictEqual(entry?.logLevel, LogLevel.Info); + assert.strictEqual(entry?.category, 'Git'); + assert.strictEqual(model.getValueInRange(entry?.range), text); + }); + + test('should parse multi-line log entry', () => { + const text = [ + '2023-10-15 14:30:45.123 [error] [Extension] Failed with error:', + 'Error: Could not load extension', + ' at Object.load (/path/to/file:10:5)' + ].join('\n'); + const model = createModel(text); + const entry = parseLogEntryAt(model, 1); + + assert.strictEqual(entry?.timestamp, new Date('2023-10-15 14:30:45.123').getTime()); + assert.strictEqual(entry?.logLevel, LogLevel.Error); + assert.strictEqual(entry?.category, 'Extension'); + assert.strictEqual(model.getValueInRange(entry?.range), text); + }); + + test('should parse log entry without category', () => { + const text = '2023-10-15 14:30:45.123 [warning] System is running low on memory'; + const model = createModel(text); + const entry = parseLogEntryAt(model, 1); + + assert.strictEqual(entry?.timestamp, new Date('2023-10-15 14:30:45.123').getTime()); + assert.strictEqual(entry?.logLevel, LogLevel.Warning); + assert.strictEqual(entry?.category, undefined); + assert.strictEqual(model.getValueInRange(entry?.range), text); + }); + + test('should return null for invalid log entry', () => { + const model = createModel('Not a valid log entry'); + const entry = parseLogEntryAt(model, 1); + + assert.strictEqual(entry, null); + }); + + test('should parse all supported log levels', () => { + const levels = { + info: LogLevel.Info, + trace: LogLevel.Trace, + debug: LogLevel.Debug, + warning: LogLevel.Warning, + error: LogLevel.Error + }; + + for (const [levelText, expectedLevel] of Object.entries(levels)) { + const model = createModel(`2023-10-15 14:30:45.123 [${levelText}] Test message`); + const entry = parseLogEntryAt(model, 1); + assert.strictEqual(entry?.logLevel, expectedLevel, `Failed for log level: ${levelText}`); + } + }); + + test('should parse timestamp correctly', () => { + const timestamps = [ + '2023-01-01 00:00:00.000', + '2023-12-31 23:59:59.999', + '2023-06-15 12:30:45.500' + ]; + + for (const timestamp of timestamps) { + const model = createModel(`${timestamp} [info] Test message`); + const entry = parseLogEntryAt(model, 1); + assert.strictEqual(entry?.timestamp, new Date(timestamp).getTime(), `Failed for timestamp: ${timestamp}`); + } + }); + + test('should handle last line of file', () => { + const model = createModel([ + '2023-10-15 14:30:45.123 [info] First message', + '2023-10-15 14:30:45.124 [info] Last message', + '' + ].join('\n')); + + let actual = parseLogEntryAt(model, 1); + assert.strictEqual(actual?.timestamp, new Date('2023-10-15 14:30:45.123').getTime()); + assert.strictEqual(actual?.logLevel, LogLevel.Info); + assert.strictEqual(actual?.category, undefined); + assert.strictEqual(model.getValueInRange(actual?.range), '2023-10-15 14:30:45.123 [info] First message'); + + actual = parseLogEntryAt(model, 2); + assert.strictEqual(actual?.timestamp, new Date('2023-10-15 14:30:45.124').getTime()); + assert.strictEqual(actual?.logLevel, LogLevel.Info); + assert.strictEqual(actual?.category, undefined); + assert.strictEqual(model.getValueInRange(actual?.range), '2023-10-15 14:30:45.124 [info] Last message'); + + actual = parseLogEntryAt(model, 3); + assert.strictEqual(actual, null); + }); + + test('should parse multi-line log entry with empty lines', () => { + const text = [ + '2025-01-27 09:53:00.450 [info] Found with version <20.18.1>', + 'Now using node v20.18.1 (npm v10.8.2)', + '', + '> husky - npm run -s precommit', + '> husky - node v20.18.1', + '', + 'Reading git index versions...' + ].join('\n'); + const model = createModel(text); + const entry = parseLogEntryAt(model, 1); + + assert.strictEqual(entry?.timestamp, new Date('2025-01-27 09:53:00.450').getTime()); + assert.strictEqual(entry?.logLevel, LogLevel.Info); + assert.strictEqual(entry?.category, undefined); + assert.strictEqual(model.getValueInRange(entry?.range), text); + + }); + + function createModel(content: string): TextModel { + return disposables.add(instantiationService.createInstance(TextModel, content, 'log', TextModel.DEFAULT_CREATION_OPTIONS, null)); + } +}); From 07764cccb73f484d11d9b4b16814df868fa45234 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 27 Jan 2025 13:09:00 +0100 Subject: [PATCH 0909/3587] Introduce Application Machine Scope - fix #235371 (#238825) * fix #235371 * fix writing application machine setting * change security.restrictUNCAccess setting also to application machine scope --- .../common/configurationRegistry.ts | 15 +- src/vs/workbench/common/configuration.ts | 4 +- .../browser/preferencesRenderers.ts | 10 +- .../preferences/browser/settingsEditor2.ts | 4 +- .../preferences/browser/settingsTree.ts | 4 +- .../workspace/browser/workspaceTrustEditor.ts | 4 +- .../configuration/browser/configuration.ts | 4 +- .../browser/configurationService.ts | 21 +- .../configuration/common/configuration.ts | 4 +- .../common/configurationEditing.ts | 4 +- .../test/browser/configurationService.test.ts | 186 +++++++++++++++++- .../browser/settingsResource.ts | 2 +- 12 files changed, 225 insertions(+), 37 deletions(-) diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index 6d15e6732fc2..d2ba316398ee 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -126,13 +126,17 @@ export interface IConfigurationRegistry { export const enum ConfigurationScope { /** - * Application specific configuration, which can be configured only in local user settings. + * Application specific configuration, which can be configured only in default profile user settings. */ APPLICATION = 1, /** * Machine specific configuration, which can be configured only in local and remote user settings. */ MACHINE, + /** + * An application machine specific configuration, which can be configured only in default profile user settings and remote user settings. + */ + APPLICATION_MACHINE, /** * Window specific configuration, which can be configured in the user or workspace settings. */ @@ -269,6 +273,7 @@ export interface IConfigurationDefaultOverrideValue { export const allSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; export const applicationSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; +export const applicationMachineSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; export const machineSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; export const machineOverridableSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; export const windowSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; @@ -744,6 +749,9 @@ class ConfigurationRegistry implements IConfigurationRegistry { case ConfigurationScope.MACHINE: machineSettings.properties[key] = property; break; + case ConfigurationScope.APPLICATION_MACHINE: + applicationMachineSettings.properties[key] = property; + break; case ConfigurationScope.MACHINE_OVERRIDABLE: machineOverridableSettings.properties[key] = property; break; @@ -769,6 +777,9 @@ class ConfigurationRegistry implements IConfigurationRegistry { case ConfigurationScope.MACHINE: delete machineSettings.properties[key]; break; + case ConfigurationScope.APPLICATION_MACHINE: + delete applicationMachineSettings.properties[key]; + break; case ConfigurationScope.MACHINE_OVERRIDABLE: delete machineOverridableSettings.properties[key]; break; @@ -795,6 +806,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { this.updatePropertyDefaultValue(overrideIdentifierProperty, resourceLanguagePropertiesSchema); allSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; applicationSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; + applicationMachineSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; machineSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; machineOverridableSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; windowSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; @@ -811,6 +823,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { }; allSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema; applicationSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema; + applicationMachineSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema; machineSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema; machineOverridableSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema; windowSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema; diff --git a/src/vs/workbench/common/configuration.ts b/src/vs/workbench/common/configuration.ts index 9fb1e03a4ef8..46f42503e1c8 100644 --- a/src/vs/workbench/common/configuration.ts +++ b/src/vs/workbench/common/configuration.ts @@ -221,13 +221,13 @@ export class DynamicWorkbenchSecurityConfiguration extends Disposable implements }, 'default': [], 'markdownDescription': localize('security.allowedUNCHosts', 'A set of UNC host names (without leading or trailing backslash, for example `192.168.0.1` or `my-server`) to allow without user confirmation. If a UNC host is being accessed that is not allowed via this setting or has not been acknowledged via user confirmation, an error will occur and the operation stopped. A restart is required when changing this setting. Find out more about this setting at https://aka.ms/vscode-windows-unc.'), - 'scope': ConfigurationScope.MACHINE + 'scope': ConfigurationScope.APPLICATION_MACHINE }, 'security.restrictUNCAccess': { 'type': 'boolean', 'default': true, 'markdownDescription': localize('security.restrictUNCAccess', 'If enabled, only allows access to UNC host names that are allowed by the `#security.allowedUNCHosts#` setting or after user confirmation. Find out more about this setting at https://aka.ms/vscode-windows-unc.'), - 'scope': ConfigurationScope.MACHINE + 'scope': ConfigurationScope.APPLICATION_MACHINE } } }); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index 4c3a8c71dd67..ce8b04770065 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -40,7 +40,7 @@ import { IWorkspaceTrustManagementService } from '../../../../platform/workspace import { RangeHighlightDecorations } from '../../../browser/codeeditor.js'; import { settingsEditIcon } from './preferencesIcons.js'; import { EditPreferenceWidget } from './preferencesWidgets.js'; -import { APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; +import { APPLICATION_SCOPES, APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorModel, ISettingsGroup } from '../../../services/preferences/common/preferences.js'; import { DefaultSettingsEditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel } from '../../../services/preferences/common/preferencesModels.js'; @@ -623,7 +623,7 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc message: nls.localize('defaultProfileSettingWhileNonDefaultActive', "This setting cannot be applied while a non-default profile is active. It will be applied when the default profile is active.") }); } else if (isEqual(this.userDataProfileService.currentProfile.settingsResource, this.settingsEditorModel.uri)) { - if (configuration.scope === ConfigurationScope.APPLICATION) { + if (configuration.scope && APPLICATION_SCOPES.includes(configuration.scope)) { // If we're in a profile setting file, and the setting is application-scoped, fade it out. markerData.push(this.generateUnsupportedApplicationSettingMarker(setting)); } else if (this.configurationService.isSettingAppliedForAllProfiles(setting.key)) { @@ -637,7 +637,7 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc } } } - if (this.environmentService.remoteAuthority && (configuration.scope === ConfigurationScope.MACHINE || configuration.scope === ConfigurationScope.MACHINE_OVERRIDABLE)) { + if (this.environmentService.remoteAuthority && (configuration.scope === ConfigurationScope.MACHINE || configuration.scope === ConfigurationScope.APPLICATION_MACHINE || configuration.scope === ConfigurationScope.MACHINE_OVERRIDABLE)) { markerData.push({ severity: MarkerSeverity.Hint, tags: [MarkerTag.Unnecessary], @@ -654,7 +654,7 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc } private handleWorkspaceConfiguration(setting: ISetting, configuration: IConfigurationPropertySchema, markerData: IMarkerData[]): void { - if (configuration.scope === ConfigurationScope.APPLICATION) { + if (configuration.scope && APPLICATION_SCOPES.includes(configuration.scope)) { markerData.push(this.generateUnsupportedApplicationSettingMarker(setting)); } @@ -671,7 +671,7 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc } private handleWorkspaceFolderConfiguration(setting: ISetting, configuration: IConfigurationPropertySchema, markerData: IMarkerData[]): void { - if (configuration.scope === ConfigurationScope.APPLICATION) { + if (configuration.scope && APPLICATION_SCOPES.includes(configuration.scope)) { markerData.push(this.generateUnsupportedApplicationSettingMarker(setting)); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 101d7f4e5a2f..a8111d15aad6 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -50,7 +50,7 @@ import { Settings2EditorModel, nullRange } from '../../../services/preferences/c import { IUserDataSyncWorkbenchService } from '../../../services/userDataSync/common/userDataSync.js'; import { preferencesClearInputIcon, preferencesFilterIcon } from './preferencesIcons.js'; import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; -import { IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; +import { APPLICATION_SCOPES, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { Orientation, Sizing, SplitView } from '../../../../base/browser/ui/splitview/splitview.js'; @@ -792,7 +792,7 @@ export class SettingsEditor2 extends EditorPane { if (options?.revealSetting) { const configurationProperties = Registry.as(Extensions.Configuration).getConfigurationProperties(); const configurationScope = configurationProperties[options?.revealSetting.key]?.scope; - if (configurationScope === ConfigurationScope.APPLICATION) { + if (configurationScope && APPLICATION_SCOPES.includes(configurationScope)) { return this.preferencesService.openApplicationSettings(openOptions); } } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index e44793b308e2..8bf195e2440b 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -64,7 +64,7 @@ import { ISettingsEditorViewState, SettingsTreeElement, SettingsTreeGroupChild, import { ExcludeSettingWidget, IBoolObjectDataItem, IIncludeExcludeDataItem, IListDataItem, IObjectDataItem, IObjectEnumOption, IObjectKeySuggester, IObjectValueSuggester, IncludeSettingWidget, ListSettingWidget, ObjectSettingCheckboxWidget, ObjectSettingDropdownWidget, ObjectValue, SettingListEvent } from './settingsWidgets.js'; import { LANGUAGE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, compareTwoNullableNumbers } from '../common/preferences.js'; import { settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from '../common/settingsEditorColorRegistry.js'; -import { APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; +import { APPLICATION_SCOPES, APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { ISetting, ISettingsGroup, SETTINGS_AUTHORITY, SettingValueType } from '../../../services/preferences/common/preferences.js'; @@ -2207,7 +2207,7 @@ export class SettingTreeRenderers extends Disposable { private getActionsForSetting(setting: ISetting, settingTarget: SettingsTarget): IAction[] { const actions: IAction[] = []; - if (setting.scope !== ConfigurationScope.APPLICATION && settingTarget === ConfigurationTarget.USER_LOCAL) { + if (!(setting.scope && APPLICATION_SCOPES.includes(setting.scope)) && settingTarget === ConfigurationTarget.USER_LOCAL) { actions.push(this._instantiationService.createInstance(ApplySettingToAllProfilesAction, setting)); } if (this._userDataSyncEnablementService.isEnabled() && !setting.disallowSyncIgnore) { diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 474a87bd92eb..8d37002068d8 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -42,7 +42,7 @@ import { EditorPane } from '../../../browser/parts/editor/editorPane.js'; import { IEditorOpenContext } from '../../../common/editor.js'; import { debugIconStartForeground } from '../../debug/browser/debugColors.js'; import { IExtensionsWorkbenchService, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID } from '../../extensions/common/extensions.js'; -import { IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; +import { APPLICATION_SCOPES, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; import { IExtensionManifestPropertiesService } from '../../../services/extensions/common/extensionManifestPropertiesService.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { WorkspaceTrustEditorInput } from '../../../services/workspaces/browser/workspaceTrustEditorInput.js'; @@ -889,7 +889,7 @@ export class WorkspaceTrustEditor extends EditorPane { const property = configurationRegistry.getConfigurationProperties()[key]; // cannot be configured in workspace - if (property.scope === ConfigurationScope.APPLICATION || property.scope === ConfigurationScope.MACHINE) { + if (property.scope && (APPLICATION_SCOPES.includes(property.scope) || property.scope === ConfigurationScope.MACHINE)) { return false; } diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index 217084c9e9bb..a3d2d34f58c8 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -11,7 +11,7 @@ import { RunOnceScheduler } from '../../../../base/common/async.js'; import { FileChangeType, FileChangesEvent, IFileService, whenProviderRegistered, FileOperationError, FileOperationResult, FileOperation, FileOperationEvent } from '../../../../platform/files/common/files.js'; import { ConfigurationModel, ConfigurationModelParser, ConfigurationParseOptions, UserSettings } from '../../../../platform/configuration/common/configurationModels.js'; import { WorkspaceConfigurationModelParser, StandaloneConfigurationModelParser } from '../common/configurationModels.js'; -import { TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES, APPLY_ALL_PROFILES_SETTING } from '../common/configuration.js'; +import { TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES, APPLY_ALL_PROFILES_SETTING, APPLICATION_SCOPES } from '../common/configuration.js'; import { IStoredWorkspaceFolder } from '../../../../platform/workspaces/common/workspaces.js'; import { WorkbenchState, IWorkspaceFolder, IWorkspaceIdentifier } from '../../../../platform/workspace/common/workspace.js'; import { ConfigurationScope, Extensions, IConfigurationRegistry, OVERRIDE_PROPERTY_REGEX } from '../../../../platform/configuration/common/configurationRegistry.js'; @@ -133,7 +133,7 @@ export class ApplicationConfiguration extends UserSettings { uriIdentityService: IUriIdentityService, logService: ILogService, ) { - super(userDataProfilesService.defaultProfile.settingsResource, { scopes: [ConfigurationScope.APPLICATION], skipUnregistered: true }, uriIdentityService.extUri, fileService, logService); + super(userDataProfilesService.defaultProfile.settingsResource, { scopes: APPLICATION_SCOPES, skipUnregistered: true }, uriIdentityService.extUri, fileService, logService); this._register(this.onDidChange(() => this.reloadConfigurationScheduler.schedule())); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.loadConfiguration().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50)); } diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index b87b49442b5e..c56106ddd6a9 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -15,9 +15,9 @@ import { ConfigurationModel, ConfigurationChangeEvent, mergeChanges } from '../. import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides, isConfigurationOverrides, IConfigurationData, IConfigurationValue, IConfigurationChange, ConfigurationTargetToString, IConfigurationUpdateOverrides, isConfigurationUpdateOverrides, IConfigurationService, IConfigurationUpdateOptions } from '../../../../platform/configuration/common/configuration.js'; import { IPolicyConfiguration, NullPolicyConfiguration, PolicyConfiguration } from '../../../../platform/configuration/common/configurations.js'; import { Configuration } from '../common/configurationModels.js'; -import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService, RestrictedSettings, PROFILE_SCOPES, LOCAL_MACHINE_PROFILE_SCOPES, profileSettingsSchemaId, APPLY_ALL_PROFILES_SETTING } from '../common/configuration.js'; +import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService, RestrictedSettings, PROFILE_SCOPES, LOCAL_MACHINE_PROFILE_SCOPES, profileSettingsSchemaId, APPLY_ALL_PROFILES_SETTING, APPLICATION_SCOPES } from '../common/configuration.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; -import { IConfigurationRegistry, Extensions, allSettings, windowSettings, resourceSettings, applicationSettings, machineSettings, machineOverridableSettings, ConfigurationScope, IConfigurationPropertySchema, keyFromOverrideIdentifiers, OVERRIDE_PROPERTY_PATTERN, resourceLanguageSettingsSchemaId, configurationDefaultsSchemaId } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { IConfigurationRegistry, Extensions, allSettings, windowSettings, resourceSettings, applicationSettings, machineSettings, machineOverridableSettings, ConfigurationScope, IConfigurationPropertySchema, keyFromOverrideIdentifiers, OVERRIDE_PROPERTY_PATTERN, resourceLanguageSettingsSchemaId, configurationDefaultsSchemaId, applicationMachineSettings } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, getStoredWorkspaceFolder, toWorkspaceFolders } from '../../../../platform/workspaces/common/workspaces.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ConfigurationEditing, EditableConfigurationTarget } from '../common/configurationEditing.js'; @@ -49,9 +49,11 @@ import { mainWindow } from '../../../../base/browser/window.js'; import { runWhenWindowIdle } from '../../../../base/browser/dom.js'; function getLocalUserConfigurationScopes(userDataProfile: IUserDataProfile, hasRemote: boolean): ConfigurationScope[] | undefined { - return (userDataProfile.isDefault || userDataProfile.useDefaultFlags?.settings) - ? hasRemote ? LOCAL_MACHINE_SCOPES : undefined - : hasRemote ? LOCAL_MACHINE_PROFILE_SCOPES : PROFILE_SCOPES; + const isDefaultProfile = userDataProfile.isDefault || userDataProfile.useDefaultFlags?.settings; + if (isDefaultProfile) { + return hasRemote ? LOCAL_MACHINE_SCOPES : undefined; + } + return hasRemote ? LOCAL_MACHINE_PROFILE_SCOPES : PROFILE_SCOPES; } class Workspace extends BaseWorkspace { @@ -492,7 +494,8 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat } isSettingAppliedForAllProfiles(key: string): boolean { - if (this.configurationRegistry.getConfigurationProperties()[key]?.scope === ConfigurationScope.APPLICATION) { + const scope = this.configurationRegistry.getConfigurationProperties()[key]?.scope; + if (scope && APPLICATION_SCOPES.includes(scope)) { return true; } const allProfilesSettings = this.getValue(APPLY_ALL_PROFILES_SETTING) ?? []; @@ -780,7 +783,8 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat const configurationProperties = this.configurationRegistry.getConfigurationProperties(); const changedKeys: string[] = []; for (const changedKey of change.keys) { - if (configurationProperties[changedKey]?.scope === ConfigurationScope.APPLICATION) { + const scope = configurationProperties[changedKey]?.scope; + if (scope && APPLICATION_SCOPES.includes(scope)) { changedKeys.push(changedKey); if (changedKey === APPLY_ALL_PROFILES_SETTING) { for (const previousAllProfileSetting of previousAllProfilesSettings) { @@ -1124,7 +1128,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat if (target === ConfigurationTarget.USER) { if (this.remoteUserConfiguration) { const scope = this.configurationRegistry.getConfigurationProperties()[key]?.scope; - if (scope === ConfigurationScope.MACHINE || scope === ConfigurationScope.MACHINE_OVERRIDABLE) { + if (scope === ConfigurationScope.MACHINE || scope === ConfigurationScope.MACHINE_OVERRIDABLE || scope === ConfigurationScope.APPLICATION_MACHINE) { return EditableConfigurationTarget.USER_REMOTE; } if (this.inspect(key).userRemoteValue !== undefined) { @@ -1207,6 +1211,7 @@ class RegisterConfigurationSchemasContribution extends Disposable implements IWo const machineSettingsSchema: IJSONSchema = { properties: Object.assign({}, + applicationMachineSettings.properties, machineSettings.properties, machineOverridableSettings.properties, windowSettings.properties, diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index a97808cda182..12e011d39777 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -24,11 +24,11 @@ export const folderSettingsSchemaId = 'vscode://schemas/settings/folder'; export const launchSchemaId = 'vscode://schemas/launch'; export const tasksSchemaId = 'vscode://schemas/tasks'; -export const APPLICATION_SCOPES = [ConfigurationScope.APPLICATION]; +export const APPLICATION_SCOPES = [ConfigurationScope.APPLICATION, ConfigurationScope.APPLICATION_MACHINE]; export const PROFILE_SCOPES = [ConfigurationScope.MACHINE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE, ConfigurationScope.MACHINE_OVERRIDABLE]; export const LOCAL_MACHINE_PROFILE_SCOPES = [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE]; export const LOCAL_MACHINE_SCOPES = [ConfigurationScope.APPLICATION, ...LOCAL_MACHINE_PROFILE_SCOPES]; -export const REMOTE_MACHINE_SCOPES = [ConfigurationScope.MACHINE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE, ConfigurationScope.MACHINE_OVERRIDABLE]; +export const REMOTE_MACHINE_SCOPES = [ConfigurationScope.MACHINE, ConfigurationScope.APPLICATION_MACHINE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE, ConfigurationScope.MACHINE_OVERRIDABLE]; export const WORKSPACE_SCOPES = [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE, ConfigurationScope.MACHINE_OVERRIDABLE]; export const FOLDER_SCOPES = [ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE, ConfigurationScope.MACHINE_OVERRIDABLE]; diff --git a/src/vs/workbench/services/configuration/common/configurationEditing.ts b/src/vs/workbench/services/configuration/common/configurationEditing.ts index 31864ea3105b..150c993d79d4 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditing.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditing.ts @@ -13,7 +13,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; import { ITextFileService } from '../../textfile/common/textfiles.js'; import { IConfigurationUpdateOptions, IConfigurationUpdateOverrides } from '../../../../platform/configuration/common/configuration.js'; -import { FOLDER_SETTINGS_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY, USER_STANDALONE_CONFIGURATIONS, TASKS_DEFAULT, FOLDER_SCOPES, IWorkbenchConfigurationService } from './configuration.js'; +import { FOLDER_SETTINGS_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY, USER_STANDALONE_CONFIGURATIONS, TASKS_DEFAULT, FOLDER_SCOPES, IWorkbenchConfigurationService, APPLICATION_SCOPES } from './configuration.js'; import { FileOperationError, FileOperationResult, IFileService } from '../../../../platform/files/common/files.js'; import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, keyFromOverrideIdentifiers, OVERRIDE_PROPERTY_REGEX } from '../../../../platform/configuration/common/configurationRegistry.js'; @@ -519,7 +519,7 @@ export class ConfigurationEditing { if (target === EditableConfigurationTarget.WORKSPACE) { if (!operation.workspaceStandAloneConfigurationKey && !OVERRIDE_PROPERTY_REGEX.test(operation.key)) { - if (configurationScope === ConfigurationScope.APPLICATION) { + if (configurationScope && APPLICATION_SCOPES.includes(configurationScope)) { throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION, target, operation); } if (configurationScope === ConfigurationScope.MACHINE) { diff --git a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts index eacc1bacd4a4..cd3b770a0d1c 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -751,6 +751,11 @@ suite('WorkspaceConfigurationService - Folder', () => { 'default': 'isSet', scope: ConfigurationScope.MACHINE }, + 'configurationService.folder.applicationMachineSetting': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION_MACHINE + }, 'configurationService.folder.machineOverridableSetting': { 'type': 'string', 'default': 'isSet', @@ -840,6 +845,7 @@ suite('WorkspaceConfigurationService - Folder', () => { 'folder': { 'applicationSetting': 'isSet', 'machineSetting': 'isSet', + 'applicationMachineSetting': 'isSet', 'machineOverridableSetting': 'isSet', 'testSetting': 'isSet', 'languageSetting': 'isSet', @@ -953,7 +959,25 @@ suite('WorkspaceConfigurationService - Folder', () => { assert.strictEqual(testObject.getValue('configurationService.folder.machineSetting', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); })); - test('get application scope settings are not loaded after defaults are registered', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + test('application machine overridable settings are not read from workspace', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationMachineSetting": "userValue" }')); + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.applicationMachineSetting": "workspaceValue" }')); + + await testObject.reloadConfiguration(); + + assert.strictEqual(testObject.getValue('configurationService.folder.applicationMachineSetting'), 'userValue'); + })); + + test('application machine overridable settings are not read from workspace when workspace folder uri is passed', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationMachineSetting": "userValue" }')); + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.applicationMachineSetting": "workspaceValue" }')); + + await testObject.reloadConfiguration(); + + assert.strictEqual(testObject.getValue('configurationService.folder.applicationMachineSetting', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); + })); + + test('get application scope settings are loaded after defaults are registered', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting-2": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.applicationSetting-2": "workspaceValue" }')); @@ -1003,6 +1027,56 @@ suite('WorkspaceConfigurationService - Folder', () => { assert.strictEqual(testObject.getValue('configurationService.folder.applicationSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); })); + test('get application machine overridable scope settings are loaded after defaults are registered', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationMachineSetting-2": "userValue" }')); + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.applicationMachineSetting-2": "workspaceValue" }')); + + await testObject.reloadConfiguration(); + assert.strictEqual(testObject.getValue('configurationService.folder.applicationMachineSetting-2'), 'workspaceValue'); + + configurationRegistry.registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.folder.applicationMachineSetting-2': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION + } + } + }); + + assert.strictEqual(testObject.getValue('configurationService.folder.applicationMachineSetting-2'), 'userValue'); + + await testObject.reloadConfiguration(); + assert.strictEqual(testObject.getValue('configurationService.folder.applicationMachineSetting-2'), 'userValue'); + })); + + test('get application machine overridable scope settings are loaded after defaults are registered when workspace folder uri is passed', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationMachineSetting-3": "userValue" }')); + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.applicationMachineSetting-3": "workspaceValue" }')); + + await testObject.reloadConfiguration(); + assert.strictEqual(testObject.getValue('configurationService.folder.applicationMachineSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'workspaceValue'); + + configurationRegistry.registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.folder.applicationMachineSetting-3': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION + } + } + }); + + assert.strictEqual(testObject.getValue('configurationService.folder.applicationMachineSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); + + await testObject.reloadConfiguration(); + assert.strictEqual(testObject.getValue('configurationService.folder.applicationMachineSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); + })); + test('get machine scope settings are not loaded after defaults are registered', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.machineSetting-2": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.machineSetting-2": "workspaceValue" }')); @@ -1366,6 +1440,11 @@ suite('WorkspaceConfigurationService - Folder', () => { .then(() => assert.fail('Should not be supported'), (e) => assert.strictEqual(e.code, ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION)); }); + test('update application machine overridable setting into workspace configuration in a workspace is not supported', () => { + return testObject.updateValue('configurationService.folder.applicationMachineSetting', 'workspaceValue', {}, ConfigurationTarget.WORKSPACE, { donotNotifyError: true }) + .then(() => assert.fail('Should not be supported'), (e) => assert.strictEqual(e.code, ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION)); + }); + test('update machine setting into workspace configuration in a workspace is not supported', () => { return testObject.updateValue('configurationService.folder.machineSetting', 'workspaceValue', {}, ConfigurationTarget.WORKSPACE, { donotNotifyError: true }) .then(() => assert.fail('Should not be supported'), (e) => assert.strictEqual(e.code, ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_MACHINE)); @@ -1620,6 +1699,11 @@ suite('WorkspaceConfigurationService - Profiles', () => { 'default': 'isSet', scope: ConfigurationScope.APPLICATION }, + 'configurationService.profiles.applicationMachineSetting': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION_MACHINE + }, 'configurationService.profiles.testSetting': { 'type': 'string', 'default': 'isSet', @@ -1726,6 +1810,13 @@ suite('WorkspaceConfigurationService - Profiles', () => { assert.strictEqual(testObject.getValue('configurationService.profiles.applicationSetting'), 'applicationValue'); })); + test('update application machine setting', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await testObject.updateValue('configurationService.profiles.applicationMachineSetting', 'applicationValue'); + + assert.deepStrictEqual(JSON.parse((await fileService.readFile(instantiationService.get(IUserDataProfilesService).defaultProfile.settingsResource)).value.toString()), { 'configurationService.profiles.applicationMachineSetting': 'applicationValue', 'configurationService.profiles.applicationSetting2': 'applicationValue', 'configurationService.profiles.testSetting2': 'userValue' }); + assert.strictEqual(testObject.getValue('configurationService.profiles.applicationMachineSetting'), 'applicationValue'); + })); + test('update normal setting', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await testObject.updateValue('configurationService.profiles.testSetting', 'profileValue'); @@ -2039,21 +2130,21 @@ suite('WorkspaceConfigurationService-Multiroot', () => { }); test('application settings are not read from workspace', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.applicationSetting": "userValue" }')); await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.applicationSetting': 'workspaceValue' } }], true); await testObject.reloadConfiguration(); - assert.strictEqual(testObject.getValue('configurationService.folder.applicationSetting'), 'userValue'); + assert.strictEqual(testObject.getValue('configurationService.workspace.applicationSetting'), 'userValue'); })); test('application settings are not read from workspace when folder is passed', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.applicationSetting": "userValue" }')); await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.applicationSetting': 'workspaceValue' } }], true); await testObject.reloadConfiguration(); - assert.strictEqual(testObject.getValue('configurationService.folder.applicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); + assert.strictEqual(testObject.getValue('configurationService.workspace.applicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue'); })); test('machine settings are not read from workspace', () => runWithFakedTimers({ useFakeTimers: true }, async () => { @@ -2721,6 +2812,11 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { 'default': 'isSet', scope: ConfigurationScope.MACHINE }, + 'configurationService.remote.applicationMachineSetting': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION_MACHINE + }, 'configurationService.remote.machineOverridableSetting': { 'type': 'string', 'default': 'isSet', @@ -2785,7 +2881,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { })); } - test('remote settings override globals', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + test('remote machine settings override globals', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await fileService.writeFile(machineSettingsResource, VSBuffer.fromString('{ "configurationService.remote.machineSetting": "remoteValue" }')); registerRemoteFileSystemProvider(); resolveRemoteEnvironment(); @@ -2793,7 +2889,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { assert.strictEqual(testObject.getValue('configurationService.remote.machineSetting'), 'remoteValue'); })); - test('remote settings override globals after remote provider is registered on activation', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + test('remote machine settings override globals after remote provider is registered on activation', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await fileService.writeFile(machineSettingsResource, VSBuffer.fromString('{ "configurationService.remote.machineSetting": "remoteValue" }')); resolveRemoteEnvironment(); registerRemoteFileSystemProviderOnActivation(); @@ -2801,7 +2897,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { assert.strictEqual(testObject.getValue('configurationService.remote.machineSetting'), 'remoteValue'); })); - test('remote settings override globals after remote environment is resolved', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + test('remote machine settings override globals after remote environment is resolved', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await fileService.writeFile(machineSettingsResource, VSBuffer.fromString('{ "configurationService.remote.machineSetting": "remoteValue" }')); registerRemoteFileSystemProvider(); await initialize(); @@ -2849,6 +2945,70 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { assert.strictEqual(testObject.getValue('configurationService.remote.machineSetting'), 'isSet'); })); + test('remote application machine settings override globals', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(machineSettingsResource, VSBuffer.fromString('{ "configurationService.remote.applicationMachineSetting": "remoteValue" }')); + registerRemoteFileSystemProvider(); + resolveRemoteEnvironment(); + await initialize(); + assert.strictEqual(testObject.getValue('configurationService.remote.applicationMachineSetting'), 'remoteValue'); + })); + + test('remote application machine settings override globals after remote provider is registered on activation', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(machineSettingsResource, VSBuffer.fromString('{ "configurationService.remote.applicationMachineSetting": "remoteValue" }')); + resolveRemoteEnvironment(); + registerRemoteFileSystemProviderOnActivation(); + await initialize(); + assert.strictEqual(testObject.getValue('configurationService.remote.applicationMachineSetting'), 'remoteValue'); + })); + + test('remote application machine settings override globals after remote environment is resolved', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(machineSettingsResource, VSBuffer.fromString('{ "configurationService.remote.applicationMachineSetting": "remoteValue" }')); + registerRemoteFileSystemProvider(); + await initialize(); + const promise = new Promise((c, e) => { + disposables.add(testObject.onDidChangeConfiguration(event => { + try { + assert.strictEqual(event.source, ConfigurationTarget.USER); + assert.deepStrictEqual([...event.affectedKeys], ['configurationService.remote.applicationMachineSetting']); + assert.strictEqual(testObject.getValue('configurationService.remote.applicationMachineSetting'), 'remoteValue'); + c(); + } catch (error) { + e(error); + } + })); + }); + resolveRemoteEnvironment(); + return promise; + })); + + test('remote application machine settings override globals after remote provider is registered on activation and remote environment is resolved', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(machineSettingsResource, VSBuffer.fromString('{ "configurationService.remote.applicationMachineSetting": "remoteValue" }')); + registerRemoteFileSystemProviderOnActivation(); + await initialize(); + const promise = new Promise((c, e) => { + disposables.add(testObject.onDidChangeConfiguration(event => { + try { + assert.strictEqual(event.source, ConfigurationTarget.USER); + assert.deepStrictEqual([...event.affectedKeys], ['configurationService.remote.applicationMachineSetting']); + assert.strictEqual(testObject.getValue('configurationService.remote.applicationMachineSetting'), 'remoteValue'); + c(); + } catch (error) { + e(error); + } + })); + }); + resolveRemoteEnvironment(); + return promise; + })); + + test('application machine settings in local user settings does not override defaults', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.remote.applicationMachineSetting": "globalValue" }')); + registerRemoteFileSystemProvider(); + resolveRemoteEnvironment(); + await initialize(); + assert.strictEqual(testObject.getValue('configurationService.remote.applicationMachineSetting'), 'isSet'); + })); + test('machine overridable settings in local user settings does not override defaults', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.remote.machineOverridableSetting": "globalValue" }')); registerRemoteFileSystemProvider(); @@ -2875,6 +3035,16 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { assert.strictEqual(testObject.inspect('configurationService.remote.machineSetting').userRemoteValue, 'machineValue'); })); + test('application machine setting is written in remote settings', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + registerRemoteFileSystemProvider(); + resolveRemoteEnvironment(); + await initialize(); + await testObject.updateValue('configurationService.remote.applicationMachineSetting', 'machineValue'); + await testObject.reloadConfiguration(); + const actual = testObject.inspect('configurationService.remote.applicationMachineSetting'); + assert.strictEqual(actual.userRemoteValue, 'machineValue'); + })); + test('machine overridable setting is written in remote settings', () => runWithFakedTimers({ useFakeTimers: true }, async () => { registerRemoteFileSystemProvider(); resolveRemoteEnvironment(); diff --git a/src/vs/workbench/services/userDataProfile/browser/settingsResource.ts b/src/vs/workbench/services/userDataProfile/browser/settingsResource.ts index e268a0b1beb7..fdb412bdd230 100644 --- a/src/vs/workbench/services/userDataProfile/browser/settingsResource.ts +++ b/src/vs/workbench/services/userDataProfile/browser/settingsResource.ts @@ -81,7 +81,7 @@ export class SettingsResource implements IProfileResource { private getIgnoredSettings(): string[] { const allSettings = Registry.as(Extensions.Configuration).getConfigurationProperties(); - const ignoredSettings = Object.keys(allSettings).filter(key => allSettings[key]?.scope === ConfigurationScope.MACHINE || allSettings[key]?.scope === ConfigurationScope.MACHINE_OVERRIDABLE); + const ignoredSettings = Object.keys(allSettings).filter(key => allSettings[key]?.scope === ConfigurationScope.MACHINE || allSettings[key]?.scope === ConfigurationScope.APPLICATION_MACHINE || allSettings[key]?.scope === ConfigurationScope.MACHINE_OVERRIDABLE); return ignoredSettings; } From cd3c0ac45b02a2b3bfd29576252fcf6338bee070 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 27 Jan 2025 13:18:52 +0100 Subject: [PATCH 0910/3587] Test failure: workspace.applyEdit drops the TextEdit if there is a RenameFile later #77735 (with opened editor) (fix #238837) (#238838) --- .../vscode-api-tests/src/singlefolder-tests/workspace.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index b871093df39b..90baf6d28702 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -924,7 +924,8 @@ suite('vscode API - workspace', () => { } }); - test('workspace.applyEdit drops the TextEdit if there is a RenameFile later #77735 (with opened editor)', async function () { + // TODO: below test is flaky and commented out, see https://github.com/microsoft/vscode/issues/238837 + test.skip('workspace.applyEdit drops the TextEdit if there is a RenameFile later #77735 (with opened editor)', async function () { await test77735(true); }); From bd434478462b130de4571e65af87f786d16028e6 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 27 Jan 2025 13:50:24 +0100 Subject: [PATCH 0911/3587] Update default and public settings for inline suggestions (#238836) update default and public settings --- src/vs/editor/common/config/editorOptions.ts | 105 +++++++++--------- .../browser/model/inlineCompletionsModel.ts | 2 +- .../view/inlineEdits/sideBySideDiff.ts | 2 +- .../browser/view/inlineEdits/view.ts | 34 +++--- .../view/inlineEdits/wordReplacementView.ts | 2 +- src/vs/monaco.d.ts | 10 -- 6 files changed, 73 insertions(+), 82 deletions(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index fac74c6535f4..89d37bb27ee9 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -4211,15 +4211,28 @@ export interface IInlineSuggestOptions { fontFamily?: string | 'default'; edits?: { - experimental?: { - enabled?: boolean; - useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; - useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; - useCodeOverlay?: 'never' | 'whenPossible' | 'moveCodeWhenPossible'; - useMultiLineGhostText?: boolean; - - useGutterIndicator?: boolean; - }; + codeShifting?: boolean; + + /** + * @internal + */ + enabled?: boolean; + /** + * @internal + */ + useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; + /** + * @internal + */ + useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; + /** + * @internal + */ + useMultiLineGhostText?: boolean; + /** + * @internal + */ + useGutterIndicator?: boolean; }; } @@ -4246,14 +4259,12 @@ class InlineEditorSuggest extends BaseEditorOption v.preview); private readonly _suggestPreviewMode = this._editorObs.getOption(EditorOption.suggest).map(v => v.previewMode); private readonly _inlineSuggestMode = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => v.mode); - private readonly _inlineEditsEnabled = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !!v.edits.experimental?.enabled); + private readonly _inlineEditsEnabled = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !!v.edits.enabled); constructor( public readonly textModel: ITextModel, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index 60ada9c8e7f7..1abc72a5a423 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -206,7 +206,7 @@ export class InlineEditsSideBySideDiff extends Disposable implements IInlineEdit private readonly toolbarRef = n.ref(); private readonly _editorContainer = n.div({ - class: ['editorContainer', this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !v.edits.experimental.useGutterIndicator && 'showHover')], + class: ['editorContainer', this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !v.edits.useGutterIndicator && 'showHover')], style: { position: 'absolute', overflow: 'hidden' }, }, [ n.div({ class: 'preview', style: {}, ref: this.previewRef }), diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 3338ce7fb65d..80e8cc5be9fa 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -31,10 +31,10 @@ import { LineReplacementView, WordInsertView, WordReplacementView } from './word export class InlineEditsView extends Disposable { private readonly _editorObs = observableCodeEditor(this._editor); - private readonly _useMixedLinesDiff = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useMixedLinesDiff); - private readonly _useInterleavedLinesDiff = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useInterleavedLinesDiff); - private readonly _useCodeOverlay = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useCodeOverlay); - private readonly _useMultiLineGhostText = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useMultiLineGhostText); + private readonly _useMixedLinesDiff = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.useMixedLinesDiff); + private readonly _useInterleavedLinesDiff = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.useInterleavedLinesDiff); + private readonly _useCodeShifting = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.codeShifting); + private readonly _useMultiLineGhostText = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.useMultiLineGhostText); private _previousView: { id: string; @@ -188,7 +188,7 @@ export class InlineEditsView extends Disposable { return store.add(this._instantiationService.createInstance(LineReplacementView, this._editorObs, e.originalRange, e.modifiedRange, e.modifiedLines, e.replacements)); }).recomputeInitiallyAndOnChange(this._store); - private readonly _useGutterIndicator = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useGutterIndicator); + private readonly _useGutterIndicator = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.useGutterIndicator); private readonly _inlineEditsIsHovered = derived(this, reader => { return this._sideBySide.isHovered.read(reader) @@ -233,8 +233,8 @@ export class InlineEditsView extends Disposable { ); const reconsiderViewEditorWidthChange = this._previousView?.editorWidth !== this._editor.getLayoutInfo().width && ( - (this._previousView?.view === 'sideBySide' && this._useCodeOverlay.read(reader) !== 'never') || - (this._previousView?.view === 'lineReplacement') + this._previousView?.view === 'sideBySide' || + this._previousView?.view === 'lineReplacement' ); if (canUseCache && !reconsiderViewAfterJump && !reconsiderViewEditorWidthChange) { @@ -252,6 +252,7 @@ export class InlineEditsView extends Disposable { if ( isSingleInnerEdit && ( this._useMixedLinesDiff.read(reader) === 'forStableInsertions' + && this._useCodeShifting.read(reader) && isSingleLineInsertionAfterPosition(diff, edit.cursorPosition) || isSingleLineDeletion(diff) ) @@ -263,20 +264,17 @@ export class InlineEditsView extends Disposable { return 'deletion'; } - if (isSingleMultiLineInsertion(diff) && this._useMultiLineGhostText.read(reader)) { + if (isSingleMultiLineInsertion(diff) && this._useMultiLineGhostText.read(reader) && this._useCodeShifting.read(reader)) { return 'insertionMultiLine'; } - const useCodeOverlay = this._useCodeOverlay.read(reader); - if (useCodeOverlay !== 'never') { - const numOriginalLines = edit.originalLineRange.length; - const numModifiedLines = edit.modifiedLineRange.length; - const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); - if (allInnerChangesNotTooLong && isSingleInnerEdit && numOriginalLines === 1 && numModifiedLines === 1) { - return 'wordReplacements'; - } else if (numOriginalLines > 0 && numModifiedLines > 0 && !InlineEditsSideBySideDiff.fitsInsideViewport(this._editor, edit, reader)) { - return 'lineReplacement'; - } + const numOriginalLines = edit.originalLineRange.length; + const numModifiedLines = edit.modifiedLineRange.length; + const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); + if (allInnerChangesNotTooLong && isSingleInnerEdit && numOriginalLines === 1 && numModifiedLines === 1) { + return 'wordReplacements'; + } else if (numOriginalLines > 0 && numModifiedLines > 0 && !InlineEditsSideBySideDiff.fitsInsideViewport(this._editor, edit, reader)) { + return 'lineReplacement'; } if ( diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index 7b8bbd60ac3c..df87b2fcae21 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -366,7 +366,7 @@ export class LineReplacementView extends Disposable implements IInlineEditsView const lowerText = new Rect(lowerBackground.left + PADDING, lowerBackground.top + PADDING, lowerBackground.right, lowerBackground.bottom); // Add ViewZone if needed - const shouldShowViewZone = this._editor.editor.getOption(EditorOption.inlineSuggest).edits.experimental.useCodeOverlay === 'moveCodeWhenPossible'; + const shouldShowViewZone = this._editor.editor.getOption(EditorOption.inlineSuggest).edits.codeShifting; if (shouldShowViewZone) { const viewZoneHeight = lowerBackground.height + 2 * PADDING; const viewZoneLineNumber = this._originalRange.endLineNumberExclusive; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index f64b9dda5d55..1968fac5a562 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4604,16 +4604,6 @@ declare namespace monaco.editor { * Font family for inline suggestions. */ fontFamily?: string | 'default'; - edits?: { - experimental?: { - enabled?: boolean; - useMixedLinesDiff?: 'never' | 'whenPossible' | 'forStableInsertions' | 'afterJumpWhenPossible'; - useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; - useCodeOverlay?: 'never' | 'whenPossible' | 'moveCodeWhenPossible'; - useMultiLineGhostText?: boolean; - useGutterIndicator?: boolean; - }; - }; } type RequiredRecursive = { From eb709b8271e14c26949d90aa2c930d421a4f5f5e Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 27 Jan 2025 14:38:03 +0100 Subject: [PATCH 0912/3587] tweak edits progress messages (#238843) * upper-case edits https://github.com/microsoft/vscode-copilot/issues/11686 * remove window progress when doing edits requests https://github.com/microsoft/vscode-copilot/issues/11686 --- .../browser/chatEditing/chatEditingService.ts | 17 +++++------------ .../contrib/chat/browser/chatEditorOverlay.ts | 4 ++-- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts index 266dcfbf9efd..c65de493065e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts @@ -21,14 +21,13 @@ import { isString } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; -import { localize, localize2 } from '../../../../../nls.js'; +import { localize } from '../../../../../nls.js'; import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { bindContextKey } from '../../../../../platform/observable/common/platformObservableUtils.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; -import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; import { IDecorationData, IDecorationsProvider, IDecorationsService } from '../../../../services/decorations/common/decorations.js'; @@ -98,7 +97,6 @@ export class ChatEditingService extends Disposable implements IChatEditingServic @ITextModelService textModelService: ITextModelService, @IContextKeyService contextKeyService: IContextKeyService, @IChatService private readonly _chatService: IChatService, - @IProgressService private readonly _progressService: IProgressService, @IEditorService private readonly _editorService: IEditorService, @IDecorationsService decorationsService: IDecorationsService, @IFileService private readonly _fileService: IFileService, @@ -351,6 +349,9 @@ export class ChatEditingService extends Disposable implements IChatEditingServic editsPromise = this._continueEditingSession(session, async (builder, token) => { for await (const item of editsSource!.asyncIterable) { + if (responseModel.isCanceled) { + break; + } if (token.isCancellationRequested) { break; } @@ -412,15 +413,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic const cancellationTokenSource = new CancellationTokenSource(); this._currentAutoApplyOperationObs.set(cancellationTokenSource, undefined); try { - await this._progressService.withProgress({ - location: ProgressLocation.Window, - title: localize2('chatEditing.startingSession', 'Generating edits...').value, - }, async () => { - await builder(stream, cancellationTokenSource.token); - }, - () => cancellationTokenSource.cancel() - ); - + await builder(stream, cancellationTokenSource.token); } finally { cancellationTokenSource.dispose(); this._currentAutoApplyOperationObs.set(null, undefined); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index 347b2697d90e..099eb88133e8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -257,8 +257,8 @@ class ChatEditorOverlayWidget implements IOverlayWidget { this._showStore.add(autorun(r => { const value = activeEntry.rewriteRatio.read(r); reset(this._progressNode, (value === 0 - ? localize('generating', "Generating edits") - : localize('applyingPercentage', "{0}% Applying edits", Math.round(value * 100)))); + ? localize('generating', "Generating Edits") + : localize('applyingPercentage', "{0}% Applying Edits", Math.round(value * 100)))); })); this._showStore.add(autorun(r => { From e748276861dc1e139547638eeed4300f680bfd1d Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 27 Jan 2025 15:00:53 +0100 Subject: [PATCH 0913/3587] EditContext: Make sure edit context is up to date before using it (#238841) make sure edit context is up to date before using --- .../editContext/native/nativeEditContext.ts | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 472a6420eba7..537c23fec5fd 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -45,13 +45,13 @@ export class NativeEditContext extends AbstractEditContext { public readonly domNode: FastDomNode; private readonly _editContext: EditContext; private readonly _screenReaderSupport: ScreenReaderSupport; + private _editContextPrimarySelection: Selection = new Selection(1, 1, 1, 1); // Overflow guard container private _parent: HTMLElement | undefined; private _decorations: string[] = []; private _primarySelection: Selection = new Selection(1, 1, 1, 1); - private _textStartPositionWithinEditor: Position = new Position(1, 1); private _targetWindowId: number = -1; private _scrollTop: number = 0; @@ -296,15 +296,19 @@ export class NativeEditContext extends AbstractEditContext { } this._editContext.updateText(0, Number.MAX_SAFE_INTEGER, editContextState.text); this._editContext.updateSelection(editContextState.selectionStartOffset, editContextState.selectionEndOffset); - this._textStartPositionWithinEditor = editContextState.textStartPositionWithinEditor; + this._editContextPrimarySelection = editContextState.editContextPrimarySelection; } private _emitTypeEvent(viewController: ViewController, e: TextUpdateEvent): void { if (!this._editContext) { return; } + if (!this._editContextPrimarySelection.equalsSelection(this._primarySelection)) { + this._updateEditContext(); + } const model = this._context.viewModel.model; - const offsetOfStartOfText = model.getOffsetAt(this._textStartPositionWithinEditor); + const startPositionOfEditContext = this._editContextStartPosition(); + const offsetOfStartOfText = model.getOffsetAt(startPositionOfEditContext); const offsetOfSelectionEnd = model.getOffsetAt(this._primarySelection.getEndPosition()); const offsetOfSelectionStart = model.getOffsetAt(this._primarySelection.getStartPosition()); const selectionEndOffset = offsetOfSelectionEnd - offsetOfStartOfText; @@ -352,37 +356,41 @@ export class NativeEditContext extends AbstractEditContext { } } - private _getNewEditContextState(): { text: string; selectionStartOffset: number; selectionEndOffset: number; textStartPositionWithinEditor: Position } | undefined { + private _getNewEditContextState(): { text: string; selectionStartOffset: number; selectionEndOffset: number; editContextPrimarySelection: Selection } | undefined { + const editContextPrimarySelection = this._primarySelection; const model = this._context.viewModel.model; - if (!model.isValidRange(this._primarySelection)) { + if (!model.isValidRange(editContextPrimarySelection)) { return; } - const primarySelectionStartLine = this._primarySelection.startLineNumber; - const primarySelectionEndLine = this._primarySelection.endLineNumber; + const primarySelectionStartLine = editContextPrimarySelection.startLineNumber; + const primarySelectionEndLine = editContextPrimarySelection.endLineNumber; const endColumnOfEndLineNumber = model.getLineMaxColumn(primarySelectionEndLine); const rangeOfText = new Range(primarySelectionStartLine, 1, primarySelectionEndLine, endColumnOfEndLineNumber); const text = model.getValueInRange(rangeOfText, EndOfLinePreference.TextDefined); - const selectionStartOffset = this._primarySelection.startColumn - 1; - const selectionEndOffset = text.length + this._primarySelection.endColumn - endColumnOfEndLineNumber; - const textStartPositionWithinEditor = rangeOfText.getStartPosition(); + const selectionStartOffset = editContextPrimarySelection.startColumn - 1; + const selectionEndOffset = text.length + editContextPrimarySelection.endColumn - endColumnOfEndLineNumber; return { text, selectionStartOffset, selectionEndOffset, - textStartPositionWithinEditor + editContextPrimarySelection }; } + private _editContextStartPosition(): Position { + return new Position(this._editContextPrimarySelection.startLineNumber, 1); + } + private _handleTextFormatUpdate(e: TextFormatUpdateEvent): void { if (!this._editContext) { return; } const formats = e.getTextFormats(); - const textStartPositionWithinEditor = this._textStartPositionWithinEditor; + const editContextStartPosition = this._editContextStartPosition(); const decorations: IModelDeltaDecoration[] = []; formats.forEach(f => { const textModel = this._context.viewModel.model; - const offsetOfEditContextText = textModel.getOffsetAt(textStartPositionWithinEditor); + const offsetOfEditContextText = textModel.getOffsetAt(editContextStartPosition); const startPositionOfDecoration = textModel.getPositionAt(offsetOfEditContextText + f.rangeStart); const endPositionOfDecoration = textModel.getPositionAt(offsetOfEditContextText + f.rangeEnd); const decorationRange = Range.fromPositions(startPositionOfDecoration, endPositionOfDecoration); @@ -453,7 +461,7 @@ export class NativeEditContext extends AbstractEditContext { const offsetTransformer = new PositionOffsetTransformer(this._editContext.text); for (let offset = e.rangeStart; offset < e.rangeEnd; offset++) { const editContextStartPosition = offsetTransformer.getPosition(offset); - const textStartLineOffsetWithinEditor = this._textStartPositionWithinEditor.lineNumber - 1; + const textStartLineOffsetWithinEditor = this._editContextPrimarySelection.startLineNumber - 1; const characterStartPosition = new Position(textStartLineOffsetWithinEditor + editContextStartPosition.lineNumber, editContextStartPosition.column); const characterEndPosition = characterStartPosition.delta(0, 1); const characterModelRange = Range.fromPositions(characterStartPosition, characterEndPosition); From 76f0616e9f5d4d38e529c347822fa32bcdeac39a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 27 Jan 2025 15:47:47 +0100 Subject: [PATCH 0914/3587] editors - advise users to check logs for details (fix microsoft/vscode-remote-release#10663) (#238851) --- src/vs/workbench/browser/parts/editor/editorPlaceholder.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts index 6c100075472d..15d87f9ad15d 100644 --- a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts +++ b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts @@ -5,7 +5,7 @@ import './media/editorplaceholder.css'; import { localize } from '../../../../nls.js'; -import { truncate, truncateMiddle } from '../../../../base/common/strings.js'; +import { truncate } from '../../../../base/common/strings.js'; import Severity from '../../../../base/common/severity.js'; import { IEditorOpenContext, isEditorOpenError } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; @@ -48,7 +48,7 @@ export interface IErrorEditorPlaceholderOptions extends IEditorOptions { export abstract class EditorPlaceholder extends EditorPane { - protected static readonly PLACEHOLDER_LABEL_MAX_LENGTH = 1024; + private static readonly PLACEHOLDER_LABEL_MAX_LENGTH = 1024; private container: HTMLElement | undefined; private scrollbar: DomScrollableElement | undefined; @@ -248,7 +248,7 @@ export class ErrorPlaceholderEditor extends EditorPlaceholder { } else if (isEditorOpenError(error) && error.forceMessage) { label = error.message; } else if (error) { - label = localize('unknownErrorEditorTextWithError', "The editor could not be opened due to an unexpected error: {0}", truncateMiddle(toErrorMessage(error), EditorPlaceholder.PLACEHOLDER_LABEL_MAX_LENGTH / 2)); + label = localize('unknownErrorEditorTextWithError', "The editor could not be opened due to an unexpected error. Please consult the log for more details."); } else { label = localize('unknownErrorEditorTextWithoutError', "The editor could not be opened due to an unexpected error."); } From d86304608812414ae5a894d10c16bae114d5b0ff Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 27 Jan 2025 16:02:40 +0100 Subject: [PATCH 0915/3587] fix https://github.com/microsoft/vscode-copilot/issues/11979 (#238853) --- .../chatEditing/chatEditingModifiedFileEntry.ts | 10 ++++++---- .../contrib/chat/browser/chatEditorOverlay.ts | 8 +++----- .../chat/browser/media/chatEditorOverlay.css | 13 ++----------- .../contrib/chat/common/chatEditingService.ts | 2 +- 4 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 1dedc199e6e0..149317c127dd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -41,6 +41,7 @@ import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelConten class AutoAcceptControl { constructor( + readonly total: number, readonly remaining: number, readonly cancel: () => void ) { } @@ -103,7 +104,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie readonly reviewMode: IObservable; private readonly _autoAcceptCtrl = observableValue(this, undefined); - readonly autoAcceptController: IObservable<{ remaining: number; cancel(): void } | undefined> = this._autoAcceptCtrl; + readonly autoAcceptController: IObservable = this._autoAcceptCtrl; private _isFirstEditAfterStartOrSnapshot: boolean = true; private _edit: OffsetEdit = OffsetEdit.empty; @@ -306,7 +307,8 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie // AUTO accept mode if (!this.reviewMode.get() && !this._autoAcceptCtrl.get()) { - const future = Date.now() + (this._autoAcceptTimeout.get() * 1000); + const acceptTimeout = this._autoAcceptTimeout.get() * 1000; + const future = Date.now() + acceptTimeout; const update = () => { const reviewMode = this.reviewMode.get(); @@ -316,12 +318,12 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie return; } - const remain = Math.round((future - Date.now()) / 1000); + const remain = Math.round(future - Date.now()); if (remain <= 0) { this.accept(undefined); } else { const handle = setTimeout(update, 100); - this._autoAcceptCtrl.set(new AutoAcceptControl(remain, () => { + this._autoAcceptCtrl.set(new AutoAcceptControl(acceptTimeout, remain, () => { clearTimeout(handle); this._autoAcceptCtrl.set(undefined, undefined); }), undefined); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index 099eb88133e8..51f631e652b7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -136,7 +136,6 @@ class ChatEditorOverlayWidget implements IOverlayWidget { const listener = this._store.add(new MutableDisposable()); - let timeIsSet = false; this._store.add(autorun(r => { assertType(this.label); @@ -145,10 +144,9 @@ class ChatEditorOverlayWidget implements IOverlayWidget { const ctrl = that._entry.read(r)?.entry.autoAcceptController.read(r); if (ctrl) { - if (!timeIsSet) { - this.element.style.setProperty('--vscode-action-item-auto-timeout', `${ctrl.remaining}s`); - timeIsSet = true; - } + const r = -100 * (ctrl.remaining / ctrl.total); + + this.element.style.setProperty('--vscode-action-item-auto-timeout', `${r}%`); this.element.classList.toggle('auto', true); listener.value = addDisposableGenericMouseMoveListener(this.element, () => ctrl.cancel()); diff --git a/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css index 90d4859bc6cc..f33b5a589c96 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css @@ -128,18 +128,9 @@ content: ''; position: absolute; top: 0; - left: -100%; + left: var(--vscode-action-item-auto-timeout, -100%); width: 100%; height: 100%; background-color: var(--vscode-toolbar-hoverBackground); - animation: slideRight var(--vscode-action-item-auto-timeout) linear forwards; -} - -@keyframes slideRight { - 0% { - left: -100%; - } - 100% { - left: 0%; - } + transition: left 0.5s linear; } diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index ee3a0a6ef039..23d97adc4113 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -141,7 +141,7 @@ export interface IModifiedFileEntry { reject(transaction: ITransaction | undefined): Promise; reviewMode: IObservable; - autoAcceptController: IObservable<{ remaining: number; cancel(): void } | undefined>; + autoAcceptController: IObservable<{ total: number; remaining: number; cancel(): void } | undefined>; enableReviewModeUntilSettled(): void; } From 2ed34cb21048f1858b2915f9f1d6eb6951e7c367 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 27 Jan 2025 07:26:07 -0800 Subject: [PATCH 0916/3587] Fallback to PriorityTaskQueue on Safari Fixes #238707 --- src/vs/editor/browser/gpu/taskQueue.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/vs/editor/browser/gpu/taskQueue.ts b/src/vs/editor/browser/gpu/taskQueue.ts index 27d64d01fe2c..c75785af7a0f 100644 --- a/src/vs/editor/browser/gpu/taskQueue.ts +++ b/src/vs/editor/browser/gpu/taskQueue.ts @@ -134,13 +134,7 @@ export class PriorityTaskQueue extends TaskQueue { } } -/** - * A queue of that runs tasks over several idle callbacks, trying to respect the idle callback's - * deadline given by the environment. The tasks will run in the order they are enqueued, but they - * will run some time later, and care should be taken to ensure they're non-urgent and will not - * introduce race conditions. - */ -export class IdleTaskQueue extends TaskQueue { +class IdleTaskQueueInternal extends TaskQueue { protected _requestCallback(callback: IdleRequestCallback): number { return getActiveWindow().requestIdleCallback(callback); } @@ -150,6 +144,16 @@ export class IdleTaskQueue extends TaskQueue { } } +/** + * A queue of that runs tasks over several idle callbacks, trying to respect the idle callback's + * deadline given by the environment. The tasks will run in the order they are enqueued, but they + * will run some time later, and care should be taken to ensure they're non-urgent and will not + * introduce race conditions. + * + * This reverts to a {@link PriorityTaskQueue} if the environment does not support idle callbacks. + */ +export const IdleTaskQueue = ('requestIdleCallback' in getActiveWindow()) ? IdleTaskQueueInternal : PriorityTaskQueue; + /** * An object that tracks a single debounced task that will run on the next idle frame. When called * multiple times, only the last set task will run. From 71a74f684c745ab1eb211d95dcb39f2dc69bd09d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 27 Jan 2025 08:18:37 -0800 Subject: [PATCH 0917/3587] Fix compile issues --- src/vs/editor/browser/gpu/atlas/textureAtlas.ts | 4 ++-- src/vs/editor/browser/gpu/taskQueue.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts index c0ed5fdd06e9..a4e5865c011c 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts @@ -14,7 +14,7 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { MetadataConsts } from '../../../common/encodedTokenAttributes.js'; import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; import type { IGlyphRasterizer } from '../raster/raster.js'; -import { IdleTaskQueue } from '../taskQueue.js'; +import { IdleTaskQueue, type ITaskQueue } from '../taskQueue.js'; import type { IReadableTextureAtlasPage, ITextureAtlasPageGlyph, GlyphMap } from './atlas.js'; import { AllocatorType, TextureAtlasPage } from './textureAtlasPage.js'; @@ -24,7 +24,7 @@ export interface ITextureAtlasOptions { export class TextureAtlas extends Disposable { private _colorMap?: string[]; - private readonly _warmUpTask: MutableDisposable = this._register(new MutableDisposable()); + private readonly _warmUpTask: MutableDisposable = this._register(new MutableDisposable()); private readonly _warmedUpRasterizers = new Set(); private readonly _allocatorType: AllocatorType; diff --git a/src/vs/editor/browser/gpu/taskQueue.ts b/src/vs/editor/browser/gpu/taskQueue.ts index c75785af7a0f..a6cf28c8c86f 100644 --- a/src/vs/editor/browser/gpu/taskQueue.ts +++ b/src/vs/editor/browser/gpu/taskQueue.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { getActiveWindow } from '../../../base/browser/dom.js'; -import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { Disposable, toDisposable, type IDisposable } from '../../../base/common/lifecycle.js'; /** * Copyright (c) 2022 The xterm.js authors. All rights reserved. * @license MIT */ -interface ITaskQueue { +export interface ITaskQueue extends IDisposable { /** * Adds a task to the queue which will run in a future idle callback. * To avoid perceivable stalls on the mainthread, tasks with heavy workload From 5e667f741bc154fc4499f6d37e8561804fc554cb Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 27 Jan 2025 17:30:59 +0100 Subject: [PATCH 0918/3587] smoke tests - run with `snapshots: true` for tests that install extensions (#238855) --- test/automation/src/code.ts | 1 + test/automation/src/playwrightBrowser.ts | 4 ++-- test/automation/src/playwrightElectron.ts | 4 ++-- test/smoke/src/areas/extensions/extensions.test.ts | 5 ++++- test/smoke/src/areas/workbench/localization.test.ts | 6 +++++- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index 6009e7c51f54..b297dd50a651 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -27,6 +27,7 @@ export interface LaunchOptions { readonly remote?: boolean; readonly web?: boolean; readonly tracing?: boolean; + snapshots?: boolean; readonly headless?: boolean; readonly browser?: 'chromium' | 'webkit' | 'firefox'; readonly quality: Quality; diff --git a/test/automation/src/playwrightBrowser.ts b/test/automation/src/playwrightBrowser.ts index e36d070cdc9b..3c52d261051a 100644 --- a/test/automation/src/playwrightBrowser.ts +++ b/test/automation/src/playwrightBrowser.ts @@ -88,7 +88,7 @@ async function launchServer(options: LaunchOptions) { } async function launchBrowser(options: LaunchOptions, endpoint: string) { - const { logger, workspacePath, tracing, headless } = options; + const { logger, workspacePath, tracing, snapshots, headless } = options; const browser = await measureAndLog(() => playwright[options.browser ?? 'chromium'].launch({ headless: headless ?? false, @@ -101,7 +101,7 @@ async function launchBrowser(options: LaunchOptions, endpoint: string) { if (tracing) { try { - await measureAndLog(() => context.tracing.start({ screenshots: true, /* remaining options are off for perf reasons */ }), 'context.tracing.start()', logger); + await measureAndLog(() => context.tracing.start({ screenshots: true, snapshots }), 'context.tracing.start()', logger); } catch (error) { logger.log(`Playwright (Browser): Failed to start playwright tracing (${error})`); // do not fail the build when this fails } diff --git a/test/automation/src/playwrightElectron.ts b/test/automation/src/playwrightElectron.ts index 660320be235d..41eb4ec41501 100644 --- a/test/automation/src/playwrightElectron.ts +++ b/test/automation/src/playwrightElectron.ts @@ -27,7 +27,7 @@ export async function launch(options: LaunchOptions): Promise<{ electronProcess: } async function launchElectron(configuration: IElectronConfiguration, options: LaunchOptions) { - const { logger, tracing } = options; + const { logger, tracing, snapshots } = options; const electron = await measureAndLog(() => playwright._electron.launch({ executablePath: configuration.electronPath, @@ -45,7 +45,7 @@ async function launchElectron(configuration: IElectronConfiguration, options: La if (tracing) { try { - await measureAndLog(() => context.tracing.start({ screenshots: true, /* remaining options are off for perf reasons */ }), 'context.tracing.start()', logger); + await measureAndLog(() => context.tracing.start({ screenshots: true, snapshots }), 'context.tracing.start()', logger); } catch (error) { logger.log(`Playwright (Electron): Failed to start playwright tracing (${error})`); // do not fail the build when this fails } diff --git a/test/smoke/src/areas/extensions/extensions.test.ts b/test/smoke/src/areas/extensions/extensions.test.ts index c78cbe870897..c20700cbc91d 100644 --- a/test/smoke/src/areas/extensions/extensions.test.ts +++ b/test/smoke/src/areas/extensions/extensions.test.ts @@ -10,7 +10,10 @@ export function setup(logger: Logger) { describe('Extensions', () => { // Shared before/after handling - installAllHandlers(logger); + installAllHandlers(logger, opts => { + opts.snapshots = true; // enable network tab in devtools for tracing since we install an extension + return opts; + }); it('install and enable vscode-smoketest-check extension', async function () { const app = this.app as Application; diff --git a/test/smoke/src/areas/workbench/localization.test.ts b/test/smoke/src/areas/workbench/localization.test.ts index 12e49ce549ec..c3ce9b04fc2b 100644 --- a/test/smoke/src/areas/workbench/localization.test.ts +++ b/test/smoke/src/areas/workbench/localization.test.ts @@ -9,8 +9,12 @@ import { installAllHandlers } from '../../utils'; export function setup(logger: Logger) { describe('Localization', () => { + // Shared before/after handling - installAllHandlers(logger); + installAllHandlers(logger, opts => { + opts.snapshots = true; // enable network tab in devtools for tracing since we install an extension + return opts; + }); it('starts with "DE" locale and verifies title and viewlets text is in German', async function () { const app = this.app as Application; From 9487f1dc4bcd7c042d12f6b3332e249298db35ec Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 27 Jan 2025 08:58:30 -0800 Subject: [PATCH 0919/3587] rename `settledAll` methods to `allSettled` for consistency (#238862) [prompts]: rename `settledAll` methods to `allSettled` for consistency --- .../browser/chatAttachmentModel/chatInstructionsAttachment.ts | 2 +- .../chat/common/promptSyntax/parsers/basePromptParser.ts | 4 ++-- .../contrib/chat/common/promptSyntax/parsers/types.d.ts | 2 +- .../chat/test/common/promptSyntax/promptFileReference.test.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts index d03092c32fe2..a9a6a6ffbe5a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsAttachment.ts @@ -52,7 +52,7 @@ export class ChatInstructionsAttachmentModel extends Disposable { * including all its possible nested child references. */ public get allSettled(): Promise { - return this.reference.settledAll(); + return this.reference.allSettled(); } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index c7d3d2a689a5..3ed34c7df66b 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -129,12 +129,12 @@ export abstract class BasePromptParser extend * Same as {@linkcode settled} but also waits for all possible * nested child prompt references and their children to be settled. */ - public async settledAll(): Promise { + public async allSettled(): Promise { await this.settled(); await Promise.allSettled( this.references.map((reference) => { - return reference.settledAll(); + return reference.allSettled(); }), ); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts index 2f99c31d7931..91aef1479f12 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts @@ -119,7 +119,7 @@ export interface IPromptReference extends IDisposable { * The same as {@linkcode settled} but for all prompts in * the reference tree. */ - settledAll(): Promise; + allSettled(): Promise; } /** diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index fe10fd243589..ec3a103dd1cd 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -119,7 +119,7 @@ class TestPromptFileReference extends Disposable { ).start(); // wait until entire prompts tree is resolved - await rootReference.settledAll(); + await rootReference.allSettled(); // resolve the root file reference including all nested references const resolvedReferences: readonly (IPromptFileReference | undefined)[] = rootReference.allReferences; From d11fdfee32e4219b10e39823a7131860d5da4bfa Mon Sep 17 00:00:00 2001 From: Alen Ajam Date: Mon, 27 Jan 2025 18:01:01 +0100 Subject: [PATCH 0920/3587] fix: check whether lastFocusedList is valid when assigned (#238765) --- src/vs/workbench/contrib/terminal/browser/terminalActions.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 7b967628a7c4..00927f236f3c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -61,6 +61,7 @@ import { isKeyboardEvent, isMouseEvent, isPointerEvent } from '../../../../base/ import { editorGroupToColumn } from '../../../services/editor/common/editorGroupColumn.js'; import { InstanceContext } from './terminalContextMenu.js'; import { AccessibleViewProviderId } from '../../../../platform/accessibility/browser/accessibleView.js'; +import { TerminalTabList } from './terminalTabsList.js'; export const switchTerminalActionViewItemSeparator = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'; export const switchTerminalShowTabsTitle = localize('showTerminalTabs', "Show Tabs"); @@ -1463,7 +1464,8 @@ function getSelectedInstances(accessor: ServicesAccessor, args?: unknown, args2? const terminalGroupService = accessor.get(ITerminalGroupService); const result: ITerminalInstance[] = []; - const list = listService.lastFocusedList; + // Assign list only if it's an instance of TerminalTabList (#234791) + const list = listService.lastFocusedList instanceof TerminalTabList ? listService.lastFocusedList : undefined; // Get selected tab list instance(s) const selections = list?.getSelection(); // Get inline tab instance if there are not tab list selections #196578 From 93c4a30f41d62c1dd89b0239b9aa31096d4627f0 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 27 Jan 2025 18:16:14 +0100 Subject: [PATCH 0921/3587] Enhance NES ghost text with highlighting and bubble (#238863) * Use ghost text with highlighting and bubble around it * :lipstick: --- .../browser/view/ghostText/ghostTextView.ts | 25 ++- .../browser/view/inlineCompletionsView.ts | 5 +- .../browser/view/inlineEdits/deletionView.ts | 2 +- .../browser/view/inlineEdits/insertionView.ts | 204 ++++++++++++++++++ .../view/inlineEdits/sideBySideDiff.ts | 2 +- .../browser/view/inlineEdits/utils.ts | 18 +- .../browser/view/inlineEdits/view.css | 10 + .../browser/view/inlineEdits/view.ts | 30 +-- 8 files changed, 259 insertions(+), 37 deletions(-) create mode 100644 src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts index ce5dfc8c9941..6030a75ffed9 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts @@ -39,6 +39,10 @@ export class GhostTextView extends Disposable { constructor( private readonly _editor: ICodeEditor, private readonly _model: IGhostTextWidgetModel, + private readonly _options: IObservable<{ + extraClasses?: string[]; + syntaxHighlightingEnabled: boolean; + }>, @ILanguageService private readonly _languageService: ILanguageService, ) { super(); @@ -47,7 +51,16 @@ export class GhostTextView extends Disposable { this._register(this._editorObs.setDecorations(this.decorations)); } - private readonly _useSyntaxHighlighting = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => v.syntaxHighlightingEnabled); + private readonly _useSyntaxHighlighting = this._options.map(o => o.syntaxHighlightingEnabled); + + private readonly _extraClassNames = derived(this, reader => { + const extraClasses = [...this._options.read(reader).extraClasses ?? []]; + if (this._useSyntaxHighlighting.read(reader)) { + extraClasses.push('syntax-highlighted'); + } + const extraClassNames = extraClasses.map(c => ` ${c}`).join(''); + return extraClassNames; + }); private readonly uiState = derived(this, reader => { if (this._isDisposed.read(reader)) { return undefined; } @@ -59,8 +72,8 @@ export class GhostTextView extends Disposable { const replacedRange = ghostText instanceof GhostTextReplacement ? ghostText.columnRange : undefined; const syntaxHighlightingEnabled = this._useSyntaxHighlighting.read(reader); - const extraClassName = syntaxHighlightingEnabled ? ' syntax-highlighted' : ''; - const { inlineTexts, additionalLines, hiddenRange } = computeGhostTextViewData(ghostText, textModel, 'ghost-text' + extraClassName); + const extraClassNames = this._extraClassNames.read(reader); + const { inlineTexts, additionalLines, hiddenRange } = computeGhostTextViewData(ghostText, textModel, 'ghost-text' + extraClassNames); const currentLine = textModel.getLineContent(ghostText.lineNumber); const edit = new OffsetEdit(inlineTexts.map(t => SingleOffsetEdit.insert(t.column - 1, t.text))); @@ -91,12 +104,12 @@ export class GhostTextView extends Disposable { const decorations: IModelDeltaDecoration[] = []; - const extraClassName = uiState.syntaxHighlightingEnabled ? ' syntax-highlighted' : ''; + const extraClassNames = this._extraClassNames.read(reader); if (uiState.replacedRange) { decorations.push({ range: uiState.replacedRange.toRange(uiState.lineNumber), - options: { inlineClassName: 'inline-completion-text-to-replace' + extraClassName, description: 'GhostTextReplacement' } + options: { inlineClassName: 'inline-completion-text-to-replace' + extraClassNames, description: 'GhostTextReplacement' } }); } @@ -115,7 +128,7 @@ export class GhostTextView extends Disposable { after: { content: p.text, tokens: p.tokens, - inlineClassName: p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration' + extraClassName, + inlineClassName: p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration' + extraClassNames, cursorStops: InjectedTextCursorStops.Left }, showIfCollapsed: true, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts index faa3c671e122..cbbbb35734e2 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts @@ -23,12 +23,14 @@ export class InlineCompletionsView extends Disposable { return model?.ghostTexts.read(reader) ?? []; }); private readonly _stablizedGhostTexts = convertItemsToStableObservables(this._ghostTexts, this._store); + private readonly _editorObs = observableCodeEditor(this._editor); private readonly _ghostTextWidgets = mapObservableArrayCached(this, this._stablizedGhostTexts, (ghostText, store) => derivedDisposable((reader) => this._instantiationService.createInstance(readHotReloadableExport(GhostTextView, reader), this._editor, { ghostText: ghostText, minReservedLineCount: constObservable(0), targetTextModel: this._model.map(v => v?.textModel), - }) + }, + this._editorObs.getOption(EditorOption.inlineSuggest).map(v => ({ syntaxHighlightingEnabled: v.syntaxHighlightingEnabled }))) ).recomputeInitiallyAndOnChange(store) ).recomputeInitiallyAndOnChange(this._store); @@ -42,7 +44,6 @@ export class InlineCompletionsView extends Disposable { }) .recomputeInitiallyAndOnChange(this._store); - private readonly _editorObs = observableCodeEditor(this._editor); private readonly _fontFamily = this._editorObs.getOption(EditorOption.inlineSuggest).map(val => val.fontFamily); constructor( diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts index 3f17b847013c..7b17b5e757f3 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts @@ -140,7 +140,7 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV layoutInfo.padding, layoutInfo.borderRadius, { hideLeft: layoutInfo.horizontalScrollOffset !== 0 } - ).build(); + ); return [ n.svgElem('path', { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts new file mode 100644 index 000000000000..96c7919586cb --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts @@ -0,0 +1,204 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { $ } from '../../../../../../base/browser/dom.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { IObservable, constObservable, derived, observableValue } from '../../../../../../base/common/observable.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; +import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; +import { Point } from '../../../../../browser/point.js'; +import { LineSource, renderLines, RenderOptions } from '../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; +import { Range } from '../../../../../common/core/range.js'; +import { ILanguageService } from '../../../../../common/languages/language.js'; +import { LineTokens } from '../../../../../common/tokens/lineTokens.js'; +import { TokenArray } from '../../../../../common/tokens/tokenArray.js'; +import { GhostText, GhostTextPart } from '../../model/ghostText.js'; +import { GhostTextView } from '../ghostText/ghostTextView.js'; +import { IInlineEditsView } from './sideBySideDiff.js'; +import { createRectangle, mapOutFalsy, n } from './utils.js'; + +export class InlineEditsInsertionView extends Disposable implements IInlineEditsView { + private readonly _editorObs = observableCodeEditor(this._editor); + + private readonly _state = derived(this, reader => { + const state = this._input.read(reader); + if (!state) { return undefined; } + + const textModel = this._editor.getModel()!; + + if (state.startColumn === 1 && state.lineNumber > 1 && textModel.getLineLength(state.lineNumber) !== 0 && state.text.endsWith('\n') && !state.text.startsWith('\n')) { + const endOfLineColumn = textModel.getLineLength(state.lineNumber - 1) + 1; + return { lineNumber: state.lineNumber - 1, column: endOfLineColumn, text: '\n' + state.text.slice(0, -1) }; + } + + return { lineNumber: state.lineNumber, column: state.startColumn, text: state.text }; + }); + + private readonly _ghostText = derived(reader => { + const state = this._state.read(reader); + if (!state) { return undefined; } + return new GhostText(state.lineNumber, [new GhostTextPart(state.column, state.text, false)]); + }); + + protected readonly _ghostTextView = this._register(this._instantiationService.createInstance(GhostTextView, + this._editor, + { + ghostText: this._ghostText, + minReservedLineCount: constObservable(0), + targetTextModel: this._editorObs.model.map(model => model ?? undefined), + }, + observableValue(this, { syntaxHighlightingEnabled: true, extraClasses: ['inline-edit'] }), + )); + + constructor( + private readonly _editor: ICodeEditor, + private readonly _input: IObservable<{ + lineNumber: number; + startColumn: number; + text: string; + } | undefined>, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILanguageService private readonly _languageService: ILanguageService, + ) { + super(); + + this._register(this._editorObs.createOverlayWidget({ + domNode: this._nonOverflowView.element, + position: constObservable(null), + allowEditorOverflow: false, + minContentWidthInPx: derived(reader => { + const info = this._editorLayoutInfo.read(reader); + if (info === null) { return 0; } + return info.code1.x - info.codeStart1.x; + }), + })); + } + + private readonly _display = derived(this, reader => !!this._state.read(reader) ? 'block' : 'none'); + + private readonly _editorMaxContentWidthInRange = derived(this, reader => { + const state = this._state.read(reader); + if (!state) { + return 0; + } + this._editorObs.versionId.read(reader); + const textModel = this._editor.getModel()!; + + const cleanText = state.text.replace('\r\n', '\n'); + const textBeforeInsertion = cleanText.startsWith('\n') ? '' : textModel.getValueInRange(new Range(state.lineNumber, 1, state.lineNumber, state.column)); + const textAfterInsertion = textModel.getValueInRange(new Range(state.lineNumber, state.column, state.lineNumber, textModel.getLineLength(state.lineNumber) + 1)); + const text = textBeforeInsertion + cleanText + textAfterInsertion; + const lines = text.split('\n'); + + const renderOptions = RenderOptions.fromEditor(this._editor).withSetWidth(false); + const lineWidths = lines.map(line => { + const t = textModel.tokenization.tokenizeLinesAt(state.lineNumber, [line])?.[0]; + let tokens: LineTokens; + if (t) { + tokens = TokenArray.fromLineTokens(t).toLineTokens(line, this._languageService.languageIdCodec); + } else { + tokens = LineTokens.createEmpty(line, this._languageService.languageIdCodec); + } + + return renderLines(new LineSource([tokens]), renderOptions, [], $('div'), true).minWidthInPx - 20; // TODO: always too much padding included, why? + }); + + // Take the max value that we observed. + // Reset when either the edit changes or the editor text version. + return Math.max(...lineWidths); + }); + + private readonly _editorLayoutInfo = derived(this, (reader) => { + this._ghostText.read(reader); + const state = this._state.read(reader); + if (!state) { + return null; + } + + const editorLayout = this._editorObs.layoutInfo.read(reader); + const horizontalScrollOffset = this._editorObs.scrollLeft.read(reader); + + const left = editorLayout.contentLeft + this._editorMaxContentWidthInRange.read(reader) - horizontalScrollOffset; + + const scrollTop = this._editorObs.scrollTop.read(reader); + + const top = state.text.startsWith('\n') + ? this._editor.getBottomForLineNumber(state.lineNumber) - scrollTop + : this._editor.getTopForLineNumber(state.lineNumber) - scrollTop; + const bottom = this._editor.getTopForLineNumber(state.lineNumber + 1) - scrollTop; + + const codeLeft = editorLayout.contentLeft; + + if (left <= codeLeft) { + return null; + } + + const code1 = new Point(left, top); + const codeStart1 = new Point(codeLeft, top); + const code2 = new Point(left, bottom); + const codeStart2 = new Point(codeLeft, bottom); + const codeHeight = bottom - top; + + return { + code1, + codeStart1, + code2, + codeStart2, + codeHeight, + horizontalScrollOffset, + padding: 2, + borderRadius: 4, + }; + }).recomputeInitiallyAndOnChange(this._store); + + private readonly _foregroundSvg = n.svg({ + transform: 'translate(-0.5 -0.5)', + style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, + }, derived(reader => { + const layoutInfoObs = mapOutFalsy(this._editorLayoutInfo).read(reader); + if (!layoutInfoObs) { return undefined; } + + const layoutInfo = layoutInfoObs.read(reader); + + const rectangleOverlay = createRectangle( + { + topLeft: layoutInfo.codeStart1, + width: layoutInfo.code1.x - layoutInfo.codeStart1.x, + height: layoutInfo.code2.y - layoutInfo.code1.y, + }, + layoutInfo.padding, + layoutInfo.borderRadius, + { hideLeft: layoutInfo.horizontalScrollOffset !== 0 } + ); + + return [ + n.svgElem('path', { + class: 'originalOverlay', + d: rectangleOverlay, + style: { + fill: 'var(--vscode-inlineEdit-modifiedChangedLineBackground, transparent)', + stroke: 'var(--vscode-inlineEdit-modifiedBorder)', + strokeWidth: '1px', + } + }), + ]; + })).keepUpdated(this._store); + + private readonly _nonOverflowView = n.div({ + class: 'inline-edits-view', + style: { + position: 'absolute', + overflow: 'visible', + top: '0px', + left: '0px', + zIndex: '0', + display: this._display, + }, + }, [ + [this._foregroundSvg], + ]).keepUpdated(this._store); + + readonly isHovered = constObservable(false); +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index 1abc72a5a423..afd6599ebb9e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -669,7 +669,7 @@ export class InlineEditsSideBySideDiff extends Disposable implements IInlineEdit 0, { topLeft: layoutInfo.borderRadius, bottomLeft: layoutInfo.borderRadius, topRight: 0, bottomRight: 0 }, { hideRight: true, hideLeft: layoutInfo.codeScrollLeft !== 0 } - ).build()), + )), style: { fill: 'var(--vscode-inlineEdit-originalBackground, transparent)', stroke: 'var(--vscode-inlineEdit-originalBorder)', diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts index 6f855179b1ee..3c80494c8ba3 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts @@ -187,7 +187,7 @@ export function createRectangle( padding: number | { top: number; right: number; bottom: number; left: number }, borderRadius: number | { topLeft: number; topRight: number; bottomLeft: number; bottomRight: number }, options: { hideLeft?: boolean; hideRight?: boolean; hideTop?: boolean; hideBottom?: boolean } = {} -): PathBuilder { +): string { const topLeftInner = layout.topLeft; const topRightInner = topLeftInner.deltaX(layout.width); @@ -232,33 +232,41 @@ export function createRectangle( if (!options.hideLeft && !options.hideTop) { path.curveTo(topLeft, topLeftAfter); + } else { + path.moveTo(topLeftAfter); } if (!options.hideTop) { - path.moveTo(topLeftAfter).lineTo(topRightBefore); + path.lineTo(topRightBefore); } if (!options.hideTop && !options.hideRight) { path.curveTo(topRight, topRightAfter); + } else { + path.moveTo(topRightAfter); } if (!options.hideRight) { - path.moveTo(topRightAfter).lineTo(bottomRightBefore); + path.lineTo(bottomRightBefore); } if (!options.hideRight && !options.hideBottom) { path.curveTo(bottomRight, bottomRightAfter); + } else { + path.moveTo(bottomRightAfter); } if (!options.hideBottom) { - path.moveTo(bottomRightAfter).lineTo(bottomLeftBefore); + path.lineTo(bottomLeftBefore); } if (!options.hideBottom && !options.hideLeft) { path.curveTo(bottomLeft, bottomLeftAfter); + } else { + path.moveTo(bottomLeftAfter); } - return path; + return path.build(); } type Value = T | IObservable; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index bda6e8daff90..350036eb214e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -216,6 +216,16 @@ border-bottom-right-radius: 4px; } + .inline-edit.ghost-text, + .inline-edit.ghost-text-decoration, + .inline-edit.ghost-text-decoration-preview, + .inline-edit.suggest-preview-text .ghost-text { + &.syntax-highlighted { + opacity: 1 !important; + } + background: var(--vscode-inlineEdit-modifiedChangedTextBackground) !important; + font-style: normal !important; + } } .monaco-menu-option { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 80e8cc5be9fa..7d09c4340a9e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { autorunWithStore, constObservable, derived, IObservable, IReader, ISettableObservable, mapObservableArrayCached } from '../../../../../../base/common/observable.js'; +import { autorunWithStore, derived, IObservable, IReader, ISettableObservable, mapObservableArrayCached } from '../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; @@ -15,13 +15,12 @@ import { SingleTextEdit, StringText } from '../../../../../common/core/textEdit. import { TextLength } from '../../../../../common/core/textLength.js'; import { DetailedLineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../../../../../common/diff/rangeMapping.js'; import { TextModel } from '../../../../../common/model/textModel.js'; -import { GhostText, GhostTextPart } from '../../model/ghostText.js'; import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; -import { GhostTextView } from '../ghostText/ghostTextView.js'; import { InlineEditsDeletionView } from './deletionView.js'; import { InlineEditsGutterIndicator } from './gutterIndicatorView.js'; import { IInlineEditsIndicatorState, InlineEditsIndicator } from './indicatorView.js'; import { IOriginalEditorInlineDiffViewState, OriginalEditorInlineDiffView } from './inlineDiffView.js'; +import { InlineEditsInsertionView } from './insertionView.js'; import { InlineEditsSideBySideDiff } from './sideBySideDiff.js'; import { applyEditToModifiedRangeMappings, createReindentEdit } from './utils.js'; import './view.css'; @@ -138,26 +137,13 @@ export class InlineEditsView extends Disposable { }) : undefined), )); - protected readonly _insertion = this._register(this._instantiationService.createInstance(GhostTextView, + protected readonly _insertion = this._register(this._instantiationService.createInstance(InlineEditsInsertionView, this._editor, - { - ghostText: derived(reader => { - const state = this._uiState.read(reader)?.state; - if (!state || state.kind !== 'insertionMultiLine') { return undefined; } - - const textModel = this._editor.getModel()!; - - // Try to not insert on the same line where there is other content afterwards - if (state.column === 1 && state.lineNumber > 1 && textModel.getLineLength(state.lineNumber) !== 0 && state.text.endsWith('\n') && !state.text.startsWith('\n')) { - const endOfLineColumn = textModel.getLineLength(state.lineNumber - 1) + 1; - return new GhostText(state.lineNumber - 1, [new GhostTextPart(endOfLineColumn, '\n' + state.text.slice(0, -1), false)]); - } - - return new GhostText(state.lineNumber, [new GhostTextPart(state.column, state.text, false)]); - }), - minReservedLineCount: constObservable(0), - targetTextModel: this._model.map(v => v?.textModel), - } + this._uiState.map(s => s && s.state?.kind === 'insertionMultiLine' ? ({ + lineNumber: s.state.lineNumber, + startColumn: s.state.column, + text: s.state.text, + }) : undefined), )); private readonly _inlineDiffViewState = derived(this, reader => { From 18daae31d96286ef17935dab02d3dc2d69e35475 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 27 Jan 2025 18:33:19 +0100 Subject: [PATCH 0922/3587] Check for deps or packs in trusting publisher dialog (#238867) --- .../common/extensionManagementService.ts | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 276b50caf0e6..e00149c287cf 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -503,7 +503,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } if (installOptions?.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] !== ExtensionInstallSource.SETTINGS_SYNC) { - await this.checkForTrustedPublisher(gallery); + await this.checkForTrustedPublisher(gallery, manifest); await this.checkForWorkspaceTrust(manifest, false); @@ -789,7 +789,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench throw new Error('No extension server found'); } - private async checkForTrustedPublisher(extension: IGalleryExtension): Promise { + private async checkForTrustedPublisher(extension: IGalleryExtension, manifest: IExtensionManifest): Promise { if (this.isPublisherTrusted(extension)) { return; } @@ -822,8 +822,9 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } }; - const customMessage = new MarkdownString('', { supportThemeIcons: true }); + const customMessage = new MarkdownString('', { supportThemeIcons: true, isTrusted: true }); customMessage.appendMarkdown(localize('message1', "The extension {0} is published by {1}. This is the first extension you're installing from this publisher.", `[${extension.displayName}](${this.productService.extensionsGallery!.itemUrl}?itemName=${extension.identifier.id})`, `[${extension.publisherDisplayName}](${joinPath(URI.parse(this.productService.extensionsGallery!.publisherUrl), extension.publisher)})`)); + customMessage.appendText('\n'); if (extension.publisherDomain?.verified) { const publisherVerifiedMessage = localize('verifiedPublisher', "This publisher has verified ownership of {0}.", `[${URI.parse(extension.publisherDomain.link).authority}](${extension.publisherDomain.link})`); @@ -832,6 +833,12 @@ export class ExtensionManagementService extends Disposable implements IWorkbench customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublisher', "This publisher is **not** [verified](https://aka.ms/vscode-verify-publisher).")}`); } + if (await this.hasDepsAndPacksFromOtherUntrustedPublishers(manifest)) { + const commandUri = URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([extension.identifier.id, manifest.extensionPack?.length ? 'extensionPack' : 'dependencies']))}`); + customMessage.appendText('\n'); + customMessage.appendMarkdown(localize('message3', "Installing this extension will also install [extensions]({0}) from other publishers, and trusting this publisher will automatically trust those publishers.", commandUri.toString())); + } + customMessage.appendText('\n'); customMessage.appendMarkdown(localize('message2', "{0} has no control over the behavior of third-party extensions, including how they manage your personal data. Please proceed only if you trust the publisher.", this.productService.nameLong)); @@ -853,6 +860,25 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } + private async hasDepsAndPacksFromOtherUntrustedPublishers(manifest: IExtensionManifest): Promise { + const infos = []; + for (const id of [...(manifest.extensionPack ?? []), ...(manifest.extensionDependencies ?? [])]) { + const [publisherId] = id.split('.'); + if (publisherId.toLowerCase() === manifest.publisher.toLowerCase()) { + continue; + } + if (this.isPublisherUserTrusted(publisherId.toLowerCase())) { + continue; + } + infos.push({ id }); + } + if (!infos.length) { + return false; + } + const extensions = await this.extensionGalleryService.getExtensions(infos, CancellationToken.None); + return extensions.some(e => !this.isPublisherTrusted(e)); + } + private async checkForWorkspaceTrust(manifest: IExtensionManifest, requireTrust: boolean): Promise { if (requireTrust || this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(manifest) === false) { const buttons: WorkspaceTrustRequestButton[] = []; @@ -995,6 +1021,10 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return true; } + return this.isPublisherUserTrusted(publisher); + } + + private isPublisherUserTrusted(publisher: string): boolean { const trustedPublishers = this.storageService.getObject(TrustedPublishersStorageKey, StorageScope.APPLICATION, []).map(p => p.toLowerCase()); this.logService.debug('Trusted publishers', trustedPublishers); return trustedPublishers.includes(publisher); From 7ef2144072967232f439002060cb3df8912284a6 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 27 Jan 2025 19:03:56 +0100 Subject: [PATCH 0923/3587] wording tweak (#238876) --- .../extensionManagement/common/extensionManagementService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index e00149c287cf..95da45fb717f 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -836,7 +836,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench if (await this.hasDepsAndPacksFromOtherUntrustedPublishers(manifest)) { const commandUri = URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([extension.identifier.id, manifest.extensionPack?.length ? 'extensionPack' : 'dependencies']))}`); customMessage.appendText('\n'); - customMessage.appendMarkdown(localize('message3', "Installing this extension will also install [extensions]({0}) from other publishers, and trusting this publisher will automatically trust those publishers.", commandUri.toString())); + customMessage.appendMarkdown(localize('message3', "Installing this extension will also install [extensions]({0}) from other publishers. Trusting this publisher will automatically trust the other publishers.", commandUri.toString())); } customMessage.appendText('\n'); From 31186604417e0e32ba1ae0e0050842c139f718cd Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:08:38 -0800 Subject: [PATCH 0924/3587] feat: expose tags in configuration schema (#238738) Fixes: https://github.com/microsoft/vscode/issues/231917 --- src/vs/workbench/api/common/configurationExtensionPoint.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 3fa10161b9a2..8b54b9e5a1d7 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -115,6 +115,13 @@ const configurationEntrySchema: IJSONSchema = { type: 'boolean', description: nls.localize('scope.ignoreSync', 'When enabled, Settings Sync will not sync the user value of this configuration by default.') }, + tags: { + type: 'array', + items: { + type: 'string' + }, + markdownDescription: nls.localize('scope.tags', 'A list of categories under which to place the setting. The category can then be searched up in the Settings editor. For example, specifying the `experimental` tag allows one to find the setting by searching `@tag:experimental`.'), + } } } ] From 4fb8a20e91f8aa8af8cfa0a7dd69b9d782ab75a2 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 27 Jan 2025 12:33:25 -0600 Subject: [PATCH 0925/3587] fix case when font > 16 and line height <= 1 for terminal suggest (#238865) --- .../workbench/services/suggest/browser/simpleSuggestWidget.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index b2996c57db79..aa898efba3c2 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -779,9 +779,9 @@ export class SimpleSuggestWidget extends Disposable { const fontWeight: string = this._configurationService.getValue('editor.fontWeight'); const letterSpacing: number = this._configurationService.getValue('editor.letterSpacing'); - if (lineHeight <= 1 && fontSize < 16) { + if (lineHeight <= 1) { // Scale so icon shows by default - lineHeight = Math.ceil(fontSize * 1.5); + lineHeight = fontSize < 16 ? Math.ceil(fontSize * 1.5) : fontSize; } else if (lineHeight <= 8) { lineHeight = fontSize * lineHeight; } From ae4b2dfd57ca1a0b57f51e7c151664c176543021 Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Mon, 27 Jan 2025 10:40:04 -0800 Subject: [PATCH 0926/3587] disable executing REPL history with enter (#238877) --- .../contrib/replNotebook/browser/repl.contribution.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts b/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts index 2dcd24876fe5..f06bc5a4193a 100644 --- a/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts +++ b/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts @@ -340,7 +340,8 @@ registerAction2(class extends Action2 { keybinding: [{ when: ContextKeyExpr.and( IS_COMPOSITE_NOTEBOOK, - ContextKeyExpr.equals('activeEditor', 'workbench.editor.repl') + ContextKeyExpr.equals('activeEditor', 'workbench.editor.repl'), + NOTEBOOK_CELL_LIST_FOCUSED.negate() ), primary: KeyMod.CtrlCmd | KeyCode.Enter, weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT @@ -348,7 +349,8 @@ registerAction2(class extends Action2 { when: ContextKeyExpr.and( IS_COMPOSITE_NOTEBOOK, ContextKeyExpr.equals('activeEditor', 'workbench.editor.repl'), - ContextKeyExpr.equals('config.interactiveWindow.executeWithShiftEnter', true) + ContextKeyExpr.equals('config.interactiveWindow.executeWithShiftEnter', true), + NOTEBOOK_CELL_LIST_FOCUSED.negate() ), primary: KeyMod.Shift | KeyCode.Enter, weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT @@ -356,7 +358,8 @@ registerAction2(class extends Action2 { when: ContextKeyExpr.and( IS_COMPOSITE_NOTEBOOK, ContextKeyExpr.equals('activeEditor', 'workbench.editor.repl'), - ContextKeyExpr.equals('config.interactiveWindow.executeWithShiftEnter', false) + ContextKeyExpr.equals('config.interactiveWindow.executeWithShiftEnter', false), + NOTEBOOK_CELL_LIST_FOCUSED.negate() ), primary: KeyCode.Enter, weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT From 8701700de0c2a64b414f9fb38a0fa3a4dbd0913e Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 27 Jan 2025 19:43:16 +0100 Subject: [PATCH 0927/3587] Fix incorrect and too small tree sitter changed ranges (#238870) * Fix tree sitter edit apply bug * Only use named nodes for tree sitter changes * Fix test --- .../treeSitter/treeSitterParserService.ts | 26 +++++++------------ .../browser/treeSitterTokenizationFeature.ts | 16 +++++------- .../treeSitterTokenizationFeature.test.ts | 6 ++--- 3 files changed, 18 insertions(+), 30 deletions(-) diff --git a/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts index a7fd4bb49bd7..4ca0e3396de8 100644 --- a/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts +++ b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts @@ -173,7 +173,7 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul get tree() { return this._lastFullyParsed; } get isDisposed() { return this._isDisposed; } - private findChangedNodes(newTree: Parser.Tree, oldTree: Parser.Tree, version: number): ChangedRange[] { + private findChangedNodes(newTree: Parser.Tree, oldTree: Parser.Tree): ChangedRange[] { const newCursor = newTree.walk(); const oldCursor = oldTree.walk(); const gotoNextSibling = () => { @@ -258,7 +258,6 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul return undefined; } }; - const childrenToVisit: Map = new Map(); do { if (newCursor.currentNode.hasChanges) { // Check if only one of the children has changes. @@ -274,16 +273,16 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul return c.hasChanges; }); if (changedChildren.length >= 1) { - if (changedChildren.length > 1) { - // We need to visit each child eventually - for (let i = 1; i < changedChildren.length; i++) { - childrenToVisit.set(changedChildren[i].id, { new: changedChildren[i], old: oldCursor.currentNode.children[i] }); - } - } next = gotoNthChild(indexChangedChildren[0]); } else if (changedChildren.length === 0) { + // walk up again until we get to the first one that's named as unnamed nodes can be too granular + while (newCursor.currentNode.parent && !newCursor.currentNode.isNamed && next) { + next = gotoParent(); + } + const newNode = newCursor.currentNode; const oldNode = oldCursor.currentNode; + const newEndPosition = new Position(newNode.endPosition.row + 1, newNode.endPosition.column + 1); const oldEndIndex = oldNode.endIndex; @@ -299,13 +298,6 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul } else { next = nextSiblingOrParentSibling(); } - if (!next && childrenToVisit.size > 0) { - const nextChildId = childrenToVisit.keys().next().value?.valueOf()!; - const nextChild = childrenToVisit.get(nextChildId)!; - childrenToVisit.delete(nextChildId); - newCursor.reset(nextChild.new); - oldCursor.reset(nextChild.old); - } } while (next); if (changedRanges.length === 0 && newTree.rootNode.hasChanges) { @@ -356,7 +348,7 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul let ranges: RangeChange[] | undefined; if (this._lastFullyParsedWithEdits && this._lastFullyParsed) { - ranges = this.calculateRangeChange(this.findChangedNodes(this._lastFullyParsedWithEdits, this._lastFullyParsed, version)); + ranges = this.calculateRangeChange(this.findChangedNodes(this._lastFullyParsedWithEdits, this._lastFullyParsed)); } const completed = await this._parseAndUpdateTree(model, version); @@ -380,7 +372,7 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul newEndIndex: change.rangeOffset + change.text.length, startPosition: { row: change.range.startLineNumber - 1, column: change.range.startColumn - 1 }, oldEndPosition: { row: change.range.endLineNumber - 1, column: change.range.endColumn - 1 }, - newEndPosition: { row: change.range.startLineNumber + summedTextLengths.lineCount - 1, column: summedTextLengths.columnCount } + newEndPosition: { row: change.range.startLineNumber + summedTextLengths.lineCount - 1, column: summedTextLengths.lineCount ? summedTextLengths.columnCount : (change.range.endColumn + summedTextLengths.columnCount) } }; this._tree?.edit(edit); this._lastFullyParsedWithEdits?.edit(edit); diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts index 28f1a2f73e22..c113c2558e1b 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts @@ -124,14 +124,12 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi if (e.versionId !== e.textModel.getVersionId()) { return; } - const maxLine = e.textModel.getLineCount(); - const ranges = e.ranges.map(range => ({ fromLineNumber: range.newRange.startLineNumber, toLineNumber: range.newRange.endLineNumber < maxLine ? range.newRange.endLineNumber : maxLine })); // First time we see a tree we need to build a token store. if (!this._tokenizationStoreService.hasTokens(e.textModel)) { - this._firstTreeUpdate(e.textModel, e.versionId, ranges); + this._firstTreeUpdate(e.textModel, e.versionId); } else { - this._handleTreeUpdate(e, ranges); + this._handleTreeUpdate(e); } })); } @@ -145,7 +143,7 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi return emptyTokens; } - private _firstTreeUpdate(textModel: ITextModel, versionId: number, ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[]) { + private _firstTreeUpdate(textModel: ITextModel, versionId: number) { const tokens: TokenUpdate[] = this._createEmptyTokens(textModel); this._tokenizationStoreService.setTokens(textModel, tokens); this._setViewPortTokens(textModel, versionId); @@ -174,13 +172,13 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi oldRangeLength: newRangeEndOffset - newRangeStartOffset }; } - this._handleTreeUpdate({ ranges: rangeChanges, textModel, versionId }, ranges); + this._handleTreeUpdate({ ranges: rangeChanges, textModel, versionId }); } /** * Do not await in this method, it will cause a race */ - private _handleTreeUpdate(e: TreeUpdateEvent, ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[], note?: string) { + private _handleTreeUpdate(e: TreeUpdateEvent) { let rangeChanges: RangeChange[] = []; const chunkSize = 10000; @@ -268,7 +266,6 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi return; } const rangeChanges: RangeChange[] = new Array(rangesToRefresh.length); - const changedRanges: { readonly fromLineNumber: number; readonly toLineNumber: number }[] = new Array(rangesToRefresh.length); for (let i = 0; i < rangesToRefresh.length; i++) { const range = rangesToRefresh[i]; @@ -278,9 +275,8 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi newRangeEndOffset: range.endOffset, oldRangeLength: range.endOffset - range.startOffset }; - changedRanges[i] = { fromLineNumber: range.range.startLineNumber, toLineNumber: range.range.endLineNumber }; } - this._handleTreeUpdate({ ranges: rangeChanges, textModel, versionId: textModel.getVersionId() }, changedRanges); + this._handleTreeUpdate({ ranges: rangeChanges, textModel, versionId: textModel.getVersionId() }); } private _rangeTokensAsUpdates(rangeOffset: number, endOffsetToken: EndOffsetToken[]) { diff --git a/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts b/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts index 74b7a9e926bb..1eab214ca26a 100644 --- a/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts +++ b/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts @@ -366,10 +366,10 @@ class y { assert.strictEqual(change.versionId, 4); assert.strictEqual(change.ranges[0].newRangeStartOffset, 7); - assert.strictEqual(change.ranges[0].newRangeEndOffset, 26); + assert.strictEqual(change.ranges[0].newRangeEndOffset, 32); assert.strictEqual(change.ranges[0].newRange.startLineNumber, 2); - assert.strictEqual(change.ranges[0].newRange.endLineNumber, 6); - assert.strictEqual(change.ranges[0].oldRangeLength, 22); + assert.strictEqual(change.ranges[0].newRange.endLineNumber, 7); + assert.strictEqual(change.ranges[0].oldRangeLength, 28); updateListener?.dispose(); modelService.destroyModel(model.uri); From eb9dbc754747f88fb12acc2c9fa5b09c5871fde3 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 27 Jan 2025 10:45:57 -0800 Subject: [PATCH 0928/3587] lists: fix bugs around user selection in lists (#238879) - Only start scroll animation on a non-empty selection - Stop scroll animation as soon as the mouse is released --- src/vs/base/browser/ui/list/listView.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 0befe20b8c01..4c8af3e908e6 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -1228,9 +1228,11 @@ export class ListView implements IListView { // Selection events also don't tell us where the input doing the selection is. So, make a poor // assumption that a user is using the mouse, and base our events on that. movementStore.add(addDisposableListener(this.domNode, 'selectstart', () => { - this.setupDragAndDropScrollTopAnimation(e); - - movementStore.add(addDisposableListener(doc, 'mousemove', e => this.setupDragAndDropScrollTopAnimation(e))); + movementStore.add(addDisposableListener(doc, 'mousemove', e => { + if (doc.getSelection()?.isCollapsed === false) { + this.setupDragAndDropScrollTopAnimation(e); + } + })); // The selection is cleared either on mouseup if there's no selection, or on next mousedown // when `this.currentSelectionDisposable` is reset. @@ -1258,6 +1260,7 @@ export class ListView implements IListView { movementStore.add(addDisposableListener(doc, 'mouseup', () => { movementStore.dispose(); + this.teardownDragAndDropScrollTopAnimation(); if (doc.getSelection()?.isCollapsed !== false) { selectionStore.dispose(); From 82083046481a79d8d2a79e85b4480fe9b455652e Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 27 Jan 2025 20:03:03 +0100 Subject: [PATCH 0929/3587] apply in editor: show target in hover (#238881) --- .../browser/actions/chatCodeblockActions.ts | 51 +++++++++++++++++-- .../contrib/chat/browser/chat.contribution.ts | 3 +- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 33b9e4c323cc..2be07929199b 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -7,18 +7,23 @@ import { AsyncIterableObject } from '../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { Disposable, markAsSingleton } from '../../../../../base/common/lifecycle.js'; import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; import { CopyAction } from '../../../../../editor/contrib/clipboard/browser/clipboard.js'; -import { localize2 } from '../../../../../nls.js'; -import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { localize, localize2 } from '../../../../../nls.js'; +import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; +import { MenuEntryActionViewItem } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { Action2, MenuId, MenuItemAction, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; import { TerminalLocation } from '../../../../../platform/terminal/common/terminal.js'; +import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { IUntitledTextResourceEditorInput } from '../../../../common/editor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { accessibleViewInCodeBlock } from '../../../accessibility/browser/accessibilityConfiguration.js'; @@ -81,6 +86,44 @@ abstract class ChatCodeBlockAction extends Action2 { abstract runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext): any; } +const APPLY_IN_EDITOR_ID = 'workbench.action.chat.applyInEditor'; + +export class CodeBlockActionRendering extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'chat.codeBlockActionRendering'; + + constructor( + @IActionViewItemService actionViewItemService: IActionViewItemService, + @IInstantiationService instantiationService: IInstantiationService, + @ILabelService labelService: ILabelService, + ) { + super(); + + const disposable = actionViewItemService.register(MenuId.ChatCodeBlock, APPLY_IN_EDITOR_ID, (action, options) => { + if (!(action instanceof MenuItemAction)) { + return undefined; + } + return instantiationService.createInstance(class extends MenuEntryActionViewItem { + protected override getTooltip(): string { + const context = this._context; + if (isCodeBlockActionContext(context) && context.codemapperUri) { + const label = labelService.getUriLabel(context.codemapperUri, { relative: true }); + return localize('interactive.applyInEditorWithURL.label', "Apply in {0}", label); + } + return super.getTooltip(); + } + override setActionContext(newContext: unknown): void { + super.setActionContext(newContext); + this.updateTooltip(); + } + }, action, undefined); + }); + + // Reduces flicker a bit on reload/restart + markAsSingleton(disposable); + } +} + export function registerChatCodeBlockActions() { registerAction2(class CopyCodeBlockAction extends Action2 { constructor() { @@ -187,7 +230,7 @@ export function registerChatCodeBlockActions() { constructor() { super({ - id: 'workbench.action.chat.applyInEditor', + id: APPLY_IN_EDITOR_ID, title: localize2('interactive.applyInEditor.label', "Apply in Editor"), precondition: ChatContextKeys.enabled, f1: true, @@ -227,7 +270,7 @@ export function registerChatCodeBlockActions() { } }); - registerAction2(class SmartApplyInEditorAction extends ChatCodeBlockAction { + registerAction2(class InsertAtCursorAction extends ChatCodeBlockAction { constructor() { super({ id: 'workbench.action.chat.insertCodeBlock', diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 3dc094237e4e..b67f3b257340 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -40,7 +40,7 @@ import { IVoiceChatService, VoiceChatService } from '../common/voiceChatService. import { EditsChatAccessibilityHelp, PanelChatAccessibilityHelp, QuickChatAccessibilityHelp } from './actions/chatAccessibilityHelp.js'; import { ChatCommandCenterRendering, registerChatActions } from './actions/chatActions.js'; import { ACTION_ID_NEW_CHAT, registerNewChatActions } from './actions/chatClearActions.js'; -import { registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from './actions/chatCodeblockActions.js'; +import { CodeBlockActionRendering, registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from './actions/chatCodeblockActions.js'; import { registerChatContextActions } from './actions/chatContextActions.js'; import { registerChatCopyActions } from './actions/chatCopyActions.js'; import { registerChatDeveloperActions } from './actions/chatDeveloperActions.js'; @@ -312,6 +312,7 @@ registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointH registerWorkbenchContribution2(LanguageModelToolsExtensionPointHandler.ID, LanguageModelToolsExtensionPointHandler, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatCommandCenterRendering.ID, ChatCommandCenterRendering, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(CodeBlockActionRendering.ID, CodeBlockActionRendering, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatImplicitContextContribution.ID, ChatImplicitContextContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatRelatedFilesContribution.ID, ChatRelatedFilesContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatViewsWelcomeHandler.ID, ChatViewsWelcomeHandler, WorkbenchPhase.BlockStartup); From a6a2d96e4331539c17260e5eaca80b7804faa63c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 27 Jan 2025 11:17:08 -0800 Subject: [PATCH 0930/3587] testing: allow expanding test output diff when inline history is visible (#238883) Fixes #237850 --- .../contrib/testing/browser/testingOutputPeek.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 808e68a6ca9a..21417a0ea2e0 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -709,10 +709,16 @@ class TestResultsPeek extends PeekViewWidget { return defaultMaxHeight; } + if (this.testingPeek.historyVisible.value) { // don't cap height with the history split + return defaultMaxHeight; + } + const lineHeight = this.editor.getOption(EditorOption.lineHeight); // 41 is experimentally determined to be the overhead of the peek view itself // to avoid showing scrollbars by default in its content. - return Math.min(defaultMaxHeight || Infinity, (contentHeight + 41) / lineHeight); + const basePeekOverhead = 41; + + return Math.min(defaultMaxHeight || Infinity, (contentHeight + basePeekOverhead) / lineHeight + 1); } private applyTheme() { From b54c554bb2ea20e567c6347097f8e9fdbac166ca Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 27 Jan 2025 13:21:15 -0600 Subject: [PATCH 0931/3587] rm webpack config from terminal-suggest (#238874) --- extensions/terminal-suggest/.vscodeignore | 2 -- .../extension-browser.webpack.config.js | 25 ------------------- .../extension.webpack.config.js | 23 ----------------- extensions/terminal-suggest/package.json | 6 +++-- 4 files changed, 4 insertions(+), 52 deletions(-) delete mode 100644 extensions/terminal-suggest/extension-browser.webpack.config.js delete mode 100644 extensions/terminal-suggest/extension.webpack.config.js diff --git a/extensions/terminal-suggest/.vscodeignore b/extensions/terminal-suggest/.vscodeignore index f05a79416be0..7251a128a038 100644 --- a/extensions/terminal-suggest/.vscodeignore +++ b/extensions/terminal-suggest/.vscodeignore @@ -2,6 +2,4 @@ src/** out/** tsconfig.json .vscode/** -extension.webpack.config.js -extension-browser.webpack.config.js package-lock.json diff --git a/extensions/terminal-suggest/extension-browser.webpack.config.js b/extensions/terminal-suggest/extension-browser.webpack.config.js deleted file mode 100644 index 4b7c7460ff08..000000000000 --- a/extensions/terminal-suggest/extension-browser.webpack.config.js +++ /dev/null @@ -1,25 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -//@ts-check - -'use strict'; - -const withBrowserDefaults = require('../shared.webpack.config').browser; - -module.exports = withBrowserDefaults({ - context: __dirname, - entry: { - extension: './src/terminalSuggestMain.ts' - }, - output: { - filename: 'terminalSuggestMain.js' - }, - resolve: { - fallback: { - 'child_process': false - } - } -}); diff --git a/extensions/terminal-suggest/extension.webpack.config.js b/extensions/terminal-suggest/extension.webpack.config.js deleted file mode 100644 index 89f3ea28d874..000000000000 --- a/extensions/terminal-suggest/extension.webpack.config.js +++ /dev/null @@ -1,23 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -//@ts-check -'use strict'; - -const withDefaults = require('../shared.webpack.config'); - -module.exports = withDefaults({ - context: __dirname, - entry: { - extension: './src/terminalSuggestMain.ts' - }, - output: { - filename: 'terminalSuggestMain.js' - }, - resolve: { - mainFields: ['module', 'main'], - extensions: ['.ts', '.js'] // support ts-files and js-files - } -}); diff --git a/extensions/terminal-suggest/package.json b/extensions/terminal-suggest/package.json index 82e488dd9f52..bcf53c171c50 100644 --- a/extensions/terminal-suggest/package.json +++ b/extensions/terminal-suggest/package.json @@ -22,7 +22,6 @@ "compile": "npx gulp compile-extension:terminal-suggest", "watch": "npx gulp watch-extension:terminal-suggest" }, - "main": "./out/terminalSuggestMain", "activationEvents": [ "onTerminalCompletionsRequested" @@ -30,5 +29,8 @@ "repository": { "type": "git", "url": "https://github.com/microsoft/vscode.git" - } + }, + "extensionKind": [ + "workspace" + ] } From 6b26184402ce939ab27a58ffd95b4af92f1d18e7 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:38:55 -0800 Subject: [PATCH 0932/3587] fix: unclear setting indicator label content Fixes #235139 --- .../preferences/browser/settingsEditorSettingIndicators.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts index 76b3878ed851..e2fe1833fe37 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts @@ -141,7 +141,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { const disposables = new DisposableStore(); const workspaceTrustElement = $('span.setting-indicator.setting-item-workspace-trust'); const workspaceTrustLabel = disposables.add(new SimpleIconLabel(workspaceTrustElement)); - workspaceTrustLabel.text = '$(warning) ' + localize('workspaceUntrustedLabel', "Setting value not applied"); + workspaceTrustLabel.text = '$(shield) ' + localize('workspaceUntrustedLabel', "Requires workspace trust"); const content = localize('trustLabel', "The setting value can only be applied in a trusted workspace."); const showHover = (focus: boolean) => { @@ -370,8 +370,8 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { this.scopeOverridesIndicator.element.style.display = 'inline'; this.scopeOverridesIndicator.element.classList.add('setting-indicator'); - this.scopeOverridesIndicator.label.text = '$(warning) ' + localize('policyLabelText', "Setting value not applied"); - const content = localize('policyDescription', "This setting is managed by your organization and its applied value cannot be changed."); + this.scopeOverridesIndicator.label.text = '$(briefcase) ' + localize('policyLabelText', "Managed by organization"); + const content = localize('policyDescription', "This setting is managed by your organization and its actual value cannot be changed."); const showHover = (focus: boolean) => { return this.hoverService.showHover({ ...this.defaultHoverOptions, From 0a964fb4f89d1cd99f59d296fc7762f0c1ad4f12 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:38:57 -0800 Subject: [PATCH 0933/3587] Clarify setting types (#238885) --- src/vs/workbench/contrib/preferences/common/preferences.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/common/preferences.ts b/src/vs/workbench/contrib/preferences/common/preferences.ts index b681d7d6787d..10244f4354b3 100644 --- a/src/vs/workbench/contrib/preferences/common/preferences.ts +++ b/src/vs/workbench/contrib/preferences/common/preferences.ts @@ -176,5 +176,5 @@ export function compareTwoNullableNumbers(a: number | undefined, b: number | und } } -export const PREVIEW_INDICATOR_DESCRIPTION = localize('previewIndicatorDescription', "This setting controls a new feature that is still under refinement yet ready to use. Feedback is welcome."); -export const EXPERIMENTAL_INDICATOR_DESCRIPTION = localize('experimentalIndicatorDescription', "This setting controls a new feature that is actively being developed and may be unstable. It is subject to change or removal."); +export const PREVIEW_INDICATOR_DESCRIPTION = localize('previewIndicatorDescription', "Preview setting: this setting controls a new feature that is still under refinement yet ready to use. Feedback is welcome."); +export const EXPERIMENTAL_INDICATOR_DESCRIPTION = localize('experimentalIndicatorDescription', "Experimental setting: this setting controls a new feature that is actively being developed and may be unstable. It is subject to change or removal."); From 6e9587290ae15e095afadbcd5c5e458dc620002e Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 27 Jan 2025 15:09:21 -0600 Subject: [PATCH 0934/3587] tweak condition to terminal suggest hiding bug (#238894) tweak condition to fix bug --- .../suggest/browser/terminal.suggest.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 7be1f75e05b0..7773917c9a92 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -155,7 +155,7 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo this.add(dom.addDisposableListener(this._ctx.instance.domElement, dom.EventType.FOCUS_OUT, (e) => { const focusedElement = e.relatedTarget as HTMLElement; - if (focusedElement?.className === SuggestDetailsClassName) { + if (focusedElement?.classList.contains(SuggestDetailsClassName)) { // Don't hide the suggest widget if the focus is moving to the details return; } From 5506dbf6f621a1db7e9d619117b08a5539277eba Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 27 Jan 2025 22:46:17 +0100 Subject: [PATCH 0935/3587] [json] update service (#238898) --- .../json-language-features/server/package-lock.json | 8 ++++---- extensions/json-language-features/server/package.json | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/extensions/json-language-features/server/package-lock.json b/extensions/json-language-features/server/package-lock.json index 88f311456259..384ce045c9c6 100644 --- a/extensions/json-language-features/server/package-lock.json +++ b/extensions/json-language-features/server/package-lock.json @@ -12,7 +12,7 @@ "@vscode/l10n": "^0.0.18", "jsonc-parser": "^3.3.1", "request-light": "^0.8.0", - "vscode-json-languageservice": "^5.4.2", + "vscode-json-languageservice": "^5.4.3", "vscode-languageserver": "^10.0.0-next.11", "vscode-uri": "^3.0.8" }, @@ -64,9 +64,9 @@ "dev": true }, "node_modules/vscode-json-languageservice": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.4.2.tgz", - "integrity": "sha512-2qujUseKRbLEwLXvEOFAxaz3y1ssdNCXXi95LRdG8AFchJHSnmI2qCg9ixoYxbJtSehIrXOmkhV87Y9lIivOgQ==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.4.3.tgz", + "integrity": "sha512-NVSEQDloP9NYccuqKg4eI46kutZpwucBY4csBB6FCxbM7AZVoBt0oxTItPVA+ZwhnG1bg/fmiBRAwcGJyNQoPA==", "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index 0253274289a8..6dcd82930d23 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -15,7 +15,7 @@ "@vscode/l10n": "^0.0.18", "jsonc-parser": "^3.3.1", "request-light": "^0.8.0", - "vscode-json-languageservice": "^5.4.2", + "vscode-json-languageservice": "^5.4.3", "vscode-languageserver": "^10.0.0-next.11", "vscode-uri": "^3.0.8" }, @@ -29,6 +29,7 @@ "watch": "npx gulp watch-extension:json-language-features-server", "clean": "../../../node_modules/.bin/rimraf out", "install-service-next": "npm install vscode-json-languageservice@next", + "install-service-latest": "npm install vscode-json-languageservice", "install-service-local": "npm link vscode-json-languageservice", "install-server-next": "npm install vscode-languageserver@next", "install-server-local": "npm link vscode-languageserver-server", From 287947bfe7ff52c90f26eb51fe145b9cc0f05028 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 27 Jan 2025 16:10:45 -0600 Subject: [PATCH 0936/3587] Set `isFile/isDirectory` for completion items (#238900) Set isFile/isDirectory for completion items Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> --- .../suggest/browser/terminalCompletionService.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index c7f4c3130698..3589d24a5bd0 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -12,7 +12,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; -import { TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; +import { GeneralShellType, TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; import { ISimpleCompletion } from '../../../../services/suggest/browser/simpleCompletionItem.js'; import { TerminalSuggestSettingId } from '../common/terminalSuggestConfiguration.js'; @@ -178,6 +178,10 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return undefined; } const completionItems = Array.isArray(completions) ? completions : completions.items ?? []; + for (const completion of completionItems) { + completion.isFile ??= completion.kind === TerminalCompletionItemKind.File || (shellType === GeneralShellType.PowerShell && completion.kind === TerminalCompletionItemKind.Method && completion.replacementIndex === 0); + completion.isDirectory ??= completion.kind === TerminalCompletionItemKind.Folder; + } if (provider.isBuiltin) { //TODO: why is this needed? for (const item of completionItems) { From ba815c5c8200509b3028214cf728d31740d9cf86 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 27 Jan 2025 14:40:37 -0800 Subject: [PATCH 0937/3587] eng: rev TPN for 1.97 (#238902) --- ThirdPartyNotices.txt | 112 +-- cli/ThirdPartyNotices.txt | 1435 ++++++++++++++++++++++++++++++++++--- package.json | 2 +- 3 files changed, 1417 insertions(+), 132 deletions(-) diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 7f8ba3ad157d..a0026469915f 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -573,7 +573,7 @@ to the base-name name of the original file, and an extension of txt, html, or si --------------------------------------------------------- -go-syntax 0.7.8 - MIT +go-syntax 0.7.9 - MIT https://github.com/worlpaker/go-syntax MIT License @@ -1562,6 +1562,22 @@ SOFTWARE. --------------------------------------------------------- +RedCMD/YAML-Syntax-Highlighter 1.3.2 - MIT +https://github.com/RedCMD/YAML-Syntax-Highlighter + +MIT License + +Copyright 2024 RedCMD + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + redhat-developer/vscode-java 1.26.0 - MIT https://github.com/redhat-developer/vscode-java @@ -1735,6 +1751,55 @@ SOFTWARE. --------------------------------------------------------- +Shopify/ruby-lsp 0.0.0 - MIT License +https://github.com/Shopify/ruby-lsp + +The MIT License (MIT) + +Copyright (c) 2022-present, Shopify Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +================================================================================ +The following files and related configuration in package.json are based on a +sequence of adaptions: grammars/ruby.cson.json, grammars/erb.cson.json, +languages/erb.json. + +Copyright (c) 2016 Peng Lv +Copyright (c) 2017-2019 Stafford Brunk +https://github.com/rubyide/vscode-ruby + + Released under the MIT license + https://github.com/rubyide/vscode-ruby/blob/main/LICENSE.txt + +Copyright (c) 2014 GitHub Inc. +https://github.com/atom/language-ruby + + Released under the MIT license + https://github.com/atom/language-ruby/blob/master/LICENSE.md + +https://github.com/textmate/ruby.tmbundle + https://github.com/textmate/ruby.tmbundle#license +--------------------------------------------------------- + +--------------------------------------------------------- + sumneko/lua.tmbundle 1.0.0 - TextMate Bundle License https://github.com/sumneko/lua.tmbundle @@ -1964,51 +2029,6 @@ to the base-name name of the original file, and an extension of txt, html, or si --------------------------------------------------------- -textmate/ruby.tmbundle 0.0.0 - TextMate Bundle License -https://github.com/textmate/ruby.tmbundle - -Copyright (c) textmate-ruby.tmbundle project authors - -If not otherwise specified (see below), files in this folder fall under the following license: - -Permission to copy, use, modify, sell and distribute this -software is granted. This software is provided "as is" without -express or implied warranty, and with no claim as to its -suitability for any purpose. - -An exception is made for files in readable text which contain their own license information, -or files where an accompanying file exists (in the same directory) with a "-license" suffix added -to the base-name name of the original file, and an extension of txt, html, or similar. For example -"tidy" is accompanied by "tidy-license.txt". ---------------------------------------------------------- - ---------------------------------------------------------- - -textmate/yaml.tmbundle 0.0.0 - TextMate Bundle License -https://github.com/textmate/yaml.tmbundle - -Copyright (c) 2015 FichteFoll - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - trond-snekvik/vscode-rst 1.5.3 - MIT https://github.com/trond-snekvik/vscode-rst diff --git a/cli/ThirdPartyNotices.txt b/cli/ThirdPartyNotices.txt index 9f51f4c7be6d..03ac5838391f 100644 --- a/cli/ThirdPartyNotices.txt +++ b/cli/ThirdPartyNotices.txt @@ -1741,7 +1741,7 @@ SOFTWARE. deranged 0.3.11 - MIT OR Apache-2.0 https://github.com/jhpratt/deranged -Copyright (c) 2022 Jacob Pratt et al. +Copyright (c) 2024 Jacob Pratt et al. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -1928,6 +1928,36 @@ SOFTWARE. --------------------------------------------------------- +displaydoc 0.2.5 - MIT OR Apache-2.0 +https://github.com/yaahc/displaydoc + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + encode_unicode 0.3.6 - MIT/Apache-2.0 https://github.com/tormol/encode_unicode @@ -3513,7 +3543,537 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -idna 0.5.0 - MIT OR Apache-2.0 +icu_collections 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_locid 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_locid_transform 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_locid_transform_data 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_normalizer 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_normalizer_data 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_properties 1.5.1 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_properties_data 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_provider 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +icu_provider_macros 1.5.0 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +idna 1.0.3 - MIT OR Apache-2.0 https://github.com/servo/rust-url/ Copyright (c) 2013-2022 The rust-url developers @@ -3545,6 +4105,38 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- +idna_adapter 1.2.0 - Apache-2.0 OR MIT +https://github.com/hsivonen/idna_adapter + +Copyright (c) The rust-url developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + indexmap 1.9.3 - Apache-2.0 OR MIT indexmap 2.2.6 - Apache-2.0 OR MIT https://github.com/indexmap-rs/indexmap @@ -4101,6 +4693,59 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- +litemap 0.7.4 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + lock_api 0.4.12 - MIT OR Apache-2.0 https://github.com/Amanieu/parking_lot @@ -8359,6 +9004,38 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- +stable_deref_trait 1.2.0 - MIT/Apache-2.0 +https://github.com/storyyeller/stable_deref_trait + +Copyright (c) 2017 Robert Grosse + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + static_assertions 1.1.0 - MIT OR Apache-2.0 https://github.com/nvzqz/static-assertions-rs @@ -8667,14 +9344,30 @@ Apache License --------------------------------------------------------- -sysinfo 0.29.11 - MIT -https://github.com/GuillaumeGomez/sysinfo +synstructure 0.13.1 - MIT +https://github.com/mystor/synstructure The MIT License (MIT) -Copyright (c) 2015 Guillaume Gomez +Copyright 2016 Nika Layzell -Permission is hereby granted, free of charge, to any person obtaining a copy +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +sysinfo 0.29.11 - MIT +https://github.com/GuillaumeGomez/sysinfo + +The MIT License (MIT) + +Copyright (c) 2015 Guillaume Gomez + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell @@ -8935,42 +9628,55 @@ SOFTWARE. --------------------------------------------------------- -tinyvec 1.6.0 - Zlib OR Apache-2.0 OR MIT -https://github.com/Lokathor/tinyvec +tinystr 0.7.6 - Unicode-3.0 +https://github.com/unicode-org/icu4x -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +UNICODE LICENSE V3 -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +COPYRIGHT AND PERMISSION NOTICE -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- +Copyright © 2020-2024 Unicode, Inc. ---------------------------------------------------------- +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. -tinyvec_macros 0.1.1 - MIT OR Apache-2.0 OR Zlib -https://github.com/Soveu/tinyvec_macros +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. -MIT License +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. -Copyright (c) 2020 Soveu +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +SPDX-License-Identifier: Unicode-3.0 -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. --------------------------------------------------------- --------------------------------------------------------- @@ -9458,10 +10164,8 @@ MIT License --------------------------------------------------------- -unicode-bidi 0.3.15 - MIT OR Apache-2.0 -https://github.com/servo/unicode-bidi - -Copyright (c) 2015 The Rust Project Developers +unicode-ident 1.0.12 - (MIT OR Apache-2.0) AND Unicode-DFS-2016 +https://github.com/dtolnay/unicode-ident Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -9490,8 +10194,10 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -unicode-ident 1.0.12 - (MIT OR Apache-2.0) AND Unicode-DFS-2016 -https://github.com/dtolnay/unicode-ident +unicode-width 0.1.12 - MIT OR Apache-2.0 +https://github.com/unicode-rs/unicode-width + +Copyright (c) 2015 The Rust Project Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -9520,8 +10226,8 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -unicode-normalization 0.1.23 - MIT/Apache-2.0 -https://github.com/unicode-rs/unicode-normalization +unicode-xid 0.2.4 - MIT OR Apache-2.0 +https://github.com/unicode-rs/unicode-xid Copyright (c) 2015 The Rust Project Developers @@ -9552,10 +10258,10 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -unicode-width 0.1.12 - MIT OR Apache-2.0 -https://github.com/unicode-rs/unicode-width +url 2.5.4 - MIT OR Apache-2.0 +https://github.com/servo/rust-url -Copyright (c) 2015 The Rust Project Developers +Copyright (c) 2013-2022 The rust-url developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -9584,10 +10290,37 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -unicode-xid 0.2.4 - MIT OR Apache-2.0 -https://github.com/unicode-rs/unicode-xid +urlencoding 2.1.3 - MIT +https://github.com/kornelski/rust_urlencoding -Copyright (c) 2015 The Rust Project Developers +The MIT License (MIT) + +© 2016 Bertram Truong +© 2021 Kornel Lesiński + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +utf-8 0.7.6 - MIT OR Apache-2.0 +https://github.com/SimonSapin/rust-utf8 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -9616,10 +10349,10 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -url 2.5.0 - MIT OR Apache-2.0 -https://github.com/servo/rust-url +utf16_iter 1.0.5 - Apache-2.0 OR MIT +https://github.com/hsivonen/utf16_iter -Copyright (c) 2013-2022 The rust-url developers +Copyright Mozilla Foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -9648,37 +10381,10 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -urlencoding 2.1.3 - MIT -https://github.com/kornelski/rust_urlencoding +utf8_iter 1.0.4 - Apache-2.0 OR MIT +https://github.com/hsivonen/utf8_iter -The MIT License (MIT) - -© 2016 Bertram Truong -© 2021 Kornel Lesiński - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - -utf-8 0.7.6 - MIT OR Apache-2.0 -https://github.com/SimonSapin/rust-utf8 +Copyright Mozilla Foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -10624,6 +11330,91 @@ THE SOFTWARE. --------------------------------------------------------- +write16 1.0.0 - Apache-2.0 OR MIT +https://github.com/hsivonen/write16 + +Copyright Mozilla Foundation + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +writeable 0.5.5 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + xattr 1.3.1 - MIT/Apache-2.0 https://github.com/Stebalien/xattr @@ -10702,10 +11493,142 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- +yoke 0.7.5 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +yoke-derive 0.7.5 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + zbus 3.15.2 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- @@ -10713,7 +11636,33 @@ LICENSE-MIT zbus_macros 3.15.2 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- @@ -10721,7 +11670,139 @@ LICENSE-MIT zbus_names 2.6.1 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +zerofrom 0.1.5 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +zerofrom-derive 0.1.5 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. --------------------------------------------------------- --------------------------------------------------------- @@ -10780,6 +11861,112 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- +zerovec 0.10.4 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + +zerovec-derive 0.10.3 - Unicode-3.0 +https://github.com/unicode-org/icu4x + +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +--------------------------------------------------------- + +--------------------------------------------------------- + zip 0.6.6 - MIT https://github.com/zip-rs/zip2 @@ -10814,7 +12001,33 @@ licences; see files named LICENSE.*.txt for details. zvariant 3.15.2 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- @@ -10822,7 +12035,33 @@ LICENSE-MIT zvariant_derive 3.15.2 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- @@ -10830,5 +12069,31 @@ LICENSE-MIT zvariant_utils 1.0.1 - MIT https://github.com/dbus2/zbus/ -LICENSE-MIT +The MIT License (MIT) + +Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. --------------------------------------------------------- \ No newline at end of file diff --git a/package.json b/package.json index 2c28df7f5e36..9a56d08cebf9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.97.0", - "distro": "69135fca1356098ff5746c9225339cdd52f21844", + "distro": "c504aa66a94bcd910e273dbbf66b2c3a9f0343f8", "author": { "name": "Microsoft Corporation" }, From 0628d3d55fc0e3fc33b377b91d5ffce748b31949 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 28 Jan 2025 00:15:21 +0100 Subject: [PATCH 0938/3587] Improve UI rendering and accessibility features (#238903) * small UI rendering improvements * :lipstick: --- .../browser/view/inlineEdits/gutterIndicatorView.ts | 6 +++++- .../browser/view/inlineEdits/inlineDiffView.ts | 2 +- .../inlineCompletions/browser/view/inlineEdits/view.css | 4 ++++ .../inlineCompletions/browser/view/inlineEdits/view.ts | 3 ++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index 9d392a863efb..6aab8e5a303d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -24,6 +24,7 @@ import { GutterIndicatorMenuContent } from './gutterIndicatorMenu.js'; import { mapOutFalsy, n, rectToProps } from './utils.js'; import { localize } from '../../../../../../nls.js'; import { trackFocus } from '../../../../../../base/browser/dom.js'; +import { IAccessibilityService } from '../../../../../../platform/accessibility/common/accessibility.js'; export const inlineEditIndicatorPrimaryForeground = registerColor( 'inlineEdit.gutterIndicator.primaryForeground', buttonForeground, @@ -77,6 +78,7 @@ export class InlineEditsGutterIndicator extends Disposable { private readonly _focusIsInMenu: ISettableObservable, @IHoverService private readonly _hoverService: HoverService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IAccessibilityService accessibilityService: IAccessibilityService, ) { super(); @@ -88,7 +90,9 @@ export class InlineEditsGutterIndicator extends Disposable { })); this._register(autorun(reader => { - this._indicator.element.classList.toggle('wiggle', this._isHoveringOverInlineEdit.read(reader)); + if (!accessibilityService.isMotionReduced()) { + this._indicator.element.classList.toggle('wiggle', this._isHoveringOverInlineEdit.read(reader)); + } })); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts index 22f7a866254c..aa4a49831e79 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts @@ -159,7 +159,7 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE }); for (const m of diff.diff) { - const showFullLineDecorations = true; + const showFullLineDecorations = diff.mode !== 'sideBySide'; if (showFullLineDecorations) { if (!m.original.isEmpty) { originalDecorations.push({ diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index 350036eb214e..747e0c0e50cd 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -132,6 +132,10 @@ border: none; } } + + .monaco-editor-background { + background-color: var(--vscode-inlineEdit-modifiedChangedLineBackground) + } } .inline-edits-view-zone.diagonal-fill { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 7d09c4340a9e..b401367da279 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -246,7 +246,8 @@ export class InlineEditsView extends Disposable { return 'insertionInline'; } - if (inner.every(m => newText.getValueOfRange(m.modifiedRange).trim() === '' && edit.originalText.getValueOfRange(m.originalRange).trim() !== '')) { + const innerValues = inner.map(m => ({ original: newText.getValueOfRange(m.originalRange), modified: newText.getValueOfRange(m.modifiedRange) })); + if (innerValues.every(({ original, modified }) => modified.trim() === '' && original.length > 0 && (original.length > modified.length || original.trim() !== ''))) { return 'deletion'; } From c1b409b421ef6a806f5ac6feaaac56ea01f73cfa Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 27 Jan 2025 16:02:07 -0800 Subject: [PATCH 0939/3587] Clear edit session when toggling agent mode (#238904) * Clear edit session when toggling agent mode And add agent-specific welcome message * Message --- .../chat/browser/actions/chatClearActions.ts | 4 +- .../browser/actions/chatExecuteActions.ts | 44 ++++++++++++++-- .../browser/chatEditing/chatEditingActions.ts | 51 ++++++++++--------- .../contrib/chat/browser/chatEditorActions.ts | 21 ++++---- .../contrib/chat/browser/chatInputPart.ts | 4 +- .../contrib/chat/browser/chatWidget.ts | 22 +++++--- .../viewsWelcome/chatViewWelcomeController.ts | 11 ++-- 7 files changed, 105 insertions(+), 52 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index 40ac18994392..87d33948281a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -26,8 +26,8 @@ import { CHAT_CATEGORY } from './chatActions.js'; import { clearChatEditor } from './chatClear.js'; export const ACTION_ID_NEW_CHAT = `workbench.action.chat.newChat`; - export const ACTION_ID_NEW_EDIT_SESSION = `workbench.action.chat.newEditSession`; +export const ChatDoneActionId = 'workbench.action.chat.done'; export function registerNewChatActions() { registerAction2(class NewChatEditorAction extends Action2 { @@ -211,7 +211,7 @@ export function registerNewChatActions() { registerAction2(class GlobalEditsDoneAction extends Action2 { constructor() { super({ - id: 'workbench.action.chat.done', + id: ChatDoneActionId, title: localize2('chat.done.label', "Done"), category: CHAT_CATEGORY, precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.editingParticipantRegistered), diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 9e979f0038eb..d82e82971b0e 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -8,6 +8,7 @@ import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize, localize2 } from '../../../../../nls.js'; import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -15,12 +16,14 @@ import { KeybindingWeight } from '../../../../../platform/keybinding/common/keyb import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { applyingChatEditsContextKey, IChatEditingService } from '../../common/chatEditingService.js'; +import { applyingChatEditsContextKey, IChatEditingService, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { chatAgentLeader, extractAgentAndCommand } from '../../common/chatParserTypes.js'; import { IChatService } from '../../common/chatService.js'; import { EditsViewId, IChatWidget, IChatWidgetService } from '../chat.js'; +import { discardAllEditsWithConfirmation } from '../chatEditing/chatEditingActions.js'; import { ChatViewPane } from '../chatViewPane.js'; import { CHAT_CATEGORY } from './chatActions.js'; +import { ChatDoneActionId } from './chatClearActions.js'; export interface IVoiceChatExecuteActionContext { readonly disableTimeout?: boolean; @@ -96,7 +99,9 @@ export class ToggleAgentModeAction extends Action2 { title: localize2('interactive.toggleAgent.label', "Toggle Agent Mode (Experimental)"), f1: true, category: CHAT_CATEGORY, - precondition: ChatContextKeys.Editing.hasToolsAgent, + precondition: ContextKeyExpr.and( + ChatContextKeys.Editing.hasToolsAgent, + ChatContextKeys.requestInProgress.negate()), toggled: { condition: ChatContextKeys.Editing.agentMode, tooltip: localize('agentEnabled', "Agent Mode Enabled (Experimental)"), @@ -122,13 +127,44 @@ export class ToggleAgentModeAction extends Action2 { }); } - override run(accessor: ServicesAccessor, ...args: any[]): void { + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { const agentService = accessor.get(IChatAgentService); + const chatEditingService = accessor.get(IChatEditingService); + const chatService = accessor.get(IChatService); + const commandService = accessor.get(ICommandService); + const dialogService = accessor.get(IDialogService); + const currentEditingSession = chatEditingService.currentEditingSession; + if (!currentEditingSession) { + return; + } + + const entries = currentEditingSession.entries.get(); + if (entries.length > 0 && entries.some(entry => entry.state.get() === WorkingSetEntryState.Modified)) { + if (!await discardAllEditsWithConfirmation(accessor)) { + // User cancelled + return; + } + } else { + const chatSession = chatService.getSession(currentEditingSession.chatSessionId); + if (chatSession?.getRequests().length) { + const confirmation = await dialogService.confirm({ + title: localize('agent.newSession', "Start new session?"), + message: localize('agent.newSessionMessage', "Toggling agent mode will start a new session. Would you like to continue?"), + primaryButton: localize('agent.newSession.confirm', "Yes"), + type: 'info' + }); + if (!confirmation.confirmed) { + return; + } + } + } + agentService.toggleToolsAgentMode(); + await commandService.executeCommand(ChatDoneActionId); } } -export const ToggleRequestPausedActionId = 'workbench.action.chat.toggleRequestPausd'; +export const ToggleRequestPausedActionId = 'workbench.action.chat.toggleRequestPaused'; export class ToggleRequestPausedAction extends Action2 { static readonly ID = ToggleRequestPausedActionId; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 3abcbda8766b..76f056f72b0c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -323,33 +323,38 @@ export class ChatEditingDiscardAllAction extends Action2 { } async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const chatEditingService = accessor.get(IChatEditingService); - const dialogService = accessor.get(IDialogService); - const currentEditingSession = chatEditingService.currentEditingSession; - if (!currentEditingSession) { - return; - } + await discardAllEditsWithConfirmation(accessor); + } +} +registerAction2(ChatEditingDiscardAllAction); - // Ask for confirmation if there are any edits - const entries = currentEditingSession.entries.get(); - if (entries.length > 0) { - const confirmation = await dialogService.confirm({ - title: localize('chat.editing.discardAll.confirmation.title', "Discard all edits?"), - message: entries.length === 1 - ? localize('chat.editing.discardAll.confirmation.oneFile', "This will undo changes made by {0} in {1}. Do you want to proceed?", 'Copilot Edits', basename(entries[0].modifiedURI)) - : localize('chat.editing.discardAll.confirmation.manyFiles', "This will undo changes made by {0} in {1} files. Do you want to proceed?", 'Copilot Edits', entries.length), - primaryButton: localize('chat.editing.discardAll.confirmation.primaryButton', "Yes"), - type: 'info' - }); - if (!confirmation.confirmed) { - return; - } +export async function discardAllEditsWithConfirmation(accessor: ServicesAccessor): Promise { + const chatEditingService = accessor.get(IChatEditingService); + const dialogService = accessor.get(IDialogService); + const currentEditingSession = chatEditingService.currentEditingSession; + if (!currentEditingSession) { + return false; + } + + // Ask for confirmation if there are any edits + const entries = currentEditingSession.entries.get(); + if (entries.length > 0) { + const confirmation = await dialogService.confirm({ + title: localize('chat.editing.discardAll.confirmation.title', "Discard all edits?"), + message: entries.length === 1 + ? localize('chat.editing.discardAll.confirmation.oneFile', "This will undo changes made by {0} in {1}. Do you want to proceed?", 'Copilot Edits', basename(entries[0].modifiedURI)) + : localize('chat.editing.discardAll.confirmation.manyFiles', "This will undo changes made by {0} in {1} files. Do you want to proceed?", 'Copilot Edits', entries.length), + primaryButton: localize('chat.editing.discardAll.confirmation.primaryButton', "Yes"), + type: 'info' + }); + if (!confirmation.confirmed) { + return false; } - - await currentEditingSession.reject(); } + + await currentEditingSession.reject(); + return true; } -registerAction2(ChatEditingDiscardAllAction); export class ChatEditingRemoveAllFilesAction extends Action2 { static readonly ID = 'chatEditing.clearWorkingSet'; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts index 15e0a659f959..23203029af91 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts @@ -327,17 +327,16 @@ export function registerChatEditorActions() { registerAction2(RejectHunkAction); registerAction2(OpenDiffAction); + MenuRegistry.appendMenuItem(MenuId.ChatEditingEditorContent, { + command: { + id: navigationBearingFakeActionId, + title: localize('label', "Navigation Status"), + precondition: ContextKeyExpr.false(), + }, + group: 'navigate', + order: -1, + when: ctxReviewModeEnabled, + }); } export const navigationBearingFakeActionId = 'chatEditor.navigation.bearings'; - -MenuRegistry.appendMenuItem(MenuId.ChatEditingEditorContent, { - command: { - id: navigationBearingFakeActionId, - title: localize('label', "Navigation Status"), - precondition: ContextKeyExpr.false(), - }, - group: 'navigate', - order: -1, - when: ctxReviewModeEnabled, -}); diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 7aa61f4e42b1..848e59cff300 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -823,7 +823,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this.instantiationService.createInstance(ModelPickerActionViewItem, action, this._currentLanguageModel, itemDelegate, { hoverDelegate: options.hoverDelegate, keybinding: options.keybinding ?? undefined }); } } else if (action.id === ToggleAgentModeActionId && action instanceof MenuItemAction) { - return this.instantiationService.createInstance(ToggleAgentCheckActionViewItem, action, options as IMenuEntryActionViewItemOptions); + return this.instantiationService.createInstance(ToggleAgentActionViewItem, action, options as IMenuEntryActionViewItemOptions); } return undefined; @@ -1700,7 +1700,7 @@ class ModelPickerActionViewItem extends MenuEntryActionViewItem { const chatInputEditorContainerSelector = '.interactive-input-editor'; setupSimpleEditorSelectionStyling(chatInputEditorContainerSelector); -class ToggleAgentCheckActionViewItem extends MenuEntryActionViewItem { +class ToggleAgentActionViewItem extends MenuEntryActionViewItem { private readonly agentStateActions: IAction[]; constructor( diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index f60446e7b9c5..92f03d8e58b8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -571,8 +571,14 @@ export class ChatWidget extends Disposable implements IChatWidget { } private renderWelcomeViewContentIfNeeded() { + if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal') { + return; + } + + const numItems = this.viewModel?.getItems().length ?? 0; const welcomeContent = this.viewModel?.model.welcomeMessage ?? this.persistedWelcomeMessage; - if (welcomeContent && this.welcomeMessageContainer.children.length === 0 && !this.viewOptions.renderStyle) { + if (welcomeContent && !numItems && (this.welcomeMessageContainer.children.length === 0 || this.location === ChatAgentLocation.EditingSession)) { + dom.clearNode(this.welcomeMessageContainer); const tips = this.viewOptions.supportsAdditionalParticipants ? new MarkdownString(localize('chatWidget.tips', "{0} or type {1} to attach context\n\n{2} to chat with extensions\n\nType {3} to use commands", '$(attach)', '#', '$(mention)', '/'), { supportThemeIcons: true }) : new MarkdownString(localize('chatWidget.tips.withoutParticipants', "{0} or type {1} to attach context", '$(attach)', '#'), { supportThemeIcons: true }); @@ -584,10 +590,9 @@ export class ChatWidget extends Disposable implements IChatWidget { dom.append(this.welcomeMessageContainer, welcomePart.element); } - if (!this.viewOptions.renderStyle && this.viewModel) { - const treeItems = this.viewModel.getItems(); - dom.setVisibility(treeItems.length === 0, this.welcomeMessageContainer); - dom.setVisibility(treeItems.length !== 0, this.listContainer); + if (this.viewModel) { + dom.setVisibility(numItems === 0, this.welcomeMessageContainer); + dom.setVisibility(numItems !== 0, this.listContainer); } } @@ -852,7 +857,12 @@ export class ChatWidget extends Disposable implements IChatWidget { this.parsedChatRequest = undefined; this.updateChatInputContext(); })); - this._register(this.chatAgentService.onDidChangeAgents(() => this.parsedChatRequest = undefined)); + this._register(this.chatAgentService.onDidChangeAgents(() => { + this.parsedChatRequest = undefined; + + // Tools agent loads -> welcome content changes + this.renderWelcomeViewContentIfNeeded(); + })); } private onDidStyleChange(): void { diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index 6942c082ded8..2e40bd0232f6 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -7,7 +7,7 @@ import * as dom from '../../../../../base/browser/dom.js'; import { Button } from '../../../../../base/browser/ui/button/button.js'; import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Event } from '../../../../../base/common/event.js'; -import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; @@ -17,7 +17,7 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { ILogService } from '../../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; -import { ChatAgentLocation } from '../../common/chatAgents.js'; +import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { chatViewsWelcomeRegistry, IChatViewsWelcomeDescriptor } from './chatViewsWelcome.js'; const $ = dom.$; @@ -127,6 +127,7 @@ export class ChatViewWelcomePart extends Disposable { @IOpenerService private openerService: IOpenerService, @IInstantiationService private instantiationService: IInstantiationService, @ILogService private logService: ILogService, + @IChatAgentService chatAgentService: IChatAgentService, ) { super(); this.element = dom.$('.chat-welcome-view'); @@ -145,9 +146,11 @@ export class ChatViewWelcomePart extends Disposable { title.textContent = content.title; // Preview indicator - if (options?.location === ChatAgentLocation.EditingSession && typeof content.message !== 'function') { + if (options?.location === ChatAgentLocation.EditingSession && typeof content.message !== 'function' && chatAgentService.toolsAgentModeEnabled) { + // Override welcome message for the agent. Sort of a hack, should it come from the participant? This case is different because the welcome content typically doesn't change per ChatWidget + content.message = new MarkdownString('Ask Copilot to edit your files in agent mode. Copilot will automatically use multiple requests to pick files to edit, run terminal commands, and iterate on errors.\n\nCopilot is powered by AI, so mistakes are possible. Review output carefully before use.'); const featureIndicator = dom.append(this.element, $('.chat-welcome-view-indicator')); - featureIndicator.textContent = localize('preview', 'PREVIEW'); + featureIndicator.textContent = localize('preview', "PREVIEW"); } // Message From 591badb5f7c07100d5bd97d0e0abc1c2727e8c7d Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:05:11 -0800 Subject: [PATCH 0940/3587] Fix #197755 (#238905) --- src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index a8111d15aad6..5806978d04ae 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -539,6 +539,7 @@ export class SettingsEditor2 extends EditorPane { // Wait for editor to be removed from DOM #106303 setTimeout(() => { this.searchWidget.onHide(); + this.settingRenderers.cancelSuggesters(); }, 0); } } From cc2e969e85e1052b495706e8bd7b21f3e8e8507b Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:20:50 -0800 Subject: [PATCH 0941/3587] fix: Settings editor left pane shows up too often (#238906) Fixes #196468 --- .../contrib/preferences/browser/settingsEditor2.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 5806978d04ae..48f25cff467c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -1597,10 +1597,7 @@ export class SettingsEditor2 extends EditorPane { separatorBorder: Color.transparent }); } else { - this.splitView.setViewVisible(0, true); - this.splitView.style({ - separatorBorder: this.theme.getColor(settingsSashBorder)! - }); + this.layoutSplitView(this.dimension); } } @@ -1659,8 +1656,7 @@ export class SettingsEditor2 extends EditorPane { this.refreshTOCTree(); this.renderResultCountMessages(); this.refreshTree(); - // Always show the ToC when leaving search mode - this.splitView.setViewVisible(0, true); + this.layoutSplitView(this.dimension); } } progressRunner.done(); From a38aa34f90427f660e0f737280144bc2ad9b4ed6 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 27 Jan 2025 16:58:41 -0800 Subject: [PATCH 0942/3587] Add setting: chat.agent.maxRequests (#238909) --- .../contrib/chat/browser/chatSetup.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 3751be60c74e..d968b47c475f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -64,7 +64,7 @@ import { ExtensionUrlHandlerOverrideRegistry } from '../../../services/extension import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationNode } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; import { equalsIgnoreCase } from '../../../../base/common/strings.js'; @@ -119,7 +119,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr @IInstantiationService private readonly instantiationService: IInstantiationService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @ICommandService private readonly commandService: ICommandService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); @@ -137,6 +137,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr this.registerChatWelcome(controller, context); this.registerActions(controller, context, requests); this.registerUrlLinkHandler(); + this.registerSetting(context); } private registerChatWelcome(controller: Lazy, context: ChatSetupContext): void { @@ -317,6 +318,29 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr } })); } + + private registerSetting(context: ChatSetupContext): void { + const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); + + let lastNode: IConfigurationNode | undefined; + const registerSetting = () => { + const node: IConfigurationNode = { + id: 'chatSidebar', + title: localize('interactiveSessionConfigurationTitle', "Chat"), + type: 'object', + properties: { + 'chat.agent.maxRequests': { + type: 'number', + description: localize('chat.agent.maxRequests', "The maximum number of requests to allow Copilot Edits to use in agent mode."), + default: context.state.entitlement === ChatEntitlement.Limited ? 5 : 15 + }, + } + }; + configurationRegistry.updateConfigurations({ remove: lastNode ? [lastNode] : [], add: [node] }); + lastNode = node; + }; + this._register(Event.runAndSubscribe(context.onDidChange, () => registerSetting())); + } } //#endregion From d3673ef5147bc15ca4e70c0aea104a89cb34cd83 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Mon, 27 Jan 2025 17:14:31 -0800 Subject: [PATCH 0943/3587] Don't overlap window controls (#238910) Fixes https://github.com/microsoft/vscode/issues/237626 --- .../browser/quickInputController.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 1196dddd8923..27061cd9d817 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -29,6 +29,10 @@ import './quickInputActions.js'; import { autorun, observableValue } from '../../../base/common/observable.js'; import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js'; import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; +import { IConfigurationService } from '../../configuration/common/configuration.js'; +import { Platform, platform } from '../../../base/common/platform.js'; +import { TitleBarSetting, TitlebarStyle } from '../../window/common/window.js'; +import { getZoomFactor } from '../../../base/browser/browser.js'; const $ = dom.$; @@ -900,6 +904,9 @@ class QuickInputDragAndDropController extends Disposable { private readonly _snapThreshold = 20; private readonly _snapLineHorizontalRatio = 0.25; + private readonly _controlsOnLeft: boolean; + private readonly _controlsOnRight: boolean; + private _quickInputAlignmentContext = QuickInputAlignmentContextKey.bindTo(this._contextKeyService); constructor( @@ -908,9 +915,16 @@ class QuickInputDragAndDropController extends Disposable { private _quickInputDragAreas: { node: HTMLElement; includeChildren: boolean }[], initialViewState: QuickInputViewState | undefined, @ILayoutService private readonly _layoutService: ILayoutService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); + const customTitleBar = this.configurationService.getValue(TitleBarSetting.TITLE_BAR_STYLE) === TitlebarStyle.CUSTOM; + + // Do not allow the widget to overflow or underflow window controls. + // Use CSS calculations to avoid having to force layout with `.clientWidth` + this._controlsOnLeft = customTitleBar && platform === Platform.Mac; + this._controlsOnRight = customTitleBar && (platform === Platform.Windows || platform === Platform.Linux); this._registerLayoutListener(); this.registerMouseListeners(); this.dndViewState.set({ ...initialViewState, done: true }, undefined); @@ -1021,6 +1035,15 @@ class QuickInputDragAndDropController extends Disposable { const snapCoordinateX = this._getCenterXSnapValue(); // Make sure the quick input is not moved outside the container topCoordinate = Math.max(0, Math.min(topCoordinate, this._container.clientHeight - this._quickInputContainer.clientHeight)); + + if (topCoordinate < this._layoutService.activeContainerOffset.top) { + if (this._controlsOnLeft) { + leftCoordinate = Math.max(leftCoordinate, 80 / getZoomFactor(dom.getActiveWindow())); + } else if (this._controlsOnRight) { + leftCoordinate = Math.min(leftCoordinate, this._container.clientWidth - this._quickInputContainer.clientWidth - (140 / getZoomFactor(dom.getActiveWindow()))); + } + } + const snappingToTop = Math.abs(topCoordinate - snapCoordinateYTop) < this._snapThreshold; topCoordinate = snappingToTop ? snapCoordinateYTop : topCoordinate; const snappingToCenter = Math.abs(topCoordinate - snapCoordinateY) < this._snapThreshold; From ffa60d8aa7fb5ab34a7cb5ed579af5b926370d28 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 27 Jan 2025 17:30:42 -0800 Subject: [PATCH 0944/3587] Ignore file edit limit when in agent mode (#238914) --- .../chat/browser/chatEditing/chatEditingService.ts | 5 +++++ .../chat/browser/chatEditing/chatEditingSession.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts index c65de493065e..1aba7d7c8cdb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts @@ -82,6 +82,10 @@ export class ChatEditingService extends Disposable implements IChatEditingServic private _editingSessionFileLimitPromise: Promise; private _editingSessionFileLimit: number | undefined; get editingSessionFileLimit() { + if (this._chatAgentService.toolsAgentModeEnabled) { + return Number.MAX_SAFE_INTEGER; + } + return this._editingSessionFileLimit ?? defaultChatEditingMaxFileLimit; } @@ -102,6 +106,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic @IFileService private readonly _fileService: IFileService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IWorkbenchAssignmentService private readonly _workbenchAssignmentService: IWorkbenchAssignmentService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IStorageService storageService: IStorageService, @ILogService logService: ILogService, @IExtensionService extensionService: IExtensionService, diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 358cb06f206d..a389cb51270c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -39,6 +39,7 @@ import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEdito import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; import { isNotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js'; import { INotebookService } from '../../../notebook/common/notebookService.js'; +import { IChatAgentService } from '../../common/chatAgents.js'; import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IModifiedFileEntry, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; @@ -178,6 +179,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio @IChatService private readonly _chatService: IChatService, @INotebookService private readonly _notebookService: INotebookService, @ITextFileService private readonly _textFileService: ITextFileService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, ) { super(); } @@ -713,8 +715,10 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } private async _acceptTextEdits(resource: URI, textEdits: TextEdit[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise { - if (!this._entriesObs.get().find(e => isEqual(e.modifiedURI, resource)) && this._entriesObs.get().length >= (await this.editingSessionFileLimitPromise)) { - // Do not create files in a single editing session that would be in excess of our limit + if (!this._chatAgentService.toolsAgentModeEnabled && !this._entriesObs.get().find(e => isEqual(e.modifiedURI, resource)) && this._entriesObs.get().length >= (await this.editingSessionFileLimitPromise)) { + // Do not create files in a single editing session that would be in excess of our limit. + // TODO- The agent mode check is done weirdly here because we don't know whether agent mode is enabled or not at the moment the chat editing session is created when the window is loading. + // Expecting that the limit will be removed soon anyway... return; } From 8b8b5bb62a7e4674d0dfbb393576b72bef6950a6 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 27 Jan 2025 17:33:01 -0800 Subject: [PATCH 0945/3587] trigger chat input re-layout when number of instruction attachments changes (#238915) [prompts]: trigger chat input re-layout when number of instruction attachments changes --- .../instructionAttachments.ts | 39 +++++++++++++++---- .../contrib/chat/browser/chatInputPart.ts | 5 +++ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionAttachments.ts b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionAttachments.ts index 79d84ebf43a0..58e60e5fbcd0 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionAttachments.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionAttachments.ts @@ -5,6 +5,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import * as dom from '../../../../../../base/browser/dom.js'; +import { Emitter } from '../../../../../../base/common/event.js'; import { ResourceLabels } from '../../../../../browser/labels.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { InstructionsAttachmentWidget } from './instructionsAttachment.js'; @@ -18,14 +19,30 @@ import { ChatInstructionAttachmentsModel } from '../../chatAttachmentModel/chatI */ export class InstructionAttachmentsWidget extends Disposable { /** - * The root DOM node of the widget. + * List of child instruction attachment widgets. */ - public readonly domNode: HTMLElement; + private children: InstructionsAttachmentWidget[] = []; /** - * List of child instruction attachment widgets. + * Event that fires when number of attachments change + * + * See {@linkcode onAttachmentsCountChange}. */ - private children: InstructionsAttachmentWidget[] = []; + private _onAttachmentsCountChange = this._register(new Emitter()); + /** + * Subscribe to the `onAttachmentsCountChange` event. + * @param callback Function to invoke when number of attachments change. + */ + public onAttachmentsCountChange(callback: () => unknown): this { + this._register(this._onAttachmentsCountChange.event(callback)); + + return this; + } + + /** + * The root DOM node of the widget. + */ + public readonly domNode: HTMLElement; /** * Get all `URI`s of all valid references, including all @@ -72,12 +89,15 @@ export class InstructionAttachmentsWidget extends Disposable { ); // handle the child widget disposal event, removing it from the list - widget.onDispose(this.onChildDispose.bind(this, widget)); + widget.onDispose(this.handleAttachmentDispose.bind(this, widget)); // register the new child widget this.children.push(widget); this.domNode.appendChild(widget.domNode); this.render(); + + // fire the event to notify about the change in the number of attachments + this._onAttachmentsCountChange.fire(); }); } @@ -85,7 +105,7 @@ export class InstructionAttachmentsWidget extends Disposable { * Handle child widget disposal. * @param widget The child widget that was disposed. */ - public onChildDispose(widget: InstructionsAttachmentWidget): this { + public handleAttachmentDispose(widget: InstructionsAttachmentWidget): this { // common prefix for all log messages const logPrefix = `[onChildDispose] Widget for instructions attachment '${widget.uri.path}'`; @@ -122,7 +142,12 @@ export class InstructionAttachmentsWidget extends Disposable { this.domNode.removeChild(widget.domNode); // re-render the whole widget - return this.render(); + this.render(); + + // fire the event to notify about the change in the number of attachments + this._onAttachmentsCountChange.fire(); + + return this; } /** diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 848e59cff300..52b71b5cd244 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -404,6 +404,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ), ); + // trigger re-layout of chat input when number of instruction attachment changes + this.instructionAttachmentsPart.onAttachmentsCountChange(() => { + this._onDidChangeHeight.fire(); + }); + this.initSelectedModel(); } From 7a3d738bbb52a0222cbd97277b07d93113b79139 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 27 Jan 2025 18:33:40 -0800 Subject: [PATCH 0946/3587] fixes the error/warning CSS classname of instruction attachment (#238917) [prompts]: fixes the error/warning CSS classname of instruction attachment --- .../instructionsAttachment/instructionsAttachment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts index d924c65f1e6b..8b0dc877a0bc 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts @@ -120,7 +120,7 @@ export class InstructionsAttachmentWidget extends Disposable { const isWarning = !isRootError; this.domNode.classList.add( - (isWarning) ? 'error' : 'warning', + (isWarning) ? 'warning' : 'error', ); const errorCaption = (isWarning) From 7f434da1e8fd624b835c03f0cd878f124b2322d7 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 28 Jan 2025 10:30:46 -0600 Subject: [PATCH 0947/3587] Revert "rm webpack config from terminal-suggest" (#238981) Revert "rm webpack config from terminal-suggest (#238874)" This reverts commit b54c554bb2ea20e567c6347097f8e9fdbac166ca. --- extensions/terminal-suggest/.vscodeignore | 2 ++ .../extension-browser.webpack.config.js | 25 +++++++++++++++++++ .../extension.webpack.config.js | 23 +++++++++++++++++ extensions/terminal-suggest/package.json | 6 ++--- 4 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 extensions/terminal-suggest/extension-browser.webpack.config.js create mode 100644 extensions/terminal-suggest/extension.webpack.config.js diff --git a/extensions/terminal-suggest/.vscodeignore b/extensions/terminal-suggest/.vscodeignore index 7251a128a038..f05a79416be0 100644 --- a/extensions/terminal-suggest/.vscodeignore +++ b/extensions/terminal-suggest/.vscodeignore @@ -2,4 +2,6 @@ src/** out/** tsconfig.json .vscode/** +extension.webpack.config.js +extension-browser.webpack.config.js package-lock.json diff --git a/extensions/terminal-suggest/extension-browser.webpack.config.js b/extensions/terminal-suggest/extension-browser.webpack.config.js new file mode 100644 index 000000000000..4b7c7460ff08 --- /dev/null +++ b/extensions/terminal-suggest/extension-browser.webpack.config.js @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withBrowserDefaults = require('../shared.webpack.config').browser; + +module.exports = withBrowserDefaults({ + context: __dirname, + entry: { + extension: './src/terminalSuggestMain.ts' + }, + output: { + filename: 'terminalSuggestMain.js' + }, + resolve: { + fallback: { + 'child_process': false + } + } +}); diff --git a/extensions/terminal-suggest/extension.webpack.config.js b/extensions/terminal-suggest/extension.webpack.config.js new file mode 100644 index 000000000000..89f3ea28d874 --- /dev/null +++ b/extensions/terminal-suggest/extension.webpack.config.js @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check +'use strict'; + +const withDefaults = require('../shared.webpack.config'); + +module.exports = withDefaults({ + context: __dirname, + entry: { + extension: './src/terminalSuggestMain.ts' + }, + output: { + filename: 'terminalSuggestMain.js' + }, + resolve: { + mainFields: ['module', 'main'], + extensions: ['.ts', '.js'] // support ts-files and js-files + } +}); diff --git a/extensions/terminal-suggest/package.json b/extensions/terminal-suggest/package.json index bcf53c171c50..82e488dd9f52 100644 --- a/extensions/terminal-suggest/package.json +++ b/extensions/terminal-suggest/package.json @@ -22,6 +22,7 @@ "compile": "npx gulp compile-extension:terminal-suggest", "watch": "npx gulp watch-extension:terminal-suggest" }, + "main": "./out/terminalSuggestMain", "activationEvents": [ "onTerminalCompletionsRequested" @@ -29,8 +30,5 @@ "repository": { "type": "git", "url": "https://github.com/microsoft/vscode.git" - }, - "extensionKind": [ - "workspace" - ] + } } From e144c1291c203c343795a7c5782cd1b191e5d33e Mon Sep 17 00:00:00 2001 From: Aaron Donaldson Date: Tue, 28 Jan 2025 17:04:07 +0000 Subject: [PATCH 0948/3587] chore: fix typo in VSIX progress notification (#238845) --- .../contrib/extensions/browser/extensionsWorkbenchService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index a7afc648e877..6a278cac432e 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1953,7 +1953,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } this.progressService.withProgress({ location: ProgressLocation.Notification }, async progress => { - progress.report({ message: nls.localize('downloading...', "Downlading VSIX...") }); + progress.report({ message: nls.localize('downloading...', "Downloading VSIX...") }); const name = `${galleryExtension.identifier.id}-${galleryExtension.version}${targetPlatform !== TargetPlatform.UNDEFINED && targetPlatform !== TargetPlatform.UNIVERSAL && targetPlatform !== TargetPlatform.UNKNOWN ? `-${targetPlatform}` : ''}.vsix`; await this.galleryService.download(galleryExtension, this.uriIdentityService.extUri.joinPath(result[0], name), InstallOperation.None); this.notificationService.info(nls.localize('download.completed', "Successfully downloaded the VSIX")); From e56b4d6fb32fae8952209fb6f47c82fa3c42b5df Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 28 Jan 2025 18:10:23 +0100 Subject: [PATCH 0949/3587] Fix some places tree sitter throws and can cause a freeze (#238987) --- src/vs/editor/common/model/tokenStore.ts | 11 +++++++++-- .../common/model/treeSitterTokenStoreService.ts | 14 ++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/common/model/tokenStore.ts b/src/vs/editor/common/model/tokenStore.ts index 862b300c1a43..a37c355ffc78 100644 --- a/src/vs/editor/common/model/tokenStore.ts +++ b/src/vs/editor/common/model/tokenStore.ts @@ -195,7 +195,11 @@ export class TokenStore implements IDisposable { } constructor(private readonly _textModel: ITextModel) { - this._root = { + this._root = this.createEmptyRoot(); + } + + private createEmptyRoot(): Node { + return { length: this._textModel.getValueLength(), token: 0, height: 0 @@ -211,6 +215,9 @@ export class TokenStore implements IDisposable { } private createFromUpdates(tokens: TokenUpdate[], needsRefresh?: boolean): Node { + if (tokens.length === 0) { + return this.createEmptyRoot(); + } let newRoot: Node = { length: tokens[0].length, token: tokens[0].token, @@ -299,7 +306,7 @@ export class TokenStore implements IDisposable { newRoot = concat(newRoot, allNodes[i]); } - this._root = newRoot; + this._root = newRoot ?? this.createEmptyRoot(); } /** diff --git a/src/vs/editor/common/model/treeSitterTokenStoreService.ts b/src/vs/editor/common/model/treeSitterTokenStoreService.ts index 4295bab91ac8..9a2de36707e2 100644 --- a/src/vs/editor/common/model/treeSitterTokenStoreService.ts +++ b/src/vs/editor/common/model/treeSitterTokenStoreService.ts @@ -49,10 +49,16 @@ class TreeSitterTokenizationStoreService implements ITreeSitterTokenizationStore storeInfo.guessVersion = e.versionId; for (const change of e.changes) { if (change.text.length > change.rangeLength) { - const oldToken = storeInfo.store.getTokenAt(change.rangeOffset)!; - // Insert. Just grow the token at this position to include the insert. - const newToken = { startOffsetInclusive: oldToken.startOffsetInclusive, length: oldToken.length + change.text.length - change.rangeLength, token: oldToken.token }; - storeInfo.store.update(oldToken.length, [newToken], true); + const oldToken = storeInfo.store.getTokenAt(change.rangeOffset); + let newToken: TokenUpdate; + if (oldToken) { + // Insert. Just grow the token at this position to include the insert. + newToken = { startOffsetInclusive: oldToken.startOffsetInclusive, length: oldToken.length + change.text.length - change.rangeLength, token: oldToken.token }; + } else { + // The document got larger and the change is at the end of the document. + newToken = { startOffsetInclusive: change.rangeOffset, length: change.text.length, token: 0 }; + } + storeInfo.store.update(oldToken?.length ?? 0, [newToken], true); } else if (change.text.length < change.rangeLength) { // Delete. Delete the tokens at the corresponding range. const deletedCharCount = change.rangeLength - change.text.length; From 1babe107703a1c7fc716cd9fc071be8f9e298bae Mon Sep 17 00:00:00 2001 From: isidorn Date: Tue, 28 Jan 2025 18:15:14 +0100 Subject: [PATCH 0950/3587] add aiplatform module to look for --- .../contrib/tags/electron-sandbox/workspaceTagsService.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts index a38705cd01af..ab86e967c418 100644 --- a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts +++ b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts @@ -195,6 +195,7 @@ const ModulesToLookFor = [ '@langchain/anthropic', 'langsmith', 'llamaindex', + '@google-cloud/aiplatform', '@mistralai/mistralai', 'mongodb', 'neo4j-driver', @@ -363,6 +364,7 @@ const PyModulesToLookFor = [ 'transformers', 'langchain', 'llama-index', + 'google-cloud-aiplatform', 'guidance', 'openai', 'semantic-kernel', @@ -572,6 +574,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.npm.@langchain/anthropic" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.langsmith" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.llamaindex" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@google-cloud/aiplatform" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@mistralai/mistralai" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.milvus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.mongodb" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -943,6 +946,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.py.langchain-fireworks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.langchain-huggingface" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.llama-index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.google-cloud-aiplatform" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.guidance" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.ollama" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.onnxruntime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, From 724d644bbdb64a2d15c2ccdaa6d1ae9b23b6b0e4 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 28 Jan 2025 12:32:48 -0600 Subject: [PATCH 0951/3587] Make terminal suggest provider enabled by default (#238869) fix #238081 --- .../suggest/browser/terminalCompletionService.ts | 2 +- .../suggest/common/terminalSuggestConfiguration.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 3589d24a5bd0..7bbc60a4baca 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -158,7 +158,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo const providerConfig: { [key: string]: boolean } = this._configurationService.getValue(TerminalSuggestSettingId.Providers); providers = providers.filter(p => { const providerId = p.id; - return providerId && providerId in providerConfig && providerConfig[providerId]; + return providerId && providerId in providerConfig && providerConfig[providerId] !== false; }); if (!providers.length) { diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts index 21e0450d2224..e11d8d0587c2 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts @@ -65,7 +65,7 @@ export const terminalSuggestConfiguration: IStringDictionary Date: Tue, 28 Jan 2025 11:11:20 -0800 Subject: [PATCH 0952/3587] remove the `.prompt.md` file extension in the attached instruction label (#239010) [instructions]: remove the `.prompt.md` file extension in the attached instruction label --- .../instructionsAttachment/instructionsAttachment.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts index 8b0dc877a0bc..4ee4942fe2d9 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts @@ -26,6 +26,7 @@ import { IContextMenuService } from '../../../../../../platform/contextview/brow import { ChatInstructionsAttachmentModel } from '../../chatAttachmentModel/chatInstructionsAttachment.js'; import { getDefaultHoverDelegate } from '../../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { getFlatContextMenuActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { PROMP_SNIPPET_FILE_EXTENSION } from '../../../common/promptSyntax/contentProviders/promptContentsProviderBase.js'; /** * Widget for a single prompt instructions attachment. @@ -114,7 +115,8 @@ export class InstructionsAttachmentWidget extends Disposable { // if there are some errors/warning during the process of resolving // attachment references (including all the nested child references), - // add the issue details in the hover title for the attachment + // add the issue details in the hover title for the attachment, one + // error/warning at a time because there is a limited space available if (topError) { const { isRootError, message: details } = topError; const isWarning = !isRootError; @@ -130,7 +132,8 @@ export class InstructionsAttachmentWidget extends Disposable { title += `\n-\n[${errorCaption}]: ${details}`; } - label.setFile(file, { + const fileWithoutExtension = fileBasename.replace(PROMP_SNIPPET_FILE_EXTENSION, ''); + label.setFile(URI.file(fileWithoutExtension), { fileKind: FileKind.FILE, hidePath: true, range: undefined, From 20adf6ce6f0a88d24b1e4a4280c9c5c6a813a71e Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 28 Jan 2025 11:49:38 -0800 Subject: [PATCH 0953/3587] Fix for no results found message on ai results --- src/vs/workbench/contrib/search/browser/searchView.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 7dcd7c9d5a48..0cb56aa9c215 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -1657,12 +1657,13 @@ export class SearchView extends ViewPane { await this.refreshAndUpdateCount(); } - const hasResults = !this.viewModel.searchResult.isEmpty(); - if (completed?.exit === SearchCompletionExitCode.NewSearchStarted) { + const allResults = !this.viewModel.searchResult.isEmpty(); + const aiResults = this.searchResult.getCachedSearchComplete(true); + if (completed?.exit === SearchCompletionExitCode.NewSearchStarted || (this.shouldShowAIResults() && !aiResults)) { return; } - if (!hasResults) { + if (!allResults) { const hasExcludes = !!excludePatternText; const hasIncludes = !!includePatternText; let message: string; From d655df4c0949bcfaf2434b2a9565c6697a845153 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 28 Jan 2025 12:25:01 -0800 Subject: [PATCH 0954/3587] rename `instructions` to `prompt` in the UI (#239013) [instructions]: rename `instructions` to `prompt` in the UI --- .../contrib/chat/browser/actions/chatContextActions.ts | 2 +- .../instructionsAttachment/instructionsAttachment.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index fd9bd39d5c7c..e1595f131bbd 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -834,7 +834,7 @@ export class AttachContextAction extends Action2 { quickPickItems.push({ kind: 'prompt-instructions', id: 'prompt-instructions', - label: localize('chatContext.promptInstructions', 'Instructions'), + label: localize('prompt', 'Prompt'), iconClass: ThemeIcon.asClassName(Codicon.lightbulbSparkle), }); } diff --git a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts index 4ee4942fe2d9..4c8eb2c972f0 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts @@ -106,12 +106,12 @@ export class InstructionsAttachmentWidget extends Disposable { const fileBasename = basename(file); const fileDirname = dirname(file); const friendlyName = `${fileBasename} ${fileDirname}`; - const ariaLabel = localize('chat.instructionsAttachment', "Prompt instructions attachment, {0}", friendlyName); + const ariaLabel = localize('chat.promptAttachment', "Prompt attachment, {0}", friendlyName); const uriLabel = this.labelService.getUriLabel(file, { relative: true }); - const currentFile = localize('openEditor', "Prompt instructions"); + const promptLabel = localize('prompt', "Prompt"); - let title = `${currentFile} ${uriLabel}`; + let title = `${promptLabel} ${uriLabel}`; // if there are some errors/warning during the process of resolving // attachment references (including all the nested child references), @@ -144,7 +144,7 @@ export class InstructionsAttachmentWidget extends Disposable { this.domNode.ariaLabel = ariaLabel; this.domNode.tabIndex = 0; - const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, localize('instructions', 'Instructions'))); + const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, promptLabel)); this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), hintElement, title)); // create the `remove` button From beb5ab21b3768d4046c8990c854cbe0ee110e648 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 28 Jan 2025 13:26:13 -0800 Subject: [PATCH 0955/3587] debug: bump js-debug to 1.97 (#239034) --- product.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/product.json b/product.json index 8411532fc64c..ed0f95789fa6 100644 --- a/product.json +++ b/product.json @@ -50,8 +50,8 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.96.0", - "sha256": "278cd8b129c133d834a8105d0e0699f2f940c5c159fa5c821c7b9a4f7ffd3581", + "version": "1.97.0", + "sha256": "8442c0578c80976ef17031868fd6dcc42206be75b0ca1b0e9b5d6ff965cfdf23", "repo": "https://github.com/microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", From 5aefee64b0acfdd4e9484c4894de363e1b3a9157 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 28 Jan 2025 14:04:48 -0800 Subject: [PATCH 0956/3587] [prompts]: fix typos (#239014) --- .../instructionsAttachment/instructionsAttachment.ts | 4 ++-- .../chatAttachmentModel/chatInstructionsFileLocator.ts | 4 ++-- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 2 +- .../contentProviders/promptContentsProviderBase.ts | 4 ++-- .../promptSyntax/languageFeatures/promptLinkProvider.ts | 4 ++-- .../chat/common/promptSyntax/parsers/basePromptParser.ts | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts index 4c8eb2c972f0..c5ee5d143e5f 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts @@ -26,7 +26,7 @@ import { IContextMenuService } from '../../../../../../platform/contextview/brow import { ChatInstructionsAttachmentModel } from '../../chatAttachmentModel/chatInstructionsAttachment.js'; import { getDefaultHoverDelegate } from '../../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { getFlatContextMenuActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; -import { PROMP_SNIPPET_FILE_EXTENSION } from '../../../common/promptSyntax/contentProviders/promptContentsProviderBase.js'; +import { PROMPT_SNIPPET_FILE_EXTENSION } from '../../../common/promptSyntax/contentProviders/promptContentsProviderBase.js'; /** * Widget for a single prompt instructions attachment. @@ -132,7 +132,7 @@ export class InstructionsAttachmentWidget extends Disposable { title += `\n-\n[${errorCaption}]: ${details}`; } - const fileWithoutExtension = fileBasename.replace(PROMP_SNIPPET_FILE_EXTENSION, ''); + const fileWithoutExtension = fileBasename.replace(PROMPT_SNIPPET_FILE_EXTENSION, ''); label.setFile(URI.file(fileWithoutExtension), { fileKind: FileKind.FILE, hidePath: true, diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts index a1e0b717f5cf..577e38c7c816 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts @@ -9,7 +9,7 @@ import { IFileService } from '../../../../../platform/files/common/files.js'; import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js'; -import { PROMP_SNIPPET_FILE_EXTENSION } from '../../common/promptSyntax/contentProviders/promptContentsProviderBase.js'; +import { PROMPT_SNIPPET_FILE_EXTENSION } from '../../common/promptSyntax/contentProviders/promptContentsProviderBase.js'; /** * Class to locate prompt instructions files. @@ -120,7 +120,7 @@ export class ChatInstructionsFileLocator { continue; } - if (!name.endsWith(PROMP_SNIPPET_FILE_EXTENSION)) { + if (!name.endsWith(PROMPT_SNIPPET_FILE_EXTENSION)) { continue; } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 52b71b5cd244..a2593207616a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -275,7 +275,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private chatCursorAtTop: IContextKey; private inputEditorHasFocus: IContextKey; /** - * Context key is set when prompt instructions are attached.3 + * Context key is set when prompt instructions are attached. */ private promptInstructionsAttached: IContextKey; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts index c6983cfb71b8..005a78f1e429 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts @@ -17,7 +17,7 @@ import { cancelPreviousCalls } from '../../../../../../base/common/decorators/ca /** * File extension for the prompt snippets. */ -export const PROMP_SNIPPET_FILE_EXTENSION: string = '.prompt.md'; +export const PROMPT_SNIPPET_FILE_EXTENSION: string = '.prompt.md'; /** * Base class for prompt contents providers. Classes that extend this one are responsible to: @@ -149,6 +149,6 @@ export abstract class PromptContentsProviderBase< * Check if the current URI points to a prompt snippet. */ public isPromptSnippet(): boolean { - return this.uri.path.endsWith(PROMP_SNIPPET_FILE_EXTENSION); + return this.uri.path.endsWith(PROMPT_SNIPPET_FILE_EXTENSION); } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts index 51849561e95a..854272e736ed 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts @@ -15,7 +15,7 @@ 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 { PROMP_SNIPPET_FILE_EXTENSION } from '../contentProviders/promptContentsProviderBase.js'; +import { PROMPT_SNIPPET_FILE_EXTENSION } from '../contentProviders/promptContentsProviderBase.js'; import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../../common/contributions.js'; @@ -24,7 +24,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr * Prompt files language selector. */ const languageSelector = { - pattern: `**/*${PROMP_SNIPPET_FILE_EXTENSION}`, + pattern: `**/*${PROMPT_SNIPPET_FILE_EXTENSION}`, }; /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index 3ed34c7df66b..3c6cf31d0997 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -20,7 +20,7 @@ import { basename, extUri } from '../../../../../../base/common/resources.js'; import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; import { ObservableDisposable } from '../../../../../../base/common/observableDisposable.js'; import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; -import { PROMP_SNIPPET_FILE_EXTENSION } from '../contentProviders/promptContentsProviderBase.js'; +import { PROMPT_SNIPPET_FILE_EXTENSION } from '../contentProviders/promptContentsProviderBase.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; @@ -507,7 +507,7 @@ export abstract class BasePromptParser extend * Check if the provided URI points to a prompt snippet. */ public static isPromptSnippet(uri: URI): boolean { - return uri.path.endsWith(PROMP_SNIPPET_FILE_EXTENSION); + return uri.path.endsWith(PROMPT_SNIPPET_FILE_EXTENSION); } /** From a50b5a6c8d5ed38dd930123261df5fdb6e5824ac Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 28 Jan 2025 15:00:20 -0800 Subject: [PATCH 0957/3587] Explain how multiple providers work for paste and drop Fixes #238978 --- src/vscode-dts/vscode.d.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 0109505b4566..804721590e9f 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -14869,22 +14869,41 @@ declare module 'vscode' { /** * Registers a new {@link DocumentDropEditProvider}. * + * Multiple drop providers can be registered for a language. When dropping content into an editor, all + * registered providers for the editor's language will be invoked based on the mimetypes they handle + * as specified by their {@linkcode DocumentDropEditProviderMetadata}. + * + * Each provider can return one or more {@linkcode DocumentDropEdit DocumentDropEdits}. The edits are sorted + * using the {@linkcode DocumentDropEdit.yieldTo} property. By default the first edit will be applied. If there + * are any additional edits, these will be shown to the user as selectable drop options in the drop widget. + * * @param selector A selector that defines the documents this provider applies to. * @param provider A drop provider. * @param metadata Additional metadata about the provider. * - * @returns A {@link Disposable} that unregisters this provider when disposed of. + * @returns A {@linkcode Disposable} that unregisters this provider when disposed of. */ export function registerDocumentDropEditProvider(selector: DocumentSelector, provider: DocumentDropEditProvider, metadata?: DocumentDropEditProviderMetadata): Disposable; /** * Registers a new {@linkcode DocumentPasteEditProvider}. * + * Multiple providers can be registered for a language. All registered providers for a language will be invoked + * for copy and paste operations based on their handled mimetypes as specified by the {@linkcode DocumentPasteProviderMetadata}. + * + * For {@link DocumentPasteEditProvider.prepareDocumentPaste copy operations}, changes to the {@linkcode DataTransfer} + * made by each provider will be merged into a single {@linkcode DataTransfer} that is used to populate the clipboard. + * + * For {@link DocumentPasteEditProvider.providerDocumentPasteEdits paste operations}, each provider will be invoked + * and can return one or more {@linkcode DocumentPasteEdit DocumentPasteEdits}. The edits are sorted using + * the {@linkcode DocumentPasteEdit.yieldTo} property. By default the first edit will be applied + * and the rest of the edits will be shown to the user as selectable paste options in the paste widget. + * * @param selector A selector that defines the documents this provider applies to. * @param provider A paste editor provider. * @param metadata Additional metadata about the provider. * - * @returns A {@link Disposable} that unregisters this provider when disposed of. + * @returns A {@linkcode Disposable} that unregisters this provider when disposed of. */ export function registerDocumentPasteEditProvider(selector: DocumentSelector, provider: DocumentPasteEditProvider, metadata: DocumentPasteProviderMetadata): Disposable; From f9213d7b48a210ac486e24e26b54a283b08a6f6d Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 28 Jan 2025 15:35:46 -0800 Subject: [PATCH 0958/3587] Support more image/video/audio types for markdown copy into files Fixes #239016 --- .../src/commands/insertResource.ts | 3 +- .../copyFiles/dropOrPasteResource.ts | 16 ++--- .../src/languageFeatures/copyFiles/shared.ts | 47 ++++----------- .../src/util/mimes.ts | 60 +++++++++++++++---- 4 files changed, 71 insertions(+), 55 deletions(-) diff --git a/extensions/markdown-language-features/src/commands/insertResource.ts b/extensions/markdown-language-features/src/commands/insertResource.ts index 61649c08017f..e6ede4367426 100644 --- a/extensions/markdown-language-features/src/commands/insertResource.ts +++ b/extensions/markdown-language-features/src/commands/insertResource.ts @@ -6,7 +6,8 @@ import * as vscode from 'vscode'; import { Utils } from 'vscode-uri'; import { Command } from '../commandManager'; -import { createUriListSnippet, linkEditKind, mediaFileExtensions } from '../languageFeatures/copyFiles/shared'; +import { createUriListSnippet, linkEditKind } from '../languageFeatures/copyFiles/shared'; +import { mediaFileExtensions } from '../util/mimes'; import { coalesce } from '../util/arrays'; import { getParentDocumentUri } from '../util/document'; import { Schemes } from '../util/schemes'; diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts index f74ff677e0de..7791d6b19e42 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts @@ -7,12 +7,12 @@ import * as vscode from 'vscode'; import { IMdParser } from '../../markdownEngine'; import { coalesce } from '../../util/arrays'; import { getParentDocumentUri } from '../../util/document'; -import { Mime, mediaMimes } from '../../util/mimes'; +import { getMediaKindForMime, MediaKind, Mime, rootMediaMimesTypes } from '../../util/mimes'; import { Schemes } from '../../util/schemes'; +import { UriList } from '../../util/uriList'; import { NewFilePathGenerator } from './newFilePathGenerator'; -import { DropOrPasteEdit, createInsertUriListEdit, createUriListSnippet, getSnippetLabelAndKind, baseLinkEditKind, linkEditKind, audioEditKind, videoEditKind, imageEditKind } from './shared'; +import { audioEditKind, baseLinkEditKind, createInsertUriListEdit, createUriListSnippet, DropOrPasteEdit, getSnippetLabelAndKind, imageEditKind, linkEditKind, videoEditKind } from './shared'; import { InsertMarkdownLink, shouldInsertMarkdownLinkByDefault } from './smartDropOrPaste'; -import { UriList } from '../../util/uriList'; enum CopyFilesSettings { Never = 'never', @@ -33,7 +33,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v public static readonly mimeTypes = [ Mime.textUriList, 'files', - ...mediaMimes, + ...Object.values(rootMediaMimesTypes).map(type => `${type}/*`), ]; private readonly _yieldTo = [ @@ -206,12 +206,14 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v interface FileEntry { readonly uri: vscode.Uri; + readonly kind: MediaKind; readonly newFile?: { readonly contents: vscode.DataTransferFile; readonly overwrite: boolean }; } const pathGenerator = new NewFilePathGenerator(); const fileEntries = coalesce(await Promise.all(Array.from(dataTransfer, async ([mime, item]): Promise => { - if (!mediaMimes.has(mime)) { + const mediaKind = getMediaKindForMime(mime); + if (!mediaKind) { return; } @@ -224,7 +226,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v // If the file is already in a workspace, we don't want to create a copy of it const workspaceFolder = vscode.workspace.getWorkspaceFolder(file.uri); if (workspaceFolder) { - return { uri: file.uri }; + return { uri: file.uri, kind: mediaKind }; } } @@ -232,7 +234,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v if (!newFile) { return; } - return { uri: newFile.uri, newFile: { contents: file, overwrite: newFile.overwrite } }; + return { uri: newFile.uri, kind: mediaKind, newFile: { contents: file, overwrite: newFile.overwrite } }; }))); if (!fileEntries.length) { return; diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts index cdd2476ad382..273fa56a6bb5 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts @@ -11,6 +11,7 @@ import { getDocumentDir } from '../../util/document'; import { Schemes } from '../../util/schemes'; import { UriList } from '../../util/uriList'; import { resolveSnippet } from './snippets'; +import { mediaFileExtensions, MediaKind } from '../../util/mimes'; /** Base kind for any sort of markdown link, including both path and media links */ export const baseLinkEditKind = vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'link'); @@ -22,39 +23,6 @@ export const imageEditKind = baseLinkEditKind.append('image'); export const audioEditKind = baseLinkEditKind.append('audio'); export const videoEditKind = baseLinkEditKind.append('video'); -enum MediaKind { - Image, - Video, - Audio, -} - -export const mediaFileExtensions = new Map([ - // Images - ['avif', MediaKind.Image], - ['bmp', MediaKind.Image], - ['gif', MediaKind.Image], - ['ico', MediaKind.Image], - ['jpe', MediaKind.Image], - ['jpeg', MediaKind.Image], - ['jpg', MediaKind.Image], - ['png', MediaKind.Image], - ['psd', MediaKind.Image], - ['svg', MediaKind.Image], - ['tga', MediaKind.Image], - ['tif', MediaKind.Image], - ['tiff', MediaKind.Image], - ['webp', MediaKind.Image], - - // Videos - ['ogg', MediaKind.Video], - ['mp4', MediaKind.Video], - - // Audio Files - ['mp3', MediaKind.Audio], - ['aac', MediaKind.Audio], - ['wav', MediaKind.Audio], -]); - export function getSnippetLabelAndKind(counter: { readonly insertedAudioCount: number; readonly insertedVideoCount: number; readonly insertedImageCount: number; readonly insertedLinkCount: number }): { label: string; kind: vscode.DocumentDropOrPasteEditKind; @@ -207,6 +175,7 @@ export function createUriListSnippet( uris: ReadonlyArray<{ readonly uri: vscode.Uri; readonly str?: string; + readonly kind?: MediaKind; }>, options?: UriListSnippetOptions, ): UriSnippet | undefined { @@ -229,7 +198,7 @@ export function createUriListSnippet( uris.forEach((uri, i) => { const mdPath = (!options?.preserveAbsoluteUris ? getRelativeMdPath(documentDir, uri.uri) : undefined) ?? uri.str ?? uri.uri.toString(); - const desiredKind = getDesiredLinkKind(uri.uri, options); + const desiredKind = getDesiredLinkKind(uri.uri, uri.kind, options); if (desiredKind === DesiredLinkKind.Link) { insertedLinkCount++; @@ -276,7 +245,7 @@ enum DesiredLinkKind { Audio, } -function getDesiredLinkKind(uri: vscode.Uri, options: UriListSnippetOptions | undefined): DesiredLinkKind { +function getDesiredLinkKind(uri: vscode.Uri, uriFileKind: MediaKind | undefined, options: UriListSnippetOptions | undefined): DesiredLinkKind { if (options?.linkKindHint instanceof vscode.DocumentDropOrPasteEditKind) { if (linkEditKind.contains(options.linkKindHint)) { return DesiredLinkKind.Link; @@ -289,6 +258,14 @@ function getDesiredLinkKind(uri: vscode.Uri, options: UriListSnippetOptions | un } } + if (typeof uriFileKind !== 'undefined') { + switch (uriFileKind) { + case MediaKind.Video: return DesiredLinkKind.Video; + case MediaKind.Audio: return DesiredLinkKind.Audio; + case MediaKind.Image: return DesiredLinkKind.Image; + } + } + const normalizedExt = URI.Utils.extname(uri).toLowerCase().replace('.', ''); if (options?.linkKindHint === 'media' || mediaFileExtensions.has(normalizedExt)) { switch (mediaFileExtensions.get(normalizedExt)) { diff --git a/extensions/markdown-language-features/src/util/mimes.ts b/extensions/markdown-language-features/src/util/mimes.ts index f33b807b83e3..32dcc021815b 100644 --- a/extensions/markdown-language-features/src/util/mimes.ts +++ b/extensions/markdown-language-features/src/util/mimes.ts @@ -8,16 +8,52 @@ export const Mime = { textPlain: 'text/plain', } as const; -export const mediaMimes = new Set([ - 'image/avif', - 'image/bmp', - 'image/gif', - 'image/jpeg', - 'image/png', - 'image/webp', - 'video/mp4', - 'video/ogg', - 'audio/mpeg', - 'audio/aac', - 'audio/x-wav', +export const rootMediaMimesTypes = Object.freeze({ + image: 'image', + audio: 'audio', + video: 'video', +}); + +export enum MediaKind { + Image = 1, + Video, + Audio +} + +export function getMediaKindForMime(mime: string): MediaKind | undefined { + const root = mime.toLowerCase().split('/').at(0); + switch (root) { + case 'image': return MediaKind.Image; + case 'video': return MediaKind.Video; + case 'audio': return MediaKind.Audio; + default: return undefined; + } +} + +export const mediaFileExtensions = new Map([ + // Images + ['avif', MediaKind.Image], + ['bmp', MediaKind.Image], + ['gif', MediaKind.Image], + ['ico', MediaKind.Image], + ['jpe', MediaKind.Image], + ['jpeg', MediaKind.Image], + ['jpg', MediaKind.Image], + ['png', MediaKind.Image], + ['psd', MediaKind.Image], + ['svg', MediaKind.Image], + ['tga', MediaKind.Image], + ['tif', MediaKind.Image], + ['tiff', MediaKind.Image], + ['webp', MediaKind.Image], + + // Videos + ['ogg', MediaKind.Video], + ['mp4', MediaKind.Video], + ['mov', MediaKind.Video], + + // Audio Files + ['mp3', MediaKind.Audio], + ['aac', MediaKind.Audio], + ['wav', MediaKind.Audio], ]); From d266e25fc4881fbd9752096b83ac2f525a0cead8 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 28 Jan 2025 15:58:42 -0800 Subject: [PATCH 0959/3587] updates the prompt label in the attachments list (#239045) [prompts]: change `Prompt` to `Prompt..` in the attachments list --- .../contrib/chat/browser/actions/chatContextActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index e1595f131bbd..dfeefadf1d74 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -834,7 +834,7 @@ export class AttachContextAction extends Action2 { quickPickItems.push({ kind: 'prompt-instructions', id: 'prompt-instructions', - label: localize('prompt', 'Prompt'), + label: localize('promptWithEllipsis', 'Prompt...'), iconClass: ThemeIcon.asClassName(Codicon.lightbulbSparkle), }); } From da261f7b0193db6293db84a3e634919a0846acd0 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 28 Jan 2025 17:18:22 -0800 Subject: [PATCH 0960/3587] add docs link to empty prompt selection dialog (#239050) * [prompts]: add docs link to the empty prompts selection dialog * [prompt]: fix a typo in localization string ID * [prompts]: refactor to reduce number of function arguments * Update src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts Co-authored-by: Harald Kirschner --------- Co-authored-by: Harald Kirschner --- .../browser/actions/chatContextActions.ts | 133 +++++++++++++----- 1 file changed, 95 insertions(+), 38 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index dfeefadf1d74..0667c006c003 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -24,6 +24,7 @@ import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/cont import { IFileService } from '../../../../../platform/files/common/files.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; +import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { AnythingQuickAccessProviderRunOptions } from '../../../../../platform/quickinput/common/quickAccess.js'; import { IQuickInputService, IQuickPickItem, IQuickPickItemWithResource, IQuickPickSeparator, QuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; import { ActiveEditorContext, TextCompareEditorActiveContext } from '../../../../common/contextkeys.js'; @@ -460,7 +461,7 @@ export class AttachContextAction extends Action2 { `:${item.range.startLineNumber}`); } - private async _attachContext(widget: IChatWidget, quickInputService: IQuickInputService, commandService: ICommandService, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, fileService: IFileService, isInBackground?: boolean, ...picks: IChatContextQuickPickItem[]) { + private async _attachContext(widget: IChatWidget, quickInputService: IQuickInputService, commandService: ICommandService, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, fileService: IFileService, openerService: IOpenerService, isInBackground?: boolean, ...picks: IChatContextQuickPickItem[]) { const toAttach: IChatRequestVariableEntry[] = []; for (const pick of picks) { if (isISymbolQuickPickItem(pick) && pick.symbol) { @@ -581,39 +582,12 @@ export class AttachContextAction extends Action2 { toAttach.push(convertBufferToScreenshotVariable(blob)); } } else if (isPromptInstructionsQuickPickItem(pick)) { - const { promptInstructions } = widget.attachmentModel; - - // find all prompt instruction files in the user project - // and present them to the user so they can select one - const filesPromise = promptInstructions.listNonAttachedFiles() - .then((files) => { - return files.map((file) => { - const result: IQuickPickItem & { value: URI } = { - type: 'item', - label: labelService.getUriBasenameLabel(file), - description: labelService.getUriLabel(dirname(file), { relative: true }), - tooltip: file.fsPath, - value: file, - }; - - return result; - }); - }); - - const selectedFile = await quickInputService.pick( - filesPromise, - { - placeHolder: localize('promptInstructions', 'Add prompt instructions file'), - canPickMany: false, - }); - - // if the quick pick dialog was dismissed, nothing to do - if (!selectedFile) { - return; - } - - // add selected prompt instructions reference to the chat attachments model - promptInstructions.add(selectedFile.value); + await selectPromptAttachment({ + widget, + quickInputService, + labelService, + openerService, + }); } else { // Anything else is an attachment const attachmentPick = pick as IAttachmentQuickPickItem; @@ -689,6 +663,7 @@ export class AttachContextAction extends Action2 { const hostService = accessor.get(IHostService); const extensionService = accessor.get(IExtensionService); const fileService = accessor.get(IFileService); + const openerService = accessor.get(IOpenerService); const context: { widget?: IChatWidget; showFilesOnly?: boolean; placeholder?: string } | undefined = args[0]; const widget = context?.widget ?? widgetService.lastFocusedWidget; @@ -853,19 +828,19 @@ export class AttachContextAction extends Action2 { const second = extractTextFromIconLabel(b.label).toUpperCase(); return compare(first, second); - }), clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, '', context?.placeholder); + }), clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, openerService, '', context?.placeholder); } - 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, query: string = '', placeholder?: string) { + 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, openerService: IOpenerService, query: string = '', placeholder?: string) { const providerOptions: AnythingQuickAccessProviderRunOptions = { handleAccept: (item: IChatContextQuickPickItem, isBackgroundAccept: boolean) => { if ('prefix' in item) { - this._show(quickInputService, commandService, widget, quickChatService, quickPickItems, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, item.prefix, placeholder); + this._show(quickInputService, commandService, widget, quickChatService, quickPickItems, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, openerService, item.prefix, placeholder); } else { if (!clipboardService) { return; } - this._attachContext(widget, quickInputService, commandService, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, isBackgroundAccept, item); + this._attachContext(widget, quickInputService, commandService, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, openerService, isBackgroundAccept, item); if (isQuickChat(widget)) { quickChatService.open(); } @@ -947,3 +922,85 @@ registerAction2(class AttachFilesAction extends AttachContextAction { return super.run(accessor, attachFilesContext); } }); + +/** + * Documentation link for the prompt snippets feature. + */ +const PROMPT_SNIPPETS_DOCUMENTATION_URL = 'https://aka.ms/vscode-ghcp-prompt-snippets'; + +/** + * Options for the {@link selectPromptAttachment} function. + */ +interface ISelectPromptOptions { + widget: IChatWidget; + quickInputService: IQuickInputService; + labelService: ILabelService; + openerService: IOpenerService; +} + +/** + * Open the prompt files selection dialog and adds + * selected prompts to the chat attachments model. + */ +const selectPromptAttachment = async (options: ISelectPromptOptions): Promise => { + const { widget, quickInputService, labelService, openerService } = options; + const { promptInstructions } = widget.attachmentModel; + + // find all prompt instruction files in the user project + // and present them to the user so they can select one + const files = await promptInstructions.listNonAttachedFiles() + .then((files) => { + return files.map((file) => { + const result: IQuickPickItem & { value: URI } = { + type: 'item', + label: labelService.getUriBasenameLabel(file), + description: labelService.getUriLabel(dirname(file), { relative: true }), + tooltip: file.fsPath, + value: file, + }; + + return result; + }); + }); + + // if not prompt files found, render the "how to add" message + // to the user with a link to the documentation + if (files.length === 0) { + const docsQuickPick: IQuickPickItem & { value: URI } = { + type: 'item', + label: localize('noPromptFilesFoundTooltipLabel', 'Learn how create reusable prompts'), + description: PROMPT_SNIPPETS_DOCUMENTATION_URL, + tooltip: PROMPT_SNIPPETS_DOCUMENTATION_URL, + value: URI.parse(PROMPT_SNIPPETS_DOCUMENTATION_URL), + }; + + const result = await quickInputService.pick( + [docsQuickPick], + { + placeHolder: localize('noPromptFilesFoundLabel', 'No prompt files found.'), + canPickMany: false, + }); + + if (!result) { + return; + } + + await openerService.open(result.value); + return; + } + + // otherwise show the prompt file selection dialog + const selectedFile = await quickInputService.pick( + files, + { + placeHolder: localize('selectPromptFile', 'Select a prompt file'), + canPickMany: false, + }); + + // if a file was selected, add it to the chat attachments model + if (selectedFile) { + promptInstructions.add(selectedFile.value); + } + + // if the file selection dialog was dismissed, nothing to do +}; From 3250a26dfecc06defce495f1e16b80681974c62b Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 28 Jan 2025 19:18:50 -0800 Subject: [PATCH 0961/3587] publish configuration setting for prompt files (#239053) * [prompts]: make the configuration setting public(experimental) * [prompts]: update config setting name * [prompts]: cleanup * [prompts]: fix localization compilation error --- .../browser/actions/chatContextActions.ts | 12 ++---- .../contrib/chat/browser/chat.contribution.ts | 13 ++++++ .../chat/common/promptSyntax/config.ts | 43 +++++++++++-------- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 0667c006c003..e293e2b5c15a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -51,6 +51,7 @@ import { IChatRequestVariableEntry } from '../../common/chatModel.js'; import { ChatRequestAgentPart } from '../../common/chatParserTypes.js'; import { IChatVariableData, IChatVariablesService } from '../../common/chatVariables.js'; import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; +import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView, showEditsView } from '../chat.js'; import { imageToHash, isImage } from '../chatPasteProviders.js'; import { isQuickChat } from '../chatWidget.js'; @@ -923,11 +924,6 @@ registerAction2(class AttachFilesAction extends AttachContextAction { } }); -/** - * Documentation link for the prompt snippets feature. - */ -const PROMPT_SNIPPETS_DOCUMENTATION_URL = 'https://aka.ms/vscode-ghcp-prompt-snippets'; - /** * Options for the {@link selectPromptAttachment} function. */ @@ -969,9 +965,9 @@ const selectPromptAttachment = async (options: ISelectPromptOptions): Promise(ConfigurationExtensions.Configuration); @@ -151,6 +152,18 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.detectParticipant.enabled', "Enables chat participant autodetection for panel chat."), default: true }, + [PromptFilesConfig.CONFIG_KEY]: { + type: ['string', 'array', 'boolean', 'null'], + title: nls.localize('chat.promptFiles.setting.title', "Prompt Files"), + markdownDescription: nls.localize( + 'chat.promptFiles.setting.markdownDescription', + "Enable support for attaching reusable prompt files (`*{0}`) for Chat, Edits, and Inline Chat sessions. [Learn More]({1}).", + '.prompt.md', + PromptFilesConfig.DOCUMENTATION_URL, + ), + default: null, + tags: ['experimental'], + }, } }); Registry.as(EditorExtensions.EditorPane).registerEditorPane( diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts index 2bf39eb41841..e8222202db51 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts @@ -6,8 +6,8 @@ import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; /** - * Configuration helper for the `prompt snippets` feature. - * @see {@link PROMPT_FILES_CONFIG_KEY} and {@link PROMPT_FILES_DEFAULT_LOCATION} + * Configuration helper for the `prompt files` feature. + * @see {@link CONFIG_KEY} and {@link DEFAULT_LOCATION} * * ### Functions * @@ -18,26 +18,26 @@ import { IConfigurationService } from '../../../../../platform/configuration/com * ### Configuration Examples * * Enable the feature, using defaults for prompt files source folder locations - * (see {@link PROMPT_FILES_DEFAULT_LOCATION}): + * (see {@link DEFAULT_LOCATION}): * ```json * { - * "chat.experimental.prompt-snippets": true, + * "chat.experimental.promptSnippets": true, * } * ``` * * Enable the feature, specifying a single prompt files source folder location: * ```json * { - * "chat.experimental.prompt-snippets": '.copilot/prompts', + * "chat.experimental.promptSnippets": '.github/prompts', * } * ``` * * Enable the feature, specifying multiple prompt files source folder location: * ```json * { - * "chat.experimental.prompt-snippets": [ - * '.copilot/prompts', + * "chat.experimental.promptSnippets": [ * '.github/prompts', + * '.copilot/prompts', * '/Users/legomushroom/repos/prompts', * ], * } @@ -50,7 +50,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com * - `undefined`/`null`: feature is disabled * - `boolean`: * - `true`: feature is enabled, prompt files source folder locations - * fallback to {@link PROMPT_FILES_DEFAULT_LOCATION} + * fallback to {@link DEFAULT_LOCATION} * - `false`: feature is disabled * - `string`: * - values that can be mapped to `boolean`(`"true"`, `"FALSE", "TrUe"`, etc.) @@ -60,7 +60,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com * - `string` items in the array are treated as prompt files source folder paths * - all `non-string` items in the array are `ignored` * - if the resulting array is empty, the feature is considered `enabled`, prompt files source - * folder locations fallback to defaults (see {@linkcode PROMPT_FILES_DEFAULT_LOCATION}) + * folder locations fallback to defaults (see {@linkcode DEFAULT_LOCATION}) * * ### File Paths Resolution * @@ -74,23 +74,28 @@ import { IConfigurationService } from '../../../../../platform/configuration/com */ export namespace PromptFilesConfig { /** - * Configuration key for the `prompt snippets` feature (also - * known as `prompt snippets`, `prompt instructions`, etc.). + * Configuration key for the `prompt files` feature (also + * known as `prompt files`, `prompt instructions`, etc.). + */ + export const CONFIG_KEY: string = 'chat.promptFiles'; + + /** + * Documentation link for the prompt snippets feature. */ - const PROMPT_FILES_CONFIG_KEY: string = 'chat.experimental.prompt-snippets'; + export const DOCUMENTATION_URL = 'https://aka.ms/vscode-ghcp-prompt-snippets'; /** * Default prompt instructions source folder paths. */ - const PROMPT_FILES_DEFAULT_LOCATION = ['.copilot/prompts', '.github/prompts']; + const DEFAULT_LOCATION = ['.github/prompts']; /** - * Get value of the `prompt snippets` configuration setting. + * Get value of the `prompt files` configuration setting. */ export const getValue = ( configService: IConfigurationService, ): string | readonly string[] | boolean | undefined => { - const value = configService.getValue(PROMPT_FILES_CONFIG_KEY); + const value = configService.getValue(CONFIG_KEY); if (value === undefined || value === null) { return undefined; @@ -139,7 +144,7 @@ export namespace PromptFilesConfig { /** * Gets the source folder locations for prompt files. - * Defaults to {@link PROMPT_FILES_DEFAULT_LOCATION}. + * Defaults to {@link DEFAULT_LOCATION}. */ export const sourceLocations = ( configService: IConfigurationService, @@ -147,7 +152,7 @@ export namespace PromptFilesConfig { const value = getValue(configService); if (value === undefined) { - return PROMPT_FILES_DEFAULT_LOCATION; + return DEFAULT_LOCATION; } if (typeof value === 'string') { @@ -159,9 +164,9 @@ export namespace PromptFilesConfig { return value; } - return PROMPT_FILES_DEFAULT_LOCATION; + return DEFAULT_LOCATION; } - return PROMPT_FILES_DEFAULT_LOCATION; + return DEFAULT_LOCATION; }; } From f07edc3dbeda358c8f927bbf5ae8544b208342a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=B5=E3=81=81?= Date: Wed, 29 Jan 2025 15:35:33 +0900 Subject: [PATCH 0962/3587] fix: update unix timestamp to milliseconds in copyFiles feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: ふぁ --- extensions/markdown-language-features/package.nls.json | 2 +- .../src/languageFeatures/copyFiles/copyFiles.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json index be3f6db00f93..fe98103daff2 100644 --- a/extensions/markdown-language-features/package.nls.json +++ b/extensions/markdown-language-features/package.nls.json @@ -72,7 +72,7 @@ "configuration.markdown.updateLinksOnFileMove.enableForDirectories": "Enable updating links when a directory is moved or renamed in the workspace.", "configuration.markdown.occurrencesHighlight.enabled": "Enable highlighting link occurrences in the current document.", "configuration.markdown.copyFiles.destination": { - "message": "Configures the path and file name of files created by copy/paste or drag and drop. This is a map of globs that match against a Markdown document path to the destination path where the new file should be created.\n\nThe destination path may use the following variables:\n\n- `${documentDirName}` — Absolute parent directory path of the Markdown document, e.g. `/Users/me/myProject/docs`.\n- `${documentRelativeDirName}` — Relative parent directory path of the Markdown document, e.g. `docs`. This is the same as `${documentDirName}` if the file is not part of a workspace.\n- `${documentFileName}` — The full filename of the Markdown document, e.g. `README.md`.\n- `${documentBaseName}` — The basename of the Markdown document, e.g. `README`.\n- `${documentExtName}` — The extension of the Markdown document, e.g. `md`.\n- `${documentFilePath}` — Absolute path of the Markdown document, e.g. `/Users/me/myProject/docs/README.md`.\n- `${documentRelativeFilePath}` — Relative path of the Markdown document, e.g. `docs/README.md`. This is the same as `${documentFilePath}` if the file is not part of a workspace.\n- `${documentWorkspaceFolder}` — The workspace folder for the Markdown document, e.g. `/Users/me/myProject`. This is the same as `${documentDirName}` if the file is not part of a workspace.\n- `${fileName}` — The file name of the dropped file, e.g. `image.png`.\n- `${fileExtName}` — The extension of the dropped file, e.g. `png`.\n- `${unixTime}` — The current Unix timestamp in seconds.", + "message": "Configures the path and file name of files created by copy/paste or drag and drop. This is a map of globs that match against a Markdown document path to the destination path where the new file should be created.\n\nThe destination path may use the following variables:\n\n- `${documentDirName}` — Absolute parent directory path of the Markdown document, e.g. `/Users/me/myProject/docs`.\n- `${documentRelativeDirName}` — Relative parent directory path of the Markdown document, e.g. `docs`. This is the same as `${documentDirName}` if the file is not part of a workspace.\n- `${documentFileName}` — The full filename of the Markdown document, e.g. `README.md`.\n- `${documentBaseName}` — The basename of the Markdown document, e.g. `README`.\n- `${documentExtName}` — The extension of the Markdown document, e.g. `md`.\n- `${documentFilePath}` — Absolute path of the Markdown document, e.g. `/Users/me/myProject/docs/README.md`.\n- `${documentRelativeFilePath}` — Relative path of the Markdown document, e.g. `docs/README.md`. This is the same as `${documentFilePath}` if the file is not part of a workspace.\n- `${documentWorkspaceFolder}` — The workspace folder for the Markdown document, e.g. `/Users/me/myProject`. This is the same as `${documentDirName}` if the file is not part of a workspace.\n- `${fileName}` — The file name of the dropped file, e.g. `image.png`.\n- `${fileExtName}` — The extension of the dropped file, e.g. `png`.\n- `${unixTime}` — The current Unix timestamp in milliseconds.", "comment": [ "This setting is use the user drops or pastes image data into the editor. In this case, VS Code automatically creates a new image file in the workspace containing the dropped/pasted image.", "It's easier to explain this setting with an example. For example, let's say the setting value was:", diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts index 26f8186dbc37..0c2461548ad4 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts @@ -96,7 +96,7 @@ function resolveCopyDestinationSetting(documentUri: vscode.Uri, fileName: string // File ['fileName', fileName], // The file name of the dropped file, e.g. `image.png`. ['fileExtName', path.extname(fileName).replace('.', '')], // The extension of the dropped file, e.g. `png`. - ['unixTime', Math.floor(Date.now() / 1000).toString()], // The current Unix timestamp in seconds. + ['unixTime', Date.now().toString()], // The current Unix timestamp in milliseconds. ]); return outDest.replaceAll(/(?\\\$)|(?\w+)(?:\/(?(?:\\\/|[^\}\/])+)\/(?(?:\\\/|[^\}\/])*)\/)?\}/g, (match, _escape, name, pattern, replacement, _offset, _str, groups) => { From 4f9f7acb37a4a7b9cebadc4a3493022e6f099820 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 28 Jan 2025 23:25:22 -0800 Subject: [PATCH 0963/3587] Change "Explore New Features" to "Show Release Notes" (#239058) --- src/vs/workbench/contrib/chat/browser/actions/chatActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index db8068585535..d7532a3514b8 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -335,7 +335,7 @@ class ChatAddAction extends Action2 { MenuRegistry.appendMenuItem(MenuId.ViewTitle, { command: { id: 'update.showCurrentReleaseNotes', - title: localize2('chat.releaseNotes.label', "Explore New Features"), + title: localize2('chat.releaseNotes.label', "Show Release Notes"), }, when: ContextKeyExpr.equals('view', ChatViewId) }); From 3f1d0f79ebebc75b51ce1bd8f5f24a46be2eea58 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 28 Jan 2025 23:29:31 -0800 Subject: [PATCH 0964/3587] Don't toggle agent mode when clicking the selected option in the dropdown (#239059) --- .../contrib/chat/browser/actions/chatExecuteActions.ts | 9 ++++++++- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 6 +++--- src/vs/workbench/contrib/chat/common/chatAgents.ts | 6 +++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index d82e82971b0e..cc5339b263cb 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -90,6 +90,11 @@ export class ChatSubmitAction extends SubmitAction { } export const ToggleAgentModeActionId = 'workbench.action.chat.toggleAgentMode'; + +export interface IToggleAgentModeArgs { + agentMode: boolean; +} + export class ToggleAgentModeAction extends Action2 { static readonly ID = ToggleAgentModeActionId; @@ -159,7 +164,9 @@ export class ToggleAgentModeAction extends Action2 { } } - agentService.toggleToolsAgentMode(); + const arg = args[0] as IToggleAgentModeArgs | undefined; + agentService.toggleToolsAgentMode(typeof arg?.agentMode === 'boolean' ? arg.agentMode : undefined); + await commandService.executeCommand(ChatDoneActionId); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index a2593207616a..0845785aa07e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -89,7 +89,7 @@ import { IChatVariablesService } from '../common/chatVariables.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; import { ChatInputHistoryMaxEntries, IChatHistoryEntry, IChatInputState, IChatWidgetHistoryService } from '../common/chatWidgetHistoryService.js'; import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js'; -import { CancelAction, ChatModelPickerActionId, ChatSubmitAction, ChatSubmitSecondaryAgentAction, IChatExecuteActionContext, ToggleAgentModeActionId } from './actions/chatExecuteActions.js'; +import { CancelAction, ChatModelPickerActionId, ChatSubmitAction, ChatSubmitSecondaryAgentAction, IChatExecuteActionContext, IToggleAgentModeArgs, ToggleAgentModeActionId } from './actions/chatExecuteActions.js'; import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js'; import { InstructionAttachmentsWidget } from './attachments/instructionsAttachment/instructionAttachments.js'; import { IChatWidget } from './chat.js'; @@ -1728,7 +1728,7 @@ class ToggleAgentActionViewItem extends MenuEntryActionViewItem { label: localize('chat.agentMode', "Agent"), class: undefined, enabled: true, - run: () => this.action.run() + run: () => this.action.run({ agentMode: true } satisfies IToggleAgentModeArgs) }, { ...this.action, @@ -1737,7 +1737,7 @@ class ToggleAgentActionViewItem extends MenuEntryActionViewItem { class: undefined, enabled: true, checked: !this.action.checked, - run: () => this.action.run() + run: () => this.action.run({ agentMode: false } satisfies IToggleAgentModeArgs) }, ]; } diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 1422b04c05f6..2a0c9041319f 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -209,7 +209,7 @@ export interface IChatAgentService { readonly onDidChangeAgents: Event; readonly onDidChangeToolsAgentModeEnabled: Event; readonly toolsAgentModeEnabled: boolean; - toggleToolsAgentMode(): void; + toggleToolsAgentMode(enabled?: boolean): void; registerAgent(id: string, data: IChatAgentData): IDisposable; registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable; registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; @@ -427,8 +427,8 @@ export class ChatAgentService extends Disposable implements IChatAgentService { return !!this._hasToolsAgentContextKey.get() && !!this._agentModeContextKey.get(); } - toggleToolsAgentMode(): void { - this._agentModeContextKey.set(!this._agentModeContextKey.get()); + toggleToolsAgentMode(enabled?: boolean): void { + this._agentModeContextKey.set(enabled ?? !this._agentModeContextKey.get()); this._onDidChangeToolsAgentModeEnabled.fire(); this._onDidChangeAgents.fire(this.getDefaultAgent(ChatAgentLocation.EditingSession)); } From 4c081d3ae7dc5d3bb5d45406c8d8a366c6f6db8a Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 29 Jan 2025 09:26:37 +0100 Subject: [PATCH 0965/3587] Fix line replacement view width computation (#239064) line replace view width computation fix --- .../browser/view/inlineEdits/wordReplacementView.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index df87b2fcae21..831f203243bc 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -325,17 +325,13 @@ export class LineReplacementView extends Disposable implements IInlineEditsView const scrollLeft = this._editor.scrollLeft.read(reader); const scrollTop = this._editor.scrollTop.read(reader); const editorLeftOffset = contentLeft - scrollLeft; - const w = this._editor.getOption(EditorOption.fontInfo).read(reader).typicalHalfwidthCharacterWidth; const PADDING = 4; - const editorModel = this._editor.editor.getModel()!; - const { prefixTrim, prefixLeftOffset } = this._maxPrefixTrim; + const textModel = this._editor.editor.getModel()!; + const { prefixLeftOffset } = this._maxPrefixTrim; - // TODO: correctly count tabs - const originalLineContents: string[] = []; - this._originalRange.forEach(line => originalLineContents.push(editorModel.getLineContent(line))); - const maxOriginalLineLength = Math.max(...originalLineContents.map(l => l.length)) - prefixTrim; - const maxLineWidth = Math.max(maxOriginalLineLength * w, requiredWidth); + const originalLineWidths = this._originalRange.mapToLineArray(line => this._editor.editor.getOffsetForColumn(line, textModel.getLineMaxColumn(line)) - prefixLeftOffset); + const maxLineWidth = Math.max(...originalLineWidths, requiredWidth); const startLineNumber = this._originalRange.startLineNumber; const endLineNumber = this._originalRange.endLineNumberExclusive - 1; From ac2084d8d14234715d2190e618b56a6ce9d70ad6 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 29 Jan 2025 09:34:42 +0100 Subject: [PATCH 0966/3587] Fix insertion view height and position calculations (#239065) insertion view fixes --- .../browser/view/ghostText/ghostTextView.ts | 14 +++++++- .../browser/view/inlineEdits/insertionView.ts | 34 +++++++++++-------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts index 6030a75ffed9..47644496fea2 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts @@ -155,6 +155,11 @@ export class GhostTextView extends Disposable { ) ); + public readonly height = derived(this, reader => { + const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); + return lineHeight + (this.additionalLinesWidget.viewZoneHeight.read(reader) ?? 0); + }); + public ownsViewZone(viewZoneId: string): boolean { return this.additionalLinesWidget.viewZoneId === viewZoneId; } @@ -231,6 +236,9 @@ export class AdditionalLinesWidget extends Disposable { private _viewZoneInfo: { viewZoneId: string; heightInLines: number; lineNumber: number } | undefined; public get viewZoneId(): string | undefined { return this._viewZoneInfo?.viewZoneId; } + private _viewZoneHeight = observableValue('viewZoneHeight', undefined); + public get viewZoneHeight(): IObservable { return this._viewZoneHeight; } + private readonly editorOptionsChanged = observableSignalFromEvent('editorOptionChanged', Event.filter( this.editor.onDidChangeConfiguration, e => e.hasChanged(EditorOption.disableMonospaceOptimizations) @@ -303,7 +311,10 @@ export class AdditionalLinesWidget extends Disposable { afterLineNumber: afterLineNumber, heightInLines: heightInLines, domNode, - afterColumnAffinity: PositionAffinity.Right + afterColumnAffinity: PositionAffinity.Right, + onComputedHeight: (height: number) => { + this._viewZoneHeight.set(height, undefined); // TODO: can a transaction be used to avoid flickering? + } }); this.keepCursorStable(afterLineNumber, heightInLines); @@ -318,6 +329,7 @@ export class AdditionalLinesWidget extends Disposable { this.keepCursorStable(this._viewZoneInfo.lineNumber, -this._viewZoneInfo.heightInLines); this._viewZoneInfo = undefined; + this._viewZoneHeight.set(undefined, undefined); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts index 96c7919586cb..c6677c0c08f8 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts @@ -4,12 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { $ } from '../../../../../../base/browser/dom.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { IObservable, constObservable, derived, observableValue } from '../../../../../../base/common/observable.js'; +import { IObservable, constObservable, derived, derivedWithStore, observableValue } from '../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { Point } from '../../../../../browser/point.js'; import { LineSource, renderLines, RenderOptions } from '../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; +import { EditorOption } from '../../../../../common/config/editorOptions.js'; +import { Position } from '../../../../../common/core/position.js'; import { Range } from '../../../../../common/core/range.js'; import { ILanguageService } from '../../../../../common/languages/language.js'; import { LineTokens } from '../../../../../common/tokens/lineTokens.js'; @@ -69,7 +71,7 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits position: constObservable(null), allowEditorOverflow: false, minContentWidthInPx: derived(reader => { - const info = this._editorLayoutInfo.read(reader); + const info = this._overlayLayout.read(reader); if (info === null) { return 0; } return info.code1.x - info.codeStart1.x; }), @@ -110,26 +112,32 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits return Math.max(...lineWidths); }); - private readonly _editorLayoutInfo = derived(this, (reader) => { + private readonly _overlayLayout = derivedWithStore(this, (reader, store) => { this._ghostText.read(reader); const state = this._state.read(reader); if (!state) { return null; } + // Update the overlay when the position changes + this._editorObs.observePosition(observableValue(this, new Position(state.lineNumber, state.column)), store).read(reader); + + const lineHeight = this._editor.getOption(EditorOption.lineHeight); + const scrollTop = this._editorObs.scrollTop.read(reader); const editorLayout = this._editorObs.layoutInfo.read(reader); const horizontalScrollOffset = this._editorObs.scrollLeft.read(reader); const left = editorLayout.contentLeft + this._editorMaxContentWidthInRange.read(reader) - horizontalScrollOffset; - const scrollTop = this._editorObs.scrollTop.read(reader); - - const top = state.text.startsWith('\n') - ? this._editor.getBottomForLineNumber(state.lineNumber) - scrollTop - : this._editor.getTopForLineNumber(state.lineNumber) - scrollTop; - const bottom = this._editor.getTopForLineNumber(state.lineNumber + 1) - scrollTop; + let height = this._ghostTextView.height.read(reader); + let top = this._editor.getTopForLineNumber(state.lineNumber) - scrollTop; + if (state.text.startsWith('\n')) { + height -= lineHeight; + top += lineHeight; + } const codeLeft = editorLayout.contentLeft; + const bottom = top + height; if (left <= codeLeft) { return null; @@ -139,14 +147,12 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits const codeStart1 = new Point(codeLeft, top); const code2 = new Point(left, bottom); const codeStart2 = new Point(codeLeft, bottom); - const codeHeight = bottom - top; return { code1, codeStart1, code2, codeStart2, - codeHeight, horizontalScrollOffset, padding: 2, borderRadius: 4, @@ -157,10 +163,10 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits transform: 'translate(-0.5 -0.5)', style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, }, derived(reader => { - const layoutInfoObs = mapOutFalsy(this._editorLayoutInfo).read(reader); - if (!layoutInfoObs) { return undefined; } + const overlayLayoutObs = mapOutFalsy(this._overlayLayout).read(reader); + if (!overlayLayoutObs) { return undefined; } - const layoutInfo = layoutInfoObs.read(reader); + const layoutInfo = overlayLayoutObs.read(reader); const rectangleOverlay = createRectangle( { From e646f3e5cbd7dcef83a5f3ec7cf0c488afbf0fb8 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 29 Jan 2025 09:45:56 +0100 Subject: [PATCH 0967/3587] fix https://github.com/microsoft/vscode-copilot/issues/12393 (#239066) --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index d393b5c86a19..88a458ba5cd4 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -128,6 +128,8 @@ configurationRegistry.registerConfiguration({ type: 'number', markdownDescription: nls.localize('chat.editing.autoAcceptDelay', "Delay after which changes made by chat are automatically accepted. Values are in seconds, `0` means disabled and `100` seconds is the maximum."), default: 0, + minimum: 0, + maximum: 100 }, 'chat.editing.confirmEditRequestRemoval': { type: 'boolean', From 6325b956c023dc19a4d17b7a5941a76e9adb0f15 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 29 Jan 2025 09:48:00 +0100 Subject: [PATCH 0968/3587] fixes https://github.com/microsoft/vscode/issues/238926 (#239067) --- .../contrib/inlineChat/browser/inlineChatController2.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts index 1e925be08a2d..688280bb744c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts @@ -248,6 +248,7 @@ export class StartSessionAction2 extends EditorAction2 { id: MenuId.ChatCommandCenter, group: 'd_inlineChat', order: 10, + when: CTX_INLINE_CHAT_HAS_AGENT2 } }); } From aefbe6c9a4d6048b1a9a184ec3cc8445f0794f6a Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Wed, 29 Jan 2025 01:02:54 -0800 Subject: [PATCH 0969/3587] filter out images from history (#239069) --- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 0845785aa07e..888e4637dd6d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -628,7 +628,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge async acceptInput(isUserQuery?: boolean): Promise { if (isUserQuery) { const userQuery = this._inputEditor.getValue(); - const entry: IChatHistoryEntry = { text: userQuery, state: this.getInputState() }; + const inputState = this.getInputState(); + inputState.chatContextAttachments = inputState.chatContextAttachments?.filter(attachment => !attachment.isImage); + const entry: IChatHistoryEntry = { text: userQuery, state: inputState }; this.history.replaceLast(entry); this.history.add({ text: '' }); } From bd23a0b300cd557cb5a9e30995b6af14777c030f Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 29 Jan 2025 10:18:19 +0100 Subject: [PATCH 0970/3587] fix https://github.com/microsoft/vscode/issues/238925 (#239070) --- .../contrib/inlineChat/browser/media/inlineChat.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index 327ae54548d7..01c83a45b7b1 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -72,10 +72,14 @@ outline: none; } -.monaco-workbench .zone-widget.inline-chat-widget:not(.inline-chat-2) .inline-chat .chat-widget .interactive-session .interactive-input-part { +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part { padding: 4px 0 0 0; } +.monaco-workbench .inline-chat-2 .inline-chat .chat-widget .interactive-session .interactive-input-part { + padding: 8px 0 0 0; +} + .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part .interactive-execute-toolbar { margin-bottom: 1px; } From e09aa10ee7b5854f2e8ddf9d0facc7f6881fdc27 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 29 Jan 2025 10:18:44 +0100 Subject: [PATCH 0971/3587] workaround for https://github.com/microsoft/vscode-copilot/issues/11811 (#239072) --- src/vs/workbench/contrib/chat/browser/chatEditorActions.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts index 23203029af91..94e831bd1adf 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts @@ -37,8 +37,11 @@ abstract class NavigateAction extends Action2 { primary: next ? KeyMod.Alt | KeyCode.F5 : KeyMod.Alt | KeyMod.Shift | KeyCode.F5, - weight: KeybindingWeight.EditorContrib, - when: ContextKeyExpr.and(ContextKeyExpr.or(ctxHasEditorModification, ctxNotebookHasEditorModification), EditorContextKeys.focus), + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and( + ContextKeyExpr.or(ctxHasEditorModification, ctxNotebookHasEditorModification), + EditorContextKeys.focus + ), }, f1: true, menu: { From 3c3730b70717e6121bf85b8ed08d4311f8705f4d Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:18:50 +0100 Subject: [PATCH 0972/3587] Fix NES freezes window issue (#239074) fix NES freezes window --- src/vs/editor/browser/rect.ts | 8 ++++---- .../browser/view/inlineEdits/wordReplacementView.ts | 7 ++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/browser/rect.ts b/src/vs/editor/browser/rect.ts index bcbe21146c3e..09a93216f268 100644 --- a/src/vs/editor/browser/rect.ts +++ b/src/vs/editor/browser/rect.ts @@ -25,10 +25,10 @@ export class Rect { } public static hull(rects: Rect[]): Rect { - let left = Number.MAX_VALUE; - let top = Number.MAX_VALUE; - let right = Number.MIN_VALUE; - let bottom = Number.MIN_VALUE; + let left = Number.MAX_SAFE_INTEGER; + let top = Number.MAX_SAFE_INTEGER; + let right = Number.MIN_SAFE_INTEGER; + let bottom = Number.MIN_SAFE_INTEGER; for (const rect of rects) { left = Math.min(left, rect.left); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index 831f203243bc..fe5996d1db9c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -338,11 +338,6 @@ export class LineReplacementView extends Disposable implements IInlineEditsView const topOfOriginalLines = this._editor.editor.getTopForLineNumber(startLineNumber) - scrollTop; const bottomOfOriginalLines = this._editor.editor.getBottomForLineNumber(endLineNumber) - scrollTop; - if (bottomOfOriginalLines <= 0) { - this._viewZoneInfo.set(undefined, undefined); - return undefined; - } - // Box Widget positioning const originalLinesOverlay = Rect.fromLeftTopWidthHeight( editorLeftOffset + prefixLeftOffset, @@ -370,6 +365,8 @@ export class LineReplacementView extends Disposable implements IInlineEditsView if (!activeViewZone || activeViewZone.lineNumber !== viewZoneLineNumber || activeViewZone.height !== viewZoneHeight) { this._viewZoneInfo.set({ height: viewZoneHeight, lineNumber: viewZoneLineNumber }, undefined); } + } else if (this._viewZoneInfo.get()) { + this._viewZoneInfo.set(undefined, undefined); } return { From 7c15e5aeb10ea318f562258fc9091c5788395316 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 29 Jan 2025 11:26:48 +0100 Subject: [PATCH 0973/3587] Improve description for comments.thread.confirmOnCollapse (#239075) Fixes microsoft/vscode-pull-request-github#6618 --- .../contrib/comments/browser/comments.contribution.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index 3cf98a0cde92..be76ed22a853 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -142,8 +142,9 @@ Registry.as(ConfigurationExtensions.Configuration).regis 'comments.thread.confirmOnCollapse': { type: 'string', enum: ['whenHasUnsubmittedComments', 'never'], + enumDescriptions: [nls.localize('confirmOnCollapse.whenHasUnsubmittedComments', "Show a confirmation dialog when collapsing a comment thread with unsubmitted comments."), nls.localize('confirmOnCollapse.never', "Never show a confirmation dialog when collapsing a comment thread.")], default: 'never', - description: nls.localize('confirmOnCollapse', "Controls whether a confirmation dialog is shown when collapsing a comment thread with unsubmitted comments.") + description: nls.localize('confirmOnCollapse', "Controls whether a confirmation dialog is shown when collapsing a comment thread.") } } }); From 4a2672945640fe262d363b831431afabe1561ac8 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:27:14 +0100 Subject: [PATCH 0974/3587] Fix gutter icon positioning over minimap (#239076) fix https://github.com/microsoft/vscode-copilot/issues/12501 --- .../browser/view/inlineEdits/gutterIndicatorView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index 6aab8e5a303d..5091bc0cc274 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -126,7 +126,7 @@ export class InlineEditsGutterIndicator extends Disposable { const space = 1; - const targetRect = Rect.fromRanges(OffsetRange.fromTo(space, layout.lineNumbersLeft + layout.lineNumbersWidth + 4), targetVertRange); + const targetRect = Rect.fromRanges(OffsetRange.fromTo(space + layout.glyphMarginLeft, layout.lineNumbersLeft + layout.lineNumbersWidth + 4), targetVertRange); const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); From d799a0b5824011842f50e1366501ae4149353992 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 29 Jan 2025 11:33:33 +0100 Subject: [PATCH 0975/3587] avoid cyclic dependency between `ChatEditorOverlayController` and `ChatEditorController` (#239077) fixes https://github.com/microsoft/vscode-copilot/issues/12252 --- .../chat/browser/chatEditorController.ts | 9 ++++++++- .../contrib/chat/browser/chatEditorOverlay.ts | 17 +++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index 88cb0f857c55..77d55bbbf1c0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -165,7 +165,14 @@ export class ChatEditorController extends Disposable implements IEditorContribut if (entry.state.read(r) !== WorkingSetEntryState.Modified) { this._overlayCtrl.hide(); } else { - this._overlayCtrl.showEntry(session, entry, entries[(idx + 1) % entries.length]); + this._overlayCtrl.showEntry( + session, + entry, entries[(idx + 1) % entries.length], + { + entryIndex: this._currentEntryIndex, + changeIndex: this._currentChangeIndex + } + ); } // scrolling logic diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index 51f631e652b7..f420d0883fca 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, observableFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; +import { autorun, IObservable, observableFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar, WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -241,7 +241,7 @@ class ChatEditorOverlayWidget implements IOverlayWidget { this._show(); } - showEntry(session: IChatEditingSession, activeEntry: IModifiedFileEntry, next: IModifiedFileEntry) { + showEntry(session: IChatEditingSession, activeEntry: IModifiedFileEntry, next: IModifiedFileEntry, indicies: { entryIndex: IObservable; changeIndex: IObservable }) { this._showStore.clear(); @@ -261,10 +261,8 @@ class ChatEditorOverlayWidget implements IOverlayWidget { this._showStore.add(autorun(r => { - const ctrl = ChatEditorController.get(this._editor); - - const entryIndex = ctrl?.currentEntryIndex.read(r); - const changeIndex = ctrl?.currentChangeIndex.read(r); + const entryIndex = indicies.entryIndex.read(r); + const changeIndex = indicies.changeIndex.read(r); const entries = session.entries.read(r); @@ -351,8 +349,11 @@ export class ChatEditorOverlayController implements IEditorContribution { this._overlayWidget.showRequest(session); } - showEntry(session: IChatEditingSession, activeEntry: IModifiedFileEntry, next: IModifiedFileEntry) { - this._overlayWidget.showEntry(session, activeEntry, next); + showEntry(session: IChatEditingSession, + activeEntry: IModifiedFileEntry, next: IModifiedFileEntry, + indicies: { entryIndex: IObservable; changeIndex: IObservable } + ) { + this._overlayWidget.showEntry(session, activeEntry, next, indicies); } hide() { From ea254757de5d467b73578f9dfb2f5633a6e548da Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 29 Jan 2025 11:51:13 +0100 Subject: [PATCH 0976/3587] fix #238952 (#239079) --- .../common/extensionManagementService.ts | 137 +++++++++++++++--- 1 file changed, 114 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 95da45fb717f..85385c2fcbbe 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -24,7 +24,7 @@ import { localize } from '../../../../nls.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { Schemas } from '../../../../base/common/network.js'; import { IDownloadService } from '../../../../platform/download/common/download.js'; -import { coalesce } from '../../../../base/common/arrays.js'; +import { coalesce, isNonEmptyArray } from '../../../../base/common/arrays.js'; import { IDialogService, IPromptButton } from '../../../../platform/dialogs/common/dialogs.js'; import Severity from '../../../../base/common/severity.js'; import { IUserDataSyncEnablementService, SyncResource } from '../../../../platform/userDataSync/common/userDataSync.js'; @@ -503,7 +503,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } if (installOptions?.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] !== ExtensionInstallSource.SETTINGS_SYNC) { - await this.checkForTrustedPublisher(gallery, manifest); + await this.checkForTrustedPublisher(gallery, manifest, !installOptions?.donotIncludePackAndDependencies); await this.checkForWorkspaceTrust(manifest, false); @@ -789,11 +789,13 @@ export class ExtensionManagementService extends Disposable implements IWorkbench throw new Error('No extension server found'); } - private async checkForTrustedPublisher(extension: IGalleryExtension, manifest: IExtensionManifest): Promise { + private async checkForTrustedPublisher(extension: IGalleryExtension, manifest: IExtensionManifest, checkForPackAndDependencies: boolean): Promise { if (this.isPublisherTrusted(extension)) { return; } + const otherUntrustedPublishers = checkForPackAndDependencies ? await this.getOtherUntrustedPublishers(manifest) : []; + type TrustPublisherClassification = { owner: 'sandy081'; comment: 'Report the action taken by the user on the publisher trust dialog'; @@ -806,7 +808,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench }; const installButton: IPromptButton = { - label: localize({ key: 'trust and install', comment: ['&& denotes a mnemonic'] }, "Trust Publisher & &&Install"), + label: otherUntrustedPublishers.length ? localize({ key: 'trust publishers and install', comment: ['&& denotes a mnemonic'] }, "Trust Publishers & &&Install") : localize({ key: 'trust and install', comment: ['&& denotes a mnemonic'] }, "Trust Publisher & &&Install"), run: () => { this.telemetryService.publicLog2('extensions:trustPublisher', { action: 'trust', extensionId: extension.identifier.id }); this.trustPublishers(extension.publisher); @@ -822,28 +824,79 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } }; + const getPublisherLink = ({ publisherDisplayName, publisher }: { publisherDisplayName: string; publisher: string }) => { + return `[${publisherDisplayName}](${joinPath(URI.parse(this.productService.extensionsGallery!.publisherUrl), publisher)})`; + }; + const unverifiedLink = 'https://aka.ms/vscode-verify-publisher'; + + const title = otherUntrustedPublishers.length + ? otherUntrustedPublishers.length === 1 + ? localize('checkTwoTrustedPublishersTitle', "Do you trust publishers \"{0}\" and \"{1}\"?", extension.publisherDisplayName, otherUntrustedPublishers[0].publisherDisplayName) + : localize('checkAllTrustedPublishersTitle', "Do you trust the publisher \"{0}\" and {1} others?", extension.publisherDisplayName, otherUntrustedPublishers.length) + : localize('checkTrustedPublisherTitle', "Do you trust the publisher \"{0}\"?", extension.publisherDisplayName); + const customMessage = new MarkdownString('', { supportThemeIcons: true, isTrusted: true }); - customMessage.appendMarkdown(localize('message1', "The extension {0} is published by {1}. This is the first extension you're installing from this publisher.", `[${extension.displayName}](${this.productService.extensionsGallery!.itemUrl}?itemName=${extension.identifier.id})`, `[${extension.publisherDisplayName}](${joinPath(URI.parse(this.productService.extensionsGallery!.publisherUrl), extension.publisher)})`)); - customMessage.appendText('\n'); - if (extension.publisherDomain?.verified) { - const publisherVerifiedMessage = localize('verifiedPublisher', "This publisher has verified ownership of {0}.", `[${URI.parse(extension.publisherDomain.link).authority}](${extension.publisherDomain.link})`); - customMessage.appendMarkdown(`$(${verifiedPublisherIcon.id}) ${publisherVerifiedMessage}`); - } else { - customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublisher', "This publisher is **not** [verified](https://aka.ms/vscode-verify-publisher).")}`); - } + if (otherUntrustedPublishers.length) { + customMessage.appendMarkdown(localize('extension published by message', "The extension {0} is published by {1}.", `[${extension.displayName}](${this.productService.extensionsGallery!.itemUrl}?itemName=${extension.identifier.id})`, getPublisherLink(extension))); + customMessage.appendMarkdown(' '); + const commandUri = URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([extension.identifier.id, manifest.extensionPack?.length ? 'extensionPack' : 'dependencies']))}`).toString(); + if (otherUntrustedPublishers.length === 1) { + customMessage.appendMarkdown(localize('singleUntrustedPublisher', "Installing this extension will also install [extensions]({0}) published by {1}.", commandUri, getPublisherLink(otherUntrustedPublishers[0]))); + } else { + customMessage.appendMarkdown(localize('message3', "Installing this extension will also install [extensions]({0}) published by {1} and {2}.", commandUri, otherUntrustedPublishers.slice(0, otherUntrustedPublishers.length - 1).map(p => getPublisherLink(p)).join(', '), getPublisherLink(otherUntrustedPublishers[otherUntrustedPublishers.length - 1]))); + } + customMessage.appendMarkdown(' '); + customMessage.appendMarkdown(localize('firstTimeInstallingMessage', "This is the first time you're installing extensions from these publishers.")); + + const allPublishers = [extension, ...otherUntrustedPublishers]; + const unverfiiedPublishers = allPublishers.filter(p => !p.publisherDomain?.verified); + const verifiedPublishers = allPublishers.filter(p => p.publisherDomain?.verified); + + if (verifiedPublishers.length) { + customMessage.appendText('\n'); + for (const publisher of verifiedPublishers) { + if (publisher.publisherDomain?.verified) { + customMessage.appendText('\n'); + const publisherVerifiedMessage = localize('verifiedPublisherWithName', "{0} has verified ownership of {1}.", getPublisherLink(publisher), `[${URI.parse(publisher.publisherDomain.link).authority}](${publisher.publisherDomain.link})`); + customMessage.appendMarkdown(`$(${verifiedPublisherIcon.id}) ${publisherVerifiedMessage}`); + } else { + unverfiiedPublishers.push(publisher); + } + } + if (unverfiiedPublishers.length) { + customMessage.appendText('\n'); + if (unverfiiedPublishers.length === 1) { + customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublisherWithName', "{0} is **not** [verified]({1}).", getPublisherLink(unverfiiedPublishers[0]), unverifiedLink)}`); + } else { + customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublishers', "{0} and {1} are **not** [verified]({2}).", unverfiiedPublishers.slice(0, unverfiiedPublishers.length - 1).map(p => getPublisherLink(p)).join(', '), getPublisherLink(unverfiiedPublishers[unverfiiedPublishers.length - 1]), unverifiedLink)}`); + } + } + } else { + customMessage.appendText('\n'); + customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('allUnverifed', "All publishers are **not** [verified]({0}).", unverifiedLink)}`); + } - if (await this.hasDepsAndPacksFromOtherUntrustedPublishers(manifest)) { - const commandUri = URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([extension.identifier.id, manifest.extensionPack?.length ? 'extensionPack' : 'dependencies']))}`); + } else { + customMessage.appendMarkdown(localize('message1', "The extension {0} is published by {1}. This is the first extension you're installing from this publisher.", `[${extension.displayName}](${this.productService.extensionsGallery!.itemUrl}?itemName=${extension.identifier.id})`, getPublisherLink(extension))); customMessage.appendText('\n'); - customMessage.appendMarkdown(localize('message3', "Installing this extension will also install [extensions]({0}) from other publishers. Trusting this publisher will automatically trust the other publishers.", commandUri.toString())); + if (extension.publisherDomain?.verified) { + const publisherVerifiedMessage = localize('verifiedPublisher', "{0} has verified ownership of {1}.", getPublisherLink(extension), `[${URI.parse(extension.publisherDomain.link).authority}](${extension.publisherDomain.link})`); + customMessage.appendMarkdown(`$(${verifiedPublisherIcon.id}) ${publisherVerifiedMessage}`); + } else { + customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublisher', "{0} is **not** [verified]({1}).", getPublisherLink(extension), unverifiedLink)}`); + } } - customMessage.appendText('\n'); - customMessage.appendMarkdown(localize('message2', "{0} has no control over the behavior of third-party extensions, including how they manage your personal data. Please proceed only if you trust the publisher.", this.productService.nameLong)); + customMessage.appendText('\n\n'); + if (otherUntrustedPublishers.length) { + customMessage.appendMarkdown(localize('message4', "{0} has no control over the behavior of third-party extensions, including how they manage your personal data. Please proceed only if you trust the publishers.", this.productService.nameLong)); + } else { + customMessage.appendMarkdown(localize('message2', "{0} has no control over the behavior of third-party extensions, including how they manage your personal data. Please proceed only if you trust the publisher.", this.productService.nameLong)); + } await this.dialogService.prompt({ - message: localize('checkTrustedPublisherTitle', "Do you trust the publisher \"{0}\"?", extension.publisherDisplayName), + message: title, type: Severity.Warning, buttons: [installButton, learnMoreButton], cancelButton: { @@ -860,7 +913,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } - private async hasDepsAndPacksFromOtherUntrustedPublishers(manifest: IExtensionManifest): Promise { + private async getOtherUntrustedPublishers(manifest: IExtensionManifest): Promise<{ publisher: string; publisherDisplayName: string; publisherDomain?: { link: string; verified: boolean } }[]> { const infos = []; for (const id of [...(manifest.extensionPack ?? []), ...(manifest.extensionDependencies ?? [])]) { const [publisherId] = id.split('.'); @@ -870,13 +923,51 @@ export class ExtensionManagementService extends Disposable implements IWorkbench if (this.isPublisherUserTrusted(publisherId.toLowerCase())) { continue; } - infos.push({ id }); + infos.push(id); } if (!infos.length) { - return false; + return []; + } + const extensions = new Map(); + await this.getDependenciesAndPackedExtensionsRecursively(infos, extensions, CancellationToken.None); + const publishers = new Map(); + for (const [, extension] of extensions) { + if (this.isPublisherTrusted(extension)) { + continue; + } + publishers.set(extension.publisherDisplayName, extension); + } + return [...publishers.values()]; + } + + private async getDependenciesAndPackedExtensionsRecursively(toGet: string[], result: Map, token: CancellationToken): Promise { + if (toGet.length === 0) { + return; + } + + const extensions = await this.extensionGalleryService.getExtensions(toGet.map(id => ({ id })), token); + for (let idx = 0; idx < extensions.length; idx++) { + const extension = extensions[idx]; + result.set(extension.identifier.id.toLowerCase(), extension); + } + toGet = []; + for (const extension of extensions) { + if (isNonEmptyArray(extension.properties.dependencies)) { + for (const id of extension.properties.dependencies) { + if (!result.has(id.toLowerCase())) { + toGet.push(id); + } + } + } + if (isNonEmptyArray(extension.properties.extensionPack)) { + for (const id of extension.properties.extensionPack) { + if (!result.has(id.toLowerCase())) { + toGet.push(id); + } + } + } } - const extensions = await this.extensionGalleryService.getExtensions(infos, CancellationToken.None); - return extensions.some(e => !this.isPublisherTrusted(e)); + return this.getDependenciesAndPackedExtensionsRecursively(toGet, result, token); } private async checkForWorkspaceTrust(manifest: IExtensionManifest, requireTrust: boolean): Promise { From b92ec2d1c4600a78088c3fb928b4b76d68f82f0a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 29 Jan 2025 12:25:56 +0100 Subject: [PATCH 0977/3587] chat - setup tweaks (#239080) --- .../contrib/chat/browser/actions/chatGettingStarted.ts | 2 +- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 1 - src/vs/workbench/contrib/chat/browser/chat.ts | 6 ++++-- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts b/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts index 208a6498d147..7beb2b8c1fd4 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts @@ -76,7 +76,7 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb // Open Copilot view showCopilotView(this.viewsService, this.layoutService); - ensureSideBarChatViewSize(this.viewDescriptorService, this.layoutService); + ensureSideBarChatViewSize(this.viewDescriptorService, this.layoutService, this.viewsService); // Only do this once this.storageService.store(ChatGettingStartedContribution.hideWelcomeView, true, StorageScope.APPLICATION, StorageTarget.MACHINE); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 88a458ba5cd4..2e98a848f065 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -120,7 +120,6 @@ configurationRegistry.registerConfiguration({ }, 'chat.commandCenter.enabled': { type: 'boolean', - tags: ['preview'], markdownDescription: nls.localize('chat.commandCenter.enabled', "Controls whether the command center shows a menu for actions to control Copilot (requires {0}).", '`#window.commandCenter#`'), default: true }, diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index ed82ef634284..7c8900defcf0 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -75,8 +75,10 @@ export function showCopilotView(viewsService: IViewsService, layoutService: IWor } } -export function ensureSideBarChatViewSize(viewDescriptorService: IViewDescriptorService, layoutService: IWorkbenchLayoutService): void { - const location = viewDescriptorService.getViewLocationById(ChatViewId); +export function ensureSideBarChatViewSize(viewDescriptorService: IViewDescriptorService, layoutService: IWorkbenchLayoutService, viewsService: IViewsService): void { + const viewId = preferCopilotEditsView(viewsService) ? EditsViewId : ChatViewId; + + const location = viewDescriptorService.getViewLocationById(viewId); if (location === ViewContainerLocation.Panel) { return; // panel is typically very wide } diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index d968b47c475f..71b1d94a62e7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -182,7 +182,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr await context.update({ hidden: false }); showCopilotView(viewsService, layoutService); - ensureSideBarChatViewSize(viewDescriptorService, layoutService); + ensureSideBarChatViewSize(viewDescriptorService, layoutService, viewsService); configurationService.updateValue('chat.commandCenter.enabled', true); } From 4d919486c8d56dd769c4111b8a8f507a83eb65d8 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 29 Jan 2025 12:26:32 +0100 Subject: [PATCH 0978/3587] Improve wording and flow of restart EH blocker (fix #233912) (#239081) --- .../common/abstractExtensionService.ts | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 786153b46b67..382683f3cf76 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -762,26 +762,16 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._logService.warn(`Extension host was not stopped because of veto (stop reason: ${reason}, veto reason: ${vetoReasonsArray.join(', ')})`); - let overrideVeto = false; - await this._dialogService.prompt({ + const { confirmed } = await this._dialogService.confirm({ type: Severity.Warning, - message: nls.localize('extensionStopVetoMessage', "Restart of extensions was prevented but is required for: {0}. Do you want to proceed anyways?", reason), + message: nls.localize('extensionStopVetoMessage', "Please confirm restart of extensions."), detail: vetoReasonsArray.length === 1 ? - nls.localize('extensionStopVetoDetailsOne', "Reason: {0}", vetoReasonsArray[0]) : - nls.localize('extensionStopVetoDetailsMany', "Reasons:\n- {0}", vetoReasonsArray.join('\n -')), - buttons: [ - { - label: nls.localize('ok', "OK"), - run: () => { /* noop */ } - }, - { - label: nls.localize('proceedAnyways', "Proceed Anyways"), - run: () => overrideVeto = true - } - ] + vetoReasonsArray[0] : + vetoReasonsArray.join('\n -'), + primaryButton: nls.localize('proceedAnyways', "Restart Anyway") }); - if (overrideVeto) { + if (confirmed) { return true; } } From c561dcb7c680e7f0d9c984c17fd656d5da671ec5 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:27:00 +0100 Subject: [PATCH 0979/3587] Fix variable name in InlineEditsView (#239082) fixes https://github.com/microsoft/vscode-copilot/issues/12286 --- .../browser/view/inlineEdits/view.ts | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index b401367da279..aaf6532692a5 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -11,7 +11,8 @@ import { observableCodeEditor } from '../../../../../browser/observableCodeEdito import { EditorOption } from '../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../common/core/lineRange.js'; import { Position } from '../../../../../common/core/position.js'; -import { SingleTextEdit, StringText } from '../../../../../common/core/textEdit.js'; +import { Range } from '../../../../../common/core/range.js'; +import { AbstractText, SingleTextEdit, StringText } from '../../../../../common/core/textEdit.js'; import { TextLength } from '../../../../../common/core/textLength.js'; import { DetailedLineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../../../../../common/diff/rangeMapping.js'; import { TextModel } from '../../../../../common/model/textModel.js'; @@ -246,7 +247,7 @@ export class InlineEditsView extends Disposable { return 'insertionInline'; } - const innerValues = inner.map(m => ({ original: newText.getValueOfRange(m.originalRange), modified: newText.getValueOfRange(m.modifiedRange) })); + const innerValues = inner.map(m => ({ original: edit.originalText.getValueOfRange(m.originalRange), modified: newText.getValueOfRange(m.modifiedRange) })); if (innerValues.every(({ original, modified }) => modified.trim() === '' && original.length > 0 && (original.length > modified.length || original.trim() !== ''))) { return 'deletion'; } @@ -320,7 +321,7 @@ export class InlineEditsView extends Disposable { if (view === 'wordReplacements') { return { kind: 'wordReplacements' as const, - replacements + replacements: growEditsToEntireWord(replacements, edit.originalText), }; } @@ -397,3 +398,40 @@ function isSingleLineDeletion(diff: DetailedLineRangeMapping[]): boolean { return true; } } + +function growEditsToEntireWord(replacements: SingleTextEdit[], originalText: AbstractText): SingleTextEdit[] { + const result: SingleTextEdit[] = []; + + replacements.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + + for (const edit of replacements) { + + // grow to the left + let startIndex = edit.range.startColumn - 1; + let prefix = ''; + const startLineContent = originalText.getLineAt(edit.range.startLineNumber); + while (startIndex - 1 >= 0 && /^[a-zA-Z]$/.test(startLineContent[startIndex - 1])) { + prefix = startLineContent[startIndex - 1] + prefix; + startIndex--; + } + + // grow to the right + let endIndex = edit.range.endColumn - 2; + let suffix = ''; + const endLineContent = originalText.getLineAt(edit.range.endLineNumber); + while (endIndex + 1 < endLineContent.length && /^[a-zA-Z]$/.test(endLineContent[endIndex + 1])) { + suffix += endLineContent[endIndex + 1]; + endIndex++; + } + + // create new edit and merge together if they are touching + let newEdit = new SingleTextEdit(new Range(edit.range.startLineNumber, startIndex + 1, edit.range.endLineNumber, endIndex + 2), prefix + edit.text + suffix); + if (result.length > 0 && Range.areIntersectingOrTouching(result[result.length - 1].range, newEdit.range)) { + newEdit = SingleTextEdit.joinEdits([result.pop()!, newEdit], originalText); + } + + result.push(newEdit); + } + + return result; +} From 1d41395b542376a0e2992f7c0625bba673b0a935 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 29 Jan 2025 12:40:25 +0100 Subject: [PATCH 0980/3587] Show comment collapse confirmation for collapse all (#239083) Fixes microsoft/vscode-pull-request-github#6617 --- .../comments/browser/commentThreadWidget.ts | 38 ++++------------- .../browser/commentThreadZoneWidget.ts | 41 ++++++++++++++++--- .../comments/browser/commentsController.ts | 4 +- .../browser/commentsEditorContribution.ts | 7 +++- .../browser/view/cellParts/cellComments.ts | 2 +- 5 files changed, 52 insertions(+), 40 deletions(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 41fbd153cfd0..f1efc0001da7 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -5,7 +5,6 @@ import './media/review.css'; import * as dom from '../../../../base/browser/dom.js'; -import * as nls from '../../../../nls.js'; import * as domStylesheets from '../../../../base/browser/domStylesheets.js'; import { Emitter } from '../../../../base/common/event.js'; import { Disposable, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; @@ -38,8 +37,6 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js'; import { LayoutableEditor } from './simpleCommentEditor.js'; import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import Severity from '../../../../base/common/severity.js'; export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; @@ -76,13 +73,11 @@ export class CommentThreadWidget extends private _commentOptions: languages.CommentOptions | undefined, private _containerDelegate: { actionRunner: (() => void) | null; - collapse: () => void; + collapse: () => Promise; }, @ICommentService private readonly commentService: ICommentService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IDialogService private readonly _dialogService: IDialogService + @IKeybindingService private readonly _keybindingService: IKeybindingService ) { super(); @@ -96,7 +91,7 @@ export class CommentThreadWidget extends CommentThreadHeader, container, { - collapse: this.collapseAction.bind(this) + collapse: this._containerDelegate.collapse.bind(this) }, this._commentMenus, this._commentThread @@ -160,19 +155,8 @@ export class CommentThreadWidget extends this.currentThreadListeners(); } - private async confirmCollapse(): Promise { - const confirmSetting = this._configurationService.getValue<'whenHasUnsubmittedComments' | 'never'>('comments.thread.confirmOnCollapse'); - - const hasUnsubmitted = !!this._commentReply?.commentEditor.getValue() || this._body.hasCommentsInEditMode(); - if (confirmSetting === 'whenHasUnsubmittedComments' && hasUnsubmitted) { - const result = await this._dialogService.confirm({ - message: nls.localize('confirmCollapse', "This comment thread has unsubmitted comments. Do you want to collapse it?"), - primaryButton: nls.localize('collapse', "Collapse"), - type: Severity.Warning - }); - return result.confirmed; - } - return true; + get hasUnsubmittedComments(): boolean { + return !!this._commentReply?.commentEditor.getValue() || this._body.hasCommentsInEditMode(); } private _setAriaLabel(): void { @@ -391,17 +375,11 @@ export class CommentThreadWidget extends } } - private async collapseAction() { - if (await this.confirmCollapse()) { - this.collapse(); - } - } - - collapse() { - if (Range.isIRange(this.commentThread.range) && isCodeEditor(this._parentEditor)) { + async collapse() { + if ((await this._containerDelegate.collapse()) && Range.isIRange(this.commentThread.range) && isCodeEditor(this._parentEditor)) { this._parentEditor.setSelection(this.commentThread.range); } - this._containerDelegate.collapse(); + } applyTheme(theme: IColorTheme, fontInfo: FontInfo) { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 87e7767a8bcd..a06c9bce0205 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -7,7 +7,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { Color } from '../../../../base/common/color.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IDisposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; +import { ICodeEditor, IEditorMouseEvent, isCodeEditor, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; import { IPosition } from '../../../../editor/common/core/position.js'; import { IRange, Range } from '../../../../editor/common/core/range.js'; import * as languages from '../../../../editor/common/languages.js'; @@ -26,6 +26,9 @@ import { commentThreadStateBackgroundColorVar, commentThreadStateColorVar, getCo import { peekViewBorder } from '../../../../editor/contrib/peekView/browser/peekView.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { StableEditorScrollState } from '../../../../editor/browser/stableEditorScroll.js'; +import Severity from '../../../../base/common/severity.js'; +import * as nls from '../../../../nls.js'; +import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; function getCommentThreadWidgetStateColor(thread: languages.CommentThreadState | undefined, theme: IColorTheme): Color | undefined { return getCommentThreadStateBorderColor(thread, theme) ?? theme.getColor(peekViewBorder); @@ -134,7 +137,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget @IThemeService private themeService: IThemeService, @ICommentService private commentService: ICommentService, @IContextKeyService contextKeyService: IContextKeyService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IDialogService private readonly dialogService: IDialogService ) { super(editor, { keepEditorSelection: true, isAccessible: true }); this._contextKeyService = contextKeyService.createScoped(this.domNode); @@ -295,7 +299,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } }, collapse: () => { - this.collapse(); + return this.collapse(true); } } ) as unknown as CommentThreadWidget; @@ -316,10 +320,33 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this.commentService.disposeCommentThread(this.uniqueOwner, this._commentThread.threadId); } - public collapse() { + private doCollapse() { this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Collapsed; } + public async collapse(confirm: boolean = false): Promise { + if (!confirm || (await this.confirmCollapse())) { + this.doCollapse(); + return true; + } else { + return false; + } + } + + private async confirmCollapse(): Promise { + const confirmSetting = this.configurationService.getValue<'whenHasUnsubmittedComments' | 'never'>('comments.thread.confirmOnCollapse'); + + if (confirmSetting === 'whenHasUnsubmittedComments' && this._commentThreadWidget.hasUnsubmittedComments) { + const result = await this.dialogService.confirm({ + message: nls.localize('confirmCollapse', "This comment thread has unsubmitted comments. Do you want to collapse it?"), + primaryButton: nls.localize('collapse', "Collapse"), + type: Severity.Warning, + }); + return result.confirmed; + } + return true; + } + public expand(setActive?: boolean) { this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Expanded; if (setActive) { @@ -504,8 +531,10 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._refresh(this._commentThreadWidget.getDimensions()); } - collapseAndFocusRange() { - this._commentThreadWidget.collapse(); + async collapseAndFocusRange() { + if (await this.collapse(true) && Range.isIRange(this.commentThread.range) && isCodeEditor(this.editor)) { + this.editor.setSelection(this.commentThread.range); + } } override hide() { diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index 88a789ba2649..e5bb377d6979 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -742,7 +742,7 @@ export class CommentController implements IEditorContribution { public collapseAll(): void { for (const widget of this._commentWidgets) { - widget.collapse(); + widget.collapse(true); } } @@ -1136,7 +1136,7 @@ export class CommentController implements IEditorContribution { const existingCommentsAtLine = this.getCommentsAtLine(commentRange); if (existingCommentsAtLine.length) { const allExpanded = existingCommentsAtLine.every(widget => widget.expanded); - existingCommentsAtLine.forEach(allExpanded ? widget => widget.collapse() : widget => widget.expand(true)); + existingCommentsAtLine.forEach(allExpanded ? widget => widget.collapse(true) : widget => widget.expand(true)); this.processNextThreadToAdd(); return; } else { diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index e0441ab93fb0..9c44f25179a0 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -28,6 +28,7 @@ import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/ import { CommentsInputContentProvider } from './commentsInputContentProvider.js'; import { AccessibleViewProviderId } from '../../../../platform/accessibility/browser/accessibleView.js'; import { CommentWidgetFocus } from './commentThreadZoneWidget.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; registerEditorContribution(ID, CommentController, EditorContributionInstantiation.AfterFirstRender); registerWorkbenchContribution2(CommentsInputContentProvider.ID, CommentsInputContentProvider, WorkbenchPhase.BlockRestore); @@ -417,8 +418,12 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape], when: ContextKeyExpr.or(ctxCommentEditorFocused, CommentContextKeys.commentFocused), - handler: (accessor, args) => { + handler: async (accessor, args) => { const activeCodeEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); + const keybindingService = accessor.get(IKeybindingService); + // Unfortunate, but collapsing the comment thread might cause a dialog to show + // If we don't wait for the key up here, then the dialog will consume it and immediately close + await keybindingService.enableKeybindingHoldMode(CommentCommandId.Hide); if (activeCodeEditor instanceof SimpleCommentEditor) { activeCodeEditor.getParentThread().collapse(); } else if (activeCodeEditor) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts index bddd86c74ae8..276dd3b8bc47 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts @@ -71,7 +71,7 @@ export class CellComments extends CellContentPart { { actionRunner: () => { }, - collapse: () => { } + collapse: async () => { return true; } } ) as unknown as CommentThreadWidget; widgetDisposables.add(widget); From 18b9ededb35ee962d7d69d4ad5a2064468f34aa0 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:54:36 +0100 Subject: [PATCH 0981/3587] fixes https://github.com/microsoft/vscode-copilot/issues/12428 (#239084) --- .../browser/view/inlineEdits/insertionView.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts index c6677c0c08f8..1361c4b7c55b 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts @@ -131,9 +131,17 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits let height = this._ghostTextView.height.read(reader); let top = this._editor.getTopForLineNumber(state.lineNumber) - scrollTop; - if (state.text.startsWith('\n')) { - height -= lineHeight; - top += lineHeight; + + if (state.text.trim() !== '') { + // Adjust for leading/trailing newlines + let i = 0; + for (; i < state.text.length && state.text[i] === '\n'; i++) { + height -= lineHeight; + top += lineHeight; + } + for (let j = state.text.length - 1; j > i && state.text[j] === '\n'; j--) { + height -= lineHeight; + } } const codeLeft = editorLayout.contentLeft; From 51ebced9a8af85e70c73bcaf4624bc320bd721fa Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 29 Jan 2025 13:05:58 +0100 Subject: [PATCH 0982/3587] Better wording in comment thread collapse dialog (#239085) --- .../contrib/comments/browser/commentThreadZoneWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index a06c9bce0205..8d6c12d8ee59 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -338,7 +338,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget if (confirmSetting === 'whenHasUnsubmittedComments' && this._commentThreadWidget.hasUnsubmittedComments) { const result = await this.dialogService.confirm({ - message: nls.localize('confirmCollapse', "This comment thread has unsubmitted comments. Do you want to collapse it?"), + message: nls.localize('confirmCollapse', "Collapsing a comment thread will discard unsubmitted comments. Do you want to collapse this comment thread?"), primaryButton: nls.localize('collapse', "Collapse"), type: Severity.Warning, }); From d81241b54bb5461191792890718037b5e34d5973 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 29 Jan 2025 13:11:19 +0100 Subject: [PATCH 0983/3587] Prevent growth if edges aren't word characters (#239087) do not grow if edges aren't word characters --- .../browser/view/inlineEdits/view.ts | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index aaf6532692a5..6f980b72df59 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -405,23 +405,27 @@ function growEditsToEntireWord(replacements: SingleTextEdit[], originalText: Abs replacements.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); for (const edit of replacements) { - - // grow to the left let startIndex = edit.range.startColumn - 1; + let endIndex = edit.range.endColumn - 2; let prefix = ''; + let suffix = ''; const startLineContent = originalText.getLineAt(edit.range.startLineNumber); - while (startIndex - 1 >= 0 && /^[a-zA-Z]$/.test(startLineContent[startIndex - 1])) { - prefix = startLineContent[startIndex - 1] + prefix; - startIndex--; + const endLineContent = originalText.getLineAt(edit.range.endLineNumber); + + if (isWordChar(startLineContent[startIndex])) { + // grow to the left + while (isWordChar(startLineContent[startIndex - 1])) { + prefix = startLineContent[startIndex - 1] + prefix; + startIndex--; + } } - // grow to the right - let endIndex = edit.range.endColumn - 2; - let suffix = ''; - const endLineContent = originalText.getLineAt(edit.range.endLineNumber); - while (endIndex + 1 < endLineContent.length && /^[a-zA-Z]$/.test(endLineContent[endIndex + 1])) { - suffix += endLineContent[endIndex + 1]; - endIndex++; + if (isWordChar(endLineContent[endIndex])) { + // grow to the right + while (isWordChar(endLineContent[endIndex + 1])) { + suffix += endLineContent[endIndex + 1]; + endIndex++; + } } // create new edit and merge together if they are touching @@ -433,5 +437,10 @@ function growEditsToEntireWord(replacements: SingleTextEdit[], originalText: Abs result.push(newEdit); } + function isWordChar(char: string | undefined) { + if (!char) { return false; } + return /^[a-zA-Z]$/.test(char); + } + return result; } From d99174934eea1507e98714b203e8b93802f7dd2f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 29 Jan 2025 13:57:19 +0100 Subject: [PATCH 0984/3587] :up: distro (#239086) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9a56d08cebf9..d540db28597c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.97.0", - "distro": "c504aa66a94bcd910e273dbbf66b2c3a9f0343f8", + "distro": "4e8c47ac95d95aa744f78305db0f163dcd407126", "author": { "name": "Microsoft Corporation" }, From 37eca1a471705d5af226c48a2ed81734170a8210 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 29 Jan 2025 15:06:16 +0100 Subject: [PATCH 0985/3587] fix #238964 (#239093) --- .../common/extensionManagement.ts | 1 + .../userDataSync/common/extensionsSync.ts | 4 +- .../browser/extensionsWorkbenchService.ts | 4 +- .../common/extensionManagement.ts | 3 +- .../common/extensionManagementService.ts | 177 +++++++++++------- .../browser/extensionsResource.ts | 8 +- .../test/browser/workbenchTestServices.ts | 1 + 7 files changed, 120 insertions(+), 78 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index c186beb87737..9e7fc0409fad 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -19,6 +19,7 @@ export const EXTENSION_IDENTIFIER_PATTERN = '^([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z export const EXTENSION_IDENTIFIER_REGEX = new RegExp(EXTENSION_IDENTIFIER_PATTERN); export const WEB_EXTENSION_TAG = '__web_extension'; export const EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT = 'skipWalkthrough'; +export const EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT = 'skipPublisherTrust'; export const EXTENSION_INSTALL_SOURCE_CONTEXT = 'extensionInstallSource'; export const EXTENSION_INSTALL_DEP_PACK_CONTEXT = 'dependecyOrPackExtensionInstall'; export const EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT = 'clientTargetPlatform'; diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 2269f4e3c58d..6ddbebf56447 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -15,7 +15,7 @@ import { URI } from '../../../base/common/uri.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; import { GlobalExtensionEnablementService } from '../../extensionManagement/common/extensionEnablementService.js'; -import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, ExtensionManagementError, ExtensionManagementErrorCode, IGalleryExtension, DISABLED_EXTENSIONS_STORAGE_PATH, EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, EXTENSION_INSTALL_SOURCE_CONTEXT, InstallExtensionInfo, ExtensionInstallSource } from '../../extensionManagement/common/extensionManagement.js'; +import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, ExtensionManagementError, ExtensionManagementErrorCode, IGalleryExtension, DISABLED_EXTENSIONS_STORAGE_PATH, EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, EXTENSION_INSTALL_SOURCE_CONTEXT, InstallExtensionInfo, ExtensionInstallSource, EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT } from '../../extensionManagement/common/extensionManagement.js'; import { areSameExtensions } from '../../extensionManagement/common/extensionManagementUtil.js'; import { ExtensionStorageService, IExtensionStorageService } from '../../extensionManagement/common/extensionStorage.js'; import { ExtensionType, IExtensionIdentifier, isApplicationScopedExtension } from '../../extensions/common/extensions.js'; @@ -489,7 +489,7 @@ export class LocalExtensionsProvider { preRelease: e.preRelease, profileLocation: profile.extensionsResource, isApplicationScoped: e.isApplicationScoped, - context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true, [EXTENSION_INSTALL_SOURCE_CONTEXT]: ExtensionInstallSource.SETTINGS_SYNC } + context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true, [EXTENSION_INSTALL_SOURCE_CONTEXT]: ExtensionInstallSource.SETTINGS_SYNC, [EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT]: true } } }); syncExtensionsToInstall.set(extension.identifier.id.toLowerCase(), e); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 6a278cac432e..4d1acfc51d53 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -20,7 +20,8 @@ import { UninstallExtensionInfo, TargetPlatformToString, IAllowedExtensionsService, - AllowedExtensionsConfigKey + AllowedExtensionsConfigKey, + EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath, IResourceExtension } from '../../../services/extensionManagement/common/extensionManagement.js'; import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId, isMalicious } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; @@ -1904,6 +1905,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension installPreReleaseVersion: extension.local?.isPreReleaseVersion, profileLocation: this.userDataProfileService.currentProfile.extensionsResource, isApplicationScoped: extension.local?.isApplicationScoped, + context: { [EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT]: true } } }); } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 927712e5b4b0..a3a3740bc835 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -6,7 +6,7 @@ import { Event } from '../../../../base/common/event.js'; import { createDecorator, refineServiceDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IExtension, ExtensionType, IExtensionManifest, IExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; -import { IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOptions, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, Metadata, UninstallExtensionEvent, DidUpdateExtensionMetadata } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOptions, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, Metadata, UninstallExtensionEvent, DidUpdateExtensionMetadata, InstallExtensionInfo } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { URI } from '../../../../base/common/uri.js'; import { FileAccess } from '../../../../base/common/network.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; @@ -93,6 +93,7 @@ export interface IWorkbenchExtensionManagementService extends IProfileAwareExten updateFromGallery(gallery: IGalleryExtension, extension: ILocalExtension, installOptions?: InstallOptions): Promise; updateMetadata(local: ILocalExtension, metadata: Partial): Promise; + requestPublisherTrust(extensions: InstallExtensionInfo[]): Promise; isPublisherTrusted(extension: IGalleryExtension): boolean; trustPublishers(...publishers: string[]): void; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 85385c2fcbbe..6b1bc2d48e58 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -12,6 +12,7 @@ import { DidUpdateExtensionMetadata, UninstallExtensionInfo, IAllowedExtensionsService, + EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT, } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IResourceExtension, IWorkbenchExtensionManagementService, IWorkbenchInstallOptions, UninstallExtensionOnServerEvent } from './extensionManagement.js'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from '../../../../platform/extensions/common/extensions.js'; @@ -24,7 +25,7 @@ import { localize } from '../../../../nls.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { Schemas } from '../../../../base/common/network.js'; import { IDownloadService } from '../../../../platform/download/common/download.js'; -import { coalesce, isNonEmptyArray } from '../../../../base/common/arrays.js'; +import { coalesce, distinct, isNonEmptyArray } from '../../../../base/common/arrays.js'; import { IDialogService, IPromptButton } from '../../../../platform/dialogs/common/dialogs.js'; import Severity from '../../../../base/common/severity.js'; import { IUserDataSyncEnablementService, SyncResource } from '../../../../platform/userDataSync/common/userDataSync.js'; @@ -446,6 +447,18 @@ export class ExtensionManagementService extends Disposable implements IWorkbench const results = new Map(); const extensionsByServer = new Map(); + const manifests = await Promise.all(extensions.map(async ({ extension }) => { + const manifest = await this.extensionGalleryService.getManifest(extension, CancellationToken.None); + if (!manifest) { + throw new Error(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", extension.displayName || extension.name)); + } + return manifest; + })); + + if (extensions.some(e => e.options?.context?.[EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT] !== true)) { + await this.checkForTrustedPublishers(extensions.map((e, index) => ({ extension: e.extension, manifest: manifests[index], checkForPackAndDependencies: !e.options?.donotIncludePackAndDependencies }))); + } + await Promise.all(extensions.map(async ({ extension, options }) => { try { const manifest = await this.extensionGalleryService.getManifest(extension, CancellationToken.None); @@ -502,8 +515,11 @@ export class ExtensionManagementService extends Disposable implements IWorkbench throw new Error(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", gallery.displayName || gallery.name)); } + if (installOptions?.context?.[EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT] !== true) { + await this.checkForTrustedPublishers([{ extension: gallery, manifest, checkForPackAndDependencies: !installOptions?.donotIncludePackAndDependencies }],); + } + if (installOptions?.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] !== ExtensionInstallSource.SETTINGS_SYNC) { - await this.checkForTrustedPublisher(gallery, manifest, !installOptions?.donotIncludePackAndDependencies); await this.checkForWorkspaceTrust(manifest, false); @@ -789,18 +805,46 @@ export class ExtensionManagementService extends Disposable implements IWorkbench throw new Error('No extension server found'); } - private async checkForTrustedPublisher(extension: IGalleryExtension, manifest: IExtensionManifest, checkForPackAndDependencies: boolean): Promise { - if (this.isPublisherTrusted(extension)) { + async requestPublisherTrust(extensions: InstallExtensionInfo[]): Promise { + const manifests = await Promise.all(extensions.map(async ({ extension }) => { + const manifest = await this.extensionGalleryService.getManifest(extension, CancellationToken.None); + if (!manifest) { + throw new Error(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", extension.displayName || extension.name)); + } + return manifest; + })); + + await this.checkForTrustedPublishers(extensions.map((e, index) => ({ extension: e.extension, manifest: manifests[index], checkForPackAndDependencies: !e.options?.donotIncludePackAndDependencies }))); + } + + private async checkForTrustedPublishers(extensions: { extension: IGalleryExtension; manifest: IExtensionManifest; checkForPackAndDependencies: boolean }[]): Promise { + const untrustedExtensions: IGalleryExtension[] = []; + const untrustedExtensionManifests: IExtensionManifest[] = []; + const manifestsToGetOtherUntrustedPublishers: IExtensionManifest[] = []; + for (const { extension, manifest, checkForPackAndDependencies } of extensions) { + if (!this.isPublisherTrusted(extension)) { + untrustedExtensions.push(extension); + untrustedExtensionManifests.push(manifest); + if (checkForPackAndDependencies) { + manifestsToGetOtherUntrustedPublishers.push(manifest); + } + } + } + + if (!untrustedExtensions.length) { return; } - const otherUntrustedPublishers = checkForPackAndDependencies ? await this.getOtherUntrustedPublishers(manifest) : []; + const otherUntrustedPublishers = manifestsToGetOtherUntrustedPublishers.length ? await this.getOtherUntrustedPublishers(manifestsToGetOtherUntrustedPublishers) : []; + const allPublishers = [...distinct(untrustedExtensions, e => e.publisher), ...otherUntrustedPublishers]; + const unverfiiedPublishers = allPublishers.filter(p => !p.publisherDomain?.verified); + const verifiedPublishers = allPublishers.filter(p => p.publisherDomain?.verified); type TrustPublisherClassification = { owner: 'sandy081'; comment: 'Report the action taken by the user on the publisher trust dialog'; action: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The action taken by the user on the publisher trust dialog. Can be trust, learn more or cancel.' }; - extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the extension for which the publisher trust dialog was shown.' }; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifiers of the extension for which the publisher trust dialog was shown.' }; }; type TrustPublisherEvent = { action: string; @@ -808,17 +852,17 @@ export class ExtensionManagementService extends Disposable implements IWorkbench }; const installButton: IPromptButton = { - label: otherUntrustedPublishers.length ? localize({ key: 'trust publishers and install', comment: ['&& denotes a mnemonic'] }, "Trust Publishers & &&Install") : localize({ key: 'trust and install', comment: ['&& denotes a mnemonic'] }, "Trust Publisher & &&Install"), + label: allPublishers.length > 1 ? localize({ key: 'trust publishers and install', comment: ['&& denotes a mnemonic'] }, "Trust Publishers & &&Install") : localize({ key: 'trust and install', comment: ['&& denotes a mnemonic'] }, "Trust Publisher & &&Install"), run: () => { - this.telemetryService.publicLog2('extensions:trustPublisher', { action: 'trust', extensionId: extension.identifier.id }); - this.trustPublishers(extension.publisher); + this.telemetryService.publicLog2('extensions:trustPublisher', { action: 'trust', extensionId: untrustedExtensions.map(e => e.identifier.id).join(',') }); + this.trustPublishers(...allPublishers.map(p => p.publisher)); } }; const learnMoreButton: IPromptButton = { label: localize({ key: 'learnMore', comment: ['&& denotes a mnemonic'] }, "&&Learn More"), run: () => { - this.telemetryService.publicLog2('extensions:trustPublisher', { action: 'learn', extensionId: extension.identifier.id }); + this.telemetryService.publicLog2('extensions:trustPublisher', { action: 'learn', extensionId: untrustedExtensions.map(e => e.identifier.id).join(',') }); this.instantiationService.invokeFunction(accessor => accessor.get(ICommandService).executeCommand('vscode.open', URI.parse('https://aka.ms/vscode-extension-security'))); throw new CancellationError(); } @@ -829,67 +873,56 @@ export class ExtensionManagementService extends Disposable implements IWorkbench }; const unverifiedLink = 'https://aka.ms/vscode-verify-publisher'; - const title = otherUntrustedPublishers.length - ? otherUntrustedPublishers.length === 1 - ? localize('checkTwoTrustedPublishersTitle', "Do you trust publishers \"{0}\" and \"{1}\"?", extension.publisherDisplayName, otherUntrustedPublishers[0].publisherDisplayName) - : localize('checkAllTrustedPublishersTitle', "Do you trust the publisher \"{0}\" and {1} others?", extension.publisherDisplayName, otherUntrustedPublishers.length) - : localize('checkTrustedPublisherTitle', "Do you trust the publisher \"{0}\"?", extension.publisherDisplayName); + const title = allPublishers.length === 1 + ? localize('checkTrustedPublisherTitle', "Do you trust the publisher \"{0}\"?", allPublishers[0].publisherDisplayName) + : allPublishers.length === 2 + ? localize('checkTwoTrustedPublishersTitle', "Do you trust publishers \"{0}\" and \"{1}\"?", allPublishers[0].publisherDisplayName, allPublishers[1].publisherDisplayName) + : localize('checkAllTrustedPublishersTitle', "Do you trust the publisher \"{0}\" and {1} others?", allPublishers[0].publisherDisplayName, allPublishers.length - 1); const customMessage = new MarkdownString('', { supportThemeIcons: true, isTrusted: true }); - if (otherUntrustedPublishers.length) { - customMessage.appendMarkdown(localize('extension published by message', "The extension {0} is published by {1}.", `[${extension.displayName}](${this.productService.extensionsGallery!.itemUrl}?itemName=${extension.identifier.id})`, getPublisherLink(extension))); - customMessage.appendMarkdown(' '); - const commandUri = URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([extension.identifier.id, manifest.extensionPack?.length ? 'extensionPack' : 'dependencies']))}`).toString(); - if (otherUntrustedPublishers.length === 1) { - customMessage.appendMarkdown(localize('singleUntrustedPublisher', "Installing this extension will also install [extensions]({0}) published by {1}.", commandUri, getPublisherLink(otherUntrustedPublishers[0]))); + if (untrustedExtensions.length === 1) { + const extension = untrustedExtensions[0]; + const manifest = untrustedExtensionManifests[0]; + if (otherUntrustedPublishers.length) { + customMessage.appendMarkdown(localize('extension published by message', "The extension {0} is published by {1}.", `[${extension.displayName}](${this.productService.extensionsGallery!.itemUrl}?itemName=${extension.identifier.id})`, getPublisherLink(extension))); + customMessage.appendMarkdown(' '); + const commandUri = URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([extension.identifier.id, manifest.extensionPack?.length ? 'extensionPack' : 'dependencies']))}`).toString(); + if (otherUntrustedPublishers.length === 1) { + customMessage.appendMarkdown(localize('singleUntrustedPublisher', "Installing this extension will also install [extensions]({0}) published by {1}.", commandUri, getPublisherLink(otherUntrustedPublishers[0]))); + } else { + customMessage.appendMarkdown(localize('message3', "Installing this extension will also install [extensions]({0}) published by {1} and {2}.", commandUri, otherUntrustedPublishers.slice(0, otherUntrustedPublishers.length - 1).map(p => getPublisherLink(p)).join(', '), getPublisherLink(otherUntrustedPublishers[otherUntrustedPublishers.length - 1]))); + } + customMessage.appendMarkdown(' '); + customMessage.appendMarkdown(localize('firstTimeInstallingMessage', "This is the first time you're installing extensions from these publishers.")); } else { - customMessage.appendMarkdown(localize('message3', "Installing this extension will also install [extensions]({0}) published by {1} and {2}.", commandUri, otherUntrustedPublishers.slice(0, otherUntrustedPublishers.length - 1).map(p => getPublisherLink(p)).join(', '), getPublisherLink(otherUntrustedPublishers[otherUntrustedPublishers.length - 1]))); + customMessage.appendMarkdown(localize('message1', "The extension {0} is published by {1}. This is the first extension you're installing from this publisher.", `[${extension.displayName}](${this.productService.extensionsGallery!.itemUrl}?itemName=${extension.identifier.id})`, getPublisherLink(extension))); } - customMessage.appendMarkdown(' '); - customMessage.appendMarkdown(localize('firstTimeInstallingMessage', "This is the first time you're installing extensions from these publishers.")); - - const allPublishers = [extension, ...otherUntrustedPublishers]; - const unverfiiedPublishers = allPublishers.filter(p => !p.publisherDomain?.verified); - const verifiedPublishers = allPublishers.filter(p => p.publisherDomain?.verified); + } else { + customMessage.appendMarkdown(localize('multiInstallMessage', "This is the first time you're installing extensions from publishers {0} and {1}.", allPublishers.slice(0, allPublishers.length - 1).map(p => getPublisherLink(p)).join(', '), getPublisherLink(allPublishers[allPublishers.length - 1]))); + } - if (verifiedPublishers.length) { + if (verifiedPublishers.length) { + for (const publisher of verifiedPublishers) { customMessage.appendText('\n'); - for (const publisher of verifiedPublishers) { - if (publisher.publisherDomain?.verified) { - customMessage.appendText('\n'); - const publisherVerifiedMessage = localize('verifiedPublisherWithName', "{0} has verified ownership of {1}.", getPublisherLink(publisher), `[${URI.parse(publisher.publisherDomain.link).authority}](${publisher.publisherDomain.link})`); - customMessage.appendMarkdown(`$(${verifiedPublisherIcon.id}) ${publisherVerifiedMessage}`); - } else { - unverfiiedPublishers.push(publisher); - } - } - if (unverfiiedPublishers.length) { - customMessage.appendText('\n'); - if (unverfiiedPublishers.length === 1) { - customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublisherWithName', "{0} is **not** [verified]({1}).", getPublisherLink(unverfiiedPublishers[0]), unverifiedLink)}`); - } else { - customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublishers', "{0} and {1} are **not** [verified]({2}).", unverfiiedPublishers.slice(0, unverfiiedPublishers.length - 1).map(p => getPublisherLink(p)).join(', '), getPublisherLink(unverfiiedPublishers[unverfiiedPublishers.length - 1]), unverifiedLink)}`); - } - } - } else { + const publisherVerifiedMessage = localize('verifiedPublisherWithName', "{0} has verified ownership of {1}.", getPublisherLink(publisher), `[${URI.parse(publisher.publisherDomain!.link).authority}](${publisher.publisherDomain!.link})`); + customMessage.appendMarkdown(`$(${verifiedPublisherIcon.id}) ${publisherVerifiedMessage}`); + } + if (unverfiiedPublishers.length) { customMessage.appendText('\n'); - customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('allUnverifed', "All publishers are **not** [verified]({0}).", unverifiedLink)}`); + if (unverfiiedPublishers.length === 1) { + customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublisherWithName', "{0} is **not** [verified]({1}).", getPublisherLink(unverfiiedPublishers[0]), unverifiedLink)}`); + } else { + customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublishers', "{0} and {1} are **not** [verified]({2}).", unverfiiedPublishers.slice(0, unverfiiedPublishers.length - 1).map(p => getPublisherLink(p)).join(', '), getPublisherLink(unverfiiedPublishers[unverfiiedPublishers.length - 1]), unverifiedLink)}`); + } } - } else { - customMessage.appendMarkdown(localize('message1', "The extension {0} is published by {1}. This is the first extension you're installing from this publisher.", `[${extension.displayName}](${this.productService.extensionsGallery!.itemUrl}?itemName=${extension.identifier.id})`, getPublisherLink(extension))); customMessage.appendText('\n'); - if (extension.publisherDomain?.verified) { - const publisherVerifiedMessage = localize('verifiedPublisher', "{0} has verified ownership of {1}.", getPublisherLink(extension), `[${URI.parse(extension.publisherDomain.link).authority}](${extension.publisherDomain.link})`); - customMessage.appendMarkdown(`$(${verifiedPublisherIcon.id}) ${publisherVerifiedMessage}`); - } else { - customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublisher', "{0} is **not** [verified]({1}).", getPublisherLink(extension), unverifiedLink)}`); - } + customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('allUnverifed', "All publishers are **not** [verified]({0}).", unverifiedLink)}`); } - customMessage.appendText('\n\n'); - if (otherUntrustedPublishers.length) { + customMessage.appendText('\n'); + if (allPublishers.length > 1) { customMessage.appendMarkdown(localize('message4', "{0} has no control over the behavior of third-party extensions, including how they manage your personal data. Please proceed only if you trust the publishers.", this.productService.nameLong)); } else { customMessage.appendMarkdown(localize('message2', "{0} has no control over the behavior of third-party extensions, including how they manage your personal data. Please proceed only if you trust the publisher.", this.productService.nameLong)); @@ -901,7 +934,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench buttons: [installButton, learnMoreButton], cancelButton: { run: () => { - this.telemetryService.publicLog2('extensions:trustPublisher', { action: 'cancel', extensionId: extension.identifier.id }); + this.telemetryService.publicLog2('extensions:trustPublisher', { action: 'cancel', extensionId: untrustedExtensions.map(e => e.identifier.id).join(',') }); throw new CancellationError(); } }, @@ -913,23 +946,25 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } - private async getOtherUntrustedPublishers(manifest: IExtensionManifest): Promise<{ publisher: string; publisherDisplayName: string; publisherDomain?: { link: string; verified: boolean } }[]> { - const infos = []; - for (const id of [...(manifest.extensionPack ?? []), ...(manifest.extensionDependencies ?? [])]) { - const [publisherId] = id.split('.'); - if (publisherId.toLowerCase() === manifest.publisher.toLowerCase()) { - continue; - } - if (this.isPublisherUserTrusted(publisherId.toLowerCase())) { - continue; + private async getOtherUntrustedPublishers(manifests: IExtensionManifest[]): Promise<{ publisher: string; publisherDisplayName: string; publisherDomain?: { link: string; verified: boolean } }[]> { + const extensionIds = new Set(); + for (const manifest of manifests) { + for (const id of [...(manifest.extensionPack ?? []), ...(manifest.extensionDependencies ?? [])]) { + const [publisherId] = id.split('.'); + if (publisherId.toLowerCase() === manifest.publisher.toLowerCase()) { + continue; + } + if (this.isPublisherUserTrusted(publisherId.toLowerCase())) { + continue; + } + extensionIds.add(id.toLowerCase()); } - infos.push(id); } - if (!infos.length) { + if (!extensionIds.size) { return []; } const extensions = new Map(); - await this.getDependenciesAndPackedExtensionsRecursively(infos, extensions, CancellationToken.None); + await this.getDependenciesAndPackedExtensionsRecursively([...extensionIds], extensions, CancellationToken.None); const publishers = new Map(); for (const [, extension] of extensions) { if (this.isPublisherTrusted(extension)) { diff --git a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts index 17138ecb9e7c..c06b870aea9b 100644 --- a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts +++ b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts @@ -8,7 +8,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { localize } from '../../../../nls.js'; import { GlobalExtensionEnablementService } from '../../../../platform/extensionManagement/common/extensionEnablementService.js'; -import { EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, InstallExtensionInfo } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT, EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, InstallExtensionInfo } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; import { ExtensionType } from '../../../../platform/extensions/common/extensions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -18,6 +18,7 @@ import { IStorageService } from '../../../../platform/storage/common/storage.js' import { IUserDataProfile, ProfileResourceType } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { IUserDataProfileStorageService } from '../../../../platform/userDataProfile/common/userDataProfileStorageService.js'; import { ITreeItemCheckboxState, TreeItemCollapsibleState } from '../../../common/views.js'; +import { IWorkbenchExtensionManagementService } from '../../extensionManagement/common/extensionManagement.js'; import { IProfileResource, IProfileResourceChildTreeItem, IProfileResourceInitializer, IProfileResourceTreeItem, IUserDataProfileService } from '../common/userDataProfile.js'; interface IProfileExtension { @@ -82,7 +83,7 @@ export class ExtensionsResourceInitializer implements IProfileResourceInitialize installGivenVersion: !!e.version, installPreReleaseVersion: e.preRelease, profileLocation: this.userDataProfileService.currentProfile.extensionsResource, - context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true } + context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true, [EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT]: true } }); this.logService.info(`Initializing Profile: Installed extension...`, extension.identifier.id, extension.version); } else { @@ -99,7 +100,7 @@ export class ExtensionsResourceInitializer implements IProfileResourceInitialize export class ExtensionsResource implements IProfileResource { constructor( - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IWorkbenchExtensionManagementService private readonly extensionManagementService: IWorkbenchExtensionManagementService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IUserDataProfileStorageService private readonly userDataProfileStorageService: IUserDataProfileStorageService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -171,6 +172,7 @@ export class ExtensionsResource implements IProfileResource { })); if (installExtensionInfos.length) { if (token) { + await this.extensionManagementService.requestPublisherTrust(installExtensionInfos); for (const installExtensionInfo of installExtensionInfos) { if (token.isCancellationRequested) { return; diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 8c5965ec8f4b..04572268c03f 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -2250,6 +2250,7 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens getInstallableServers(extension: IGalleryExtension): Promise { throw new Error('Method not implemented.'); } isPublisherTrusted(extension: IGalleryExtension): boolean { return false; } trustPublishers(...publishers: string[]): void { } + async requestPublisherTrust(extensions: InstallExtensionInfo[]): Promise { } } export class TestUserDataProfileService implements IUserDataProfileService { From bd5e834f05c2ed8451b7cf92b69001bfcd33ef85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dirk=20B=C3=A4umer?= Date: Wed, 29 Jan 2025 15:14:24 +0100 Subject: [PATCH 0986/3587] Exclude out-of-scope from verification. --- .vscode/notebooks/endgame.github-issues | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index ef066abaeafd..d1c128a6d0a7 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -112,7 +112,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE is:issue is:closed reason:completed sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:z-author-verified -label:unreleased -label:*not-reproducible" + "value": "$REPOS $MILESTONE is:issue is:closed reason:completed sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:z-author-verified -label:unreleased -label:*not-reproducible -label:*out-of-scope" }, { "kind": 1, From f565ac4f97c2be1957c41d2eb19b7251ce8d9159 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 29 Jan 2025 15:25:13 +0100 Subject: [PATCH 0987/3587] fix #238920 (#239096) --- .../extensionManagement/common/extensionGalleryService.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index b8fee5a8c4ce..90b110c251ae 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -1031,6 +1031,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi for (const rawGalleryExtension of rawGalleryExtensions) { const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension); const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId }; + const includePreRelease = isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : !!criteria.includePreRelease.find(extensionIdentifierWithPreRelease => areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier))?.includePreRelease; const rawGalleryExtensionVersion = await this.getRawGalleryExtensionVersion( rawGalleryExtension, { @@ -1038,7 +1039,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi targetPlatform: criteria.targetPlatform, productVersion: criteria.productVersion, version: criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version - ?? (criteria.includePreRelease ? VersionKind.Latest : VersionKind.Release) + ?? (includePreRelease ? VersionKind.Latest : VersionKind.Release) }, allTargetPlatforms ); @@ -1073,7 +1074,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi targetPlatform: criteria.targetPlatform, productVersion: criteria.productVersion, version: criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version - ?? (criteria.includePreRelease ? VersionKind.Latest : VersionKind.Release) + ?? (includePreRelease ? VersionKind.Latest : VersionKind.Release) }, allTargetPlatforms ); From 4a24ab7c7e71a1102c19e66b209b05f5c2c18787 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:33:23 +0100 Subject: [PATCH 0988/3587] Revert "SCM - disable actions for resource groups that do not have any resources (#236813)" (#239098) This reverts commit 151ef3514e76629f4e7bf3951439b1e0dae0a6e5. --- extensions/git/package.json | 14 ++++++------- src/vs/workbench/contrib/scm/browser/menus.ts | 20 +++++++++---------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index d23ef7b949c9..600196ef2ce2 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -164,14 +164,14 @@ "title": "%command.stageAll%", "category": "Git", "icon": "$(add)", - "enablement": "!operationInProgress && scmResourceGroupResourceCount > 0" + "enablement": "!operationInProgress" }, { "command": "git.stageAllTracked", "title": "%command.stageAllTracked%", "category": "Git", "icon": "$(add)", - "enablement": "!operationInProgress && scmResourceGroupResourceCount > 0" + "enablement": "!operationInProgress" }, { "command": "git.stageAllUntracked", @@ -244,7 +244,7 @@ "title": "%command.unstageAll%", "category": "Git", "icon": "$(remove)", - "enablement": "!operationInProgress && scmResourceGroupResourceCount > 0" + "enablement": "!operationInProgress" }, { "command": "git.unstageSelectedRanges", @@ -271,14 +271,14 @@ "title": "%command.cleanAll%", "category": "Git", "icon": "$(discard)", - "enablement": "!operationInProgress && scmResourceGroupResourceCount > 0" + "enablement": "!operationInProgress" }, { "command": "git.cleanAllTracked", "title": "%command.cleanAllTracked%", "category": "Git", "icon": "$(discard)", - "enablement": "!operationInProgress && scmResourceGroupResourceCount > 0" + "enablement": "!operationInProgress" }, { "command": "git.cleanAllUntracked", @@ -902,14 +902,14 @@ "title": "%command.viewChanges%", "icon": "$(diff-multiple)", "category": "Git", - "enablement": "!operationInProgress && scmResourceGroupResourceCount > 0" + "enablement": "!operationInProgress" }, { "command": "git.viewStagedChanges", "title": "%command.viewStagedChanges%", "icon": "$(diff-multiple)", "category": "Git", - "enablement": "!operationInProgress && scmResourceGroupResourceCount > 0" + "enablement": "!operationInProgress" }, { "command": "git.viewUntrackedChanges", diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index 352f63b6f3ec..f14d8ea79f67 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -6,7 +6,7 @@ import { IAction } from '../../../../base/common/actions.js'; import { equals } from '../../../../base/common/arrays.js'; import { Emitter } from '../../../../base/common/event.js'; -import { DisposableStore, IDisposable, MutableDisposable, dispose } from '../../../../base/common/lifecycle.js'; +import { DisposableStore, IDisposable, dispose } from '../../../../base/common/lifecycle.js'; import './media/scm.css'; import { localize } from '../../../../nls.js'; import { getActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; @@ -70,14 +70,13 @@ interface IContextualResourceMenuItem { class SCMMenusItem implements IDisposable { - private readonly _resourceGroupMenu = new MutableDisposable(); + private _resourceGroupMenu: IMenu | undefined; get resourceGroupMenu(): IMenu { - const contextKeyService = this.contextKeyService.createOverlay([ - ['scmResourceGroupResourceCount', this.group.resources.length], - ]); + if (!this._resourceGroupMenu) { + this._resourceGroupMenu = this.menuService.createMenu(MenuId.SCMResourceGroupContext, this.contextKeyService); + } - this._resourceGroupMenu.value = this.menuService.createMenu(MenuId.SCMResourceGroupContext, contextKeyService); - return this._resourceGroupMenu.value; + return this._resourceGroupMenu; } private _resourceFolderMenu: IMenu | undefined; @@ -93,9 +92,8 @@ class SCMMenusItem implements IDisposable { private contextualResourceMenus: Map | undefined; constructor( - private readonly group: ISCMResourceGroup, - private readonly contextKeyService: IContextKeyService, - private readonly menuService: IMenuService + private contextKeyService: IContextKeyService, + private menuService: IMenuService ) { } getResourceMenu(resource: ISCMResource): IMenu { @@ -208,7 +206,7 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { ['multiDiffEditorEnableViewChanges', group.multiDiffEditorEnableViewChanges], ]); - result = new SCMMenusItem(group, contextKeyService, this.menuService); + result = new SCMMenusItem(contextKeyService, this.menuService); this.resourceGroupMenusItems.set(group, result); } From e5fcc9620d0a2a87e69988ef1de8c4ff4e6d3f97 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 29 Jan 2025 15:40:36 +0100 Subject: [PATCH 0989/3587] File Comments: Floating triangle (#239099) Fixes #238929 --- .../contrib/comments/browser/commentThreadZoneWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 8d6c12d8ee59..582462c23daa 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -140,7 +140,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget @IConfigurationService private readonly configurationService: IConfigurationService, @IDialogService private readonly dialogService: IDialogService ) { - super(editor, { keepEditorSelection: true, isAccessible: true }); + super(editor, { keepEditorSelection: true, isAccessible: true, showArrow: !!_commentThread.range }); this._contextKeyService = contextKeyService.createScoped(this.domNode); this._scopedInstantiationService = this._globalToDispose.add(instantiationService.createChild(new ServiceCollection( From 3b7a247edadadbc5f9474695ed7820343fa07ec7 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 29 Jan 2025 16:15:09 +0100 Subject: [PATCH 0990/3587] #238937 remove redundant code (#239100) --- .../contrib/chat/common/languageModelStats.ts | 130 +----------------- 1 file changed, 6 insertions(+), 124 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/languageModelStats.ts b/src/vs/workbench/contrib/chat/common/languageModelStats.ts index 0a43eb15506f..960462e4a255 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelStats.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelStats.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; @@ -21,144 +20,27 @@ export interface ILanguageModelStatsService { update(model: string, extensionId: ExtensionIdentifier, agent: string | undefined, tokenCount: number | undefined): Promise; } -interface LanguageModelStats { - extensions: { - extensionId: string; - requestCount: number; - tokenCount: number; - participants: { - id: string; - requestCount: number; - tokenCount: number; - }[]; - }[]; -} - export class LanguageModelStatsService extends Disposable implements ILanguageModelStatsService { - private static readonly MODEL_STATS_STORAGE_KEY_PREFIX = 'languageModelStats.'; - private static readonly MODEL_ACCESS_STORAGE_KEY_PREFIX = 'languageModelAccess.'; - declare _serviceBrand: undefined; - private readonly _onDidChangeStats = this._register(new Emitter()); - readonly onDidChangeLanguageMoelStats = this._onDidChangeStats.event; - - private readonly sessionStats = new Map(); - constructor( @IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService, - @IStorageService private readonly _storageService: IStorageService, + @IStorageService storageService: IStorageService, ) { super(); - this._register(_storageService.onDidChangeValue(StorageScope.APPLICATION, undefined, this._store)(e => { - const model = this.getModel(e.key); - if (model) { - this._onDidChangeStats.fire(model); + // TODO: @sandy081 - remove this code after a while + for (const key in storageService.keys(StorageScope.APPLICATION, StorageTarget.USER)) { + if (key.startsWith('languageModelStats.') || key.startsWith('languageModelAccess.')) { + storageService.remove(key, StorageScope.APPLICATION); } - })); - } - - hasAccessedModel(extensionId: string, model: string): boolean { - return this.getAccessExtensions(model).includes(extensionId.toLowerCase()); + } } async update(model: string, extensionId: ExtensionIdentifier, agent: string | undefined, tokenCount: number | undefined): Promise { await this.extensionFeaturesManagementService.getAccess(extensionId, CopilotUsageExtensionFeatureId); - - // update model access - this.addAccess(model, extensionId.value); - - // update session stats - let sessionStats = this.sessionStats.get(model); - if (!sessionStats) { - sessionStats = { extensions: [] }; - this.sessionStats.set(model, sessionStats); - } - this.add(sessionStats, extensionId.value, agent, tokenCount); - - this.write(model, extensionId.value, agent, tokenCount); - this._onDidChangeStats.fire(model); - } - - private addAccess(model: string, extensionId: string): void { - extensionId = extensionId.toLowerCase(); - const extensions = this.getAccessExtensions(model); - if (!extensions.includes(extensionId)) { - extensions.push(extensionId); - this._storageService.store(this.getAccessKey(model), JSON.stringify(extensions), StorageScope.APPLICATION, StorageTarget.USER); - } } - private getAccessExtensions(model: string): string[] { - const key = this.getAccessKey(model); - const data = this._storageService.get(key, StorageScope.APPLICATION); - try { - if (data) { - const parsed = JSON.parse(data); - if (Array.isArray(parsed)) { - return parsed; - } - } - } catch (e) { - // ignore - } - return []; - - } - - private async write(model: string, extensionId: string, participant: string | undefined, tokenCount: number | undefined): Promise { - const modelStats = await this.read(model); - this.add(modelStats, extensionId, participant, tokenCount); - this._storageService.store(this.getKey(model), JSON.stringify(modelStats), StorageScope.APPLICATION, StorageTarget.USER); - } - - private add(modelStats: LanguageModelStats, extensionId: string, participant: string | undefined, tokenCount: number | undefined): void { - let extensionStats = modelStats.extensions.find(e => ExtensionIdentifier.equals(e.extensionId, extensionId)); - if (!extensionStats) { - extensionStats = { extensionId, requestCount: 0, tokenCount: 0, participants: [] }; - modelStats.extensions.push(extensionStats); - } - if (participant) { - let participantStats = extensionStats.participants.find(p => p.id === participant); - if (!participantStats) { - participantStats = { id: participant, requestCount: 0, tokenCount: 0 }; - extensionStats.participants.push(participantStats); - } - participantStats.requestCount++; - participantStats.tokenCount += tokenCount ?? 0; - } else { - extensionStats.requestCount++; - extensionStats.tokenCount += tokenCount ?? 0; - } - } - - private async read(model: string): Promise { - try { - const value = this._storageService.get(this.getKey(model), StorageScope.APPLICATION); - if (value) { - return JSON.parse(value); - } - } catch (error) { - // ignore - } - return { extensions: [] }; - } - - private getModel(key: string): string | undefined { - if (key.startsWith(LanguageModelStatsService.MODEL_STATS_STORAGE_KEY_PREFIX)) { - return key.substring(LanguageModelStatsService.MODEL_STATS_STORAGE_KEY_PREFIX.length); - } - return undefined; - } - - private getKey(model: string): string { - return `${LanguageModelStatsService.MODEL_STATS_STORAGE_KEY_PREFIX}${model}`; - } - - private getAccessKey(model: string): string { - return `${LanguageModelStatsService.MODEL_ACCESS_STORAGE_KEY_PREFIX}${model}`; - } } export const CopilotUsageExtensionFeatureId = 'copilot'; From fa2f90b630e6923907198b6ea464ea0d42af7f81 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:36:21 +0100 Subject: [PATCH 0991/3587] Git - fix issue related to opening the first commit of the repository from the git blame hover (#239102) --- extensions/git/src/commands.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index abb00af22bab..47b1f087b94b 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -4070,17 +4070,13 @@ export class CommandCenter { } const commit = await repository.getCommit(item.ref); - const commitParentId = commit.parents.length > 0 ? commit.parents[0] : `${commit.hash}^`; - const changes = await repository.diffBetween(commitParentId, commit.hash); + const commitParentId = commit.parents.length > 0 ? commit.parents[0] : await repository.getEmptyTree(); + const changes = await repository.diffTrees(commitParentId, commit.hash); + const resources = changes.map(c => toMultiFileDiffEditorUris(c, commitParentId, commit.hash)); const title = `${item.shortRef} - ${truncate(commit.message)}`; const multiDiffSourceUri = Uri.from({ scheme: 'scm-history-item', path: `${repository.root}/${commitParentId}..${commit.hash}` }); - const resources: { originalUri: Uri | undefined; modifiedUri: Uri | undefined }[] = []; - for (const change of changes) { - resources.push(toMultiFileDiffEditorUris(change, commitParentId, commit.hash)); - } - return { command: '_workbench.openMultiDiffEditor', title: l10n.t('Open Commit'), @@ -4341,11 +4337,11 @@ export class CommandCenter { const rootUri = Uri.file(repository.root); const commit = await repository.getCommit(historyItemId); const title = `${getCommitShortHash(rootUri, historyItemId)} - ${truncate(commit.message)}`; - const historyItemParentId = commit.parents.length > 0 ? commit.parents[0] : `${historyItemId}^`; + const historyItemParentId = commit.parents.length > 0 ? commit.parents[0] : await repository.getEmptyTree(); const multiDiffSourceUri = Uri.from({ scheme: 'scm-history-item', path: `${repository.root}/${historyItemParentId}..${historyItemId}` }); - const changes = await repository.diffBetween(historyItemParentId, historyItemId); + const changes = await repository.diffTrees(historyItemParentId, historyItemId); const resources = changes.map(c => toMultiFileDiffEditorUris(c, historyItemParentId, historyItemId)); await commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources }); From 243da33637ab53974e5c88e73b93e039d3b00a24 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:37:42 +0100 Subject: [PATCH 0992/3587] Fix misalignment of arrow and suggestion box in NES Insertion View (#239104) fix https://github.com/microsoft/vscode-copilot/issues/12461 --- src/vs/editor/browser/rect.ts | 8 +++ .../view/inlineEdits/gutterIndicatorView.ts | 6 ++- .../browser/view/inlineEdits/insertionView.ts | 54 ++++++++++++------- .../browser/view/inlineEdits/view.ts | 25 +++++++-- 4 files changed, 67 insertions(+), 26 deletions(-) diff --git a/src/vs/editor/browser/rect.ts b/src/vs/editor/browser/rect.ts index 09a93216f268..308e0395c91b 100644 --- a/src/vs/editor/browser/rect.ts +++ b/src/vs/editor/browser/rect.ts @@ -138,7 +138,15 @@ export class Rect { return new Rect(this.left - delta, this.top, this.right - delta, this.bottom); } + moveRight(delta: number): Rect { + return new Rect(this.left + delta, this.top, this.right + delta, this.bottom); + } + moveUp(delta: number): Rect { return new Rect(this.left, this.top - delta, this.right, this.bottom - delta); } + + moveDown(delta: number): Rect { + return new Rect(this.left, this.top + delta, this.right, this.bottom + delta); + } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index 5091bc0cc274..ec949f05a51d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -73,6 +73,7 @@ export class InlineEditsGutterIndicator extends Disposable { constructor( private readonly _editorObs: ObservableCodeEditor, private readonly _originalRange: IObservable, + private readonly _verticalOffset: IObservable, private readonly _model: IObservable, private readonly _isHoveringOverInlineEdit: IObservable, private readonly _focusIsInMenu: ISettableObservable, @@ -130,7 +131,8 @@ export class InlineEditsGutterIndicator extends Disposable { const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); - const pillRect = targetRect.withHeight(lineHeight).withWidth(22); + const pillOffset = this._verticalOffset.read(reader); + const pillRect = targetRect.withHeight(lineHeight).withWidth(22).moveDown(pillOffset); const pillRectMoved = pillRect.moveToBeContainedIn(viewPortWithStickyScroll); const rect = targetRect; @@ -142,7 +144,7 @@ export class InlineEditsGutterIndicator extends Disposable { return { rect, iconRect, - arrowDirection: (iconRect.top === targetRect.top ? 'right' as const + arrowDirection: (targetRect.containsRect(iconRect) ? 'right' as const : iconRect.top > targetRect.top ? 'top' as const : 'bottom' as const), docked: rect.containsRect(iconRect) && viewPortWithStickyScroll.containsRect(iconRect), }; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts index 1361c4b7c55b..73539134b370 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts @@ -11,6 +11,7 @@ import { observableCodeEditor } from '../../../../../browser/observableCodeEdito import { Point } from '../../../../../browser/point.js'; import { LineSource, renderLines, RenderOptions } from '../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; import { EditorOption } from '../../../../../common/config/editorOptions.js'; +import { LineRange } from '../../../../../common/core/lineRange.js'; import { Position } from '../../../../../common/core/position.js'; import { Range } from '../../../../../common/core/range.js'; import { ILanguageService } from '../../../../../common/languages/language.js'; @@ -112,6 +113,32 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits return Math.max(...lineWidths); }); + private readonly _trimVertically = derived(this, reader => { + const text = this._state.read(reader)?.text; + if (!text || text.trim() === '') { + return { top: 0, bottom: 0 }; + } + + // Adjust for leading/trailing newlines + const lineHeight = this._editor.getOption(EditorOption.lineHeight); + let topTrim = 0; + let bottomTrim = 0; + + let i = 0; + for (; i < text.length && text[i] === '\n'; i++) { + topTrim += lineHeight; + } + + for (let j = text.length - 1; j > i && text[j] === '\n'; j--) { + bottomTrim += lineHeight; + } + + return { top: topTrim, bottom: bottomTrim }; + }); + + public readonly startLineOffset = this._trimVertically.map(v => v.top); + public readonly originalLines = this._state.map(s => s ? new LineRange(s.lineNumber, s.lineNumber + 2) : undefined); + private readonly _overlayLayout = derivedWithStore(this, (reader, store) => { this._ghostText.read(reader); const state = this._state.read(reader); @@ -122,35 +149,22 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits // Update the overlay when the position changes this._editorObs.observePosition(observableValue(this, new Position(state.lineNumber, state.column)), store).read(reader); - const lineHeight = this._editor.getOption(EditorOption.lineHeight); - const scrollTop = this._editorObs.scrollTop.read(reader); const editorLayout = this._editorObs.layoutInfo.read(reader); const horizontalScrollOffset = this._editorObs.scrollLeft.read(reader); const left = editorLayout.contentLeft + this._editorMaxContentWidthInRange.read(reader) - horizontalScrollOffset; - - let height = this._ghostTextView.height.read(reader); - let top = this._editor.getTopForLineNumber(state.lineNumber) - scrollTop; - - if (state.text.trim() !== '') { - // Adjust for leading/trailing newlines - let i = 0; - for (; i < state.text.length && state.text[i] === '\n'; i++) { - height -= lineHeight; - top += lineHeight; - } - for (let j = state.text.length - 1; j > i && state.text[j] === '\n'; j--) { - height -= lineHeight; - } - } - const codeLeft = editorLayout.contentLeft; - const bottom = top + height; - if (left <= codeLeft) { return null; } + const { top: topTrim, bottom: bottomTrim } = this._trimVertically.read(reader); + + const scrollTop = this._editorObs.scrollTop.read(reader); + const height = this._ghostTextView.height.read(reader) - topTrim - bottomTrim; + const top = this._editor.getTopForLineNumber(state.lineNumber) - scrollTop + topTrim; + const bottom = top + height; + const code1 = new Point(left, top); const codeStart1 = new Point(codeLeft, top); const code2 = new Point(left, bottom); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 6f980b72df59..4bc8b06bc734 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -185,12 +185,29 @@ export class InlineEditsView extends Disposable { || this._lineReplacementView.read(reader).some(v => v.isHovered.read(reader)); }); + private readonly _gutterIndicatorOffset = derived(this, reader => { + // TODO: have a better way to tell the gutter indicator view where the edit is inside a viewzone + if (this._uiState.read(reader)?.state?.kind === 'insertionMultiLine') { + return this._insertion.startLineOffset.read(reader); + } + return 0; + }); + + private readonly _originalDisplayRange = derived(this, reader => { + const state = this._uiState.read(reader); + if (state?.state?.kind === 'insertionMultiLine') { + return this._insertion.originalLines.read(reader); + } + return state?.originalDisplayRange; + }); + protected readonly _indicator = this._register(autorunWithStore((reader, store) => { if (this._useGutterIndicator.read(reader)) { store.add(this._instantiationService.createInstance( InlineEditsGutterIndicator, this._editorObs, - this._uiState.map(s => s && s.originalDisplayRange), + this._originalDisplayRange, + this._gutterIndicatorOffset, this._model, this._inlineEditsIsHovered, this._focusIsInMenu, @@ -200,9 +217,9 @@ export class InlineEditsView extends Disposable { this._editorObs, derived(reader => { const state = this._uiState.read(reader); - if (!state || !state.state) { return undefined; } - const range = state.originalDisplayRange; - const top = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); + const range = this._originalDisplayRange.read(reader); + if (!state || !state.state || !range) { return undefined; } + const top = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader) + this._gutterIndicatorOffset.read(reader); return { editTop: top, showAlways: state.state.kind !== 'sideBySide' }; }), this._model, From 50e77b543f4b1a8441878d3ab4b23c13d5131581 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Wed, 29 Jan 2025 16:27:10 +0100 Subject: [PATCH 0993/3587] @vscode/proxy-agent 0.31.0 --- package-lock.json | 8 ++++---- package.json | 2 +- remote/package-lock.json | 8 ++++---- remote/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index e1adc3a25994..4963a5eb7444 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.8", - "@vscode/proxy-agent": "^0.30.0", + "@vscode/proxy-agent": "^0.31.0", "@vscode/ripgrep": "^1.15.10", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", @@ -2850,9 +2850,9 @@ } }, "node_modules/@vscode/proxy-agent": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.30.0.tgz", - "integrity": "sha512-3MicZnHWbJna0WIHFDgrCFMgT8cWrzXe9VXKrJQTay8jWC+7Xi3+FTYgK9kC7RWAf7GVcwkjJh5UGTIbEDW7Fg==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.31.0.tgz", + "integrity": "sha512-HY6UaJ9mKsBxHGnbj9EKigH5vczvB+9wTa8csNqPIjE8i2XjinKcGTBa+ky2+Eb+LGQNvyV/v56+XF6P11xRaQ==", "license": "MIT", "dependencies": { "@tootallnate/once": "^3.0.0", diff --git a/package.json b/package.json index d540db28597c..53c91131ccf9 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.8", - "@vscode/proxy-agent": "^0.30.0", + "@vscode/proxy-agent": "^0.31.0", "@vscode/ripgrep": "^1.15.10", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", diff --git a/remote/package-lock.json b/remote/package-lock.json index e039f2ab2f6f..e16b93eee5ea 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -13,7 +13,7 @@ "@parcel/watcher": "2.5.0", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.30.0", + "@vscode/proxy-agent": "^0.31.0", "@vscode/ripgrep": "^1.15.10", "@vscode/spdlog": "^0.15.0", "@vscode/tree-sitter-wasm": "^0.0.5", @@ -419,9 +419,9 @@ "integrity": "sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg==" }, "node_modules/@vscode/proxy-agent": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.30.0.tgz", - "integrity": "sha512-3MicZnHWbJna0WIHFDgrCFMgT8cWrzXe9VXKrJQTay8jWC+7Xi3+FTYgK9kC7RWAf7GVcwkjJh5UGTIbEDW7Fg==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.31.0.tgz", + "integrity": "sha512-HY6UaJ9mKsBxHGnbj9EKigH5vczvB+9wTa8csNqPIjE8i2XjinKcGTBa+ky2+Eb+LGQNvyV/v56+XF6P11xRaQ==", "license": "MIT", "dependencies": { "@tootallnate/once": "^3.0.0", diff --git a/remote/package.json b/remote/package.json index 433a22b487bb..b381cd45b106 100644 --- a/remote/package.json +++ b/remote/package.json @@ -8,7 +8,7 @@ "@parcel/watcher": "2.5.0", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.30.0", + "@vscode/proxy-agent": "^0.31.0", "@vscode/ripgrep": "^1.15.10", "@vscode/spdlog": "^0.15.0", "@vscode/tree-sitter-wasm": "^0.0.5", From 40bba096e5e1aed1b619f1f8e76a6907a15ef36c Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 29 Jan 2025 16:49:41 +0100 Subject: [PATCH 0994/3587] Make showing comment collapse dislog default (#239107) Fixes microsoft/vscode-pull-request-github#6613 --- .../contrib/comments/browser/commentThreadZoneWidget.ts | 4 ++++ .../contrib/comments/browser/comments.contribution.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 582462c23daa..0039a4030eaa 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -341,7 +341,11 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget message: nls.localize('confirmCollapse', "Collapsing a comment thread will discard unsubmitted comments. Do you want to collapse this comment thread?"), primaryButton: nls.localize('collapse', "Collapse"), type: Severity.Warning, + checkbox: { label: nls.localize('neverAskAgain', "Never ask me again"), checked: false } }); + if (result.checkboxChecked) { + await this.configurationService.updateValue('comments.thread.confirmOnCollapse', 'never'); + } return result.confirmed; } return true; diff --git a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index be76ed22a853..79dc09225cfb 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -143,7 +143,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis type: 'string', enum: ['whenHasUnsubmittedComments', 'never'], enumDescriptions: [nls.localize('confirmOnCollapse.whenHasUnsubmittedComments', "Show a confirmation dialog when collapsing a comment thread with unsubmitted comments."), nls.localize('confirmOnCollapse.never', "Never show a confirmation dialog when collapsing a comment thread.")], - default: 'never', + default: 'whenHasUnsubmittedComments', description: nls.localize('confirmOnCollapse', "Controls whether a confirmation dialog is shown when collapsing a comment thread.") } } From 4b2a52e9424d5cff09c5e53c03e54a3e445fd849 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 29 Jan 2025 16:53:18 +0100 Subject: [PATCH 0995/3587] Fixing smoketests, using different selection offsets depending on product quality (#239108) fixing smoketests, using different selection offsets --- test/automation/src/settings.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/automation/src/settings.ts b/test/automation/src/settings.ts index a7a40ece4311..f61ff0dc36c6 100644 --- a/test/automation/src/settings.ts +++ b/test/automation/src/settings.ts @@ -25,7 +25,8 @@ export class SettingsEditor { await this.openUserSettingsFile(); await this.code.dispatchKeybinding('right'); - await this.editor.waitForEditorSelection('settings.json', s => s.selectionStart === 1 && s.selectionEnd === 1); + const selectionOffset = this._editContextSelectionOffset(); + await this.editor.waitForEditorSelection('settings.json', s => s.selectionStart === selectionOffset && s.selectionEnd === selectionOffset); await this.editor.waitForTypeInEditor('settings.json', `"${setting}": ${value},`); await this.editors.saveOpenedFile(); } @@ -40,7 +41,8 @@ export class SettingsEditor { await this.openUserSettingsFile(); await this.code.dispatchKeybinding('right'); - await this.editor.waitForEditorSelection('settings.json', s => s.selectionStart === 1 && s.selectionEnd === 1); + const selectionOffset = this._editContextSelectionOffset(); + await this.editor.waitForEditorSelection('settings.json', (s) => s.selectionStart === selectionOffset && s.selectionEnd === selectionOffset); await this.editor.waitForTypeInEditor('settings.json', settings.map(v => `"${v[0]}": ${v[1]},`).join('')); await this.editors.saveOpenedFile(); } @@ -83,4 +85,8 @@ export class SettingsEditor { private _editContextSelector() { return this.code.quality === Quality.Stable ? SEARCH_BOX_TEXTAREA : SEARCH_BOX_NATIVE_EDIT_CONTEXT; } + + private _editContextSelectionOffset(): number { + return this.code.quality === Quality.Stable ? 0 : 1; + } } From a29a921f0a20c6f24fb12b8b898db5c60618a3cf Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Wed, 29 Jan 2025 08:00:52 -0800 Subject: [PATCH 0996/3587] Improve video styling and centering in Getting Started section (#239109) --- .../browser/gettingStartedDetailsRenderer.ts | 15 +++++++++------ .../browser/media/gettingStarted.css | 5 ++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts index ca5d099418cc..6755eb5d760a 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts @@ -211,12 +211,15 @@ export class GettingStartedDetailsRenderer { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css index 3657036b268c..b7db145f2b5b 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css @@ -580,7 +580,10 @@ } .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent.video > .getting-started-media { - height: inherit; + grid-area: title-start / media-start / steps-end / media-end; + align-self: unset; + display: flex; + justify-content: center; } .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent.markdown > .getting-started-media { From 2ae3d5ae3582b281b1af417014fe9395ca7cf0b7 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:03:52 +0100 Subject: [PATCH 0997/3587] Git - fix bug with opening the first commit from the timeline view (#239111) --- extensions/git/src/fileSystemProvider.ts | 12 ++++++++++++ extensions/git/src/timelineProvider.ts | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/extensions/git/src/fileSystemProvider.ts b/extensions/git/src/fileSystemProvider.ts index 6e30118128db..8b8f9fc65afb 100644 --- a/extensions/git/src/fileSystemProvider.ts +++ b/extensions/git/src/fileSystemProvider.ts @@ -144,6 +144,12 @@ export class GitFileSystemProvider implements FileSystemProvider { const details = await repository.getObjectDetails(sanitizeRef(ref, path, repository), path); return { type: FileType.File, size: details.size, mtime: this.mtime, ctime: 0 }; } catch { + // Empty tree + if (ref === await repository.getEmptyTree()) { + this.logger.warn(`[GitFileSystemProvider][stat] Empty tree - ${uri.toString()}`); + return { type: FileType.File, size: 0, mtime: this.mtime, ctime: 0 }; + } + // File does not exist in git. This could be because the file is untracked or ignored this.logger.warn(`[GitFileSystemProvider][stat] File not found - ${uri.toString()}`); throw FileSystemError.FileNotFound(); @@ -194,6 +200,12 @@ export class GitFileSystemProvider implements FileSystemProvider { try { return await repository.buffer(sanitizeRef(ref, path, repository), path); } catch { + // Empty tree + if (ref === await repository.getEmptyTree()) { + this.logger.warn(`[GitFileSystemProvider][readFile] Empty tree - ${uri.toString()}`); + return new Uint8Array(0); + } + // File does not exist in git. This could be because the file is untracked or ignored this.logger.warn(`[GitFileSystemProvider][readFile] File not found - ${uri.toString()}`); throw FileSystemError.FileNotFound(); diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index f29264e4078b..90d8d28c3965 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -222,6 +222,7 @@ export class GitTimelineProvider implements TimelineProvider { const openComparison = l10n.t('Open Comparison'); + const emptyTree = await repo.getEmptyTree(); const unpublishedCommits = await repo.getUnpublishedCommits(); const remoteHoverCommands = await provideSourceControlHistoryItemHoverCommands(this.model, repo); @@ -243,7 +244,8 @@ export class GitTimelineProvider implements TimelineProvider { const message = emojify(c.message); - const item = new GitTimelineItem(c.hash, commits[index + 1]?.hash ?? `${c.hash}^`, message, date?.getTime() ?? 0, c.hash, 'git:file:commit'); + const previousRef = commits[index + 1]?.hash ?? emptyTree; + const item = new GitTimelineItem(c.hash, previousRef, message, date?.getTime() ?? 0, c.hash, 'git:file:commit'); item.iconPath = new ThemeIcon('git-commit'); if (showAuthor) { item.description = c.authorName; From a504bf1a0a553a36aef3a900717d1858a95623c6 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 29 Jan 2025 17:12:03 +0100 Subject: [PATCH 0998/3587] Fix editors not getting colorized right away with tree sitter (#239113) --- .../browser/treeSitterTokenizationFeature.ts | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts index c113c2558e1b..34bcb6b7536c 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts @@ -151,26 +151,28 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi private _setViewPortTokens(textModel: ITextModel, versionId: number) { const maxLine = textModel.getLineCount(); + let rangeChanges: RangeChange[]; const editor = this._codeEditorService.listCodeEditors().find(editor => editor.getModel() === textModel); - if (!editor) { - return; - } - - const viewPort = editor.getVisibleRangesPlusViewportAboveBelow(); - const ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[] = new Array(viewPort.length); - const rangeChanges: RangeChange[] = new Array(viewPort.length); - - for (let i = 0; i < viewPort.length; i++) { - const range = viewPort[i]; - ranges[i] = { fromLineNumber: range.startLineNumber, toLineNumber: range.endLineNumber < maxLine ? range.endLineNumber : maxLine }; - const newRangeStartOffset = textModel.getOffsetAt(range.getStartPosition()); - const newRangeEndOffset = textModel.getOffsetAt(range.getEndPosition()); - rangeChanges[i] = { - newRange: range, - newRangeStartOffset, - newRangeEndOffset, - oldRangeLength: newRangeEndOffset - newRangeStartOffset - }; + if (editor) { + const viewPort = editor.getVisibleRangesPlusViewportAboveBelow(); + const ranges: { readonly fromLineNumber: number; readonly toLineNumber: number }[] = new Array(viewPort.length); + rangeChanges = new Array(viewPort.length); + + for (let i = 0; i < viewPort.length; i++) { + const range = viewPort[i]; + ranges[i] = { fromLineNumber: range.startLineNumber, toLineNumber: range.endLineNumber < maxLine ? range.endLineNumber : maxLine }; + const newRangeStartOffset = textModel.getOffsetAt(range.getStartPosition()); + const newRangeEndOffset = textModel.getOffsetAt(range.getEndPosition()); + rangeChanges[i] = { + newRange: range, + newRangeStartOffset, + newRangeEndOffset, + oldRangeLength: newRangeEndOffset - newRangeStartOffset + }; + } + } else { + const valueLength = textModel.getValueLength(); + rangeChanges = [{ newRange: new Range(1, 1, maxLine, textModel.getLineMaxColumn(maxLine)), newRangeStartOffset: 0, newRangeEndOffset: valueLength, oldRangeLength: valueLength }]; } this._handleTreeUpdate({ ranges: rangeChanges, textModel, versionId }); } From 8c5563e6f4df79209d075a0b68f80736236358b4 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 29 Jan 2025 17:33:17 +0100 Subject: [PATCH 0999/3587] use dummy MD element when rendering inline progress with request (#239115) fixes https://github.com/microsoft/vscode/issues/239091 --- .../contrib/chat/browser/chatListRenderer.ts | 13 +++++--- .../contrib/chat/browser/media/chat.css | 31 ++++++++++++++----- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index e66dde9f6c93..e61430a76851 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -504,6 +504,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer', { supportHtml: true }), kind: 'markdownContent' }); + } else { + templateData.value.classList.remove('inline-progress'); + } + } else if (isResponseVM(element)) { if (element.contentReferences.length) { value.push({ kind: 'references', references: element.contentReferences }); @@ -581,11 +589,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRendererP>SPAN:empty { + display: inline-block; + width: 11px; + } + + .rendered-markdown:last-of-type>P>SPAN:empty::after { + content: ''; + white-space: nowrap; + overflow: hidden; + width: 3em; + animation: ellipsis steps(4, end) 1s infinite; + } +} + .interactive-item-container .value .rendered-markdown { line-height: 1.5em; } @@ -564,12 +585,6 @@ have to be updated for changes to the rules above, or to support more deeply nes display: inherit; } -.interactive-session .chat-editing-session .monaco-list-row .chat-collapsible-list-action-bar .action-label.checked { - color: var(--vscode-inputOption-activeForeground); - background-color: var(--vscode-inputOption-activeBackground); - box-shadow: inset 0 0 0 1px var(--vscode-inputOption-activeBorder); -} - .interactive-session .chat-editing-session .chat-editing-session-container.show-file-icons .monaco-scrollable-element .monaco-list-rows .monaco-list-row { border-radius: 2px; } @@ -1401,7 +1416,7 @@ have to be updated for changes to the rules above, or to support more deeply nes outline: 1px solid var(--vscode-focusBorder); } -.interactive-session .chat-used-context .chat-used-context-label .monaco-button .codicon { +.interactive-session .chat-used-context-label .monaco-button .codicon { font-size: 12px; } @@ -1524,6 +1539,8 @@ have to be updated for changes to the rules above, or to support more deeply nes width: initial; width: 27px; height: 27px; + width: 27px; + height: 27px; .codicon { margin: 0px; From b3f0b18cbb482a3f79a102cf301fa97620713186 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 29 Jan 2025 09:04:13 -0800 Subject: [PATCH 1000/3587] remove file extension from labels in the prompt attachment selection dialog (#239116) [prompts]: remove file extension from labels in the prompt attachment selection dialog --- .../contrib/chat/browser/actions/chatContextActions.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index e293e2b5c15a..65710233e74b 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -8,7 +8,7 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { Schemas } from '../../../../../base/common/network.js'; import { isElectron } from '../../../../../base/common/platform.js'; -import { dirname } from '../../../../../base/common/resources.js'; +import { basename, dirname } from '../../../../../base/common/resources.js'; import { compare } from '../../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -52,6 +52,7 @@ import { ChatRequestAgentPart } from '../../common/chatParserTypes.js'; import { IChatVariableData, IChatVariablesService } from '../../common/chatVariables.js'; import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; +import { PROMPT_SNIPPET_FILE_EXTENSION } from '../../common/promptSyntax/contentProviders/promptContentsProviderBase.js'; import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView, showEditsView } from '../chat.js'; import { imageToHash, isImage } from '../chatPasteProviders.js'; import { isQuickChat } from '../chatWidget.js'; @@ -947,9 +948,11 @@ const selectPromptAttachment = async (options: ISelectPromptOptions): Promise { return files.map((file) => { + const fileBasename = basename(file); + const fileWithoutExtension = fileBasename.replace(PROMPT_SNIPPET_FILE_EXTENSION, ''); const result: IQuickPickItem & { value: URI } = { type: 'item', - label: labelService.getUriBasenameLabel(file), + label: fileWithoutExtension, description: labelService.getUriLabel(dirname(file), { relative: true }), tooltip: file.fsPath, value: file, From e6125a356ff6ebe7214b183ee1b5fb009a2b8d31 Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Wed, 29 Jan 2025 09:13:45 -0800 Subject: [PATCH 1001/3587] align setting names (#239044) --- .../workbench/contrib/notebook/browser/notebook.contribution.ts | 2 +- src/vs/workbench/contrib/notebook/common/notebookCommon.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 1a0305147033..41efb33bbb84 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -1257,7 +1257,7 @@ configurationRegistry.registerConfiguration({ default: false }, [NotebookSetting.markupFontFamily]: { - markdownDescription: nls.localize('notebook.markupFontFamily', "Controls the font family of rendered markup in notebooks. When left blank, this will fall back to the default workbench font family."), + markdownDescription: nls.localize('notebook.markup.fontFamily', "Controls the font family of rendered markup in notebooks. When left blank, this will fall back to the default workbench font family."), type: 'string', default: '', tags: ['notebookLayout'] diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 6fd9ec8fa9ac..4c60c2284a65 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -1034,7 +1034,7 @@ export const NotebookSetting = { cellFailureDiagnostics: 'notebook.cellFailureDiagnostics', outputBackupSizeLimit: 'notebook.backup.sizeLimit', multiCursor: 'notebook.multiCursor.enabled', - markupFontFamily: 'notebook.markupFontFamily', + markupFontFamily: 'notebook.markup.fontFamily', } as const; export const enum CellStatusbarAlignment { From 89100e8e1c4fe0708723f2b54a10ff2668a33d61 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 29 Jan 2025 18:22:09 +0100 Subject: [PATCH 1002/3587] serve-web: redirect to the base path when accepting the connection token (#239120) --- src/vs/server/node/webClientServer.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/vs/server/node/webClientServer.ts b/src/vs/server/node/webClientServer.ts index 5ce490dc1394..e36113b4bfed 100644 --- a/src/vs/server/node/webClientServer.ts +++ b/src/vs/server/node/webClientServer.ts @@ -239,6 +239,15 @@ export class WebClientServer { * Handle HTTP requests for / */ private async _handleRoot(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise { + + const getFirstHeader = (headerName: string) => { + const val = req.headers[headerName]; + return Array.isArray(val) ? val[0] : val; + }; + + // Prefix routes with basePath for clients + const basePath = getFirstHeader('x-forwarded-prefix') || this._basePath; + const queryConnectionToken = parsedUrl.query[connectionTokenQueryName]; if (typeof queryConnectionToken === 'string') { // We got a connection token as a query parameter. @@ -259,18 +268,13 @@ export class WebClientServer { newQuery[key] = parsedUrl.query[key]; } } - const newLocation = url.format({ pathname: parsedUrl.pathname, query: newQuery }); + const newLocation = url.format({ pathname: basePath, query: newQuery }); responseHeaders['Location'] = newLocation; res.writeHead(302, responseHeaders); return void res.end(); } - const getFirstHeader = (headerName: string) => { - const val = req.headers[headerName]; - return Array.isArray(val) ? val[0] : val; - }; - const replacePort = (host: string, port: string) => { const index = host?.indexOf(':'); if (index !== -1) { @@ -305,9 +309,6 @@ export class WebClientServer { _wrapWebWorkerExtHostInIframe = false; } - // Prefix routes with basePath for clients - const basePath = getFirstHeader('x-forwarded-prefix') || this._basePath; - if (this._logService.getLevel() === LogLevel.Trace) { ['x-original-host', 'x-forwarded-host', 'x-forwarded-port', 'host'].forEach(header => { const value = getFirstHeader(header); From 743850cccffba952c81d3229d81b9dc50346885b Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 29 Jan 2025 09:22:31 -0800 Subject: [PATCH 1003/3587] Agent preview -> experimental (#239121) --- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 3 ++- .../chat/browser/viewsWelcome/chatViewWelcomeController.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 71b1d94a62e7..cff336d26b4c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -332,7 +332,8 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr 'chat.agent.maxRequests': { type: 'number', description: localize('chat.agent.maxRequests', "The maximum number of requests to allow Copilot Edits to use in agent mode."), - default: context.state.entitlement === ChatEntitlement.Limited ? 5 : 15 + default: context.state.entitlement === ChatEntitlement.Limited ? 5 : 15, + tags: ['experimental'] }, } }; diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index 2e40bd0232f6..1975aba545fd 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -150,7 +150,7 @@ export class ChatViewWelcomePart extends Disposable { // Override welcome message for the agent. Sort of a hack, should it come from the participant? This case is different because the welcome content typically doesn't change per ChatWidget content.message = new MarkdownString('Ask Copilot to edit your files in agent mode. Copilot will automatically use multiple requests to pick files to edit, run terminal commands, and iterate on errors.\n\nCopilot is powered by AI, so mistakes are possible. Review output carefully before use.'); const featureIndicator = dom.append(this.element, $('.chat-welcome-view-indicator')); - featureIndicator.textContent = localize('preview', "PREVIEW"); + featureIndicator.textContent = localize('experimental', "EXPERIMENTAL"); } // Message From f9f14453e683eb0c27be7ea06c9712cbf5ab5487 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 29 Jan 2025 18:28:03 +0100 Subject: [PATCH 1004/3587] Remove insertion view and enhance edit growth logic (#239123) Do not show insertion view anymore. Grow the edit if possible, otherwise --- .../browser/view/inlineEdits/view.ts | 54 +++++++++++++------ .../view/inlineEdits/wordReplacementView.ts | 3 ++ 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 4bc8b06bc734..636137c0fa64 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -26,7 +26,7 @@ import { InlineEditsSideBySideDiff } from './sideBySideDiff.js'; import { applyEditToModifiedRangeMappings, createReindentEdit } from './utils.js'; import './view.css'; import { InlineEditWithChanges } from './viewAndDiffProducer.js'; -import { LineReplacementView, WordInsertView, WordReplacementView } from './wordReplacementView.js'; +import { LineReplacementView, WordReplacementView } from './wordReplacementView.js'; export class InlineEditsView extends Disposable { private readonly _editorObs = observableCodeEditor(this._editor); @@ -164,11 +164,7 @@ export class InlineEditsView extends Disposable { protected readonly _inlineDiffView = this._register(new OriginalEditorInlineDiffView(this._editor, this._inlineDiffViewState, this._previewTextModel)); protected readonly _wordReplacementViews = mapObservableArrayCached(this, this._uiState.map(s => s?.state?.kind === 'wordReplacements' ? s.state.replacements : []), (e, store) => { - if (e.range.isEmpty()) { - return store.add(this._instantiationService.createInstance(WordInsertView, this._editorObs, e)); - } else { - return store.add(this._instantiationService.createInstance(WordReplacementView, this._editorObs, e, [e])); - } + return store.add(this._instantiationService.createInstance(WordReplacementView, this._editorObs, e, [e])); }).recomputeInitiallyAndOnChange(this._store); protected readonly _lineReplacementView = mapObservableArrayCached(this, this._uiState.map(s => s?.state?.kind === 'lineReplacement' ? [s.state] : []), (e, store) => { // TODO: no need for map here, how can this be done with observables @@ -275,10 +271,18 @@ export class InlineEditsView extends Disposable { const numOriginalLines = edit.originalLineRange.length; const numModifiedLines = edit.modifiedLineRange.length; - const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < 100 && TextLength.ofRange(m.modifiedRange).columnCount < 100); + const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < WordReplacementView.MAX_LENGTH && TextLength.ofRange(m.modifiedRange).columnCount < WordReplacementView.MAX_LENGTH); if (allInnerChangesNotTooLong && isSingleInnerEdit && numOriginalLines === 1 && numModifiedLines === 1) { - return 'wordReplacements'; - } else if (numOriginalLines > 0 && numModifiedLines > 0 && !InlineEditsSideBySideDiff.fitsInsideViewport(this._editor, edit, reader)) { + // Make sure there is no insertion, even if we grow them + if ( + !inner.some(m => m.originalRange.isEmpty()) || + !growEditsUntilWhitespace(inner.map(m => new SingleTextEdit(m.originalRange, '')), edit.originalText).some(e => e.range.isEmpty() && TextLength.ofRange(e.range).columnCount < WordReplacementView.MAX_LENGTH) + ) { + return 'wordReplacements'; + } + } + + if (numOriginalLines > 0 && numModifiedLines > 0 && !InlineEditsSideBySideDiff.fitsInsideViewport(this._editor, edit, reader)) { return 'lineReplacement'; } @@ -336,9 +340,15 @@ export class InlineEditsView extends Disposable { } if (view === 'wordReplacements') { + let grownEdits = growEditsToEntireWord(replacements, edit.originalText); + + if (grownEdits.some(e => e.range.isEmpty())) { + grownEdits = growEditsUntilWhitespace(replacements, edit.originalText); + } + return { kind: 'wordReplacements' as const, - replacements: growEditsToEntireWord(replacements, edit.originalText), + replacements: grownEdits, }; } @@ -417,6 +427,14 @@ function isSingleLineDeletion(diff: DetailedLineRangeMapping[]): boolean { } function growEditsToEntireWord(replacements: SingleTextEdit[], originalText: AbstractText): SingleTextEdit[] { + return _growEdits(replacements, originalText, (char) => /^[a-zA-Z]$/.test(char)); +} + +function growEditsUntilWhitespace(replacements: SingleTextEdit[], originalText: AbstractText): SingleTextEdit[] { + return _growEdits(replacements, originalText, (char) => !(/^\s$/.test(char))); +} + +function _growEdits(replacements: SingleTextEdit[], originalText: AbstractText, fn: (c: string) => boolean): SingleTextEdit[] { const result: SingleTextEdit[] = []; replacements.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); @@ -429,17 +447,17 @@ function growEditsToEntireWord(replacements: SingleTextEdit[], originalText: Abs const startLineContent = originalText.getLineAt(edit.range.startLineNumber); const endLineContent = originalText.getLineAt(edit.range.endLineNumber); - if (isWordChar(startLineContent[startIndex])) { + if (isIncluded(startLineContent[startIndex])) { // grow to the left - while (isWordChar(startLineContent[startIndex - 1])) { + while (isIncluded(startLineContent[startIndex - 1])) { prefix = startLineContent[startIndex - 1] + prefix; startIndex--; } } - if (isWordChar(endLineContent[endIndex])) { + if (isIncluded(endLineContent[endIndex]) || endIndex < startIndex) { // grow to the right - while (isWordChar(endLineContent[endIndex + 1])) { + while (isIncluded(endLineContent[endIndex + 1])) { suffix += endLineContent[endIndex + 1]; endIndex++; } @@ -454,9 +472,11 @@ function growEditsToEntireWord(replacements: SingleTextEdit[], originalText: Abs result.push(newEdit); } - function isWordChar(char: string | undefined) { - if (!char) { return false; } - return /^[a-zA-Z]$/.test(char); + function isIncluded(c: string | undefined) { + if (c === undefined) { + return false; + } + return fn(c); } return result; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index fe5996d1db9c..2eccc056602d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -40,6 +40,9 @@ export const transparentHoverBackground = registerColor( ); export class WordReplacementView extends Disposable implements IInlineEditsView { + + public static MAX_LENGTH = 100; + private readonly _start = this._editor.observePosition(constObservable(this._edit.range.getStartPosition()), this._store); private readonly _end = this._editor.observePosition(constObservable(this._edit.range.getEndPosition()), this._store); From 5c61dd2bfe7a4905f90603cd13ba241ae62ed5b9 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 29 Jan 2025 09:38:48 -0800 Subject: [PATCH 1005/3587] change prompt attachment icon (#239125) [prompts]: change LightBulb icon to the Bookmark one --- .../contrib/chat/browser/actions/chatContextActions.ts | 2 +- .../instructionsAttachment/instructionsAttachment.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 65710233e74b..4123c4b8d347 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -812,7 +812,7 @@ export class AttachContextAction extends Action2 { kind: 'prompt-instructions', id: 'prompt-instructions', label: localize('promptWithEllipsis', 'Prompt...'), - iconClass: ThemeIcon.asClassName(Codicon.lightbulbSparkle), + iconClass: ThemeIcon.asClassName(Codicon.bookmark), }); } diff --git a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts index c5ee5d143e5f..f959d7eabf11 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts @@ -138,7 +138,7 @@ export class InstructionsAttachmentWidget extends Disposable { hidePath: true, range: undefined, title, - icon: ThemeIcon.fromId(Codicon.lightbulbSparkle.id), + icon: ThemeIcon.fromId(Codicon.bookmark.id), extraClasses: [], }); this.domNode.ariaLabel = ariaLabel; From 8958ba54c3377d70dcb6290217f915b7478285d3 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 29 Jan 2025 10:19:46 -0800 Subject: [PATCH 1006/3587] chat: fix agent switcher able to be opened when disabled (#239127) Fixes https://github.com/microsoft/vscode-copilot/issues/12554 Maybe should be done in the base view item instead, but this is endgame. --- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 888e4637dd6d..0bb471e47d8d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -1769,9 +1769,11 @@ class ToggleAgentActionViewItem extends MenuEntryActionViewItem { } private _openContextMenu() { - this._contextMenuService.showContextMenu({ - getAnchor: () => this.element!, - getActions: () => this.agentStateActions - }); + if (this.action.enabled) { + this._contextMenuService.showContextMenu({ + getAnchor: () => this.element!, + getActions: () => this.agentStateActions + }); + } } } From c1c052a2a6b79a5fba3ec529536c02c4e8db38ec Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 29 Jan 2025 10:56:53 -0800 Subject: [PATCH 1007/3587] Enable chat.agent.maxRequests for an experiment (#239130) --- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index cff336d26b4c..4ab7a2eb6505 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -333,7 +333,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr type: 'number', description: localize('chat.agent.maxRequests', "The maximum number of requests to allow Copilot Edits to use in agent mode."), default: context.state.entitlement === ChatEntitlement.Limited ? 5 : 15, - tags: ['experimental'] + tags: ['experimental', 'onExp'] }, } }; From 128a67bfd95e4958bec963cc2c89f770dc0d6b1e Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Wed, 29 Jan 2025 11:23:27 -0800 Subject: [PATCH 1008/3587] Clear nb inline values when setting turned off (#239132) * clear on config turned off * i wouldn't dare have imports out of order --- .../contrib/notebookVariables/notebookInlineVariables.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts index 8c53b1cf1112..2898b0763442 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts @@ -5,6 +5,7 @@ import { CancellationToken, CancellationTokenSource } from '../../../../../../base/common/cancellation.js'; import { onUnexpectedExternalError } from '../../../../../../base/common/errors.js'; +import { Event } from '../../../../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../../base/common/map.js'; import { isEqual } from '../../../../../../base/common/resources.js'; @@ -68,6 +69,14 @@ export class NotebookInlineVariablesController extends Disposable implements INo await this.updateInlineVariables(e); } })); + + this._register(Event.runAndSubscribe(this.configurationService.onDidChangeConfiguration, e => { + if (!e || e.affectsConfiguration(NotebookSetting.notebookInlineValues)) { + if (!this.configurationService.getValue(NotebookSetting.notebookInlineValues)) { + this.clearNotebookInlineDecorations(); + } + } + })); } private async updateInlineVariables(event: ICellExecutionStateChangedEvent): Promise { From bda07cff3ac8d208a443c029c37c022319ace431 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 29 Jan 2025 11:33:26 -0800 Subject: [PATCH 1009/3587] improve error message for references to non-existent files (#239133) [error label]: improve error message for references to non-existent files --- .../common/promptSyntax/parsers/basePromptParser.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index 3c6cf31d0997..9ef8199579b6 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -469,12 +469,14 @@ export abstract class BasePromptParser extend /** * Get message for the provided error condition object. * - * @param error Error object. + * @param error Error object that extends {@link ParseError}. * @returns Error message. */ - protected getErrorMessage(error: ParseError): string { - // if failed to resolve prompt contents stream, return - // the approprivate message and the prompt path + protected getErrorMessage(error: TError): string { + if (error instanceof FileOpenFailed) { + return `${errorMessages.fileOpenFailed} '${error.uri.path}'.`; + } + if (error instanceof FailedToResolveContentsStream) { return `${errorMessages.streamOpenFailed} '${error.uri.path}'.`; } From d3996512fddf758adbc9315b7ce37ba2eab07bad Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 29 Jan 2025 12:49:41 -0800 Subject: [PATCH 1010/3587] Fix reset defaults behavior for Quick Input (#239137) Fixes https://github.com/microsoft/vscode/issues/239007 --- src/vs/workbench/browser/actions/layoutActions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index d8217f384aba..4e41395fb039 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -1588,6 +1588,7 @@ registerAction2(class CustomizeLayoutAction extends Action2 { } commandService.executeCommand('workbench.action.alignPanelCenter'); + commandService.executeCommand('workbench.action.alignQuickInputTop'); } })); From 9b9093e4c966e647d3b1c98cd1e197987a48669c Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 29 Jan 2025 12:50:52 -0800 Subject: [PATCH 1011/3587] make non-prompt file references non-readonly by default (#239138) [prompts]: make non-prompt file references non-readonly by default --- .../chatAttachmentModel/chatInstructionAttachmentsModel.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts index 0bfa66a79f0d..b0bc196ebc46 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts @@ -30,13 +30,13 @@ const toChatVariable = ( reference: Pick, isRoot: boolean, ): IChatRequestVariableEntry => { - const { uri } = reference; + const { uri, isPromptSnippet } = reference; // default `id` is the stringified `URI` let id = `${uri}`; // for prompt files, we add a prefix to the `id` - if (reference.isPromptSnippet) { + if (isPromptSnippet) { // the default prefix that is used for all prompt files let prefix = 'vscode.prompt.instructions'; // if the reference is the root object, add the `.root` suffix @@ -57,7 +57,7 @@ const toChatVariable = ( enabled: true, isFile: true, isDynamic: true, - isMarkedReadonly: true, + isMarkedReadonly: isPromptSnippet, }; }; From 10e711524df1b15ce38f99f05a5a9eaf870e6344 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 29 Jan 2025 23:11:40 +0100 Subject: [PATCH 1012/3587] fix #238937 (#239142) --- .../common/userDataAutoSyncService.ts | 78 ++++++++++--------- .../userDataSync/common/userDataSync.ts | 4 +- .../node/userDataAutoSyncService.ts | 2 +- .../common/userDataAutoSyncService.test.ts | 14 ++-- .../browser/extensionsWorkbenchService.ts | 2 +- .../browser/userDataSyncTrigger.ts | 4 +- .../browser/userDataSyncWorkbenchService.ts | 2 +- .../userDataAutoSyncService.ts | 6 +- 8 files changed, 59 insertions(+), 53 deletions(-) diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index 5ab5b3dd21d5..685053d0186a 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -17,7 +17,7 @@ import { localize } from '../../../nls.js'; import { IProductService } from '../../product/common/productService.js'; import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; -import { IUserDataSyncTask, IUserDataAutoSyncService, IUserDataManifest, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncService, IUserDataSyncStoreManagementService, IUserDataSyncStoreService, UserDataAutoSyncError, UserDataSyncError, UserDataSyncErrorCode } from './userDataSync.js'; +import { IUserDataSyncTask, IUserDataAutoSyncService, IUserDataManifest, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncService, IUserDataSyncStoreManagementService, IUserDataSyncStoreService, UserDataAutoSyncError, UserDataSyncError, UserDataSyncErrorCode, SyncOptions } from './userDataSync.js'; import { IUserDataSyncAccountService } from './userDataSyncAccount.js'; import { IUserDataSyncMachinesService } from './userDataSyncMachines.js'; @@ -87,21 +87,21 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto if (this.syncUrl) { - this.logService.info('Using settings sync service', this.syncUrl.toString()); + this.logService.info('[AutoSync] Using settings sync service', this.syncUrl.toString()); this._register(userDataSyncStoreManagementService.onDidChangeUserDataSyncStore(() => { if (!isEqual(this.syncUrl, userDataSyncStoreManagementService.userDataSyncStore?.url)) { this.lastSyncUrl = this.syncUrl; this.syncUrl = userDataSyncStoreManagementService.userDataSyncStore?.url; if (this.syncUrl) { - this.logService.info('Using settings sync service', this.syncUrl.toString()); + this.logService.info('[AutoSync] Using settings sync service', this.syncUrl.toString()); } } })); if (this.userDataSyncEnablementService.isEnabled()) { - this.logService.info('Auto Sync is enabled.'); + this.logService.info('[AutoSync] Enabled.'); } else { - this.logService.info('Auto Sync is disabled.'); + this.logService.info('[AutoSync] Disabled.'); } this.updateAutoSync(); @@ -111,9 +111,9 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto this._register(userDataSyncAccountService.onDidChangeAccount(() => this.updateAutoSync())); this._register(userDataSyncStoreService.onDidChangeDonotMakeRequestsUntil(() => this.updateAutoSync())); - this._register(userDataSyncService.onDidChangeLocal(source => this.triggerSync([source], false, false))); - this._register(Event.filter(this.userDataSyncEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerSync(['resourceEnablement'], false, false))); - this._register(this.userDataSyncStoreManagementService.onDidChangeUserDataSyncStore(() => this.triggerSync(['userDataSyncStoreChanged'], false, false))); + this._register(userDataSyncService.onDidChangeLocal(source => this.triggerSync([source]))); + this._register(Event.filter(this.userDataSyncEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerSync(['resourceEnablement']))); + this._register(this.userDataSyncStoreManagementService.onDidChangeUserDataSyncStore(() => this.triggerSync(['userDataSyncStoreChanged']))); } } @@ -149,16 +149,16 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto private isAutoSyncEnabled(): { enabled: boolean; message?: string } { if (!this.userDataSyncEnablementService.isEnabled()) { - return { enabled: false, message: 'Auto Sync: Disabled.' }; + return { enabled: false, message: '[AutoSync] Disabled.' }; } if (!this.userDataSyncAccountService.account) { - return { enabled: false, message: 'Auto Sync: Suspended until auth token is available.' }; + return { enabled: false, message: '[AutoSync] Suspended until auth token is available.' }; } if (this.userDataSyncStoreService.donotMakeRequestsUntil) { - return { enabled: false, message: `Auto Sync: Suspended until ${toLocalISOString(this.userDataSyncStoreService.donotMakeRequestsUntil)} because server is not accepting requests until then.` }; + return { enabled: false, message: `[AutoSync] Suspended until ${toLocalISOString(this.userDataSyncStoreService.donotMakeRequestsUntil)} because server is not accepting requests until then.` }; } if (this.suspendUntilRestart) { - return { enabled: false, message: 'Auto Sync: Suspended until restart.' }; + return { enabled: false, message: '[AutoSync] Suspended until restart.' }; } return { enabled: true }; } @@ -211,6 +211,7 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto } private async onDidFinishSync(error: Error | undefined): Promise { + this.logService.debug('[AutoSync] Sync Finished'); if (!error) { // Sync finished without errors this.successiveFailures = 0; @@ -223,19 +224,19 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto // Session got expired if (userDataSyncError.code === UserDataSyncErrorCode.SessionExpired) { await this.turnOff(false, true /* force soft turnoff on error */); - this.logService.info('Auto Sync: Turned off sync because current session is expired'); + this.logService.info('[AutoSync] Turned off sync because current session is expired'); } // Turned off from another device else if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff) { await this.turnOff(false, true /* force soft turnoff on error */); - this.logService.info('Auto Sync: Turned off sync because sync is turned off in the cloud'); + this.logService.info('[AutoSync] Turned off sync because sync is turned off in the cloud'); } // Exceeded Rate Limit on Client else if (userDataSyncError.code === UserDataSyncErrorCode.LocalTooManyRequests) { this.suspendUntilRestart = true; - this.logService.info('Auto Sync: Suspended sync because of making too many requests to server'); + this.logService.info('[AutoSync] Suspended sync because of making too many requests to server'); this.updateAutoSync(); } @@ -244,13 +245,13 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto await this.turnOff(false, true /* force soft turnoff on error */, true /* do not disable machine because disabling a machine makes request to server and can fail with TooManyRequests */); this.disableMachineEventually(); - this.logService.info('Auto Sync: Turned off sync because of making too many requests to server'); + this.logService.info('[AutoSync] Turned off sync because of making too many requests to server'); } // Method Not Found else if (userDataSyncError.code === UserDataSyncErrorCode.MethodNotFound) { await this.turnOff(false, true /* force soft turnoff on error */); - this.logService.info('Auto Sync: Turned off sync because current client is making requests to server that are not supported'); + this.logService.info('[AutoSync] Turned off sync because current client is making requests to server that are not supported'); } // Upgrade Required or Gone @@ -258,19 +259,19 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto await this.turnOff(false, true /* force soft turnoff on error */, true /* do not disable machine because disabling a machine makes request to server and can fail with upgrade required or gone */); this.disableMachineEventually(); - this.logService.info('Auto Sync: Turned off sync because current client is not compatible with server. Requires client upgrade.'); + this.logService.info('[AutoSync] Turned off sync because current client is not compatible with server. Requires client upgrade.'); } // Incompatible Local Content else if (userDataSyncError.code === UserDataSyncErrorCode.IncompatibleLocalContent) { await this.turnOff(false, true /* force soft turnoff on error */); - this.logService.info(`Auto Sync: Turned off sync because server has ${userDataSyncError.resource} content with newer version than of client. Requires client upgrade.`); + this.logService.info(`[AutoSync] Turned off sync because server has ${userDataSyncError.resource} content with newer version than of client. Requires client upgrade.`); } // Incompatible Remote Content else if (userDataSyncError.code === UserDataSyncErrorCode.IncompatibleRemoteContent) { await this.turnOff(false, true /* force soft turnoff on error */); - this.logService.info(`Auto Sync: Turned off sync because server has ${userDataSyncError.resource} content with older version than of client. Requires server reset.`); + this.logService.info(`[AutoSync] Turned off sync because server has ${userDataSyncError.resource} content with older version than of client. Requires server reset.`); } // Service changed @@ -280,7 +281,7 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto // Then turn off settings sync and ask user to turn on again if (isWeb && userDataSyncError.code === UserDataSyncErrorCode.DefaultServiceChanged && !this.hasProductQualityChanged()) { await this.turnOff(false, true /* force soft turnoff on error */); - this.logService.info('Auto Sync: Turned off sync because default sync service is changed.'); + this.logService.info('[AutoSync] Turned off sync because default sync service is changed.'); } // Service has changed by the user. So turn off and turn on sync. @@ -288,7 +289,7 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto else { await this.turnOff(false, true /* force soft turnoff on error */, true /* do not disable machine */); await this.turnOn(); - this.logService.info('Auto Sync: Sync Service changed. Turned off auto sync, reset local state and turned on auto sync.'); + this.logService.info('[AutoSync] Sync Service changed. Turned off auto sync, reset local state and turned on auto sync.'); } } @@ -327,32 +328,35 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto } private sources: string[] = []; - async triggerSync(sources: string[], skipIfSyncedRecently: boolean, disableCache: boolean): Promise { + async triggerSync(sources: string[], options?: SyncOptions): Promise { if (this.autoSync.value === undefined) { return this.syncTriggerDelayer.cancel(); } - if (skipIfSyncedRecently && this.lastSyncTriggerTime - && Math.round((new Date().getTime() - this.lastSyncTriggerTime) / 1000) < 10) { - this.logService.debug('Auto Sync: Skipped. Limited to once per 10 seconds.'); + if (options?.skipIfSyncedRecently && this.lastSyncTriggerTime && new Date().getTime() - this.lastSyncTriggerTime < 15_000) { + this.logService.debug('[AutoSync] Skipping because sync was triggered recently.', sources); return; } this.sources.push(...sources); return this.syncTriggerDelayer.trigger(async () => { - this.logService.trace('activity sources', ...this.sources); + this.logService.trace('[AutoSync] Activity sources', ...this.sources); this.sources = []; if (this.autoSync.value) { - await this.autoSync.value.sync('Activity', disableCache); + await this.autoSync.value.sync('Activity', !!options?.disableCache); } }, this.successiveFailures - ? this.getSyncTriggerDelayTime() * 1 * Math.min(Math.pow(2, this.successiveFailures), 60) /* Delay exponentially until max 1 minute */ - : this.getSyncTriggerDelayTime()); + ? Math.min(this.getSyncTriggerDelayTime() * this.successiveFailures, 60_000) /* Delay linearly until max 1 minute */ + : options?.immediately ? 0 : this.getSyncTriggerDelayTime()); } protected getSyncTriggerDelayTime(): number { - return 2000; /* Debounce for 2 seconds if there are no failures */ + if (this.lastSyncTriggerTime && new Date().getTime() - this.lastSyncTriggerTime > 15_000) { + this.logService.debug('[AutoSync] Sync immediately because last sync was triggered more than 15 seconds ago.'); + return 0; + } + return 10_000; /* Debounce for 10 seconds if there are no failures */ } } @@ -392,11 +396,11 @@ class AutoSync extends Disposable { this._register(toDisposable(() => { if (this.syncPromise) { this.syncPromise.cancel(); - this.logService.info('Auto sync: Cancelled sync that is in progress'); + this.logService.info('[AutoSync] Cancelled sync that is in progress'); this.syncPromise = undefined; } this.syncTask?.stop(); - this.logService.info('Auto Sync: Stopped'); + this.logService.info('[AutoSync] Stopped'); })); this.sync(AutoSync.INTERVAL_SYNCING, false); } @@ -413,7 +417,7 @@ class AutoSync extends Disposable { if (this.syncPromise) { try { // Wait until existing sync is finished - this.logService.debug('Auto Sync: Waiting until sync is finished.'); + this.logService.debug('[AutoSync] Waiting until sync is finished.'); await this.syncPromise; } catch (error) { if (isCancellationError(error)) { @@ -444,7 +448,7 @@ class AutoSync extends Disposable { } private async doSync(reason: string, disableCache: boolean, token: CancellationToken): Promise { - this.logService.info(`Auto Sync: Triggered by ${reason}`); + this.logService.info(`[AutoSync] Triggered by ${reason}`); this._onDidStartSync.fire(); let error: Error | undefined; @@ -455,9 +459,9 @@ class AutoSync extends Disposable { error = e; if (UserDataSyncError.toUserDataSyncError(e).code === UserDataSyncErrorCode.MethodNotFound) { try { - this.logService.info('Auto Sync: Client is making invalid requests. Cleaning up data...'); + this.logService.info('[AutoSync] Client is making invalid requests. Cleaning up data...'); await this.userDataSyncService.cleanUpRemoteData(); - this.logService.info('Auto Sync: Retrying sync...'); + this.logService.info('[AutoSync] Retrying sync...'); await this.createAndRunSyncTask(disableCache, token); error = undefined; } catch (e1) { diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 02824fa34676..1a231715b510 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -601,13 +601,15 @@ export interface IUserDataSyncResourceProviderService { resolveUserDataSyncResource(syncResourceHandle: ISyncResourceHandle): IUserDataSyncResource | undefined; } +export type SyncOptions = { immediately?: boolean; skipIfSyncedRecently?: boolean; disableCache?: boolean }; + export const IUserDataAutoSyncService = createDecorator('IUserDataAutoSyncService'); export interface IUserDataAutoSyncService { _serviceBrand: any; readonly onError: Event; turnOn(): Promise; turnOff(everywhere: boolean): Promise; - triggerSync(sources: string[], hasToLimitSync: boolean, disableCache: boolean): Promise; + triggerSync(sources: string[], options?: SyncOptions): Promise; } export const IUserDataSyncUtilService = createDecorator('IUserDataSyncUtilService'); diff --git a/src/vs/platform/userDataSync/node/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/node/userDataAutoSyncService.ts index f8642d424a42..821040b4bcfb 100644 --- a/src/vs/platform/userDataSync/node/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/node/userDataAutoSyncService.ts @@ -33,7 +33,7 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { this._register(Event.debounce(Event.any( Event.map(nativeHostService.onDidFocusMainWindow, () => 'windowFocus'), Event.map(nativeHostService.onDidOpenMainWindow, () => 'windowOpen'), - ), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, true, false))); + ), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, { skipIfSyncedRecently: true }))); } } diff --git a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts index 9c3164a97b7c..07362eacb024 100644 --- a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts @@ -22,7 +22,7 @@ class TestUserDataAutoSyncService extends UserDataAutoSyncService { protected override getSyncTriggerDelayTime(): number { return 50; } sync(): Promise { - return this.triggerSync(['sync'], false, false); + return this.triggerSync(['sync']); } } @@ -44,7 +44,7 @@ suite('UserDataAutoSyncService', () => { const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Trigger auto sync with settings change - await testObject.triggerSync([SyncResource.Settings], false, false); + await testObject.triggerSync([SyncResource.Settings]); // Filter out machine requests const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`)); @@ -69,7 +69,7 @@ suite('UserDataAutoSyncService', () => { // Trigger auto sync with settings change multiple times for (let counter = 0; counter < 2; counter++) { - await testObject.triggerSync([SyncResource.Settings], false, false); + await testObject.triggerSync([SyncResource.Settings]); } // Filter out machine requests @@ -96,7 +96,7 @@ suite('UserDataAutoSyncService', () => { const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Trigger auto sync with window focus once - await testObject.triggerSync(['windowFocus'], true, false); + await testObject.triggerSync(['windowFocus']); // Filter out machine requests const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`)); @@ -121,7 +121,7 @@ suite('UserDataAutoSyncService', () => { // Trigger auto sync with window focus multiple times for (let counter = 0; counter < 2; counter++) { - await testObject.triggerSync(['windowFocus'], true, false); + await testObject.triggerSync(['windowFocus'], { skipIfSyncedRecently: true }); } // Filter out machine requests @@ -440,7 +440,7 @@ suite('UserDataAutoSyncService', () => { await testClient.setUp(); const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); - await testObject.triggerSync(['some reason'], true, true); + await testObject.triggerSync(['some reason'], { disableCache: true }); assert.strictEqual(target.requestsWithAllHeaders[0].headers!['Cache-Control'], 'no-cache'); }); }); @@ -454,7 +454,7 @@ suite('UserDataAutoSyncService', () => { await testClient.setUp(); const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); - await testObject.triggerSync(['some reason'], true, false); + await testObject.triggerSync(['some reason']); assert.strictEqual(target.requestsWithAllHeaders[0].headers!['Cache-Control'], undefined); }); }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 4d1acfc51d53..ecb4f7da5738 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -2674,7 +2674,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } else { this.extensionsSyncManagementService.updateIgnoredExtensions(extension.identifier.id, !isIgnored); } - await this.userDataAutoSyncService.triggerSync(['IgnoredExtensionsUpdated'], false, false); + await this.userDataAutoSyncService.triggerSync(['IgnoredExtensionsUpdated']); } async toggleApplyExtensionToAllProfiles(extension: IExtension): Promise { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts index 0e7f57554aee..b775cd53968d 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts @@ -39,9 +39,9 @@ export class UserDataSyncTrigger extends Disposable implements IWorkbenchContrib Event.map(hostService.onDidChangeFocus, () => 'windowFocus'), Event.map(event, source => source!), ), (last, source) => last ? [...last, source] : [source], 1000) - (sources => userDataAutoSyncService.triggerSync(sources, true, false))); + (sources => userDataAutoSyncService.triggerSync(sources, { skipIfSyncedRecently: true }))); } else { - this._register(event(source => userDataAutoSyncService.triggerSync([source!], true, false))); + this._register(event(source => userDataAutoSyncService.triggerSync([source!], { skipIfSyncedRecently: true }))); } } diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index ea27f81b3bdf..b80dbf6c2b06 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -365,7 +365,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } syncNow(): Promise { - return this.userDataAutoSyncService.triggerSync(['Sync Now'], false, true); + return this.userDataAutoSyncService.triggerSync(['Sync Now'], { immediately: true, disableCache: true }); } private async doTurnOnSync(token: CancellationToken): Promise { diff --git a/src/vs/workbench/services/userDataSync/electron-sandbox/userDataAutoSyncService.ts b/src/vs/workbench/services/userDataSync/electron-sandbox/userDataAutoSyncService.ts index 57065b450eee..5c265c0ee654 100644 --- a/src/vs/workbench/services/userDataSync/electron-sandbox/userDataAutoSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-sandbox/userDataAutoSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataAutoSyncService, UserDataSyncError } from '../../../../platform/userDataSync/common/userDataSync.js'; +import { IUserDataAutoSyncService, SyncOptions, UserDataSyncError } from '../../../../platform/userDataSync/common/userDataSync.js'; import { ISharedProcessService } from '../../../../platform/ipc/electron-sandbox/services.js'; import { IChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { Event } from '../../../../base/common/event.js'; @@ -22,8 +22,8 @@ class UserDataAutoSyncService implements IUserDataAutoSyncService { this.channel = sharedProcessService.getChannel('userDataAutoSync'); } - triggerSync(sources: string[], hasToLimitSync: boolean, disableCache: boolean): Promise { - return this.channel.call('triggerSync', [sources, hasToLimitSync, disableCache]); + triggerSync(sources: string[], options?: SyncOptions): Promise { + return this.channel.call('triggerSync', [sources, options]); } turnOn(): Promise { From 94c2ff587ae24dff8d6fcbe3253e380dfa94f939 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 29 Jan 2025 14:13:00 -0800 Subject: [PATCH 1013/3587] Use more clear label for markdown validation item --- .../src/languageFeatures/diagnostics.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts index 8ea0cac41c12..48e579da7fce 100644 --- a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts @@ -91,7 +91,7 @@ function registerMarkdownStatusItem(selector: vscode.DocumentSelector, commandMa const enabled = vscode.workspace.getConfiguration('markdown', markdownDoc).get(enabledSettingId); if (enabled) { - statusItem.text = vscode.l10n.t('Link validation enabled'); + statusItem.text = vscode.l10n.t('Markdown link validation enabled'); statusItem.command = { command: commandId, arguments: [false], @@ -99,7 +99,7 @@ function registerMarkdownStatusItem(selector: vscode.DocumentSelector, commandMa tooltip: vscode.l10n.t('Disable validation of Markdown links'), }; } else { - statusItem.text = vscode.l10n.t('Link validation disabled'); + statusItem.text = vscode.l10n.t('Markdown link validation disabled'); statusItem.command = { command: commandId, arguments: [true], From 61a9d60dab9d4eb0e8635fe1b9bfe1f2cf357b1f Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Wed, 29 Jan 2025 14:26:30 -0800 Subject: [PATCH 1014/3587] uses common variable ID for the #file: variables that reference prompts (#239150) [prompt]: use common variable ID for the #file: use cases --- .../chatInstructionAttachmentsModel.ts | 2 +- .../contrib/chat/browser/chatInputPart.ts | 23 ++++++++++--------- .../chatDynamicVariables/chatFileReference.ts | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts index b0bc196ebc46..f4c1c40284d8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionAttachmentsModel.ts @@ -26,7 +26,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com * @param isRoot If the reference is the root reference in the references tree. * This object most likely was explicitly attached by the user. */ -const toChatVariable = ( +export const toChatVariable = ( reference: Pick, isRoot: boolean, ): IChatRequestVariableEntry => { diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 0bb471e47d8d..4ff156d53410 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -94,6 +94,7 @@ import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAt import { InstructionAttachmentsWidget } from './attachments/instructionsAttachment/instructionAttachments.js'; import { IChatWidget } from './chat.js'; import { ChatAttachmentModel, EditsAttachmentModel } from './chatAttachmentModel.js'; +import { toChatVariable } from './chatAttachmentModel/chatInstructionAttachmentsModel.js'; import { hookUpResourceAttachmentDragAndContextMenu, hookUpSymbolAttachmentDragAndContextMenu } from './chatContentParts/chatAttachmentsContentPart.js'; import { IDisposableReference } from './chatContentParts/chatCollections.js'; import { CollapsibleListPool, IChatCollapsibleListItem } from './chatContentParts/chatReferencesContentPart.js'; @@ -193,17 +194,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge continue; } - for (const childUri of variable.allValidReferencesUris) { - contextArr.push({ - id: variable.id, - name: basename(childUri.path), - value: childUri, - isSelection: false, - enabled: true, - isFile: true, - isDynamic: true, - }); - } + // the usual URIs list of prompt instructions is `bottom-up`, therefore + // we do the same here - first add all child references to the list + contextArr.push( + ...variable.allValidReferences.map((link) => { + return toChatVariable(link, false); + }), + ); + // then add the root reference itself + contextArr.push( + toChatVariable(variable, true), + ); } contextArr diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts index 9b225f8c1897..d4a4574aeace 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts @@ -7,8 +7,8 @@ import { URI } from '../../../../../../base/common/uri.js'; import { assert } from '../../../../../../base/common/assert.js'; import { IDynamicVariable } from '../../../common/chatVariables.js'; import { IRange } from '../../../../../../editor/common/core/range.js'; -import { FilePromptParser } from '../../../common/promptSyntax/parsers/filePromptParser.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { FilePromptParser } from '../../../common/promptSyntax/parsers/filePromptParser.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; From 99d5479e35a3cb7f6c939a5229dc7d9bff9ad0ec Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 29 Jan 2025 14:31:17 -0800 Subject: [PATCH 1015/3587] Use `getTitleBarStyle` to fix drag not going to top corner when using native tabs (#239143) Fixes https://github.com/microsoft/vscode/issues/238941 --- src/vs/platform/quickinput/browser/quickInputController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 27061cd9d817..01ef4e4e0915 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -31,7 +31,7 @@ import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js'; import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { Platform, platform } from '../../../base/common/platform.js'; -import { TitleBarSetting, TitlebarStyle } from '../../window/common/window.js'; +import { getTitleBarStyle, TitlebarStyle } from '../../window/common/window.js'; import { getZoomFactor } from '../../../base/browser/browser.js'; const $ = dom.$; @@ -919,7 +919,7 @@ class QuickInputDragAndDropController extends Disposable { @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); - const customTitleBar = this.configurationService.getValue(TitleBarSetting.TITLE_BAR_STYLE) === TitlebarStyle.CUSTOM; + const customTitleBar = getTitleBarStyle(this.configurationService) === TitlebarStyle.CUSTOM; // Do not allow the widget to overflow or underflow window controls. // Use CSS calculations to avoid having to force layout with `.clientWidth` From de90eca1183bb2b4a27141dd55159e4b415c2de4 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 29 Jan 2025 16:17:40 -0800 Subject: [PATCH 1016/3587] Update edits agent mode welcome view message (#239158) Add "agent mode" subtitle --- .../chat/browser/media/chatViewWelcome.css | 17 +++++++++++------ .../viewsWelcome/chatViewWelcomeController.ts | 8 +++++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css index 657d1f5869a7..15cbada5e3d5 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css @@ -50,13 +50,18 @@ div.chat-welcome-view { text-align: center; } - & > .chat-welcome-view-indicator { - color: var(--vscode-badge-foreground); - background: var(--vscode-badge-background); - padding: 1px 8px; - border-radius: 14px; + & > .chat-welcome-view-indicator-container { + display: flex; margin-top: 10px; - font-size: 11px; + gap: 9px; + + & > .chat-welcome-view-indicator { + font-size: 11px; + color: var(--vscode-badge-foreground); + background: var(--vscode-badge-background); + padding: 1px 8px; + border-radius: 14px; + } } & > .chat-welcome-view-message { diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index 1975aba545fd..9fe96562bba2 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -148,9 +148,11 @@ export class ChatViewWelcomePart extends Disposable { // Preview indicator if (options?.location === ChatAgentLocation.EditingSession && typeof content.message !== 'function' && chatAgentService.toolsAgentModeEnabled) { // Override welcome message for the agent. Sort of a hack, should it come from the participant? This case is different because the welcome content typically doesn't change per ChatWidget - content.message = new MarkdownString('Ask Copilot to edit your files in agent mode. Copilot will automatically use multiple requests to pick files to edit, run terminal commands, and iterate on errors.\n\nCopilot is powered by AI, so mistakes are possible. Review output carefully before use.'); - const featureIndicator = dom.append(this.element, $('.chat-welcome-view-indicator')); - featureIndicator.textContent = localize('experimental', "EXPERIMENTAL"); + const agentMessage = localize({ key: 'agentMessage', comment: ['{Locked="["}', '{Locked="]({0})"}'] }, "Ask Copilot to edit your files in [agent mode]({0}). Copilot will automatically use multiple requests to pick files to edit, run terminal commands, and iterate on errors.\n\nCopilot is powered by AI, so mistakes are possible. Review output carefully before use.", 'https://aka.ms/vscode-copilot-agent'); + content.message = new MarkdownString(agentMessage); + const container = dom.append(this.element, $('.chat-welcome-view-indicator-container')); + dom.append(container, $('.chat-welcome-view-subtitle', undefined, localize('agentModeSubtitle', "Agent Mode"))); + dom.append(container, $('.chat-welcome-view-indicator', undefined, localize('experimental', "EXPERIMENTAL"))); } // Message From f297a74b4778302a000937289bf41bd2b036c380 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 30 Jan 2025 02:49:50 +0100 Subject: [PATCH 1017/3587] fix #238992 (#239162) --- .../browser/extensions.contribution.ts | 48 ++++++++++++++--- .../common/extensionManagement.ts | 9 +++- .../common/extensionManagementService.ts | 52 ++++++++++++++----- .../test/browser/workbenchTestServices.ts | 4 +- 4 files changed, 89 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index e65cd1903cde..bfa6ded54f1d 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -8,8 +8,8 @@ import { KeyMod, KeyCode } from '../../../../base/common/keyCodes.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuRegistry, MenuId, registerAction2, Action2, IMenuItem, IAction2Options } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApiConfigKey, AllowedExtensionsConfigKey, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; -import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; +import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApiConfigKey, AllowedExtensionsConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { EnablementState, IExtensionManagementServerService, IPublisherInfo, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; @@ -1871,6 +1871,39 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi run: () => runAction(this.instantiationService.createInstance(ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceRecommendedExtensionsAction.ID, ConfigureWorkspaceRecommendedExtensionsAction.LABEL)) }); + this.registerExtensionAction({ + id: 'workbench.extensions.action.manageTrustedPublishers', + title: localize2('workbench.extensions.action.manageTrustedPublishers', "Manage Trusted Extensions Publishers"), + category: EXTENSIONS_CATEGORY, + f1: true, + run: async (accessor: ServicesAccessor) => { + const quickInputService = accessor.get(IQuickInputService); + const extensionManagementService = accessor.get(IWorkbenchExtensionManagementService); + const trustedPublishers = extensionManagementService.getTrustedPublishers(); + const trustedPublisherItems = trustedPublishers.map(publisher => ({ + id: publisher.publisher, + label: publisher.publisherDisplayName, + description: publisher.publisher, + picked: true, + })).sort((a, b) => a.label.localeCompare(b.label)); + const result = await quickInputService.pick(trustedPublisherItems, { + canPickMany: true, + title: localize('trustedPublishers', "Manage Trusted Extensions Publishers"), + placeHolder: localize('trustedPublishersPlaceholder', "Choose which publishers to trust"), + }); + if (result) { + const untrustedPublishers = []; + for (const { publisher } of trustedPublishers) { + if (!result.some(r => r.id === publisher)) { + untrustedPublishers.push(publisher); + } + } + trustedPublishers.filter(publisher => !result.some(r => r.id === publisher.publisher)); + extensionManagementService.untrustPublishers(...untrustedPublishers); + } + } + }); + } private registerExtensionAction(extensionActionOptions: IExtensionActionOptions): IDisposable { @@ -1924,18 +1957,17 @@ class TrustedPublishersInitializer implements IWorkbenchContribution { constructor( @IWorkbenchExtensionManagementService extensionManagementService: IWorkbenchExtensionManagementService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, - @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, @IProductService productService: IProductService, @IStorageService storageService: IStorageService, ) { - const trustedPublishersInitStatusKey = 'trusted-publishers-migration'; + const trustedPublishersInitStatusKey = 'trusted-publishers-init-migration'; if (!storageService.get(trustedPublishersInitStatusKey, StorageScope.APPLICATION)) { for (const profile of userDataProfilesService.profiles) { extensionManagementService.getInstalled(ExtensionType.User, profile.extensionsResource) .then(async extensions => { - const trustedPublishers = new Set(); + const trustedPublishers = new Map(); for (const extension of extensions) { - if (!extension.publisherId) { + if (!extension.publisherDisplayName) { continue; } const publisher = extension.manifest.publisher.toLowerCase(); @@ -1943,10 +1975,10 @@ class TrustedPublishersInitializer implements IWorkbenchContribution { || (extension.publisherDisplayName && productService.trustedExtensionPublishers?.includes(extension.publisherDisplayName.toLowerCase()))) { continue; } - trustedPublishers.add(publisher); + trustedPublishers.set(publisher, { publisher, publisherDisplayName: extension.publisherDisplayName }); } if (trustedPublishers.size) { - extensionManagementService.trustPublishers(...trustedPublishers); + extensionManagementService.trustPublishers(...trustedPublishers.values()); } storageService.store(trustedPublishersInitStatusKey, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE); }); diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index a3a3740bc835..9c6c53338b17 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -63,6 +63,11 @@ export interface IWorkbenchInstallOptions extends InstallOptions { servers?: IExtensionManagementServer[]; } +export interface IPublisherInfo { + readonly publisher: string; + readonly publisherDisplayName: string; +} + export const IWorkbenchExtensionManagementService = refineServiceDecorator(IProfileAwareExtensionManagementService); export interface IWorkbenchExtensionManagementService extends IProfileAwareExtensionManagementService { readonly _serviceBrand: undefined; @@ -95,7 +100,9 @@ export interface IWorkbenchExtensionManagementService extends IProfileAwareExten requestPublisherTrust(extensions: InstallExtensionInfo[]): Promise; isPublisherTrusted(extension: IGalleryExtension): boolean; - trustPublishers(...publishers: string[]): void; + getTrustedPublishers(): IPublisherInfo[]; + trustPublishers(...publishers: IPublisherInfo[]): void; + untrustPublishers(...publishers: string[]): void; } export const enum EnablementState { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 6b1bc2d48e58..21b23d576762 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -14,7 +14,7 @@ import { IAllowedExtensionsService, EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT, } from '../../../../platform/extensionManagement/common/extensionManagement.js'; -import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IResourceExtension, IWorkbenchExtensionManagementService, IWorkbenchInstallOptions, UninstallExtensionOnServerEvent } from './extensionManagement.js'; +import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IPublisherInfo, IResourceExtension, IWorkbenchExtensionManagementService, IWorkbenchInstallOptions, UninstallExtensionOnServerEvent } from './extensionManagement.js'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from '../../../../platform/extensions/common/extensions.js'; import { URI } from '../../../../base/common/uri.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; @@ -49,6 +49,7 @@ import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlCon import { joinPath } from '../../../../base/common/resources.js'; import { verifiedPublisherIcon } from './extensionsIcons.js'; import { Codicon } from '../../../../base/common/codicons.js'; +import { IStringDictionary } from '../../../../base/common/collections.js'; const TrustedPublishersStorageKey = 'extensions.trustedPublishers'; @@ -174,14 +175,14 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } this._register(this.onProfileAwareDidInstallExtensions(results => { - const untrustedPublishers = new Set(); + const untrustedPublishers = new Map(); for (const result of results) { if (result.local && result.source && !URI.isUri(result.source) && !this.isPublisherTrusted(result.source)) { - untrustedPublishers.add(result.source.publisher); + untrustedPublishers.set(result.source.publisher, { publisher: result.source.publisher, publisherDisplayName: result.source.publisherDisplayName }); } } if (untrustedPublishers.size) { - this.trustPublishers(...untrustedPublishers); + this.trustPublishers(...untrustedPublishers.values()); } })); } @@ -855,7 +856,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench label: allPublishers.length > 1 ? localize({ key: 'trust publishers and install', comment: ['&& denotes a mnemonic'] }, "Trust Publishers & &&Install") : localize({ key: 'trust and install', comment: ['&& denotes a mnemonic'] }, "Trust Publisher & &&Install"), run: () => { this.telemetryService.publicLog2('extensions:trustPublisher', { action: 'trust', extensionId: untrustedExtensions.map(e => e.identifier.id).join(',') }); - this.trustPublishers(...allPublishers.map(p => p.publisher)); + this.trustPublishers(...allPublishers.map(p => ({ publisher: p.publisher, publisherDisplayName: p.publisherDisplayName }))); } }; @@ -1151,18 +1152,41 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } private isPublisherUserTrusted(publisher: string): boolean { - const trustedPublishers = this.storageService.getObject(TrustedPublishersStorageKey, StorageScope.APPLICATION, []).map(p => p.toLowerCase()); - this.logService.debug('Trusted publishers', trustedPublishers); - return trustedPublishers.includes(publisher); + const trustedPublishers = this.getTrustedPublishersFromStorage(); + return !!trustedPublishers[publisher]; } - trustPublishers(...publishers: string[]): void { - const trustedPublishers = this.storageService.getObject(TrustedPublishersStorageKey, StorageScope.APPLICATION, []).map(p => p.toLowerCase()); - publishers = publishers.map(p => p.toLowerCase()).filter(p => !trustedPublishers.includes(p)); - if (publishers.length) { - trustedPublishers.push(...publishers); - this.storageService.store(TrustedPublishersStorageKey, trustedPublishers, StorageScope.APPLICATION, StorageTarget.USER); + getTrustedPublishers(): IPublisherInfo[] { + const trustedPublishers = this.getTrustedPublishersFromStorage(); + return Object.keys(trustedPublishers).map(publisher => trustedPublishers[publisher]); + } + + trustPublishers(...publishers: IPublisherInfo[]): void { + const trustedPublishers = this.getTrustedPublishersFromStorage(); + for (const publisher of publishers) { + trustedPublishers[publisher.publisher.toLowerCase()] = publisher; + } + this.storageService.store(TrustedPublishersStorageKey, JSON.stringify(trustedPublishers), StorageScope.APPLICATION, StorageTarget.USER); + } + + untrustPublishers(...publishers: string[]): void { + const trustedPublishers = this.getTrustedPublishersFromStorage(); + for (const publisher of publishers) { + delete trustedPublishers[publisher.toLowerCase()]; } + this.storageService.store(TrustedPublishersStorageKey, JSON.stringify(trustedPublishers), StorageScope.APPLICATION, StorageTarget.USER); + } + + private getTrustedPublishersFromStorage(): IStringDictionary { + const trustedPublishers = this.storageService.getObject>(TrustedPublishersStorageKey, StorageScope.APPLICATION, {}); + if (Array.isArray(trustedPublishers)) { + this.storageService.remove(TrustedPublishersStorageKey, StorageScope.APPLICATION); + return {}; + } + return Object.keys(trustedPublishers).reduce>((result, publisher) => { + result[publisher.toLowerCase()] = trustedPublishers[publisher]; + return result; + }, {}); } } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 04572268c03f..463276cd547b 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -2249,7 +2249,9 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens resetPinnedStateForAllUserExtensions(pinned: boolean): Promise { throw new Error('Method not implemented.'); } getInstallableServers(extension: IGalleryExtension): Promise { throw new Error('Method not implemented.'); } isPublisherTrusted(extension: IGalleryExtension): boolean { return false; } - trustPublishers(...publishers: string[]): void { } + getTrustedPublishers() { return []; } + trustPublishers(): void { } + untrustPublishers(): void { } async requestPublisherTrust(extensions: InstallExtensionInfo[]): Promise { } } From 0685260a033a37548a36c3da19d47bbdcb468329 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 30 Jan 2025 02:50:35 +0100 Subject: [PATCH 1018/3587] fix #238998 (#239163) --- .../workbench/contrib/output/browser/output.contribution.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index b1cbad16c0d9..63e3d0de084c 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -188,7 +188,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { super({ id: 'workbench.action.output.addCompoundLog', title: nls.localize2('addCompoundLog', "Add Compound Log..."), - category: Categories.Developer, + category: nls.localize2('output', "Output"), f1: true, menu: [{ id: MenuId.ViewTitle, @@ -235,7 +235,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { super({ id: 'workbench.action.output.remove', title: nls.localize2('removeLog', "Remove Output..."), - category: Categories.Developer, + category: nls.localize2('output', "Output"), f1: true }); } From 71090984a5156a26675182714274aaf9bc02ef11 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 30 Jan 2025 02:51:09 +0100 Subject: [PATCH 1019/3587] fix #238938 (#239164) --- .../common/extensionManagementService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 21b23d576762..e0420986b016 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -903,7 +903,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench customMessage.appendMarkdown(localize('multiInstallMessage', "This is the first time you're installing extensions from publishers {0} and {1}.", allPublishers.slice(0, allPublishers.length - 1).map(p => getPublisherLink(p)).join(', '), getPublisherLink(allPublishers[allPublishers.length - 1]))); } - if (verifiedPublishers.length) { + if (verifiedPublishers.length || unverfiiedPublishers.length === 1) { for (const publisher of verifiedPublishers) { customMessage.appendText('\n'); const publisherVerifiedMessage = localize('verifiedPublisherWithName', "{0} has verified ownership of {1}.", getPublisherLink(publisher), `[${URI.parse(publisher.publisherDomain!.link).authority}](${publisher.publisherDomain!.link})`); @@ -912,14 +912,14 @@ export class ExtensionManagementService extends Disposable implements IWorkbench if (unverfiiedPublishers.length) { customMessage.appendText('\n'); if (unverfiiedPublishers.length === 1) { - customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublisherWithName', "{0} is **not** [verified]({1}).", getPublisherLink(unverfiiedPublishers[0]), unverifiedLink)}`); + customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublisherWithName', "{0} is [**not** verified]({1}).", getPublisherLink(unverfiiedPublishers[0]), unverifiedLink)}`); } else { - customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublishers', "{0} and {1} are **not** [verified]({2}).", unverfiiedPublishers.slice(0, unverfiiedPublishers.length - 1).map(p => getPublisherLink(p)).join(', '), getPublisherLink(unverfiiedPublishers[unverfiiedPublishers.length - 1]), unverifiedLink)}`); + customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('unverifiedPublishers', "{0} and {1} are [**not** verified]({2}).", unverfiiedPublishers.slice(0, unverfiiedPublishers.length - 1).map(p => getPublisherLink(p)).join(', '), getPublisherLink(unverfiiedPublishers[unverfiiedPublishers.length - 1]), unverifiedLink)}`); } } } else { customMessage.appendText('\n'); - customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('allUnverifed', "All publishers are **not** [verified]({0}).", unverifiedLink)}`); + customMessage.appendMarkdown(`$(${Codicon.unverified.id}) ${localize('allUnverifed', "All publishers are [**not** verified]({0}).", unverifiedLink)}`); } customMessage.appendText('\n'); From 6fcd6fe6de80b1a173854c1d0aeda1d92a2573d4 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 30 Jan 2025 02:59:29 +0100 Subject: [PATCH 1020/3587] fix #238029 (#239165) --- .../contrib/extensions/browser/media/extensionsViewlet.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index 2420cc4c2b5b..266478a38159 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -9,7 +9,7 @@ } .extensions-viewlet .hidden { - display: none; + display: none !important; visibility: hidden; } From 92fb591f1b8f26539a80c5269fa6fb6f0b9499ee Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 30 Jan 2025 10:30:58 +0800 Subject: [PATCH 1021/3587] fix: add number of files to working set aria label (#239167) --- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 4ff156d53410..4dd9eb3b703a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -1311,8 +1311,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } else { suggestedFilesInWorkingSetCount = entries.filter(e => e.kind === 'reference' && e.state === WorkingSetEntryState.Suggested).length; } - overviewTitle.ariaLabel = overviewTitle.textContent; - overviewTitle.tabIndex = 0; if (excludedEntries.length > 0) { overviewFileCount.textContent = ' ' + localize('chatEditingSession.excludedFiles', '({0}/{1} files)', this.chatEditingService.editingSessionFileLimit + excludedEntries.length, this.chatEditingService.editingSessionFileLimit); @@ -1321,6 +1319,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge overviewFileCount.textContent = ' ' + (fileCount === 1 ? localize('chatEditingSession.oneFile', '(1 file)') : localize('chatEditingSession.manyFiles', '({0} files)', fileCount)); } + overviewTitle.ariaLabel = overviewWorkingSet.textContent + overviewFileCount.textContent; + overviewTitle.tabIndex = 0; + const fileLimitReached = remainingFileEntriesBudget <= 0; overviewFileCount.classList.toggle('file-limit-reached', fileLimitReached); if (fileLimitReached) { From d9ea0ed4de672951418709eeb55d349cdd77cd1d Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 29 Jan 2025 20:35:56 -0800 Subject: [PATCH 1022/3587] Set up maxRequests setting to be controlled by separate experiments for free vs pro users (#239170) --- .../contrib/chat/browser/chatSetup.ts | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 4ab7a2eb6505..7b48a7c35f95 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -68,6 +68,7 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigu import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; import { equalsIgnoreCase } from '../../../../base/common/strings.js'; +import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -120,6 +121,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @ICommandService private readonly commandService: ICommandService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, ) { super(); @@ -324,23 +326,29 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr let lastNode: IConfigurationNode | undefined; const registerSetting = () => { - const node: IConfigurationNode = { - id: 'chatSidebar', - title: localize('interactiveSessionConfigurationTitle', "Chat"), - type: 'object', - properties: { - 'chat.agent.maxRequests': { - type: 'number', - description: localize('chat.agent.maxRequests', "The maximum number of requests to allow Copilot Edits to use in agent mode."), - default: context.state.entitlement === ChatEntitlement.Limited ? 5 : 15, - tags: ['experimental', 'onExp'] - }, - } - }; - configurationRegistry.updateConfigurations({ remove: lastNode ? [lastNode] : [], add: [node] }); - lastNode = node; + const treatmentId = context.state.entitlement === ChatEntitlement.Limited ? + 'chatAgentMaxRequestsFree' : + 'chatAgentMaxRequestsPro'; + this.experimentService.getTreatment(treatmentId).then(value => { + const defaultValue = value ?? (context.state.entitlement === ChatEntitlement.Limited ? 5 : 15); + const node: IConfigurationNode = { + id: 'chatSidebar', + title: localize('interactiveSessionConfigurationTitle', "Chat"), + type: 'object', + properties: { + 'chat.agent.maxRequests': { + type: 'number', + description: localize('chat.agent.maxRequests', "The maximum number of requests to allow Copilot Edits to use in agent mode."), + default: defaultValue, + tags: ['experimental'] + }, + } + }; + configurationRegistry.updateConfigurations({ remove: lastNode ? [lastNode] : [], add: [node] }); + lastNode = node; + }); }; - this._register(Event.runAndSubscribe(context.onDidChange, () => registerSetting())); + this._register(Event.runAndSubscribe(Event.debounce(context.onDidChange, () => { }, 1000), () => registerSetting())); } } From 63c0355e5bc04b124b8600ce42fe30c73b7a5a5e Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 29 Jan 2025 20:59:24 -0800 Subject: [PATCH 1023/3587] Fix flickering when clearing edits session (#239171) This layout code needs to be cleaned up. Basically the view renders without the working set for an instant before the new model is initialized, then the working set comes back. The welcome view tries to center itself vertically, that's where the flicker comes from. I already fixed the same issue for followups in the chat view, we reserve some space for them. Applying the same to the working set's height. --- .../workbench/contrib/chat/browser/chatInputPart.ts | 6 ++++++ src/vs/workbench/contrib/chat/browser/chatWidget.ts | 13 +++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 4dd9eb3b703a..a6406dff79a8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -256,6 +256,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this._followupsHeight; } + private _editSessionWidgetHeight: number = 0; + get editSessionWidgetHeight() { + return this._editSessionWidgetHeight; + } + private _inputEditor!: CodeEditorWidget; private _inputEditorElement!: HTMLElement; @@ -1514,6 +1519,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._inputPartHeight = data.inputPartVerticalPadding + data.followupsHeight + inputEditorHeight + data.inputEditorBorder + data.attachmentsHeight + data.toolbarsHeight + data.chatEditingStateHeight; this._followupsHeight = data.followupsHeight; + this._editSessionWidgetHeight = data.chatEditingStateHeight; const initialEditorScrollWidth = this._inputEditor.getScrollWidth(); const newEditorWidth = width - data.inputPartHorizontalPadding - data.editorBorder - data.inputPartHorizontalPaddingInside - data.toolbarsWidth - data.sideToolbarWidth; diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 92f03d8e58b8..58480d70b207 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1208,10 +1208,15 @@ export class ChatWidget extends Disposable implements IChatWidget { this.tree.layout(listHeight, width); this.tree.getHTMLElement().style.height = `${listHeight}px`; - // Push the welcome message down so it doesn't change position when followups appear - const followupsOffset = this.viewOptions.renderFollowups ? Math.max(100 - this.inputPart.followupsHeight, 0) : 0; - this.welcomeMessageContainer.style.height = `${listHeight - followupsOffset}px`; - this.welcomeMessageContainer.style.paddingBottom = `${followupsOffset}px`; + // Push the welcome message down so it doesn't change position when followups or working set appear + let extraOffset: number = 0; + if (this.viewOptions.renderFollowups) { + extraOffset = Math.max(100 - this.inputPart.followupsHeight, 0); + } else if (this.viewOptions.enableWorkingSet) { + extraOffset = Math.max(100 - this.inputPart.editSessionWidgetHeight, 0); + } + this.welcomeMessageContainer.style.height = `${listHeight - extraOffset}px`; + this.welcomeMessageContainer.style.paddingBottom = `${extraOffset}px`; this.renderer.layout(width); const lastItem = this.viewModel?.getItems().at(-1); From 98ef4c3e7adaa2792d26b2a184629c977bc8b914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 30 Jan 2025 10:15:12 +0100 Subject: [PATCH 1024/3587] fixes #222504 (#239179) --- build/win32/code.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/win32/code.iss b/build/win32/code.iss index 2f73942ba09d..a59d7c047f5a 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -916,7 +916,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.plist\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.plist\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.plist"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Properties file}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\plist.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles From fbcc9292e75b2caf57823b13e1923b4dd8520b8a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 30 Jan 2025 10:32:23 +0100 Subject: [PATCH 1025/3587] setup - remove unused params (#239181) --- src/vs/workbench/browser/parts/dialogs/dialogHandler.ts | 3 +-- src/vs/workbench/contrib/chat/browser/chatQuotasService.ts | 1 - src/vs/workbench/contrib/chat/browser/chatSetup.ts | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index 039233d3dbe1..b80950387c84 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts @@ -20,7 +20,6 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; import { defaultButtonStyles, defaultCheckboxStyles, defaultDialogStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { ResultKind } from '../../../../platform/keybinding/common/keybindingResolver.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; export class BrowserDialogHandler extends AbstractDialogHandler { @@ -106,7 +105,7 @@ export class BrowserDialogHandler extends AbstractDialogHandler { } } - private async doShow(type: Severity | DialogType | undefined, message: string, buttons?: string[], detail?: string, cancelId?: number, checkbox?: ICheckbox, inputs?: IInputElement[], customOptions?: ICustomDialogOptions, cancellationToken?: CancellationToken): Promise { + private async doShow(type: Severity | DialogType | undefined, message: string, buttons?: string[], detail?: string, cancelId?: number, checkbox?: ICheckbox, inputs?: IInputElement[], customOptions?: ICustomDialogOptions): Promise { const dialogDisposables = new DisposableStore(); const renderBody = customOptions ? (parent: HTMLElement) => { diff --git a/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts b/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts index 0da5b5a3cfa5..65d287a23f30 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts @@ -139,7 +139,6 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService }, ], custom: { - closeOnLinkClick: true, icon: Codicon.copilotWarningLarge, markdownDetails: [ { markdown: new MarkdownString(message, true) }, diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 7b48a7c35f95..a67ceb16865f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -137,7 +137,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr const controller = new Lazy(() => this._register(this.instantiationService.createInstance(ChatSetupController, context, requests))); this.registerChatWelcome(controller, context); - this.registerActions(controller, context, requests); + this.registerActions(context, requests); this.registerUrlLinkHandler(); this.registerSetting(context); } @@ -151,7 +151,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr }); } - private registerActions(controller: Lazy, context: ChatSetupContext, requests: ChatSetupRequests): void { + private registerActions(context: ChatSetupContext, requests: ChatSetupRequests): void { const chatSetupTriggerContext = ContextKeyExpr.or( ChatContextKeys.Setup.installed.negate(), From 46ed85145cfff3c31e937d3bf20a4add9d9c1a6f Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 30 Jan 2025 11:27:24 +0100 Subject: [PATCH 1026/3587] Lazy ftw, fixes https://github.com/microsoft/vscode/issues/239174 (#239189) --- .../contrib/chat/browser/chatEditorOverlay.ts | 16 ++-- .../browser/inlineChatController2.ts | 94 ++++++++++--------- 2 files changed, 57 insertions(+), 53 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index f420d0883fca..acf876c17c4c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -28,6 +28,7 @@ import { IChatService } from '../common/chatService.js'; import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; import { rcut } from '../../../../base/common/strings.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { Lazy } from '../../../../base/common/lazy.js'; class ChatEditorOverlayWidget implements IOverlayWidget { @@ -330,33 +331,30 @@ export class ChatEditorOverlayController implements IEditorContribution { return editor.getContribution(ChatEditorOverlayController.ID) ?? undefined; } - private readonly _overlayWidget: ChatEditorOverlayWidget; + private readonly _overlayWidget = new Lazy(() => this._instaService.createInstance(ChatEditorOverlayWidget, this._editor)); constructor( private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instaService: IInstantiationService, - ) { - this._overlayWidget = this._instaService.createInstance(ChatEditorOverlayWidget, this._editor); - - } + ) { } dispose(): void { this.hide(); - this._overlayWidget.dispose(); + this._overlayWidget.rawValue?.dispose(); } showRequest(session: IChatEditingSession) { - this._overlayWidget.showRequest(session); + this._overlayWidget.value.showRequest(session); } showEntry(session: IChatEditingSession, activeEntry: IModifiedFileEntry, next: IModifiedFileEntry, indicies: { entryIndex: IObservable; changeIndex: IObservable } ) { - this._overlayWidget.showEntry(session, activeEntry, next, indicies); + this._overlayWidget.value.showEntry(session, activeEntry, next, indicies); } hide() { - this._overlayWidget.hide(); + this._overlayWidget.rawValue?.hide(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts index 688280bb744c..b735b3edcc0e 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts @@ -6,6 +6,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; +import { Lazy } from '../../../../base/common/lazy.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { autorun, autorunWithStore, constObservable, derived, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; @@ -65,49 +66,53 @@ export class InlineChatController2 implements IEditorContribution { const ctxHasSession = CTX_HAS_SESSION.bindTo(contextKeyService); const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); - const location: IChatWidgetLocationOptions = { - location: ChatAgentLocation.Editor, - resolveData: () => { - assertType(this._editor.hasModel()); - - return { - type: ChatAgentLocation.Editor, - selection: this._editor.getSelection(), - document: this._editor.getModel().uri, - wholeRange: this._editor.getSelection(), - }; - } - }; - - // inline chat in notebooks - // check if this editor is part of a notebook editor - // and iff so, use the notebook location but keep the resolveData - // talk about editor data - for (const notebookEditor of this._notebookEditorService.listNotebookEditors()) { - for (const [, codeEditor] of notebookEditor.codeEditors) { - if (codeEditor === this._editor) { - location.location = ChatAgentLocation.Notebook; - break; + const zone = new Lazy(() => { + + + const location: IChatWidgetLocationOptions = { + location: ChatAgentLocation.Editor, + resolveData: () => { + assertType(this._editor.hasModel()); + + return { + type: ChatAgentLocation.Editor, + selection: this._editor.getSelection(), + document: this._editor.getModel().uri, + wholeRange: this._editor.getSelection(), + }; + } + }; + + // inline chat in notebooks + // check if this editor is part of a notebook editor + // and iff so, use the notebook location but keep the resolveData + // talk about editor data + for (const notebookEditor of this._notebookEditorService.listNotebookEditors()) { + for (const [, codeEditor] of notebookEditor.codeEditors) { + if (codeEditor === this._editor) { + location.location = ChatAgentLocation.Notebook; + break; + } } } - } - const zone = this._instaService.createInstance(InlineChatZoneWidget, - location, - { - enableWorkingSet: 'implicit', - // filter: item => isRequestVM(item), - rendererOptions: { - renderCodeBlockPills: true, - renderTextEditsAsSummary: uri => isEqual(uri, _editor.getModel()?.uri) - } - }, - this._editor - ); + const result = this._instaService.createInstance(InlineChatZoneWidget, + location, + { + enableWorkingSet: 'implicit', + rendererOptions: { + renderCodeBlockPills: true, + renderTextEditsAsSummary: uri => isEqual(uri, _editor.getModel()?.uri) + } + }, + this._editor + ); + + result.domNode.classList.add('inline-chat-2'); - zone.domNode.classList.add('inline-chat-2'); + return result; + }); - const overlay = ChatEditorOverlayController.get(_editor)!; const editorObs = observableCodeEditor(_editor); @@ -171,23 +176,24 @@ export class InlineChatController2 implements IEditorContribution { const session = visibleSessionObs.read(r); if (!session) { - zone.hide(); + zone.rawValue?.hide(); _editor.focus(); ctxInlineChatVisible.reset(); } else { ctxInlineChatVisible.set(true); - zone.widget.setChatModel(session.chatModel); - if (!zone.position) { - zone.show(session.initialPosition); + zone.value.widget.setChatModel(session.chatModel); + if (!zone.value.position) { + zone.value.show(session.initialPosition); } - zone.reveal(zone.position!); - zone.widget.focus(); + zone.value.reveal(zone.value.position!); + zone.value.widget.focus(); session.editingSession.getEntry(session.uri)?.autoAcceptController.get()?.cancel(); } })); this._store.add(autorun(r => { + const overlay = ChatEditorOverlayController.get(_editor)!; const session = sessionObs.read(r); const model = editorObs.model.read(r); if (!session || !model) { From 481518aad9900321d244576dd5d63e99f5d8a899 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 30 Jan 2025 11:27:58 +0100 Subject: [PATCH 1027/3587] Apply in Editor: new code is created twice (#239190) Apply in Editor: new code is created twice #239152 --- .../chat/browser/actions/codeBlockOperations.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index f206b6e1065c..2e1339c558ac 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -244,9 +244,9 @@ export class ApplyCodeBlockOperation { private async waitForFirstElement(iterable: AsyncIterable): Promise> { const iterator = iterable[Symbol.asyncIterator](); - const firstResult = await iterator.next(); + let result = await iterator.next(); - if (firstResult.done) { + if (result.done) { return { async *[Symbol.asyncIterator]() { return; @@ -256,8 +256,10 @@ export class ApplyCodeBlockOperation { return { async *[Symbol.asyncIterator]() { - yield firstResult.value; - yield* iterable; + while (!result.done) { + yield result.value; + result = await iterator.next(); + } } }; } From c573a0d2791e785498ce4023b5a55458a5a8cef6 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:51:14 +0100 Subject: [PATCH 1028/3587] Fix rendering gaps in NES display (#239192) fixes https://github.com/microsoft/vscode-copilot/issues/12420 --- .../browser/view/inlineEdits/insertionView.ts | 6 +++--- .../inlineCompletions/browser/view/inlineEdits/view.css | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts index 73539134b370..be0cc7f963bd 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts @@ -176,7 +176,7 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits code2, codeStart2, horizontalScrollOffset, - padding: 2, + padding: 3, borderRadius: 4, }; }).recomputeInitiallyAndOnChange(this._store); @@ -193,8 +193,8 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits const rectangleOverlay = createRectangle( { topLeft: layoutInfo.codeStart1, - width: layoutInfo.code1.x - layoutInfo.codeStart1.x, - height: layoutInfo.code2.y - layoutInfo.code1.y, + width: layoutInfo.code1.x - layoutInfo.codeStart1.x + 1, + height: layoutInfo.code2.y - layoutInfo.code1.y + 1, }, layoutInfo.padding, layoutInfo.borderRadius, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index 747e0c0e50cd..60744b4e378d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -208,6 +208,7 @@ .inlineCompletions-modified-bubble, .inlineCompletions-original-bubble { pointer-events: none; + display: inline-block; } .inlineCompletions-modified-bubble.start { @@ -229,6 +230,7 @@ } background: var(--vscode-inlineEdit-modifiedChangedTextBackground) !important; font-style: normal !important; + display: inline-block !important; } } From 7a1cd85011e8bde3ece00567a8e010aa6a2a5959 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 30 Jan 2025 11:54:20 +0100 Subject: [PATCH 1029/3587] Apply In Editor: Don't require previous inlineChatPreview to complete (#239194) --- .../browser/actions/codeBlockOperations.ts | 37 +++---------------- 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index 2e1339c558ac..00bad0fa2c35 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -101,8 +101,6 @@ type IComputeEditsResult = { readonly editsProposed: boolean; readonly codeMappe export class ApplyCodeBlockOperation { - private inlineChatPreview: InlineChatPreview | undefined; - constructor( @IEditorService private readonly editorService: IEditorService, @ITextFileService private readonly textFileService: ITextFileService, @@ -118,13 +116,6 @@ export class ApplyCodeBlockOperation { } public async run(context: ICodeBlockActionContext): Promise { - if (this.inlineChatPreview && this.inlineChatPreview.isOpen()) { - await this.dialogService.info( - localize('overlap', "Another code change is being previewed. Please apply or discard the pending changes first."), - ); - return; - } - let activeEditorControl = getEditableActiveCodeEditor(this.editorService); if (context.codemapperUri && !isEqual(activeEditorControl?.getModel().uri, context.codemapperUri)) { @@ -150,7 +141,7 @@ export class ApplyCodeBlockOperation { let result: IComputeEditsResult | undefined = undefined; if (activeEditorControl) { - await this.handleTextEditor(activeEditorControl, context); + result = await this.handleTextEditor(activeEditorControl, context); } else { const activeNotebookEditor = getActiveNotebookEditor(this.editorService); if (activeNotebookEditor) { @@ -194,9 +185,9 @@ export class ApplyCodeBlockOperation { this.notify(localize('applyCodeBlock.noCodeMapper', "No code mapper available.")); return undefined; } + let editsProposed = false; const editorToApply = await this.codeEditorService.openCodeEditor({ resource }, codeEditor); - let result = false; if (editorToApply && editorToApply.hasModel()) { const cancellationTokenSource = new CancellationTokenSource(); @@ -210,7 +201,7 @@ export class ApplyCodeBlockOperation { }, () => cancellationTokenSource.cancel() ); - result = await this.applyWithInlinePreview(iterable, editorToApply, cancellationTokenSource); + editsProposed = await this.applyWithInlinePreview(iterable, editorToApply, cancellationTokenSource); } catch (e) { if (!isCancellationError(e)) { this.notify(localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message)); @@ -220,7 +211,7 @@ export class ApplyCodeBlockOperation { } } return { - editsProposed: result, + editsProposed, codeMapper }; } @@ -267,19 +258,7 @@ export class ApplyCodeBlockOperation { private async applyWithInlinePreview(edits: AsyncIterable, codeEditor: IActiveCodeEditor, tokenSource: CancellationTokenSource): Promise { const inlineChatController = InlineChatController.get(codeEditor); if (inlineChatController) { - let isOpen = true; - const promise = inlineChatController.reviewEdits(edits, tokenSource.token); - promise.finally(() => { - isOpen = false; - tokenSource.dispose(); - }); - this.inlineChatPreview = { - promise, - isOpen: () => isOpen, - cancel: () => tokenSource.cancel(), - }; - return true; - + return inlineChatController.reviewEdits(edits, tokenSource.token); } return false; } @@ -301,12 +280,6 @@ export class ApplyCodeBlockOperation { } -type InlineChatPreview = { - isOpen(): boolean; - cancel(): void; - readonly promise: Promise; -}; - function notifyUserAction(chatService: IChatService, context: ICodeBlockActionContext, action: ChatUserAction) { if (isResponseVM(context.element)) { chatService.notifyUserAction({ From 38f08a445a16aacc10a6f3e9a14cc04702fb84bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 30 Jan 2025 12:02:48 +0100 Subject: [PATCH 1030/3587] bump inno_updater (#239198) fixes #239186 --- build/win32/Cargo.lock | 4 ++-- build/win32/Cargo.toml | 2 +- build/win32/inno_updater.exe | Bin 437248 -> 437760 bytes 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/win32/Cargo.lock b/build/win32/Cargo.lock index 6cd09ec75d59..4c169ba0f973 100644 --- a/build/win32/Cargo.lock +++ b/build/win32/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "bitflags" @@ -95,7 +95,7 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "inno_updater" -version = "0.12.0" +version = "0.12.1" dependencies = [ "byteorder", "crc", diff --git a/build/win32/Cargo.toml b/build/win32/Cargo.toml index bee143450e42..e2130dd2bfae 100644 --- a/build/win32/Cargo.toml +++ b/build/win32/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "inno_updater" -version = "0.12.0" +version = "0.12.1" authors = ["Microsoft "] build = "build.rs" diff --git a/build/win32/inno_updater.exe b/build/win32/inno_updater.exe index 5e6f775b70040fac3552035ceac2c365910c05d9..ef7cb934eb637a39ef4246012216e64dc6d5cc68 100644 GIT binary patch literal 437760 zcmeFadwf*Yxj(#z3^M584j44lSb|PlqNs^t4H9*bBt{J}NC;>#qCyNvA;Jt|)i_~K zFw4zQ+Cx2_)_Sn@ZEb5?%8T5zmaQ*&pFMoz+1i?cua8*o zD1CiIMa`@Q*|qcM-8cX4?`Pk8_uRSjeA)NR%%1O`n>}l8cHym)vcErX#>~qG4LZ|f zS3N7|H|C`!;g0ynlUvd89lRI3v1QNY^0|4>Ncn8tGeSOF_FRQ$!*3^Wk!}`Vfjov7^y9Nxxd!MKxcog zb}a*a{#Kr$EzQ@o+F$n9D*7bJ*XR0cnFy@_2jO@Dx9B9|z>5F4qkYMdn*GfA&Q~&!cy0Q3G)){b_G$}; z8{PNnj|)T^-Dm2?Y6RT5jfDkiw?@eA%SC}lglB6tEKyl*O?cI*&1&oF&@{(UeO;va zlYX;4c^FkTe=_*~cW316c`4TUkN*0)tz(Y8g3{WsBl@~Emo8sWq?rXCecjT53|5QL z1=XUopkL^*w=mpURco2 z2%ivLgm`h8EYUk@6Fa#PUDiT{1+jg5yXJvVGzHDl}YN}cvP8y-rJ+i&Pe%0XXVq2i)>cH!{fum^gb$#tl z{e~T)xv|38Z#`Qa)3IKbiVo`vjfvrnvRt!L=Ne4__k`~L!_z_wgqEmk7g*3=}9#FpivRlF*Cnm=_0KF}NA6CKVefj4sl z$AIjZzP3rfAsqTa%=l7oEX)8yJ-8!!U81P!Ut?IjiqfJd@R8ow-FVEM8)&^c5JUMU zeeFK|h7J@KU5(QUPJ|wdJ-9=$Ld%JeGb87OBoTN|Q6+j6B(4X-HVhmaxmL1m?@ql! z+$y?`c1n7JYpt7~v56T#YHwmf2ULnTVt<;7fh0C`oVlKbiT*hEkWMjEQ zt4c5c)CE*XdLTc|Ki;f$npIBoIy0@I$#=$-Si7;=X*}%Ie8Y@~T?6N*nU5Wh4SgE5 zPMNKh$A&(Ikb197yaS#@)lwr(sWirD;tk(9W}UNdVWZBiF(LirgiIzZ%sRRH-nDPv z4)A>*w^0WuhnUy(tthrIBQ|se;H) zNYe*vt=sjr{Wrf=q~&eh$br*({Q;*2WI0+lI7urRckSy=Ev}BYv%FdAahc`LSgp(Y z{&OUrGuZ&rU1mwV?}vr7lgc(Y-jZ>YX@K*8nu zispi8`idr38#uxhb%pUwz|5CDYxpSiWFOMg3q6DJUFsQTKJDS4p`b>I>ksLJOFjAc zF7)J1vVMBt8D^HyGFtes3%FNOC{8W{t1DeGyDSrU-ovH=4y1GMGJrt{I}eX`Bz zvdyVTcghsJBiB^4DuSMXLil}RjVFj;;|fnB9w@QY^CJ}qdT31{s~lZHs_Jl1T4q|m z#-InvT$631f2g%kwor<;AC_(VWZQGhpv0lfGDl|dLFUtuEgeauE`pS$ha2a3GDAx} z0Yu3PM#u_=A$epq_+%6so#wgBn(TLI^_y2BmlUd1j_ARfry5`?%-QOs$;c{I#P<-P z57!D1F&>{8_%J*XpS$o`gikjH#U!H};d@Bgcw&&F?w;S|WO|1bt0dBO=-twCGK8EG+fRAi8P-sLI;< zZ%J%b*x2%W*({W`69|;pdf*Z99>PxXm&h-ifcrI7brK7IzMQ=th8iPJAp^lDuf)M$ zhOh+x-w}`%-{CZ`5q~+k3q#QbbCOFOwf=}cv(0FwDD`*=v8eD&N8xhzLP39)4LuS; z@6q)~NoNaEWd;(-ZT6wXK!U#2>ZmTsfTh4%HwTM#3f zMSI>(N1K8L$df{a1PRw*G~)qeS&WFtxx-P2_r)aBpaW=`>F7^Zgrt3zG5XqVqQz=@ z!l9v@>lf$+%q}SqS7W#~uVo(dE~k09qP~qkn$l;I@)7Grj1ql$mmOe7SY=RE`AtG( z5fGQra-0J&KtZ?I1+Ypwq0%4UhFq#^dHYa!Dqh(!-#Pfw8#wyd{V4TJb+kKLpOxrD zQPh^eM1j+Pp-iA^TaPlRsgyOb4T;-7v!bE0Yk*YB|ArHQGcuY0$^`58!jT{b`_ZiW zF#<&+93Jd!OPCFnFw6TY3$Y(L$|mft8a?-;>8TG65W6H(7%eM|ra4BBGv0aK*ufeB zUL5YAWS#_w>7;%x-Zg$nMgLe zv-F1B5%^r6mxj+RM3&3ESyG!rTD9&QR1Ou7XaDK)xv#le-`#}CoTmA&jBrq7k@c#` z5WGf*z48q_He3K00Fz5!+a?M;mcP-)TZLz&Wj%{$z;hC^Ha#J0#v`5? zn$Mm)CDl|PT|8>0(!~!z@rsdRk*$kM61o`G_%vSztgu?uNOB$o(sY=9ey4y$_;jE7Qis5Gq_E{7=BKjN8|33XX_B@ywoLWHFCAf)!e zXXMKh5PpuBr4jLEEEZCOd5wma(QoS+)Q`G>6wgMwbn>=}7bHnBzSC*(+)$7PWsseJ zGyv@2k@v-`df+b@cIv-89*aGuKfaYi&?mkN6ZjGzTd{kIEHq!J2jQosCGClx2_Os6 z11+x!xbm=rg~&1r)XPQEXttyWFM-^@uX!;{|2W#6>P=0fv=*rLXuom zWPX9D8a%sUYC#i#Lo<(?86Ghw(-#gwz%GpLZl`~Mz7F}KIvR45_ZI`EW{Xy02~3ZrXh+r4yC$cq>7vw{2BL~=;q*jl zQ>G)JueIwOhdM>{E9%)dBL$)}z{A;5Sp6uCU^<~1f*R_-1TZfB@mc5`h%mw?!Ukdv zba?O{#k4xmF6MYDqJ!2WBcZRrgO*Sn51>EsHyM4LQ*Par(zf1k4$2TCC7_tAL8upJ`R-!N21>p{r^lZ{W2cZNZpV?m^RkmSVqIMSbs; z>lt+s-XedJQM%Cpg1vG9qc3FiPw_?e!+37Gz=LKQBq9Uaa3E}sjD&XzqRMiK7NVD% z$jdUPEwGpc675TK@+`EJVS0~)LU0+;$;Czz%GQn=?McKW^oDZ!Y`#me>_Aivo41B` zrAyhp?u4#I8@Ubp*o5yA5GTXDls>f3L(b?KMmXaZ-?_$P$2H#<=G7^h_%G29f#p%~ zRGk7Wq+kq4rZ^MeA;p`T(retv1f`rk&`j};CM~$gfupgQh??V_=2-x=9NM@q($R}@ zV-akb2@E9(CbB%nBG*9O_!DFxvJPJM*5{C~?OX}u8muft~ zdOFbp3b1+9^r77}5=zmYInIg5a)by6f)xnPIS=!jT9-N1Z4tDo$h#TO&^T9Use}@$ z!(C?ly*uQ)!c{)8GPX-}8!+4+%V>z{jjvq+B(9MLiCLQt(?ps;z-Fp7)?+m6NQ7HPRTw0$PuF*;9&W)P%JS4Q~~Az?U$v}di4r>c={cpkdER5A+|(Qa|Ud{I0Dvn9LqFvEr`RzvgVHgmtQ?Szfp{+WAsVa%rHHLj|BObbMefOTu-MKMWK7u@gO|M!Y$$IgN?|U_I0yVK*+S5{<#6I}iOAbDdH|Gs~@nI}EW4(cOV7=k6uh)75 z)=MPNDm}rzaHlJ-w#7jx-C)8ahaklYTu*SNmzc^GF7>5SGLhbn_jt#Hp11Kv_x@XW zYW=LsFLzRHP}TT1d+SAQWD=q=!@eSBLjz?Y-x=jV7Y_LJyhEj-02@Hx*84(jO)J3%_&av>M^&%+u(8fH%!EA zCi6CH%I~FvCIi4WG_kC{qNU-me`WN~;=P<>HfQ?2+Z=ToVP|w8)_qX*ExvE{nU^3V z(Vm0^HAVk|DMz~I`$mG5*qvTkAOLhipw}Gm8=8Py(pe7@31A{3xIg)#glr+SUX!uXUkpXOSlZKMUYDh;S8G!jFffrTb z%G+W)fTp~yc{^*gnXg}9{rFdDn%E0YCA45k)~AdH!X2r?tiF%ft(vIB?r|tyh+V$G z{)(j7=?x8#mdH=>b;_Q0B>DT~o~Cq$oi0SDqeZ8z#`#qU>r;(Si3<5O1t+J(W*mj! zD>vv!St?f;Ll+BCIvp_?s_hH}VLWJgG3pW*5%Ncd4mfhMgZAL zjLz;*p~Gb~yFm9g#F#g@p$KSz+iC!6nq5ZnW+oSU+|k^=ItglUl%3eSl+EH0wna$4 z5!Bp{C%`3dc>ouJWbHl}2Lv`!*z{YcW3f0Fp%UUpyB4hYv|?ff5ZbldBj%|_fV4H9 zc4USc;e%@AptTl@KDHX6;&{LdEkdx+FT&A>&q{8!Ei~BG#H&>B)%raOxsmrU`+&k> zL3?;WcQ~?h3F{H;Dr@d^iW3-&^YL+p+yjiJE+`>a=tk71HPvlThdMB zG?1suXi6UoM~NQHfU$@%wb+IH)6H*UyHB-V*EBXL#l?SFIl4)~nIeh)G(y(4=bZp| zo?4{^nbspxDkk5#*y`}4qG@ugQ8WlXV;-AY`B2iQq1MxK6&J)MRe|$Wsub189I&Ju z$k!kaJVbh;UOrG6*_c!Tf~Yw_JkkjXDZ>USOCuVwDPx7iZcy?t65`{Dxd@wk+_xGZ zb*H^7dDs>g)>=LVtu&%Csb_#5_6<8L2O}*ubopQo&3f?n1mk7TNIYs>;L6!hQDN&R zzm^tRjT6yu{ZEMvU25mQ40%NJwf$8w4<5QPAHF%UEw9P3t$KO2@p2)tqWN52qveGE zGtmarODw3_K)JQ&RcW(uVLGKkqvm{5GadwD>0K zYrhe?!4cf|P*>ns=lnf^W1sl5#zm&j&;pnleZI<|D?sy9{r8oFl3CdfMrL67!s6ju0?+vRrL|F4{lLHw(&CA z1?DvYwg`(`o#MO=G^HX7f&3b+M^ec;`2R>6#fAn!8qigHRMHAtPir)+1jU91Y?56E zq+IJjs%{j5Qnndt>#cID2dhD9oFV_h(M;w663LRH`N$rB0h>H*lBL|7)2MHUE` z9jd^NlTuF!*q)5?h!RV(4*pY0Xgl){mPkolu=dzm91fFGTbL+U%xP_y13F@h7+0mx zMqfPu#IzwBDj>Yl#hfG+9W7S=&5Bh23FBF`Zt=S3hrO2kmT8+i0>J)koh@_&iZNyV_>F@V{%`UMXt7EC7 zFZa#7Np-1Ul+>~q8UeQmOSXeLqsYC07|r)BFvu#VDQYX^C07S+CcwNqcm*85(B}ED zOG=zrEe8hjBNrf2ND(-w*1txB1p=tAxzm-VWLydhvjq0Sbb)SaLNanmiNJ0YU_&Yl zY>>Nti#_{OSVZGg9@zC1KoRVi1^YaLNHU0-(U3mZOR!%?n%a1_Ld^o-H<8U~c1Ev8 zD@^5k=tc)RQ5X)DI$Y6jo;o41nGR5KH`)%<8C^P{ir^A7MPX+ z{-HN}2%egln$tX9bDWMTqXVCt=d*Ek)+yz0IQ1&GxzFM7Wl$+GwD2*?4J9O^F%9OK@+ z829Pb24V~!<37EycyOOy%-*MW2?jO^+wG*wDD4W%88K}8pNT*ep>af8N}4>_T~j_0CT=6@0X=7v$b>RB^bs`*HhzxUd&Bxg5?0xC z%1Dxg^~3)&umZeXf|r085j3XMSP5<01aLLqxk|qxkxC^o+0BbZ?&tDaY8gy^l>U@D z8(!_(BR5jn?olqDb8DRR=7b)T-kjus=0d&JedCq(zwX;M+mnHN>d%9>dS!5SCTqsddAZ67oDg{VAbrPh8SW0gLsl5dV(j-v^0=QTzSg3}` zhQnx#`|JLib0X)M9@N2e$Q=Om&7Y=$#q!M(m+5ui1bJ~8`@5l>-Rv2byyNy?NLd=0 zM%V?O;_z6WoTVWzIjxTez&S~JZ*2bL19=Ex_5C;KaEQ#KkhWP*h+{W1&j$zn@O?3ic0>$~(8abauuVk-#8T3*aGuk+T<5BBS5hlUu zs1RKx4qVqK=25mvh~pE3lf>0h&oq@iKCOXAfElqisJ>@Xw`ZT#f{Cbf&qGi18p}PM)^_5!N2&hyk^mu`;onQ4j9= zEEY2!!=m-#4G5VFk4OH5XZ;p<&WG3k9h4n@ex=5xPt{DEyY|kaWNob!GE(iYT>a2trBwe1n!TgZS>(MR&JMdM2 z5rn4{2!t`&C4$-2|Gy%VyG2YiSY&D@m};n3y&|9 z4m|jnq)9+Y__Aq2JIL?ifr2xaUA3M7dGC)w@U4hF<5sr-a*I_HmF4K zADNtXXp2ER4VEuZXI^z+7pC}!v0UQvjVn`|C~Zxureg5qLbOjI2k;@9*e4A4K*J3d zT!>Tf11H)&e77TJonK}kB2pkkeI!{HFdDayH(`#TQK z&clkC9PlG&_#XnvIiN%R&qbA_nGtjN&(qgBjP6T(cf7(58Qquq$|EB&mkNxzL~l43 zfpJFcOq_tz7Z1Tp-eKta>BeUceQ`bZWQeYfSaH{mysxhvtS@ghTH1}Cfe)S)+~*%4 zwv2PUz2Gn4{pk{B%mcB}B)P#Qxg#f3?D!=yXta7+IW%YgavaQ})2UjoZJLZgi)bFP z!#Ik$&SP$}f!J-dI*i>=KwWxx5Oe$AP4Z1PK!SNgVrteu#wz|<$O(Buol03$H1(3E zTG*S;>|&}*EOKx0W>O}ZiD?iyV#~TX2x2HT6vwP@a93;q+hRU_*=N`hnuG&pxn}w8 z_WPs#aPJmr5ujo8h<)mcp@Kj3s$et{tkHG_yRtxY5F5I84q6icl$(sF!w6Ltp|n{$ zkp%0~>%vz_x|o_7_GZaGlw>3GB2nTI zM1HHO00Wj=Kgpx>1G`(*vXpTG8~>&dlr_p$P_BJ29+^=YRp2mpSL|%?z(H%vK^|H> z3j-Ck2cq^^<1=B}g6rj80PDp+;0@aV=_30f`fInY43~>)var}}jaz8jQc6kbYhU)f zgw}8Z*~6)o?J9x%xiPXxp#Vi}I#^$fQ4H`Jfadl+ck@TweCHIc15Y&n-8Jx#xWy^X zNdmN@*`aOZ35Uqm437)m!H8KlWh z-8zx*BU}z=0`>hQTmQ%2hNcCw*rZv4b!C?|XQ;x{h3JmhEm~|sOtH?HM-FhxSsL4x ze8wJb^Z^IR){Tn6Zc9CET^cEjneDcQIb}(-b6kN%iE8_10*6m!CWgVjI1GAL5wxJJ z6G&O3-iQ>u6{pQ+Y5PJu#H$+-?1AYt&vuA}BSDQ$~V zVtJCn>rw^$3JJA!P6p<_Tgcz%v4VCRvEmR01t-owe*ZM+g`l$&$W9wiA{dgR5X>0J z9D8p4zx@=JSI8YpcI`-HX6w5+`~czsktAK}PMG&V5^w{11Et2^BsG_utGx7G#SlM+ zvjqzgt<46k!cIlPCxa`K!KP$zg&pMZ3~sRf zS$n`1`Cuxi^;4Wd1MQ$Y34z!Pb^2>;cqOam#W30H1Wec)>vgI%Ha9cT|GrfE3B)05 zD~_OzRsKX1t9yFjA0UX7V>%L~9Is&=_NrVj6Lki(2Rcj=5=;aW`b`-%h#}9fyrhtc z9mw?Z1^-p^R5G`PW~BD$BuwL6C0IekuI|y5KmfI1Ww|<04XS>K9CF(Q%UG+wqj+5T zSipYF4fgBA!#LJY555bAttp3!fe>4WEXR|3=gO@!ml4HoEDw=o=>p|L4QBS3B@V)a z%v7uAXqtwBpIyb?j`5hOKO)cy}N`61F8_UXYhfSH;S)#3rW#c7Bux1I{g7D1#o zz>cb^6O(I;R6uB-n6>tP33~(Un#@PX_Ff1z?Nw{yv*;MiJItsZdP6G#!J%@v*t0>( z50L{XB4)@%_+(mID<(&qUGyY9hj{P=XZHRqRLB7G6jWMH1sN4pJ|M?-DnD!jY+pZP zgKyrvIeBfyp#=k5ur&mUcD#S?thsi)zbw)Z2@8f`Uzh)e`s?nltzG25Ch{pde{yC$ z_%`B=*g!we%dY^dRay;8C1Th-Ed$7EEs%h}Y*>XZ;DI;+9_Y{~&h!-jS7&LV&&sOx6%++Da29Wq1<}z4#`b4?JKC2kVo{u zw(wXN!=(f1DUACb!5LUF0qo=s7$?)3CVW1N7Lv z#*QqNnr_;h3mzd8yI8PK@7nP)SVC-AxqQJ5 z1Hhr=Qf0ka!x*i!1TLb`8}>tSBMYfvU3xZp-mW58M(njdzM*&a-=$sj_q~p!*1;BBFFs!>v#zRN3_Zz1SHiW+`_WaiLaUa?Aj=ME9y4?R!#D&+sn7imxmzc z3hK<7N5B@=jS6arcP3ZR`cyW|uB>Zt@5*elk%HFDaj8VWs9+PjNpLnfl;< zDC&!|k9b6eDI(S};i{IvcQ`QPCfqbP_4PGLj z(vgEL){s1Mg*{khCe2Xmv_l>8azFrovlwk8{=o)(2Yo_JCm#6EAT__-Ozkjla+(ue z=1uOKjrY4l73r=@w9EqKSdz@V4deXNC?fX?K$vI*09O=8d0@?jAv_gSN;IkJQNz)l62|#>!Mlc2D4y2Z-u6`}>p*#}1fDq1v-<;4whv2l)Xks=W zXM<2_Z*X$|1#lMqkAMTzH31AIv%n?GSrc zLU|BPAt-i~-OivI@qtR>y8N`AgXLBdpAi&d{ae{WL^LW&`>9`J_KdK`z-P)u4O_VObdkOLufo-{@5B~vuSa@J1(14@9$WiIF}!~**TV0^Kc z_QwITcVGJh2thvE`uGsRa_+BXYWjH>ud|;p`9n zDGEx{yEqVtR%^B@Pd)>9QevhcO0wv6a47)`l0{o^g-XPW<+Kg&JcLo^oSU#>516%$ zZhTq!pf|8b2EqiadJT9yn7ZoHaa!#F^})6?>Euz962y?t@IQ6P0Hui|8&$|dOP9LD zRxRi=5sHMkW|6xuu}opkcgeFkrFkySY2+X?F?Jj8l9Gy%Bd}C+6f$&|;MlrNPPsTx zNaZBOqRiAkT|}B)F=Eqd54IwWCsr88WUK*Fj!#IXh{&HqzLR zdmlh6PA4IAa+cLX*Y%OTdf;aGkO3t~gj5MGcikiR0X$?sKIHovPd6UG0+Ce$q@oLG z679&od?5po-H>zHNhbm_>j6~;^GF1MWXT~xPZ#rJa&4mLh$S}B=Py*F z>}6Kc=ZCb^K^GG^5*DS&{;Utd%n&}5tiW%+dToUCM!!zse6ojQW=Oj5dN18@yB2}V zJw0%0mdC-$#@-^bWD0=ZA>KH%0esemjB#cID!v|oljQmhP$msOcO2-hr&EV5`@sg!)o~rWWSdTm!E_?^4RmllJfWWrcR2f6?e3zV$*il zvMVWQOeH1cgWgyeSA1}SrT_)*sI(y=^vgg#Tw*I~c4BW}fF5rfx3|zhJBNAH++T|fNo7g#cCBG@-l)Oyca^FHjf9^m#%`Dkm5#+ zK1fB}YqoPc>@yfXuukM`$GW|5kt`D0*6vG3rwkR3@g-%bn8)|TP?4PVJdTSzY-CtJ z`vX?oCd(;W(DNRE<$A)qcw$K7PBHFl-D3Uak2J5Ksr2VgxSBt5!O~ga9C%cm6xBg^SFexb0)r52VQi z;=#xCGn|`OoriFc%~qjf2sWJymRDR~63VCt^OvFkPrYzT#JN+29&A7c>{fU3HVda& zdq8v@>8QP*FQ`)vqPFp`4EWI010kiopk>DM(zjdOP$(rXC=<=PR-Yu~r`z1HYc2$Gcui!AiK`};Q^xO!X zkjt$6fu{i{S!{NhfE0IPK66vNyB6TnlIbd18OC)lrLn2SCA+ei@ z%Tyxnw!vZxB0zSId#H(YL1sdP%gZpVp%hd)$5v_p{Of0}`Y^5qmV8( z*rr2rH+%stzmZ%v@Cekp9EcPL*0{0CSd)!=2f!`l+;Xc9QUwICW*~r7^+CYTFNh<+ z=VRxS@)<3YLgh^A&Ce zL*||?l07VhmiZGCdrx(rQBgG5P=wN9nJTJFMNuZKMTn9c4fG&tlS~QCLg-hXG{UD0 z;v_kc7ay$=Vvtsxi`!jbD8V_$tF!1DvH9j5q#_f=7XKNHdw~AgUK?Hzz)DsGq~0S^ z?HW=*V&nOz+iSyJS)eV!wgEzwDdyVOfKBbSp-)kcwgY%MTyi^=x*YDH)iD-GR13Vo zy7bx&z0{!?Y6w&h?XuX=ZfyL-cw{f=SNsAlmwl0?uVSL4L^D1(9JMmfm$NR3!XE3V z*HZubz9qKcKGyF)FU^Cg&X+6>bYQ!#yVj}OCh@qyyep2X)ac!+E8`A>Wp zuE*!6_^iQa#yQxUhtKcvVR$J%&*RgAPyGIi#QhZY2fQ@X{x1G7lFwg_!gqK{KE44& zEWBhiLg{Z{wplhKv?LcVd9iS@7n(tvVZwogBFh*%56f<{!~6@C;m+^b{bu(ow|?_uWtY_9{$pJ6$?Z<&xh7KN zu?qqkTCx&#i@J)i46q?li94UBiK^-0CC~94f90{EK1!KHow$Sjjf7$rgKmFX1 z(;=#I;cmHZSWkVc7HGb>e#eNidrs8XuJQ|G<+^X%i<|x6j0I z9DD4#zl<%*J^wmA^a-3^xP>ZInF%B*GAXDY$%3kqz(|5G;UtBBq8N8rImNQ)5e}8r z!k<*^N>BG9FQ;A9YSxSSz*wCxR8}E+Vha~p{pA?sH0o+kwK24Es#!cORF|nwX-eWF zyoB}hPt~hJgn^6su;-R+w4anp}{ ztJ>T)(SdWCZAMcr9@r&12e;@yR+*dR@Z}=^Mx$wTeJp^I{zJ(|0;tNm)ow!CJR^&9 zMYFNLDKyb5#ubFec~L?O*9Z8(Uv;>?Ne068o8^;VDD-rr4kxDlZ|F+a~WN?J*N$~)J;d&?W z16~Q&yBNYp{D&LgHdEEpL!fec5yF4uEVM&g>PuuqF>OR>mu!3olH9PRc$AdkC!a&S zfPBW818fUcAmfFl-kC$ngqJiSf~R-3!!*TU0ujZIfP0|~FdHc5l%UKVUXsZc@dumD zjR$#ZIi6rc1eZN!M-gg7={ZXXG9JVV9-ZnzeB-T2Jyxm5NsWq_44#Gc+gTjb*QN*>;>;=RS$3>CXWb?$I+dOBXX zai%O6%FeoMEml36un-KwisHf${#TVMrjvG>RDMVUZ%Nn@5|t(4;>?jFSZArN#-AZG zhKtK-p(T}|D9!>K%hJUjE)q@&6=#;m7Uo)0b}07`hLmM1Q8c#DYb|(8P67|h@c_FA z1_KMGY~NIh_DSYcu$3khb063gAPF@rifcsKoKP9QOEGu>LT(+jGOwg^&N3^rv6UY) zpzGnx$iUnWP8g#O$2L#i%3f7s))oE>XCvgUVzb!EN!eLunbWLvS-qMc>TI;#j@_NF=`XaoqCcC}7MOKkOej?< z`AxRq*mM8Z8ERBlbyondHMyK0%S^bR6D_F9IYz3=kIh1ymrzotTN>J;PH@R8WW7{P zD+$cBaj38GhGczuqCToHYcs1Nxzw#UFD70o`_`}c`Z7SWrlJ^akg}{~BQ;A2EGP3* zto1;B{QS{m{QgGh#mzXWWlBR>+G?kw0^Ff@wpK6in?K%lXBo}DjFeyAv45{3ejnV| zvA46|CMffOh)C{P5r8kV*`B` z>+4=Kj%NEVG>%^AzX(PyG6W7fL|Hbg+!MVvS>Iu3INzmX>+=1pLuL6J+kc4BS5&2o zI#|{iVJRI1%o_clXQ?!uXC~Te;7vx`nmv5uZ*E(9ByHbPO%0zNJ|?wbV!9LjM@XJ}vu?BzOK`(jU1;PO*`zdRte28DsDISN&r z9pbH)krDp3&MO2JS8$&irRgjc%}jP(=kFz%Rc?I=2ekT_obU_}NsvU9TO6YigF=|| z48xBP1=)_4^PYb)elbU2%2Sn%T=5CdBw!3@hhjtLfC)qxbDp!6;^0y#X3W}{_x%5s zGTT{lA!a-K7Ueqj;oAEucQl~T!x0^H5PLb*nPP2fP*a_%e6tvMPLBqW_Z8@}SfmtR z7y**S#_ZM~Pe-lL)fh3RSU)z@M5oGU*43DmbIh_@qb?s~YPRnpam=hMz~#2M*w)r& z;=RP_NfWo-B8hXIwJm!6POyN{orbZO{nr~-;@bo-kwI`dp(xHa=?@s9Ac3)s;m|FQ zc$rA5bIYqrhw*08Q~rXHJ;W+7VhddfFjRe{aTNEl@)8GvPYqnqR|6-r+E6COZCbD3 z5uz1%t=QO^U_>TZe_fNNMTVh+Ca(6a#Bqt@Y*7j0qBcQqecgGu197JwgpH@+62)*o zqi2NvL-;2(9GdYBGTe(;T^b~s^54<_zsGyOolGh8@8Z~Sqw%)lNmdrqH0D?+n5f0@e-n>DuX+E7Y~Cp z0X@dCY~6T4#?Ocx!!s$9%g2`0`15hmGROcf-nc4(3gXhn;PXEPHY#19nZC9v6^~}| z98v7578xbu@H=v4wUO)K%n>d4wIvWIn8z2CS*MH2OlU?uNaHpSd$w3p!w@7#E+vGr z7Ycf=C{bg-4_}sXEPZ}C>aEHUyTmr5vny133lxFN(IP6-MGII4`UKAUiCy>_Y&2&Z zEnWW4(DyN|dhlncKrxx|fu7Re>ag?XY1+73{D&iTDhn}iIs3lyRDFNpKhXD6bsQCy zT!>*6Gvv&1P6k6BXSk$7TZ#|vaE(0Cbn<}aB59~C#x4bLIKnQaGO?( zE9euennBUnM%BMKd zAg`&~xL(7pF>%IdRE)8_FLJCOKPBbr6l{^#)Yy)$gbHp%-E8neEwb;_2%*c07%2*z zgb}GhB`2rri;KuCU8tjBtAC*6E+=-qo>KJ7v0qnd;<9OD%PRaAh%z^&yeZKGXco6XLdq+qAIB_?~W&Br6xsh zu796?L=N+1=>x&Q5~R)qNML)-YWgEUs)KI_IHdtE2H@fvSQ7$2L2|2Gu44ie=o_(# z^LMd{b1d-;N7K-gjBy+l%Q!{Eh-Up8`j@wLY+3#TMW)PXKEos)ONZN4v#-26llR@} z!C{DzciwfG12D|Q`+(6FE;ZiS9o=m%Veyx!7A}#KyJpLYMRa|9N67heVl!n$HN`UK8ss%1TVdqKh8BkR8`1d6cBY&VZ||( z1`GQ|gN3~WO>D#EZD}<`vzD91wU9oyC>+iUT8nF_gonc~XtV;uHK8Xt7+8-3vDn{O zTvIy>m?cxRnM<;ef}t0zamkVlgwY@GZ1-%OHF|ylR*FDRY&fW!8vVJkiEU$4%Valg zKd$dwQxf5%p|N-dGe;jZH?dOl88(x1%vWF|d}vY^N&O}78ys%mHwfIHg0-Uij7`$> z!Zz8TzHOSt6=vB~Q9J`z8HWKUt8EB`2w zhyU20cM%!KZ5&PW=h7AF%`mzL`18!F%ng@bT6_1#yXP;MX^v07(sgBd+Q4mka4z^X zbW3`q2Br1IX>ycNrTY7an`N0BFTM0Wzj;&omAEnWCZ}<5*1#R8UJounIW(d-oWUYx z_-&K{p__03iclKeL-dAE5ilKCK*r?4n@*P|e+*?gtZ4o?%1Zg%hq&F%*YZbE^aiY+ zCuKp+`5HAP4GS{SZMrbwzeK0vF3fR-=rKHf0Y2~-t`Dlf|ENz>`5^%u>Zk9Bp)JY} zNslUib75*WM&ibI&qIe7Wnrlhs7VU-UaKXR20K)5xB~H!EPR;`&|>ENjktJfew+{u zIMocdOgq1qy@l^oR#0vkvVsI7$qI@Iq=Yg>6jz9{sSs3AmJgu@cSyHP{RmWAH^;23 zO}b?s{h95SslgObADWPr$RlnNwjWpqKO->?+ynO}Z`FZO=Dz@^Ywp9TEYKiUn{Hi* zH73YD`WD~1hJb-2N zkhDiJN0OHnNVyE4EhU$IL?F)m>|Qw0eXH?c8eS>3&m~IYB}I6YYgVc!SqzDG!`3Y* zthT_^cVl=NmtI|;f9XQK;aeDv;;_|x4cMNc3c_5l4{QgpSvc}5EU1~|v$0D=I^gui zspz=*FqozTg>0rVrI!Zw?_TS19N8l4yo%Jwe!!aGD+h(=3m?e&JUSx1$ZMt2&6Wd+7D%&a!Ryi#`_=)ow$FA-?mdw^qtc#E8) zBda8+-J5dU()ht-*%<{l_7-pIWJj%>AFA!Gax6CGcqDeNm2S2$K(83N`Mr{jQ;z?G@G9Em|GxX+*P4D2_e5MpJ=B9F0ZpjM7De*iV zz^JwC5m+XbJ^|2SL6+5mB%ydWY=l+jL$h%cY>Mc=8%fiiZNtqCSY#f>ww?8d16~9$ zznYHoOsQ#rbx;>jhCJ*B^#!^ys2fq@nGTe}s1+!|GigTEHDmrD^{&zEFqW4hQ4@{K z6LP-NfXkXfPD99LB0YF3CdV`ZaZMVR^hC!?ov0b>cT3Wd65%o;*6PuQ`z$)X^m$m@ z^I?r|_cr_jZ;@EC5*b9B*dg|QuD{$NiZjxxvPZloc7&YoA_dpkm~xG(dd#?P-x#wP zlRmSuJX8{^9)T1V_T#t47NTw6HdMNmtJ-ro>VUmV9qC3>`iLFT`sGLVnT30ghunL` zF0Zla3)U>g9rq21o3u%P2Ua%Gz*xI?__-F%%ny}h*nrBF=V^>wBIz%yGB;(D7s~LP zL*?R`Zr0r_mK?zIs+spaG)HW*5Uf8q^ZWA_U}7k`Ij=r?)!p-F&aFS_pShqGomO1| zf=tPa`r1Y5Vw+gjgo5U>k*K}?(B1Rr%@CWOM~rw42wV1{Ok<9`?)uNaKX1m|s}{`k zJtW?%KX}iKnF}3N*@G+79mN@g%Ut?%y9enn>^cX2gy*)U>(4c(1CdwOWnWp9er1^} zuf@?l81rY$EP&o_P;ntGnCc9CRk;SkZTVcsVEu*nbW`$0raXS0}`t+tlxLF4D!&l@YOzrG>2-g2d z`6tx`hk9njZpgiJEPo3Sp4j0V=(<$?8ONJOb9%32-vP*flcQVg9?`C^b*BZlHiGj6 z*F(*+^iX-1s4E}Q)96lXEIkJfCrFCqs^-qLz<#WKZq1RZU+l?gKGIPwD$=^c$L>ka(@bz}&>%qJb6Sxp(Y`m&8h@^`W_VKXR2ma+m^oE6<`$bm`o4Yk$WTB`fR z>+(!aauNPF}Lay|-$JE9rRW_BLy#Krk;nw-T2(6q4K zH{&~8T!L@x`%cSg4vp(JnvaWRj1Mg%8N?F#T~+~e9ZNE+=3)W-J;!W{zg#=x5mPT9$#a0YMDJdJ;gG^ zFL4gS+Kp|Q^<&Vs%+^Dt#x1jt5l&iW?{FNx6Xuz`mFhmcjWdp&8TuY>rQ%MGyrx%R z;AuwGF@MC7kK|oc#pQ;58C9UJT=VxI=V)K>p5#xXE*o7GVY?40>jW6qip#z7E-IK8 zCtpI9q#ga=i7Tmok8a~sz(2o|N-Z8(_hQ~c)9^m@hn^f*N1S*ib~<>eV^VLTfT%=g*Pzh_;gCzKCF(#^er^>RWPYPdL;dtBY!dFSUC?QX#+&Be$VyZ`!j1*Hx zDFs#Io5lrd???1wVwFUCeQ;xh-mru%)D@Uj`LVTBzxGv9`*@XOt%rY27y#-( ze}C)5>v*ZmQ1ibBW;nFxVcP!;b%d8tWD6nNSUL-jJoLT8j z-pDw?l(6$gMw)h5oY)t)6_c7G4+CO0ZeZNNn;7r!xI%Os*)MiCe|E;KFfU@PKgMtU z$IUsdIFTM~)*BuNpfzdQa2y)ouJYK>8n{rQ-ilqmd7^s*cM}|m%z7>TJ1~3OjJ;5r zRM0{WsPS#+%8`fdFWCPuLl*Z9pBB`M(>@NJJz>a5 z@bWJzW>+u8oLpz1V$S*s#Vp77gg(9i4gV|pcvn=(^KO*;7xi)G=+o%qA*id5KAvMY z^B9u8N*Sw*0CxcS$qJ+{zQnFz0xFQYIO8jIaj&cYFkO>3|6!*xF8}MLi`VQwNf$rL z7K#hZx_k(BFLfOAVczB9W-hGcxGd{gdApxHk243V7TXAvMt%<``KXb}5qbNJI=2%m zoAXD}!Cf*6#>|E0qLIc?WT1Y= zAl}-3oIrwgVr<%uBDh`G+|-U|Ujy^_!ecgkxy9s9-|c|Vl(Gt5Zd9K0)5=JM~0{0_yf2XKpkHEM!W zi#+)$YNL$bhM7b!8J~rS6dAt@ui~iM_V(i|X^L$GC4Eedmt?7Ic7R+Z4t zif=gM2(QM5)D35JZJ zr5nVf0RryNwtny`PM}p`rzAF$Kr9Muu_&;1VCyTmt@#7jz+*yAQ6dJh!hL$Ozcr)T zKd;3rYoq_9ytpj;1VL zp&Hgn@(i4ihDXZDH#vlrTMZyp3ZO6a1#~9TrM|`fxXjf3bljtZb*x1gXwG67S4#fJ zfVdQ508faHu{qP;U)QU=A#Fi9`%v~Df4Vt21glcX*%SBY{m+)K!|;=k^yC7uY#9E( z|M~VXnxArhb{hPgZqL%`@cA$1$F$a3znpp&5i92+ve+NOeX6)ufR{zM!cGsET-#FU8K~9Na(i*k_vF0KbmLd&af}yw{~GL| zX>s_kg6{WUjhdY}#5GX!aK) zjg*{rYW(=qY2dH@SKyy@$Nw_;(`)Mg75IO1`~N!lfAp`wADQ~U4E{ki{1ND{!~dPH zj|2bz{|Uc+{_lWS%=|E1)CD6X7A`uF%Z(&BiQzi;jV9{%M#|eU%Gk zdBR@*C5yC*9lX+ehnBd|`#rmbp`%!_bFol$%XYT(@5ZvCCNAjW>f)d;@ZAR|iB(oB0&=U^rSqmZTQ1y>6fg-|%8H};nD}U%1IAnx1 z8Vj4a^q}aSEk*A*5XnjS1{TcQDTntpY0Sk66n~=%9A|h#njPn3YcM>;W~OcV zD^tD{tQ&>9J-%tH$9n%=ZXj!D;_VEYUeu0@_IQdBJJ`;alvbTq^&1B~z2wIW`4JP^ zAwuHqzCr{*v33_sL$&4^d9c!58;KZwF?WXoaf=GDKGB5FR$L4yb|rlo(w@Td#FS2a z-G#-Fx%XiiWA1}o$eP!M1tl!n%$VU_3v7KrBFXBlCy(Ta@s9Kz>s zNcuaHw)_8t@3t{mQ>ksns>lg03bny2a6&GR_|DJS5vpi+rb3{lU1PMkZt{0yvW6f3 zCPLx!F=FX(|KKofUUswpQb&wG^Su(i@r_JH=#Sn}Gicmt7H8z>*)~VZwS~Nxc=Roi zA_X?HzqPRjd{k*ISF{f{`8{UQJLZITtm+iKCC2d z1ejz_6pFqn8Cdaexp}7>Vpo4G08}j)Za&}|o97>hADH?Q@8T%Lu`w&RSUlj0{0I=G1n4!_M3*w)lIxhr}H@?eIw;36g-+LR^}-GObLNECaS@BQlo z+X9V~4@NI%md`g)D>Q#RB(SZ^&f-pF=~dv6IoN#275L!#z-xiV%$%Mvhvs_#Uubfd zqr399a`Boq4L9O~42~n>VHXv%Q=#BB(Nk&t9=H*Eh=GS)(Keet6I_zPVt%)EDg0`w z>V@>10vk6IClKDq+iG;je7A?n9iajZ#~*Io{R*eC(2x4~9&rEla2%xL^qQE^DT*w~ z()zyS|LQI{xoitAuZSKW1IYfU+4qI&_pIibs( z{DpeohIdJ-6nYs4Mw@rKda0aps+_mwpp9Ka+(j-X+oO}-$Xl(&1YBSqh1N;Rx57motVK*{ijGK%K=pF7Xmg`;_;_(=$JohD;%)mV*U0*B{kThu$DikG}5B`lGRZ2t58HfPk`ku#vE=J2UhnEt`Rz z^*tKa&h*8#i2oKg*J<_p`5nf%hd*PBv$^xk&{AOBp~30n!2Mv&mK!YN0Vj32!AqTu zMf*itbL0$hNU~F4|Mh{bfyN2%hbDX=Pv2pF20+B4o9mATQ00Ovphm=dV~g5FtKM)S zBtA5uT}<9?bGD_y3t~R{mU-tp=H%^>en_ugj!sG5??icR!MWyy&5H9QX9Tu3HBJDT z_eU?m-Ba5DM)SvRg0QWVP)zO=hZ-mBm!c4w@P7P!4}KU1$eX+!@I!^Q=j<7CaDEON z4LOHbh9-1EoI~=bHQWFJzZD)wdxHRM(W`7o3HDy$f2Y!V{R-)HG4Jd!{qIEA*``Cd z=q=zBn3Oa%i#p5+ZPcCZw(g8xXcn~@kG8|(h`#_8+gp7#QNq?SHjkD9C6%Y;?v88C zz1C-@?>qFu zEu!cE4ukfXk9Il0RPMtqPCfV@GC{2Yhk5((vqeRndXU&NPPn)Y3b=d}-{zxktcVP4 zlDk{xUC@qaXsR<|+6^||*S$Ja$htIOB|3uVT~_`raf{z%ZV{h$KROC&COhS_Y76PFmygU#v#fg*3B-p&t%=2CQ%p?2bpLjqoG(A%($?lIjx0+-+@+mx=-Ux@8jX-7x|hnDH&LH&ra$l+Utznx z02#M1RJ|Z8_h3f}$@-Of$3n+G5jYk}7v=&1GjLd6e82095qEpU9UgAt&u^MLz9to} z3}fSXYNWpso+jE~t;$A@5lG%dCj%^6)n8b)^zgg#v9}e!|3s*58BNSkEUv@1d zyu_a4ZA*0YyE63t+%r@7K%KeA6@ApsH#g6AceBt)@W7nh9ttOtV=Lbs&`&MbR4ssh zsKa~?s=l!eY9^*oikRdDK28YTBM;Ok{KrbbB-^bKQ$#ozf6W0stRD$nO7Umz&K@uy z!D;)G6z_#ZV(4+op;dh>%r`7fe^Z3(4b%Yqe)*%HL!~xVn=^pqF<{{WW?Sj6q;A&7 zD}l=i{`nEHV%9Ly*R~W>3Dza~%&+6xGKmK{nVT`#F&EhFfe$S)euJITlk6Wh6Fyzb ze$MWp6_OTV?DP1&_>n#S%_jalLAM@fhHVBFDlhMwiJ{Kkfi1%0hol4_)^3u|-@$I} z@e8Z9AFyelz&fcZiyem1N)aC^vzFL=WUb|6s<*@Atzo7=&VFTW3#o!oX88|VDHY_o zK+w9jK-!ai%@2U&`@K95(17~(1F?={V^ezK8b0-ddThYxQVz*W?CU@QO_nx|S*?YD z;&E1w(m#?@QXd~DBNzKBtzi*YdYrFk=);)~mX%fZeEz|KA7^xij%rmFk!2tS-o(66(eLQB z(*ecsl~H_}?dez94VkYf>i)=L2K!NeWCk$NTL-XWU02#9ouR(cV}fhYdj_;93p=T2 zT|oAdq&{6^4mbwgbsgt8AFB8WDbEzRAlD6BcKc>v>7@^1OwijL2#rZTU-6My{u{uq z%6(Bk7Wmj|Orna5YAPMln>gYyHwA(z=5XvtZG^oW^15vITlAx|gpIjUik3x|%JQfi zO5${s1hkf-2dUx^n#KDYk%|Cjo&{hRucr`WLk_~swawjx&P z>^n{>PXDdy3)VnS0L$qmES|5M_2RQygM&-j{nk zHxeI!YxP$D0<(U2es-PSXg=*2a%InSw!2nnJJT!AJlXCUY%3k>84Q<>^)Il5TKx+W zLe2gKiJ>Fty!vMrwgb(NzZZ7R#(K+F9;~c+G9j-`R=wW=gQHvX{wu$pyf4;h{+D^m zt?77?A95PydJX{jFG&g6NK+IGy3L$r!No$oh`_ILX6Tj+NE=T{qj@ zdLQd|VK_Umet8J9WCFL<$lbUqWuEk(Jy+pb-Q5Nna?fZx^Rp6dReA5h<}bkNqLOy# z9s@T<;hX{ zH$`tb)Djh$m?~LdzTVlMhz3&zA*!ZizCd%n6Fc@2Z1jPk?1cb*GweV~auaRke5%e&N3~`4mGNrb;pR$M5 zb|zLM@Xfu7fa+V^>6;6Wb$wBIDHmF@pYZ0@n%vjPUb`S*QQL(OGS~t1s<}13#qHjK z1Xq^t{-9)^tg;_6xo`6=JY-=?48w!6WBka5VJrF6#$R`SAPT_31HQR^iz3r%BTH&6 zN<`DD{uPXP>$2v1>)W)dsdO;(J>LQw$6k+d8tO|21I3;Gv_to*&QC%Z8`b}ULFs&& zbUrd|Fs|qBg&@N}jfLqzQRL3ZQlmNdo!)xu{F&~s`}Z==#XY~)x+Z~*)Zv+_px0vVdF z1p`P}b6jndK2CjXU{!3}lij~xHe1@RgO2mYo@3W@$+CHxSRP)jZ<{W%?P~qG>6nZ$ z$2`CKl>~AqhA>rK!T3D!`iF$nfR-EE^5^+`Y}&nndeB3 zXJD+hvjzKWZO$QXdU|e_)Vv3Ek?BE4U^hReiOln`ey;Sr&;v%b=R_X-y!yWJKdTS8 z_X(Wd+97}CgzwXC_K@1;EiGD`P@;Vo&GqA|W9tdT()1aGW%rHRj#es+cr{?q7@JH#?{aBuc~}pO425B@)!2KRIjpyM zv78*^ZLoOjuDV0AXuKnJuo#qwy?v9l$}Sa6Z*9pWm0 z4$uIrMPMmc+}23=ecV^_b91fa7{S=J3P{&N0MrNl(+osKbe8ofe7i6FCCSu#i?|cW zO}EHq7H|DkcTfO~xTbjf!HrP6FrmC^ylF||11f>@xll4mx@wq9$=rAK z)MdHv@2Sgouc$n;@dv$i=-W)LRK0)if?J%Xwc~@YQYShW_~_|ikYOcMiO5m^u(FOk zedsZX7ws|fP?hT)QcY4aF}OCCn(um*RGXwG1(z79h!OupVwNP14c=oU8m+sxgnnkk z%d#Oa1@E{S3zW%Zh)es=tWmO7l)u5BDmp(Pr>s}Mc!dYz)3G>fsm|T}d!2tT@~>J; zt=h;;;71k@ewG_Q3ydEz&?0d%KLpr-8bH7xL@iF7{&lE?f8n=Ok%-q4Ctvaql^Jw* zJBJ$R&t$*u7}P4iM+x5@ICj=q@)1`$5gpOV>Fy8o@-|lg79EWsQd{u}SwEbOKIaEA zj9#JX-Y=sE%{`O&o?qb5p06`Ept{qISN(kI$=TkTt8Vm0EcIbDi}fHMGf zWJ}ZR@QnhAGJo0}wc0$?o@ks;bS$M-)vL?E@3(C6-*5I$6KRajjOr7r1UG8LAcZSm z;Ir@RO!r#LY}FrT5FD2-m3MM@P~Jq>nBAi1V)ZOkb8vtzhF_0k{XRX>;7 z673H?>Csy`wS&dk2&OW*>^ss8BcF6b6y_%!0c{@Dx4~389;GKV?Q@6yYRpPrp}C@E z!MigJz2Po-*ZV3C2xSr7g6$6Z|Z)s?&QO0QAze+VtIP3jPRd^&2 z6u|{S<2Dq5lT7z|2cVGzh4!$yQPwK&Ig0r5eo}Q%&wIvwPNJJkB18?^kkad!rco+l znOz!ud7=W=a8pWjhB-Dcxt_*H#oK?I_Us!{&yr`Z_MY*6IEjn>ICf}m4(S1GxUcp0 z-L~OLJ@Xln*9(K7XSO4NxLG#+GwZ8=!H{6{5#Neu`rwnz7k$)nzWH+UNi@ovp1Hcf z;cPEG4OsHg{tQ$cbr1}r&r)dHb0^br?~TAxq9*2l-pCP4AP$vjBaCBwtJavNi1vPy zw|kQIevjrnr|sX6?!7Vz8BK)z(|hL>rWESo?AG3olRQP^3wE>InY_oxW1Zw}1t1RM zYqXv7Z`d#C)y~TTNtJ|d*8O(=M#)e1G8~-;4`mobS{O9yF)FD#Ay(#4bk#H-bpH(g z`m6HzTM{eyvsl3s%q(g9yh(pQM{-wf6|+nKHnGL5dXc}wWmXzx{8b*d|0OR)L6~o( z_SW<6X{gEsNa)klP_>C%^5mZ7_p1(c2WrrSwI-h6^NC}x zYSVTePu!vK#u|#A+uW^hGd?_Dn{Pes2)J9YKyyuy3M=oS!iGw}Kq1(f&BhAh_@GZ= zPWKB4jz(>Mqmpsn!H|^I5BPSeBF7rLxf|0(CwtTOT5jk+OVeU}bL52ED zinKuJY`u{aOEnE#_xbY_%kjS>@l{EUzIOx&-wV=ygR`B&5YqwO-3aRpWx`JwtiliUD(RKuXsrBfDZjGaZ&711 zf-dqyF`Illl3oEgp}!=lc%KlaBElA=1&HvpXTZGShmg#?+?uz{qslX1q+{C7f!-Eo zjLb*v`Fbx(x?b)1TKzqPs;u42m=nwxVw~ac^cKrD=f)!M_$!m7y#EFH8>MDiTmQW1 zgg8uwWzP?I`{0plC>W(}jyYrWggmjy_uh2DTJD*d?@br@_Z#rnY4gt`>sFot{y~Ai z2KW!Y{aN_^GK$*HL4jW_KicyRQTXeSgQQ2Zt%Ht6;FqXvM_@#rLl$O8=qB*z-!Wav z``?$pjv?)Zx}Gz%w*Gm&kwesxX}^zRQTcon{-W^9*#5WhEByZr{8d7=QTU_ykApvo z|3QU+f&V&zzcxVzAPT>f_rEWHN5LQ9kk`?r&F}IKf&Hr--uNxDsTyVRV$jcfNwRDS z{EB|!@elnB^}ari&tD@UlddLyY?<(m}p)aF#D&G4xu#8Iz9SFi(mW|y8V zGqWHgm+8gr@xzlZ>+@7!l2? zdQh|8uX<8sFsq{A$y8%`Q8QInY-$)Nc+^i>7vNFOpzGn$Jj(apFn@Y4%AcHV!k`A? z7}VI}w&;9w-K;p&%VH-H$Du-FWAMVJFp>HUk4hyw_z?UmKC#Wj?F8_K5cOZ_YkH6toH9ds!l-$~L5@2LTqxM`rzgW`s>=y*Wz-)@l za?cp-ee`(5_*s;Hs4YJy0q_r(%R4k#dGT@aknA{LdPnB&JiS*0uJRhD|3AteIm#XV z@5=~9`Nik*$7z&P{P7W~?KvfRmJE@xB{*M9pWBN2JIodBii2P&z%d4wkGp!tbLzS*t3+ z(4*tG{9(k^jC?`<4zK75WwYQwOv&9;7|~D1=#P!i19A#+EdM^wrt2aO*C#`k>mB_Y zw8TPd(b}9v!M)e1iQ+9M^oF;No}rNRm1A0~vo=n-G+(5dqK}lw6j@qCEQ%LBF z+nt;wVb8nhAogZTsaBqX&5{S+DY3lY3V!}`KC$QZ%E^yeJ{3(rjk{XqD!vH zyvDU)k~GO;KHNo{vZ*-s^HrQ~x-+BYQa4N1F~G$aevz!FJ#n}UXRHfG;Z z(Yb;tj=tSD*7qZe-k98{ReF)kxCzuZ$0o{!uH=ALSxJ&8ALgX^zLlm2l237Un&c|% zG&iOWYFp0p!WODe?Vv94;tg=}q3thCSKB7FG4jgZ>7eF3rM=%Mo2D*f-=v4Ng2M!K z_{^3mVr-ev53o<~)YZ;he_xc~{;C}Q4)0w4-KNOGUsVm24ewn1Mrv&5BHJs7oSjQ0 zx#Y=xi(hFH*j+q?4JZ2UQTr3g7P?jTC-Nlwlc~eGjQxqIRb+qC6#P0D_hG`~d&k(s zNddpVjWtCr_gt{>>9vNC_l|6?bT`LP*<}mi_tOKH*d0XApL{P(KT8=j+@?dnoXKq} z*P1M7xJ`F{?|@#ZKABI6!tSKEp%<^&^_f0Z9d(uN7)6tFh0OEb7d56ig1!?mcQ|X= zA8GauX|`s#y=6snP~F@jJ&Lf z&q@pe)>GL(#Yx}7FMK-`4%GfBrn8CepNtRB*Uo1D)M)IVZi-e|`92jkR8|T4WSH_p zV{&Uap(^a8Wd#e3D=F{;MA<-F!XCwfgg@1|@{wN!#Y%n^WUBS*m?65JXT2)>28KJ@ zuHqCH>iNhYj+3ZDH2=TTP=_vZS!I$jdh(a`QFc_a%*nz|{mcD%if8!Wk-;C`VbkB+ zZ_px3>11jEebTn#BrQPCIxh9(hbBa!qNig=@+ zV3%W#u(m&-&8-*qQYYfeJ4zGq3LWylES=zhcJb0+S+XHx$k}al3nSYmn{x zN*y2t7cUFWHS(ih6-|Zy&J4*sWjM2<>&5<;WsnvJua9Ov#j+mV#mFE82Ro@;4Vlpd zwkXaId{t7H&YE2%5ODS;28ANkUj_fZVwRk?*?L1vL0Wircz!S(mA_`K^1e4tux9e# zYjK6x;zTz$)X)c0^rxwv&AUPs%(M~qyj2u!z8w>!RU(}3m>`o?djZj<&RZ*@%lX{n zc?tSZ=}nkN47jiu&_nrh>$&h$QuFj3ZXsQ`@oErC=3fO5TVbt z*0*h(=xTs>+qgJ*m8f+-&%EK=Xzpj`9u%0k#?CK87=B@6q-PUh6emho4@tsR7&K0&=|3$q5W1m2a6L$#rsVyn*! zYV|SnIkE%(iDWs9{sDmpYOIt<2uMn7W2(+nr(NTEII0E7J!mXU>d@!!uSa4g(aAvE zBTLQ7cC{SM7yLw*BWdLni;q^d3R(zFRdyAf_U;f#1bN!dMBxLEPA#d zy4%sLJ^K>thp1AC*yI`(7h$LWL@eu*brbw{vlv zDHf~Uw{tLR5E(kne_tejk%-Y>2N21G9POVE4?Vi+PVzds%f>LA)tRYtxWTzudbomW zb;h_=Or%ov*^Kd%$mb$(8uIzdcD=!IW&N3suS?GOlYq@hNV6kywM9Dll%$Qw)oGIU zkCHYbSKB3Rv7{{)Nq!wXB<(ZO{S?ylQzb4bqseoBoY*}CjVk;W$mcL`aRT=? zN$Zrf5rMl>(pn{LGOfndbdS{3#YnA^9J%t=T{2CcZ{;~AjmPA1Z+l-w=T>U)-G!-y z-j~dL|FKBgbF98^*)Y*eNp9uxP%E@2sozTR%}Eo7Y5iPsy^*F5na_z!b85Y|rI9={ zH*qT}?-N~=+H!~#^WgXbyXDt3(xz3dApz6Z1-|>z^*lSFcg|}CeOf^s<_JUnZ?^Gi zvWV=j4y0g~N*O5Z{;=lk6+buUVZE2gi^sSOS9`U~+FMJNjJW?F`ZVW|<~*cvu-8a6 zi?3?0_E`d#EWH78P&%MFo2f&u^Z#f_d-Y0!z`_B_fIm%b_$h}4A{PX-_@>#>2b|G< z5y(EOz1Ga36Yxyop4W*5+@I`HH~6kv4Ui?Pv-`E`?I@`+eogaUxCbI68wj(4E*mDj z>Dp_(&6j!tS-^(lN+0>`%|YG?0qMK~K;)mM{dP#@2vcCm9dcE)%U*N`6(x6TRY62k zU!Gk+(+9NIq=rYEFRJ{t+&RkFG=HMrnze#+RAAM9TPvBRjTPKB?Q9{jD0d)NOl?>3 zWpZ*e7XS7Vt;$c;yI)}z?D?mIg-=Z>u&2G~w|EGypR^rKe{To2j00l>!AWMUQWk06 zbov>%DA)bku<}j!z4Zq4gdN2aJlX&Gb$Jd0cpkx*R@i6g7CWy?>{-1Z(O4$;L@cpS z<`WlQ@?`7%h$`&4HReN9mxid2xWu06Z8Gsq{Pd@CCAO5y*TYcJG*Z)hFY%tEN-yMF zgL<-K<81A{B>g2{-s;5a?ZA;+(|gK1U{spJFs0E(3YAv~sd%InZVRV{3#vwGVTOK^ zFeU}|0$R$8en|oTV|>f%{ZM&zyS1?iq z^wO4?XXiLRaT6BLEv5>PAyACpWS`2bx4|rbFW!R<23ZNeN`u;Od$reEn?Gs(5IZG9 zrR1z*KF|$|R?v$MNHF1Ij_lIk61Cb>RSdnttt5M;%x-zo=G1cPL@v*jq|l=kLlU5B?_aLnbUd%5JI{lG`}N&`{s@^}41Amj z_Vt#spY3^c3QO=mvd8vi+NGu3@G@Z>0 za*0qCkfzME*)wE~-_k&$VWOIEO0?NiPtO#1gvMm9TPLotXO&K)gpCTbq;&RRMz_=l zur|_F4hL5p%eRcv;AGtp+a>^6PskVK6o})q9qect(Z{&9x?{yK64Z4 znR+<2G5e&TGvf&FQ>iCSpeK(YOqL59^;@$0GP-9L*k@!MlKoi$^KQ!pPyt1!>D{Q( zghsrFN%7R(4mX}=9}zxfzd{Ks>)#9?xM96YZKUor!H{mTI}T;^l}_Q{i8^S`0`8}f zJ)@g;?b%1@hRuO3D>N~nZ4vD+8HGmdd{3da8QF&>PP01(^e}uj2k@YTGaGTXB# zI9aOjm0PP7DzIwbE@9t=*s9*xGuX#uIM(oXP6I3=Tqw(-FjkcgCQj2|%qG##-%FUi2(Qn073LnKgK8|H>rw4kcijLB0jrmOlZBqdTKt z+k*EgyB67+K&koyA*bbPf6^!)1UQBf;Inu&=@xrPB}mWd_e@z3n$?;#wQLp;XPMl0 z(!vaLYibwcElhQW-Uwo+mOepxCgr9ooVR9lPl14ef2yM|<51aRQu74by=l@;U++an zo!|CjJ)F^~nDK(rY#QIwD#CML?*#!idmwNjktvjYC=uL_hXx(36VvQvH*i%p|Dj%@ zs}1sKin&A}yw^#-K`vy7 zlfSo0+V>^ji2Uu5wB@Af$M!Cg2HFI;S_(!*?3g_khdKCsh_w6@Rv?i)k%IphkD?0P zRfxw2LDULbwSrpQ&7Ww~oLzb#aFGzi=pa_D8{Tq;cn-f$SMBiS2o*2iO;5kYJgvCjSP3;#o z+4V{=%noCq^j`{vY+>-_MpULX?X|(?k95d}{xF-ruj;znWU?x2WQ?$%-HDi!0qc46k-I1^(8yE?H##JxQ= z&|-D~399e_GPw3z#N^iI%ZjUV#td80m)m>A8$nw)R#@suOOCHN#o-QD7knMf{=ZQO zMVFbgNpi^YrMD_+XPzPG46>Y-?cS5aK|t{7UIN@z5HNBFQ8!;ctJ#NM_u2p;Oyc$w8JDL-6hPe_~ z^@E7JY1!hXMKW4N5%-C;rjF}`wcyKfI=4n`ln4XBc)L4ApgSjg+1afR7+;XKsJD+q zRcmeH${@9lcj8eh6)(k4wrFw3TdK=vb0Z79ZTEmysOdwAW6Vl0Hduk>)E*(+`#8%fPl>;k7tT z?YI}OT?s?tVeB=l)tGY*qA z4wC^bEa%Cm%2Ls$nvW@!S_#pp(PGJ=jD9k7KZ;$>*;$WJdi5)%*92bgHMCxNM&3}u z+qp*m`KukGM$~>)Gt*7H%|KApH{ggQz*GEV=} z%{ZMZc~qXCH=ftY^D{iF;fjuzc)gvVmmWus_RM+J-EynXnL^8}t0Jo#ObMo^@78uj zkWv`C=CU8)Hr%goA13TDzCc8yno)TY76 zv!qqIFGQ+@g7tNiLrLmL$*>20MJW4bK9{^9eg7qa%=f&<)80vvc2?now0L|}bXM}s zHzyOaeH|wvk;q=p(yDHxDRlxDi_uU@f(notvoDCoXV8zz)d3Qr;gIl#?8Y^XMsS7k z1Xs|au0h#hw5G~wC@wz3b2o}#i+*giUU}mWdGgP_s@H9JUE(xM{cqzmY)g_NDpUjh zG@AEA;8s30Nsp7hphHAt!x!jC?0o4k*PZkQKCyT{)T+oqM?4p_z{hOutN)-M z_SAjD^M@QyKehis+j&NtbILoU>Bp*Hnx(qBIkMrXQTx8XseNd~x#%7hbV7Uf&nTih z(r6llMm>cLC;^XZR0IkG!W{;Lu^<%Bz8`fKl2KMfVhp~qekuRL^tIQOO#SNT#z#{A z8y-p`;wd<_T{v(@?Xk37t!OJx>#aCzaf9LK`)hGYH3*$9K9K(Pkqw8v4Zv%QB6F5_ zZYn%xi*u9VP!&A!9-hhvwtgPG#(Il3Lj(<^JUfjw@vgJbU382|`H#f5+`Q~T7ZgIPi z#CI=zf(35ucb%q6Y+;(TodMs2>E3W+(L<39(SToaYI7RI_~wfvErO4HQ2|orOO5db z@_W3JSrGr}H9jp9_gst&4p{ZNj%_3m?KF#;XnJj8$$_tpD&k%bU~t!Z=M}TR@CuQB z=BE)NP5felz6S{a@<K>qE-rjFfp`hQaThCn(yWs%(k|gRKj-4o`k# z{;T6`-OqvZ- z9Bd;WB3tg6t)9F|V_j?KI#!0=pLWP~+apB{|HOW8L!$R1Vga=41KEdM#n{5f9Y7<~ zwR`e1#v1H?uH&@(6EV2r)Uu|}QH!0#`kc`Ekpk=w%G>>$63ymT)`ug8{8WAeG1>w8;z>t*3^UrX>lZ-aGa z@rm{IGmFEne?&wUte^4QFrEqOz1b(T8%DrEzbC-E#?0k5`M-qn%*~(+!iqsyHZhdK zD~ZD?$<*k{OCRg{wxbtQv1epfhOVUzqom%;y3~h;=Ol>MuecVgp0#=ie|Pg*OG%9) z6&X&wg#p#Oje-UlPC+)Tx{6r@S)VhVqw<#! z+j6Tf$G6eqnMp9|Xwy-Td#G9bpHpE>-ad#eu_)@F|2Y-1KA=a0w zReOZDQl8S}OewXkT&+8M{3LsO5)~hVTT8g78+S_kX%FLvY0NU<`5l)0cozT6 zr7Uq1nEEB$1i~vP{>a>h!*}U}$`RSJrdv!90-uau)1t&O_V}OmPabyY9_^=G{Hf}e zbW;z9X6{Xb=(+fPRcXw{xm(G1pQJw4vMRet39zI5oTV7~$Cf1v`X+SWC;rWGI4+pb zg*&xa?VJ1s6RsdQGq+7qES}sUSX43jjP<{w)5BN#sN@76wmz}-h=bCBVQH?_5--fv zXmc98{hI$r5xuLra)L-78=64Uz6$E1@oe$t7U3h-bX4^CCar&-=%KJ~z~k-A^Eo`+ zq@*9&&K*DVtGR72EO7+$jCJ>6i*K&5rn^|%P&h^Qj3Zk1Ug1$%!7<uiyA)}V|l41Hx?3||XnPY;!1$kNUR!6eR- z?_)%0788s%mAmtOJdyN2vwzrS}r|7{dLU%!T1S0?qi9 zCvce|A|zTpFwU`+UfiT^=^;$2)5h8B1x~V$p+^!h%|ohMvF9+}Wy70#SWbvIOT*Hp z4iHn(;hn|fbbNX1et}k;xr`>HasU=h<5@YuRcFO5daL>99tCm>dKLkfvsTp^^I&S1 z_Pq0Q??r8A`DMLcbWLYd2NDZUj9=L#XS2smiG{7>5mwc+_QsW)v^QFA3VbY)0MRX` zuh(01BWSG-51yUjHXmAZMAC%&x0wTeXZ!f#V3FVX>!RQVHRe{_M|-bzhLc78qgDQk zB-V8L3*T3|frV{KHz2<5gmh_7<13dwj)MznulB}`J&uOoIiYke3IKOlukXFM=AiM? zCO{a#nClb-Xcr6^RvIu=lm~@^fS}^M&{OFR3;c!usUSFX-hjZ_#=_+MwcMtG;c8*p zexXXgE9uw6q5BqXmsM!RXr>dR=S+#%I@22&uRZgREV1lHh)ih;Jgzt0*!|2c#M$`Y ztY;YQN_OxU>b7wRcu@CW(!JKX3W=C zmENWtKN&zUqBqHMG&QR5@O~@=)xS#f<}#kEz3k+@-{Q%0FsFLq-H``79_1y9OUC$c z@Qq`MrZr*dCxg4kdv4-f^Et7%*dtLanXViWT#%?*8ZNu{Kp7q5*3=3oRbvzgrao39 z~G!Xnk43Bi^E_iohC_ z4*I;raTT=QDFIqQ_el5q^qDc$&h^3x6_iSG4O~iO2&@SxP9GfqSQt%?QEN7eKWYJ! zG%Nu(ha0RfCQ9FX{r3NxA*G zeYr!q1KIr<0~tdZgf6lzE?OEa#vbx>H=t4eR%jCuIdyLmJIu9~;PpyCAqvx8lO!Ys zlVS<6^X%ZYbII=_7s_jghq&HbA^8U3n%{%ZVm*yQ<;Tk*FamxLt zjv9)n@9qz@o!ynkHJ0;=B z_*h+-p@iN{&s(iZ}4*N{Cq zkbI=$eh4#g=^C7gc$uwUY;5`x-EAE?ylGXNXa_BGx}&*_Xi}YQnf7nKwqv#Qq)O%| zD-U8{A{(W9ZHPG%lPwrIrZzPMfFgtH`xDo3x+0o^;CqGHIyW9h`*paz2@2~J#k9lu~!Z4_Bd;OL@kZ;#8bZLve@ab>_Q38Tw( zm`DP96g4;E4@T6JXSdJ_r%g~+)fsi}=bA*XzbNM}^gDe&Cyu8!+d|^hcrZi+4EGWo zEWYSyA`5GWk)>5X{PZkL2CTWqLl*t8Th0lx51AVuDq057#`f`Vn%!e#RLfZf;Zr9o zPKY9hFm9bC8<~H&xV*;-;AD;vH3s@6ss^V)5G3@+cu0(oc2PIZMG?L zX?25nMNY>T7z7B{Jyu>}H#oN7rx&^tyXy_sWBzF_}5R+9P<{x7H1 zJ3mGhF7Z*coxjT3$U*E$Z=*SIIYnHeKGM8hnefQ!m7(FU{@Fu;%Qik$uk0L73|zLV zls^xrcu{FywrD$_mpb|OT_I}k_0tKsFY#td zs_*3KOY*duw6&8%UzMy01W#$BUIVXttzRe8CCQX?2TyWFRPrckYo`YL00Z-qE;hEZ zdd-yfPS!im3DVE<+{74w@L{}Z?}tIYwEJwtwj_65EH^nn|Yd60LM~ z5@)6+ttgC^d0RtPTUKjUA8Vr#PPggSw#X#YthWCEqm>`ZG#VmQH&hi z>W+c|Gg(U?@az_9-v2I>)U^2qkMh~x|E^H#W*b2tcZ>BBfqUyb@1_5h$Nm0y%1V%@M`Hi_>(kwf@2HUJ_W=SJS8PLt!9zVa2l{8jDQ-}35c(Vl%xe!H}1`}xH~ zU|SuG=bsv^=iOsGdDa?jVHans&ANMA+3k84S9)|L$~n%{X3p*oYg=vwD06*OUk9H^ zzTb0q4d)P>u9y#3ox$+GnWN6al;E4h;%71as;r}%KMvgn>rB^n9)fR9hGD4qgdP7O z&<6-z%9Urqnp24;S3tcI5dAIQw4IyMVRMtbA1O{ZP21^8PY8T;jrY<$$wRQZ(z~>s z1?h>zMT4OQE_^{)Afa_;NuX4%as%8fGzQL?+^;vP_GWpcJ!h~iJ#>5Ef-U)o9v+om znz2VPK1;Ekaa3bAZq4(aGMP{5D_`_GxA}`-WQTthETdf(j7L35^um{juBtm&8n;j-%r^OpUm=u%zmRMPivBQ{FO5W#R8NJiOn<47!+PY2 zp!tvB@0j2DB4ubYFIP1w^bu{6SO;P2wrODU155lmLHq!OrtPY}Jk)5xq8`O$)* z8<8#uv0r#aNf*IIXgC)e0W@7_M0aY0ax>&vt{dTSn#rf(RlD$FBtlJJl&jSeN?zv} zdib6zG&k35H%k;<--DQ725%;-im4mxi~Jh>5!dp4?2B>rytbnM_H?m{?)K%qL$oQA zf2lR_(Y1~SZGMX{?}ZtXQfi5&NS+*(2da+dnfAg|N%@gEaOqkU_wxsQY459)4d%eb zYl%-bzn4>mymuy(%P6$1HnrP3aI^NTPw;PfLB8y}3?T=C6G(Sm=QHXId@=!ZgN6Ge zETFgKytQ>Hfo&4rP1WGu7y2{$5h-5M{td?bGW#|XzC;xzCnVt<3B&kO3gne(xjlZ%9E3S- z=N$8lulOHI@aJ9Q{;H!_+ad>*Gs{!7XT{ts5i5e2eiHHPx&Fk8Io6YA(qj)$j0kNy0#}MANKJfo;nnL z6v<+b7Z@4~cKn$~v`5mE9WPa*ji7RJ8S;>H${f3oe?E&`-`s@1vmbOZ03wGy0Q5y3UdFC5Nt=V14UHjxEyFR)8<|D%=KkC@0ymLM0{nqS$*B98xn+I}xw$8A8-IYAg z(dF*p*kp`(Na0oQ27E*B6oVgq6DLHr+@Ad`F|5L%&@CffB!2Y5>&^P1DbOIAX)NJV^&^h+W%sd{GaqxD(!7tdpmQW{onf0%+U&l0 zj@o3*`dCSO`XKQj1$xhn)atppCXqz{;-L3PC#ky}H*RWPVtXV~u8(B!^pz5)TGete($wSq28 zntROw33jgPTR?RR16VYM-jG$=O&AObNT`DA5SDD-Kz5h5h0AnBh^3$1+LlLoah%qk zT_uSDE-oURz;6yK#UQEzce`s6<0Fe%y?4+e6W+T4v`eHI?wh!sMFWdB1KEbe62*9E z{8oR#B<1Q>*FSK6MJiH7rj;z zaMh;kn2T5vW&}##a%as~@xD0A>nGN=R?x_n=cu_g^zXy?pG$KJ6b|%4&nfE2J);t$ z5hK}U3?GOx{sniK(IGTUDG$y>^H3j!?(1z%9$Depo zt^wa5c*fEY_}E>V!*TG7&~mwi9ypCRX!aq3WMURjc{?cfMl6~) ze+1Vzf@{WoH)IP)?42%_yTW1AqE zmtHcyt3q4@U=)(e5UBpjU&%}bOOLv%`TKzbsMKvJLYz-?VZgeYVhO(QnoftauLxT} zUvr&UY8pcp^PN)^uAKY2k~wniOzxX^zx$8O3DhZ!B4VLjhax}FFQV!^s_3{2EHUw9 z5(;5PM_XUn*3nAR=OGPH{}yQ@#G&TX|F8H1ub+oM_YL$a*1uDsf)S8ee+zBVT?6wb zH)cW`MJPtJr{*R1C7*Vk|j{`s5~#{c@%?=@dF!vtYY%? zII#K>SaFc$S*4xf&|RJZBKUuYbpIm>EVl`}*HIIm!*e+SO!i!;={coU$P`0|#&pyN z9ar#9Rr+mEIK%%4H~Bt^uHFZ>ztVp4Fy!3vE~P$GK#Z8Az?Y+TO!Fz| z8Tjeb4|e>7FYj#6UFR9dDg9b#UG))9{UpzjP21AT_S!IP;?gyq zLrWy1Vaaqa7%DG_Vn_8vuru73C$jyIB!W0>o8nW)qnK0*g$9s-a3V&nVvAu*&pwwu zsE^f&!48EaaPmmVpE*|jTgKHE!=?ag9HZmc-skPy=yRrF^6M+l@_o} zq6Rw2Vc_xJ-u-fg_&L^UUzZfgrCormJ#|isTHygwbHCDzIj+RSj zZ~gmkZNl$2$>_6iC!_orZzM^pZmK5hexJ(S%s!>Fap(Z zS4YQ}rD2vuyruw}sjWK#>mMZ}DYVSTa%+d&6k&+_VuHw}NBeU4jqbO>f3TN7+Ml z-Wrr<4W0pqw)3zy_ek}^ytGnFCDvh{0dase{LkE(>u*N{R(`9Nn~6~q!9cQBt`x?} z&dR?T5WSs$d7&>D8;GZn+*Nu3Zd*1xwh5IqZ)^}AQ9^%cxr$N|TC`jArJ?(Fqarqi z=8~CiOygNx;sj>-#Ri*?%XeGlXHrFZiX8fBTc#;#C-=C*FhrfXTjov_WyUnKfV8Rl zsqNyNIs>HU#n%pwOnkTD;gv&YC#$LFE6yTq1!)WUeU#q?{I23Rjo%V}rxJ6D$M5sF zM4NM-$8~z+>t;O0QnZ2A2f!9ZiTiM_o5(*P81; z(;hK0?w0%C=9wGh{ula{Se}mio{h^#n5#zx$ujVU_a^R*5}Kh#kQt^2xPV;`PO|iH zAUKwX{Mf4+?tb~o=|r201EDsz0Jqh-N1&^$ zSu(AO)_clSI-ZkTdE;RF)3=fS`I_Xf?ujIs{99j&j4|OYX}kHDe{1^Hai*kj>lo9` zO}XL_n#Dn_zrbt;8KEq_){hT!a6AK~?9IIm_^e`bM_Dy%%zbo_Xa#1?iNEU{=6gZ2 zxi-6H{f*QlKFb9tFLut(N=&QDyoDo?i-bJv31#phMOD0)ZW$Zx-g(C^ANxe1N?lv$ zdm?>4NU@BdeD&4`U8xaVQx_XoE};_o1UtG|RgY6u)VS^*hQhWV4l4J+3AIJ{b238a zKADzZh^uElrT!GMK-e}MX>qcyiTOG1*(7>|^|gtn)m!^nDNpmSn}6~w3@?t~J<1)f zTmlVAyOwZEE3$LG6$i+Crcu`R2T&Fk*zNFv;mOKiS;2snbemEN(hZJPpaN3TvTj99 zN!SXWr%I{NH@JW|nmy|kfa1evt5cbyGj+IH--BtsN79*qSyX4yPZtGWe^K-Y7DKzI zRVg3WdDu8fY??P{N?8H?g$Lg}n`bjdU`$@hE_WySo=yoRdK+xMF+j3Jo1B!-W(7d3 zH3mQ;KgqjrX~aMZNnZ-2cl<6OQF*IV;X(mOIx8^GkPk=*dD4`CDy1@`9ZBI`IwP!` z_~xPi6$iZgKHoeE4q@SY);5oUlqd{pK?DX$M{6*!Oq0?!CFE$78h>Hb7#s1uvM`ox zmtnts|7WDr;qmUo4BV=2FY%c@6H)d9ms20>U6tLnFLv+DIW^-a;vcqa$34oj0XMX$tBKtEc>*(sSuOs}S4)5C z?NhgYj<%IKhT-C9o5WcyVvNwpo?_T?an{KPxt4syzCmSCx9Ru`X0v%RBvwN2 zX$6NWE-^(n%fYZ`v)l4kVrV_a#!QNeOOj%wyS~g2TX7P(Xs+sU--ur8K}krG8**}c zJl$rs{|@E7l@OXCR!Sa|8{H$S3>7h7_DTtFnUhM$9g=kO!I45qYa}({EsM}ME6)nZ zVig61Tpv{gxXE5Z`u8ygv9GIMVlVa74r!Iok%Ls36(XRb)9CgLB<>NVp5CgUQJB-_ za9zWssx$A-cB28#=a05vP+M>X{(!N69ic$*g7oy8T|dO+1*15^Ik-1h9p~szrZ&M2 zfPm-8%F8Oc4#-DH8zfItK2aT3GWp8!kJ*s7Cgpq_R?YU4EW}fy^3m7;1@{{>x5;&Ne86{TZib?8|dg9Fv_o*$EtVWL2`#&K$ODTyL|C7f2F;W z>_x#jzfpzb+t~aPvp9+jLYsX_Ayg{XQ;R!8+ttICNfy}s{7ZNn>wYy|Om=*OhPdr!5>U0ioM!77s2q2e$ zkQEe>VOXx_%R-_nEwK|_$z?C8USVJBsT*=Vs_ks{3`XZi!@FuC%sCXeJgKT?w@eLV zvMgxQ7PK-oGU?W7Bgr`+v*W(#)cCFp6|9B7_UO>dUceN}kNXfM{>ei8>a-Pt$cm~^ zLlfgNMk!xqlsv*lLM4bb7T(GrpezQks3`bL!}JNV&BBr&vYo8RRwUmPMYh(^glGw2 z_mrsqXQ4ES|6qlNMP$xm0hM_WTv@M(5KRhkP{5&VR8qiK8YQhkIV_K{A+?(x;WtFI z&=0AN@i)D9ncWLekNfVk;9nqq2Bj%Ib26>C&WidB&la1w*W-U&{sC_9=#8=2_fUFKa2ts99?0ns2T0$X^ks`8rE%Uvaw3n$S+sDC zg7N5udnoOM%VNGYrx8OQ(b9x*9+kBE{4Dc!ZXBm2*LR46=dH8)ZTE)8`4(R_pY+z* zyn#;{*IkSIH~|aE4R~MHc9jhnv5LxWQlr0`m^-D%`MXM)--oUje(D<^+OO?usTdIB zy}hzTaqAm;ajseHUyt80o(^F^NlbfZ(^r+Sc?3hDEbO)XLPKcD`{Y=MZ(|yVpYs6!0apsQBQA?97GEf&!posFN}`iTZdxa;9k4_tOm?y1eas4V z(Qx~)eCcc#&-w^r>ToslC?WSHippHn!-$tyb^iE!m-R&{)!)wYMp^1su}%9L{|ckO zaq=biX`{qDNj%4S7LMGm=LWv$4a_kqPNJD0uZfQ21|HFt`yRIXE&1Z`WAS2x#(_Wh ziSPHuEh#F;zypmOO!<(O}Ji_8@bhbhNV{H}=h>S43_;7An}4IjuSL+*K$ zwP+P<>h}Rl*47fpN2Hr|tWB=fEK(b*m)L)V+#}Eu|3tn58Uh_MkFt+Z({+!rtV;|& zn_eIIB(}I~vaUBOkq3D%x~_L~$A$1YSDBK0>X3Z2Di1Xq($8d(e#8$FaHd9?agMMj zS-;VEuwu?Iq@N@*$wDh1C?)x$93V$HfRe>nfG6knsFpi~NyX)jG69vhLnK9nojB=- z@zeJAD$%E_V#pAEzCjImWR(Bq0F1#t1qo6< zX z%oXKK$4hEDveRHXQjqD`qZJTn14^~|Gm|+ek!=VQ5g%nH;!*dQw-YFRn)6Vc*Q&%oGes~1hoior0-U}(U^*TZsuE0ZUi+} zK0eYaw-9bcy_$?SYp?d3l~9E+6^(L+Z)LqqM;MhMSUu`f5wrewPA;^&!0|eftu|J# zU``@itwkG&Y^C&tdP`Kc3dr<3z9&w$I;Jf+#T>N9$yVKYX4eYf#{!fXvk3%wg)U`o zFw^)uElOfDgv5-k0eR!TN9Wm$cu9+!?TvY+45&xvkIprfUz!SiUq1?kdZ`|g%@L9< z3Qkp0x`mmNV@O*w)J(~NrqUG=q=eE(3WI;LxW3!W~caOL6XxJ5g_KydiH@ zsl&wfR@;Zep{KD73r^kH3vZjMtH|@KN~f4=-kLGFmL-~?*}FWVJRl3Pp+Xa5Zl4pC z+hB;_E`)Axvh0cQh#U$1fe+1h+cq5Qw7s|Mw~&pJZC3CiehdN&Vk)RCAb&$ zQJ$kuDj8IHj^2hX@R}Z_%K7_lqrjxxM(XunS{rraQ5+~)BJ<7qWNqiWiE?)J}p7Qn~gvc{%w&M?J42 z=-!HD2m7F?f8DaB!4>};(}vOpeyLsBu2X?f!kRM%!K6MmxGK9}UB&I_Rr*kj3iXp2 zjQd@p)vj}b~awsTRw}Q zZ^ybj+I7Brm7oYLz+__Ue1TFA8(5kaD>nM$uo3v#D!z~GxsV2% zv1F?dtjG52AR8f)SiFQaf-nOi3vrDaVd^7cjqtM%AU~Y1Hm)t-Em|5!69S%G$=>Oo zC$pn;7u175ZOlyLE^{R~OmT-+8vv=zIw#PPo_qL?f`6d|#xzUpiGY6nj6<$GV6GLIFOI-`(UROGi3t@K<6z#Z z*8=m!IGBrak5?X45~=f0bMTJ&5-DI9Q-*wovG|iI`Vr0mGQ3Zxnd--wP}od(Db;Dh zW1%QA;YFUjLlM{Y;m89Pck~m|Nc7Xgk3{2E7_|+rcVe%S;=8eg(xdGOmqsRRBT=~u zM<%S0$3)X4ZXZX$F-4kDl(g&hNXr%ihC~qPO_`tmMn1{^$e}41zE4<1DKhR@8{y?q zUi5}&wcbd?{kzb=#em@#e(UW_$J%~xI1lBvGx(nR>13thtxt#!5=K+OkNC`(f@@!m z-$YSfKeQ)BJ|z7+9}NCo(fJA~`uEx?3!GTnb*?*7fX6{_uayt0eB=0wmt%0%zsm!+ z=j=>6Z~_(eETk||DXqiufN*aL3X}ZR>oClfW)MT8c~hf#7n7H%pf@y(QTgYqV)`T2 zhWWPqe{`CjeN$vSQP=F?7e3>6Zf&Sq0$7&ErOR7ee<(kqk>uwU{_^vp(dC*p_3eNt z{u-Pw$u}-9^%qv^Rm-Ws@2t_Q=7B(q=DTv9~CINiV8YDR@;>v2|^T@K&M zYOzi^O(j0}bG~aBqiYsNnpknXPg_hvVAkZ)MBhJLH3)3+FcpO(8aILT@0Ch}-?~ML zTSem%*0%qA8@2g=B7e22U-0)|Yrx~L0cNvS^>4h3L;lZ2Dy<|&-*5Ppa0m+7`cY6S z!x3z78dx6?jns_l<(7BX(Gvnriem1XWd?{(ai2z|ute`u3e3$>Fh}M=$18kgX`L4~ zZx@sUHtw!~(y@>~`q5yd21~sDI0!2rHaOP&jsVYe~}f;x9?T{ z5(?J$^H+HA4kqa3CmNoSPqN?DsxlZq)O{&&)mOD5({m@7+xm<-F9GW-UVYd3mRf?p z-@=E8(ktS%Ic;Q-GEg6J6>wl?Ae-Y@+G=ZUZRvM=@3mHMeImg} z6M_jKAV}rm6MUXwpay&pK$-9V-}{`I$pmO?`+dL5??>i5_TFdjwbx#2?X}iktJQtA z|KZ>j&cGMOm=0s&SLP4Ul{i8b8LK!J#5MC@k$9eCRg?g&9fuiUDIw9Ga0PlB_w&;H z5q9v#(v_*FyfoiAmos23tHta1OcFHcV=*y^q7hxVf?=qpY(iH@G(~H$7@BI#9ni7^ z4fR!19xwtcg2$j7qt_MI2L1-4Q7#taTXl)Knt9e^Eb3bP5}yI`!3Dk5d$Zqn@$EkA zfqpj|{R>gin6imEO%V$kDdC+)Nt>~tO*0lx#VhLHmhz-@WBYcjnVOEl{ADtZFmbg# zktH{H`cK#my|;z2Hol%QT;c0gXaK%aMZj2|HI|e!v~Uxs!7hKV_C(J>H?th)5)oMh zllVdja9TZlK95k^(*;|AL73{|CW$SP<6k8b;}ZP~JtG{kL%4>s`s<)g53apr!8YxZ zl5IVY2apPK7nX?it*uZ!CHsN-cef|{26~8t=JR-A)NXB0AaB|pd_j+lXS}Cv=6Bf< zAlHaJv7va9YAS*n(~+m>uNDP|h97yk&_9ciqqRnSEFq!Pu0M5^rboWd?q{_PljM)V zb|ZOwa<|98QTDsxASA!tHp~r>Vkrx}L?;KpbGJRwJJ1h6GyAHJsxG0sfj;P@7-)AA zD(CR80Ey`66>DbQjq;enV8)aOGG=tdP|m1-fsoF_lQhxx z!iO^uTY;0LhwEUzkKKen_HgP}d@dv-LZzX5D2)Bv;VQ|vugGBr2=q4(_|Cl|l9?6)MvMkzVZa$ePI zVlM=U9LfANpJaZJC)gi6=6Lzb!xQ|?mkbaJV95cYnB;p%c0~|2au8P=*+ZaAW3q%i zl=evu2+MUXc;D~0Dc%Rw9x~6PH>HuLz}VsKPT3>ar`Qv5sFo`0spcH3G%_*xnPmfJ zr@)!Pm!IVK{5{$eOF4|!-XT2Mtuu`9BeiH8HfdTNQ z#!Ib!SK9Y;uWu1sd7RC2eGA)C)xQd*Rkz+IM-TZB`-K|gS~}Pu+JlVBE!Gzxf=bzL z|M!ebDgGilK#Kok!Dwj@#O!B8Qc}$5p1!_X=S-t5u8s18}DHVo5ike4&akV4tR52 zsTt#3SB2j1h*o-BSPF$e64x~~iE*@-f=jHiavhqT8*poFy)W{_(>cN-sh~i zeHhInT<(>qSA%DHGxrSRu;oz9<|&}TaIQt)Dxnr1C3XWl^U$jK{7!UdovXZu9-)@3 zIDssT)>bJcbnFxS0(g_M1C^*YnFw@6rfduG`($!$CbAYctsxj~D-%#h z`eI{HH~=PgW}+ZY$+gE{Pvb6cKbRsgieAqsP5_MMx90$R6eX=V3xJCETanoq$VIku zx)YPvqh>m>wh)Iw`&Y{ypDvhp8}s8a9|WDo#+IbXi+z5M*m(HOS?6M7>F{i3?n;EVKQCseHJF?TMJh8nVEO6K=IydVR8<>Kg#NNu7%0c=xZLnPGWKmIz0-Q zJRyb22mWeda$w=VhQ(U+?_e?G*C;GLVqx(?{KBH*!+$2X(J-$r;G9>qW+DlEz4wLI z&5#r|4exILIsCt*b}GnO1lu<5^T{37vp()UFrPI>*vGvjsO=2cJ`wBbN>`|rly(BB zY$eGr>XpNO>A7qUgF+%y4p&F6H7#>qP;CaP`OQIhby;icFI8)87``jl+KiWz zYb|{UODI2L0&1inBj?mwYF+0PJWoz02o!o+B$piILUdy)?n}Ac%oTN5X7JPm8J~*z zsTT)rhOu;;waoMgzlo*xmOT|IRQpF6#;=50YGf8AK1KH@rs*`U#JVEw*Oz>cD{byq zu?yr%!^GwzDZ(sm@wy;7Ay*T?Ba?jPN8TbS@_(2K=T3zFv!A30y(LpAdS5T;ed#1c z?{fi>^8Wb53FtjAE0+|WN2f zM$02(<#-DD%*MA0d^7J+g>OE*l0+Ln3g19ktfiihk4*;RTaT)qEWqnp5K2 z1XP=BzB;!HzD>GB;ae%bEAeeYC)#fHdr7q-?WJ;kxY=I|+k6%UDU$u=EP+7ay~iT# zUi7)z>T`}o*yWi9^BnXU*hW#h8W>lTBJ5rWCQ?KREcn+5`$fxa!mb%OAeN!z)58}f z>3Tn0Q97Z#0-An4{tq;OU6`~}k@fmavL215VVSLemGnGa{~Cn;6*6r!fULzg8-pe2 z7MeGUOXXFR?ziGWrjCF^=SY2pZ7wu8u}|$4E4gAgOL8u^;h(oWv39+i?fSQ3SY=SU6%O`-9LrlHfB^ z4kG8QQ^98t&E_{!Xs*!=7vz6qwrfo2HfkHh5%c_`Ss3qq zM(qP)HwQG|B_3RWek1}9nHRjO=4dp+z-@}wu7@JaD5k``I@zr*^Mb*^uWcwCHr z*3u0`;WK*(s<}Gz!HsD>vb%Fesyol3J8-mf#5UoN>I3_&x>3q*EPNEf4|dHut8+J4 zFT{Fe>PF!4=}x$J?Md{nkmST)0lS7Jh^OdkXyLPHZr3x|YS6r-W*oKBG@@@4K!Gdc zla*G42HRa-8CDIKL>VNo`X!I$2g&0X&q%L?NO1dJRzgIja4QmsrAh@Mio?@vyZUlRS*yEq?1n zuto@DwdrX&^`_^IB?>5 zCia9RGjiv`v)1d4+D+(D4$qoqE^6$MJK#5bvyf(*Gz(&yFnNZ^CO@(CQ*w$=>pI0) zj-B;W5IB^E(o z;1EHlTaYfCqo^+dl{0GqGHG&H!@~7%--Tx{V$5k!ZN_by_C@JmpaK+Io>KOnzA6M~ zx{>1+tOHA#q_3)SQpq_TkEC<9zt&jfYTlFe66t0%w*s*dlfg@rJ)+GWYWdv%T|=ii z>8V?D0tXEJX{RyN1$7THJ1?E!bOnzl0z0FXPFIc6hQ~KTb!6P(ZL7*bt2pIc5owA} zjyeJbVprpz9((Mu0`u_Rvbg^qv9tNp?D$QoPflAAdc|G#j{inuAqXczdf}5}a}X|W zq{|rVe%hJ(puaDTUfV#(FNBFIzCP{qe6c$Ik<#~|y|Ik`BW)uHr0Fn^!cRJGh8DRU z{ytFUSLFsN6rUE)4K2!XAk&8RB;ZcLFmNXdb}!HJUk`kh>=jD;YyK-c-Ix~s71odW z4jz^;A+iPO@GtNbNJkOBamH_OwB#Z{qyZab$jMs$jG?gKmRF2`OYHfWd?@c}O^E-> zd=Txx{Ls)^<#fiEq|JCz{}tL_jq{zaLOp~??+&BiQ%*=Zg~d(Lr&r<&u>39OFQhbR z{-4k~+x>Won|8nDzef=yaEUbI0lLVj2Cnu*5B>hfq0U7h;X~vRA@eEfuO)D)GgfRnWLB?_~ zUGgH|AEC!8)RLWSb05e~66|{Te{gMN8CO-vZ~kmODjk0P+>K>xc@LAel z$xh*oF`R+&Cuv573p*p8gYqDSOtd-?I+CS_pmTDh7R^TxH&PR>N7``@kx}+vU$sO6 z3JVAM2Q9c*Bga|)sQWoI1-og~d!;nGq~kL)pS^*_H8iyL^Rj!i=nvh2{bgJ9r9&{H zXzrDVw;AORIS)pWD82F_H_oNtP|CQPYs`n2NKyv@UK=B9`*h@CG!$yIfhYmEm;i+C z$^|NaKVHXvg+HKRk*Dzc*^1P7mw3Q^_~Ie?qSLD{fTVo2(0cXRicYV0Sg)^7d(9#l z#r|v+yZi*j?Be!sTEFI7)o7-*Uw_lzv}Vt>n*Ae4-8XIS-&HTITg$O^B&g}Umgrv& zh9yxtiIz)lwF)3Q{fP@yTLreBtN?eX{KTJ6R-n68fMUmDDl(WsJ;K9j*epV>&w**h zW6oXfyiP%QO~# zfnSpA9-!klVg7BEDah+*u(b;cYVY?LoS1o-kV!06V+zX!;i zv1f3NfZ`9;qm@y?Y?#1lb4yN_9vO`S*yiS#@pB}&DDnV#u1|F{(A@kOXx+>O`&=hOAxdS{qPy_ZW$|ZIzn{hd#)a zehF>dqnHz6LM!b!`njPF4{1tYqSXWzC!%JxLBPK5Sii`myTPN|I0%w^Ko zs|bE`p<~^o@TL!c6#`0+!@DEv{W+my^x}a?o}n70Jh%zY=&FOK8;jhkM9P8YRDHA@ z$&@|rE{}07WI|xugUwKl4TT^-7y?-XocJILq2Ik>iq}_-^7VjB99jqk{5he8C-K^0 zvFx@g7z{uks$8_f=e_FJT9J*jJyGom3`7Pq9Pxy3winW%OG<;iqm?dO8dP=qSip9f zcLOt}^mQqsH=twRHLmym6ULTPzyM>B!daTQBBOVG7X9Qe6(`{i91c+t}$k5V!P1~^zZ(aSPY<|9>|sM@g}80AfM6Q z7*k>Vf`X`$P9d3ARYaEZ0fOxImd?0xPcsgJsLQwnD+lo*$bD51ES#QZi;T|06Zbiw z(O!v)dW6Pq>O(+!OU*L|fTmAIcqYHE6cm?IaSmdN!Pf#c^NKq_zlyDgT3(@ak1edTiFc85^g{Vw3{y}IsWj%3tr)?GdD}?H_5s@?1mRb z=7vy8gUNNGhi}31K)$wtmDmaof+j*OpU){O%=r#F=eAn+m|t6vLVIvus8sG&^(6T} z{#>qIl-AN1gU8>bn8EDT5K7)mV*)G51ipJWcE?Zh>Sm}5xB{*9g#bUfb;P-mGx1*|$dvALHA8>NyoFd*~A zw6?{3<8&BfxXFgm%v%vZGqpXSKSZ;8B(??!IE8}0SI%)47xE(mjLL{38oUhGWy5p) zJB(Y^e)u*|U?aBQSf$xwxR-mJt2^zC10cT1t+;WAyX4G)0;qTxP zSCzfZ>)#Cp5c)!R!Xpt5;Lu`+Z(bnw3M}Tl|qj2{Y(RauP|t#Ak0%Qovv<1C|b zubsP$_M~N+GZytOi&iePcHHtHr^bEvd2`eAr;|U6pN%~2{}(^|`gD7nlHzAa7MzNo-FDBZ_}Rr4N1vMsM@ zzdo(_CViE2`FRcJHXwEMQ0MZSoDDZR8-{_54$WGAQ&z)`Sq;PZCVTl!*$p>lHw?om zo1xnBo3w@-wT5Bv4}Id z-@|^VjQcTccPyWT)ON>Ml+$GgM71r-T1Kg~vQ@dt`+ZEVF)`1q#mOsUkaQ%?MvQwR_ti# zhhi~$_*M3iirV64C|T$9fM)hlr(ZY9eJP=CEEOkiC}oWt!Wqh0-a^8vNa|)`8cWm7 z_CurTkjce;k~t*7b3Q-)GzVb~7r`yMsAgACADay!G*chD7`0EKkL`84Ao7@%VE0fIQws#o4*!lUB)VPf#*b$=SK)T&w25f1qY&OPTkbTh6sweg(2=X378G_nmvT z1Vo*XzX(0EbhMZ^t|ijZB$h6@&ML=%m8UBAeXHDCz|B*Y>t&UD=I@kyA3JRJ&p5O*X+hb^?nJg27|u~M_5zu5Bo z5bM4b9E9GhMaZ_vXq6?HR3A3~?&Riw*MOsVUaB;@NRuTsx~0(Qma<*(Cd2|CH~08o zN)e&X+}_KUlw=GOJ~UsOBPFHSO%zkTX(U}La9u?LXqNW=*6vz}A<$Z+ zGassTSGRH#1OARK@OUnxaP37bBvb5D@*_?!$O8ge;A|op=D;mRy2My_TMrOVN83oS z7?_@WijNz=g^SPrGy3C>ljMt9yr0}339dUGkzENFEwKZKWO06uO<}KIYaQj&!-KFI z#prcqAl)9mF|E)7@V`S^)QVqIfiI8Ac1^im)8R;3tuO0*$Rl-W%c5|7kl z^2AFs`1g^S_}d!s<1c)$+QIl8(J0@qnY;qWy`1LYoeCBaJ~SR=CF40+X~g?Z)IcXu zBM-|6F#vH`bE5eI_#5{g5k52?@$v6-`78kcjG33f3n%)Z?15}jJRN^x03FA{Kzw-`llENM|->ot&8_Ov~wFxF)W4MTfiPxD_@*gH`(@IRWro63Df;V^T2@%)aWeP) zlZ__$fwN)C2eSAyySxqL)G9~>)+hR}B*wDcWIPJHKPSPa))DWk3bwz$RlaNcF#V-9 zpL8;^$_E#IkcO?PUdhg+v`4nStq`nclnJOgc>Ksip?(R`4+l&T?kZaT2459#kinb> zR;4r!@GmM~?+CbthT1v;pCjt&&VtwUC6qw*Fh!hb;d&hX19wI`VoRUY#1s#=;~hnw zv+-M33tq@@;qDsqe{dgKhtPwd{T*6hHaQ5_!f|{)T9=5He62bnT4dP8fx}i@Gkn=jADNsVLyMFy8S2Oe6cT&j2jViG^5yXRHd>>{W2S3vWts7oWjKg7>)2 z58Q<4m_~28L+(2c!}cV$f@?v8paKq@&GRL5G_eI|F=ur#SE?yaE#uf*SZVlFMXN)L z=YYey+QT5O_yr&3Zv|K>pasr~AU{Zz=bZ^D;O!fcXc9K(H74%Jx*PBt?)vUv2~Wo5y?eGc=@{6u4Tk5{3KVKYiCr@ZZKEWgqYY1hkfyKvc`e4jDc? zEzNtf0fMyP{?eYo+58W*Z(I~=flqM<)PVa99Z_S&`in-o%3hh9C$Oz1t3PTsp&ach z?s9DBf6hueq@=RVb7sL5Y|L{BoaF#Gob9y;yZ&F-OLFD>o0#VsgbDFqF26^iZ%_f! z;LW>$pmVm@Av`Kl?5WatROmeJ{2F}`Gi9~F77jt&p~ZW&RS_T#h%fO1W26TOS`QkY z+VQ5ij2zb0I@0ZkQWR0dwu1J9mX{(e>4|Z4i^|L9|RC0 zz8YgZ#cyHUTzhzw8~34OZi!-%11OeSa4QIaH}$2@v+~Y@_w*%PLg)b1s2-zVw2%i@ zTy?eP2baoT5`L~$SevkzW+Dz1@`B$0_!!S z6#eolJGUL2Vj)c215RqSP3FtRA5B>^TK*!V?r7aUh*SmF8@@ zW_%5n;$W!K8Pj@vWh#j08sIQje_mH{98bIW$wP4tswB*%PevP1*12(e}I z)XLahA4UyxEv8)TuAh20t}nZR`5w*%=%|wU4*vfq{u8GaKIh<2K#@fc0Zxt|A$Gz$ zPzdNz2>JR0Y!N)ja$%E#$a=hk{f5N|U9o%3=NNHa?L@Pzm@|PG@`|W2ZHC5F3`~I{ zOoU5YvJzL$E#j4UGv}jvshkh5MSuK{#~GzkL#BrF13xxy(69=!u?NkhFo+l8Kbz-j zKw(}5g}K(EFe!?nrX@|shpnLaAap$; zDU!*;^*&-j;5wyF-z0-q!ifo^^oYG?A8a(_=C2nfkYBICSFHuVDB02d3uAG}iOMI=Q?+-0cqbJ?Y2R*q3v*ijd0zH`mGXZQ=Jx)MR zaL@tt1R8qKlh;5`?y~4fihd}-q@-vpSx@Y0QWMQbVseHg4OeZ5zkq*W=Cw#O1V1@*HPW{Mq%g?iT^2` zA8QfEeM2424x3uv--%k!Bemw4v`lKvm@WT^JeYzup({R?aU(Ixd`YcewnV~wK)AaC ztr9*O-{vZP4}e9oMoG6@>LlG}nkNp5benkc@8fskD0m&GQDUd!b?4e}d=SqSuan40 zUiXS6pG8WeY53_Re*SObcgDnjfZus=^f=!k5J)8aTZ|M_l^;b0#qGr#fR7pFhmG_cRG}Zc=D$xaLl+h$F$5_ZAQHpADyk`l zVYj8&dvkqmB~WHE)LqCU&IZ+k-WWeD;DvlS1TP`*=8KrR|FqWP0x6QN@F~}Lm;)4> zx%~DKBEvL+)0saYp%Z(JnRF`FrfcJnK4u5HfR1XA@u{e1%3h)QrZY>2R)e*jTZcnS zo;tWpPsk3-PwBEIvs~q`iC-iX)EkbZYkj&RF(H^X? zLNd`;1D~XSWnvw|fpuy>aK-rAI%=@f`WLuRwhuv~Ey&UFA0^sz6`2EYvD5OljsWHB zs~(}eTdhhJ7!yLF1gx9De0lz6C5o zp$z#~aCsW8!oIMq+HbONhAiX9i>p{18+!R*G9R!K6M7+dx{Aa-!))1_B{x?^Nb{hF zxwT^uoMK=GgBg{8FO`DmmW#Fq#D3&H1vQz)WRxjnP24e$n9P6&Uum~EMrQ;}U#R7n zcC=g*>-FJphMJBAk960Etu1ST;SH|=5$Vs2wAB7eAV-8JV1q}x>C1jZntZJERh*hY zIXcPq7sn9Q(#Uls-?q%JK{C@_~M3lX>O^oIs=RwKx6@4hHv6j`r8DWpN#O zd*PVP;&q%Txn39=mca#>Eg=0`N-{ad0D-(Dp<@Yu00(xF7waz?DweI)N3LB%WqPQi zn?Ae)2T0$aIbkr2NB7PHUqcwCF0oBkJ28WcN1Unl(Y zqa&Qr2^TwFNcGum_jwnV=le4!syODLD4cMO+FY&xqm~J`m<)iql$eD{27~e>D(G}oWYjWX z617r}I{02rG)KP6xr8fDSBud9%l@qFhLnyc>d*7diTTb<9q!ZAu=L<|(5f1b7;SZY*E=Azd*Mx7=OV)^*bS2A=D_&;I8 zrR2{WIyEI13h50*h&+ri7p-~GPEM$~qxt=8Yrpe4PM6;03N}@KiS2rXZqgKu?!j-g{2ht;V>-mqY4E4<=fWG=8b9(=8Mi_!lae1_ zLJfEh0DNvZdg+myaNp*XGcu0Km0EM(xv=1oJ7Y%!R{$x9>ya?3VJK3Y-j`n;k9;KJ z6-$ktYX73bEmWtsbqF)?nJ}67o$7R^;lWV!a5?9`FBR(4$&0?uDydH=$--__Uq&Vm zn#Dn$g`)8^V}UL2ul!Xfd0!77f}%UN*y^BCtiOjx@mXwDs=aO@-P4sONbaP(QnQXR zs?#=SQx%DQ>SpXyZ?*QRsSSIkRw#x*{|T`@{ulsK$$iv`wCj}+nLtE}B)3=SykeBx zPDwqxALd_B(i~gQo|DwG=d7{x>^YX6T`oi{$b5`Y>etDHaPQ;2X`mscXFnHLE5INf zw2mw!!114RMx?d547~ht5N1c_);sBQ$mNAas;`=i?J+4R2rAmh6$i&B=4pw`A>q&| zp(ruaYvw&jvXrA%tn~rb=1TFRvIha2X8KI=Hj6=EWfoc5HTx zea58Q@{(9tLFSQK_^fch4 z4c;8@4>V@THCOP6#s_-jLVWA*H_*)}_aSRAD}z3wvL5+@pT9lr%K&T5cqQP!5wbQQ z$baj#D}u$yitAYsi>>&z5R9o53q5Q4W8S9c*1t9!foiEg#+efgeQw&zbLyYs%X{%s z90o-4#{uTK>U%iFl>DL|;jvV?Q=!jLZ}l<8`8=M?Pd;F1QH$KY9J@q&iMyAp0{boj zHbENF!=zcMJ`Jq@89tj@%z(dn+XZq*4iVNvL6Ax&Ob6wCHDhJ##jE)*5*~Wi>*>__b@_2^}y9ZAk*J3BH^vAclrs1ponoSQpl}-oK z^g;wfH`sz&_m)-dAhy2y0aLT`O1br|#MW2de_jPzaJ2p_zRSluL<^T&JNaNIURvu( zDK@vj3cAzW({3q8b=a>GmrqR8@h=Rz2tZ(1Bf2oTj5#i2xEmI*4n0IhBi=<>7a|{_VWn*3;o+qHlUCWk;sFd7 zl#gr7dtAhyq)B!l+LqRfzp$+isvD_ZgSxRT-U57s{i5i1(1-1^^VQ?#RU)mki?PBe|zuQuC#$ zC6lvm=|YYwz2u6@Y{$2O=kyCKz<+936};)aSOG|v8{apkyLk5O2Ddm^&_Y#2L5zFn zvfYNRhZsx^tB4jtx&LtPo_$T!d)Z5XsiSP8K5}E|NKbuuEOKD}AcUYIy{nKBrs?Gj zJuyhJwOP@`9xU@5=}Q70f7wBjWEZG#jx(V1d($8jIlmL5>J&4ZEBl8J3+P=glzVxQ zbZ0ZvFqjFqvwb|2NdBQpi3Agk^W8S3@ka=MwBTS4_`QguIn#yspMRiqWm(MweXs%7 z#1u`cInI)2S@q}rJ|{k?HH^-lCIZd|>~?B3VTLTzi?tHekAfd<#WjFmC1$Rgdv;C{@mJ* z(8O=zV%%0-ypM~1)ASi1Ow*6A!}X13sJR02C0-rEe}p!f@iAU)3Ot3s>&x)PnazL0 zWiu|;*YX+i_HOW>3)qbk`wG@ZwN_jH%VuD#s{$QyO}tXP!5EMA<~D|FWrqU8%9>|i z!5YmUWw#CIj62$fE9kc3%T_!`+6xB|$Eq0jG4x6YHmz}Et`?KLpm*7!*-S#6hn4Iz zhI@?hUa_Uk5+#TbcwUHgnpb1~9ByftCYif7ALKMgvp$Nb%frDsxu|9K;e*Am!w>2+ z=Qi*q_^SfR+y&b)eWQqRvF5$m@yk*WUIA9BuhJV_&ck9Cro$Ur%MAyn&Qs71x!p;< znY%15TIOB`(d7Hah1B%D3yMlDjkoL_eb^VF<4*tTiKwF0F`ls3Ja&borI6m-@&87n z)$q2Ur%oiQDXok8usF5prh_PDy z6NO(q`<{=dKIS)??J}-)8AILT>w;YcUzC}OPLek2p{-~f`qwbhcRJvIxLte{FUb@a zGi5E(TJfp)2$Z#Kj;4oKp>AluHXcUz>mk~u5zEtXny{>0v<7y@XcOjnQ_#oCKS=r4 zmDmH=SyXW`UeoF|`>}XGOrG<6Sl;=PkA4YqDZcI+L01pcZ&XE6tS0fh_p4Y1f1TOi zckY6cOYAUL3|4EriX7Po9#GZhjMM#XkxKw5z#I@n%!8D2C0XP%vcP!E=r3(HxHtOj zhxG<*Re0jBPg}vQC~XMTPxF8J0$1=a#B5gsP8A?MmBv_x)?UPfQvo*)K1&J*B$jId zzqT2ciZN2l{DK{Nm=>_m@t!Ym-2iEPPditAM<4z!HcbsA$6^mE0@zaevB=_hbKw3o zvN)69XSLYpECw{Gg9eKE2{2xGk8tO@ccOV4!ir(te91WI=TT{xLvIs$fBwE6#wnxE z4Ez;WYa%;VfrnB~$bpXuV3ZxZ1W)*x7;X809O*7)SLO+K^M!NgM0myaDwng+HbPN> z5sC`DLKkUHfVQq!s(5hH3h*?X-Y~~GO_wt(YL6`xRb0F;u9## zgUy4WD$#$r*0mAMlm?MxI$)ep_6{f}_S&2UdhK|9Mjh2d61zY$xszZHdWbetd+?Kr zWO}jcG~4bCk~z{HxRb3KULUAtOYs4Ww_LpI+*k0ovUYt~JBV7~B7Id>gERiU&b$*+ z=0SWCvsPYXKJjx&uHr=#uNh}%|G*w%k@E`-V0c{I3BMyvv{EYRgD)tlxVso5ZRcIH zY1#WP%E2Xo3H27Gq;9j^WuFDUPxe0DRbUPps?E-C8>+BvsKhq0)fh(#YTnje4q9}m zl%J4Z%E~^QJ>D3~^sa6e+9E+Wlscpn0PP}GU-B6XXv&KRK&x^Gu49h?vDV_ZobMqn za)}SosP5K4c|R>{(}#B$Sx&QVTn$Lm7o`dsRc`0kWu{*B3d(~_VefZ`OBJQl#e5Q} zBEqL1S|+EUbn8N#!D`U0t`G|Cr)r4~8~NzJs|J=9{BpO#U+LK)z~}ZFHQe-TR-9Y} zJmF%bR#EENYRsEY=X&$CWVAQqo3`gowl@iVn~bqc#m6ePs`e^5Rk*Lgk0V;`78+Hh z8soo$AiAQ}?5x|m^+c=r)@$A3kQaxV>nVly5u!r0x)8TM8Nw69G+JGR+d|%kno3Zh zNTr0w!;+a_qka1R@4;QUbyp2QRN#IT`&488@H7Hqvb`w@9?70vO(ny18mxIc{)reM znIuOkBDucdosao}L&|2TF1__lxM6t0J(o!Y0a{9JFCWY3gRbcnI3|z?Xy$7@$ZmOh z7$iHCd|w|#&l3S}0#TP9PcK3*7V!zy$Kr@pC@&GnHJ7Cq+J{2M2p_07X*2DKE=&gm zj$@iMIOxNU)j<%sO!W>==jcWs+To?XptsaewR0+D?E9<)>OSdzvt)3rhY1G6|vS!I%)p<& z@Q-=%Tl56d!-$sdD2O}%7CJUVjaRgqm8z{q78Hlf*$03acpcgRfUR!78u|{Z1f|IG zZ*h8Bz40nUC^{c)#vc3kHhh2$LoecpPxv=>*9#D=;XZNkI^wewsa6l>!+drh3+H2F z@aLF0`tV(@R!%A|w!arP4>*MN>QN{sGZNmVT8aP1>L1&mVT%tKm&z9Z#S9Cw(by0O zj|d*`ZM?Y(BF~}v7cmNR&`+e`A;P|`xRr}@B6yp{LSL#s4(j0H3JeS_Zi-P8ZS-fC z^i}K4*WeAd`hNEJB0wPaAx4&Z`7~cP^5u84PeDye9DF-y7 zUi}`_F+aaVsaQxq#Cni?FXa5PEprDWdgC>kJbCIosN(+_Gahz{dI)SD?O{r`QSIUA zgooYZ;4~cizFdrBAkWF^!m%ly%ety0slQS32U0p|#@%l2ZW#K|E#QTF9F$Y0bO@Xi zAD!IK2|zBOiP6}~xubL3Wyk%6;xh!YnX4IByKsbe_Pf&-Uvcrpft}dry_ZQ64x& zz6bE-28&17wSqT9u^6W{AIQ4(6^0QNusc}pAW4y);|D2(zuNp7@tXhQr1v0p>u;LJ zcVq!k#4E6Byc@`oa|C}Qd8lJNKwJE!G*^tjrq=9HtcdyL;!X{z0wuKBgev?U6jq;v z{i9rd^J1Cm<~5k4+qAfzgfBD`W03FWUOB#jYJAee^^N$4ve9JE)rFcr`i~)myxwpP za9F$n3BW%oodRMgmtZKQd}C3<7>$Hj24-?g!Mk&YI@e=Tf6u*8+X%uOlouY$kSpb` zhzF7#LF!qqa>ZW9HR>4wjrj-=sWqqI-MEgQ&@pGAhrY_u&?A025J($eQ)hnuJqZ%b z2JnSgQ+#0BieUNo9R9o!W%0llFIAw4(6J%&&T8ZpU^NL#!n_->g4UpcF5vzJ)+hkV zedbIA#spg3D_Uw{6?;%M50Mw{K(%HF4%i{=b0Fn7{b#ZJkSOqNkGN1=r-|tq0w64s1c9y z_4;Dmb(%42yP`e%iB9}l0DEmU3 zA#Tv-qsPjxp+#u4R#gT4u1i3 z(G*_$kT8lYaTQM#bbcTU22NL?#`s)G&v|=EN}$yf$Q}5wT5J32<%3V~z=EQJUoHYA zK+;2Sgz}y#We)WZ(?aFL9DY|tsO>oHd+UK6*a)35O71*45^D6QRidQr`LfxGDWLd+ zJ8xhvkZU#zoMq(t^ye4&1~l*WL<_Tfz83FGm)Mv-;?=U4c-22zUv+x)YG?TMyV`?C z_ssqvc=SLZr!3~bGy$N9T?h$@pUcE`7#c2f>1w^#6b&w7;h1Vo)dLvfwln#{?n037e|*?#ljor7eLRejf~GN zPP|NlAe9uBQ44~zq!GmjVn4$xeN~+^^wGf3`$K|9-kV#duNs-v{C@Y)o*_NA29LDs zODd%(Ha0u-QIF<5-9hS_KRPY6cSz6Xo|_lLR069~pI8aXG=GGbSv}w2hl>wL`>$&a zcx~e4Y<#8QNT@kmAHJ=5Uskj-%Sl7*@b2?c zSbvN(k892AlWTgKV8}GNg~oW~jwn7{{LLC-ae_UlAnJ%WktjE-#zX`YYbkiRVNRY# zo3=v4rV00?;o=KuM7*%)-#HCVLq90~5=V60f%gGsmyA3lyuN~tP*XNiF*CWknKca? z?9i@+bA9|5speo`Kv+$e|3VbkSDm4++82I({uTJ*NUOo0#-B(pT?v&ZBUnb70HosA z`At1s0;m;lfa3FSF`Ak5RrBNweLN(1RL;=Ytmcoqa}Ev?5i2o+a(4FGv-AFto*R3< zfsd*Q(wBY@t;8?E18b7LP@T~u^fQjp8?E^xcc?A9=jOCZl=7lcY*QSIKT2Q@n`6Be zZ+5&5;cO%j!i~F2BA|(r4=~io4|6;Kg|9sGp3E9vG7f77(aqc$F$AA}37= zE-GP!>oHnuRf*H2;UqdH*Zc{7!*6wp?errR>BK(Gv-$Di- zy%Nsw>9O09K^B&fZx*lJi9)sYl zNa*Mad}iq8C|2K>l9Vud=U~i#P`^*0uM!Ub=GZ|4@X2o!JBSmN7XN1^0^z`m2d1t{ zU%kVLzonpCL1P%g_jK3qf4)12*i4_G-vaFcAjE@a<_7vjt8-NZpME>h4|29ic}z;} zo=eRFVyqitfuTY3fDpb0)NwweI>(sgZL5&VBE&h1Y>1YJ0%zM&`7QwYJ^$?r$P*#3 z1LQX#_9S(i2(SP5=I_-+2@)@|~Ar+~8CbugZ&cho+b12;&!yF@3t$>DoDyWV;gT9b}t8zW% zG$1e_juKKHFnUjF4eF5w+ymZn8)D}?F!0Q%$^)Z7dPqbF85t ze4r3&N6Tdlxz&uoE?Y9ZZ~3=XPPONKZxF@wQHBp{I%;d z)2{5-&#n4T{bThr+snMS+h?F?***;&?BBS5Py3nQ`)BX%{*AEv$J4>=-&2@mD5@eB; zsFAU(xIIi?R=aVZxvU@poN8IoHIcG^=6d1;onk#bd~TQZGzT^8^>o=EPPm@vxt6(} z+R;C(CuzkC##Um~U`oG$w?*2(!WUpondc6d(HKEC?BpFjJf_HpWtf-Z-EhJ2L0y?w z|6z)U|M=VB`3KD_GTNOF#gMrH4-ttdt=#|~pQ2q2Vu2+a^g+9JQtLNh3bZ2xO*#+p zhGwE+h7o!cFUTlbE1b^QuW@77(9nPT`cZa;{bLPAPEOX?h#S*qy}4a=O|>-@H;tcj zQ_>CnX!N3A<}qJ3!c-val$T+r0>~6Zyd{?X575>uhs&G&$bT^QFrL}BdgMObK@86` zCpMuJg3)6~#GMi|@M=sd1ma%=E?061JOPW_8H+v{!RWAf_blkT7{FNXxs`@bU4}3A z%dIL~#V)jn2jt4}pEvoYzLxt$N}+k!zB0g3zS$cjF|C)@%KuPF)km!Lusg< zh{yfL(0{8RU%AA@LOiIV8`l^fzL`ib!&1@RJnb-mWk*H6l$j!m--`E7?2um%9qS$1 zE~j!n#t}N!ecs1XWL_cGirt^BsMx*_imA`y+Z%nHvytax_EPcl|H>KtEKf?xmb|h* z$5`m@6l~~81RIi}hAzd35d1;}>`+4u|BVM?^xL9#kKfiyV6%VJT-gHcS_}4{$8Fu( zQKdCGd=7PMa;sJcQ^@kawZjFlKtO$c*{!>AmZJi7Y}B6FbofpkTKrop8XL-lKlQckQR0 z9rA4b)DE8D-vC%5Rk$8eq1*0nB5Wh)f=9ONYwoCk7ZV%-W#Gl=%R#(@aCJX_^+-3A zNh6pMDs!#vHP)-9uUfho|q^# zV}+=MG!3PC1eBDdd;7se0;Q@dP$b-!O)hd{|Nm_SUrs*{33+#Hy1e`NBb0YTJRXZ} zh?T-P?GWsu95j-L6bH7xJEQ&#tG-9pw}Ky~*Y~OVc_*n~Xw}b^_46`SpT?phRlo2g z_4ofq^*`T%b7vF@78Pc;5o-s)q7o6z=+8lv?q3N&n@ez+2G;PKC?yVB+P~PkX=rQd zLnvsx`~d#8$!lC#+sneA1TGS$vKw$4_%*VDz0rIiJAQKtH)Q@l>^DEfenp4-iwd33 z{U)-HjdMRE(}7LSas_d24STAdf!EEmIUMBL$a2*nPw-}yPYb%(cli35_kj!P=@C|kDwt! z5}oKR2ox)70?UzK!kiQuzvZ%sdo#wHfghu+{)JJN;h^MN%A6rX_7II;VEmTAPPac~ zwD;A+YP>7#_E^44dtH;0*jil#cf7W&v+(peqy+OrJe1)UJClR(LA<1-krJOM2th## zDas8g%B6CksgR2UbF&?FYR=NY7ioro61xX{&Rm4Aa0&k$Pg*6(amJS6I;A4vw{UvK z-+heG+&eLfTk+m zA>WthfR$u6Y<0orr`}G?vn%AWoe7F5 zNv?M*Ug*>x)uuDMPkUxLI;Ur}&i^v9n%II^cUg)4C6G*{Z-0y~srh0QxsqD@bL$;ragO?1_%L%Y zC!golH}MSSZ@%RBTuN^*-R>ZTB88GFEqOb%^^q@O@CC9-lne@(`!2yijlr}jMkjT$ zC$@NKPrOIk6E9K=SDqZ&8UGOOZD2!1>R}?yY7o!0dIvNDm?3lUpxpx)dQOXH%xpU=uQbr?Dn($rwf{xnalRUCVRyi zWxzos*whRs=+J3(KzI+e(y;FodD5NRyhJM{=lY9H2^qG zfG!J?i;q~KbgOW#F>YYQMjR0$EYED-*PTP(*hBz%eT0$JzQ=Dt34*hrd7nGDaSz^! zjVQ_T8-hWYk2VGyCIFyt?QE0)y{viPX~B){vLNdui=3!l7V2e7x+UJH-vcy$l)A6A z@xg=P(Br8PBgv<4Y+vIJS3#_7_uPRvK<$i;uNh~K=qG89^BuTw-jQ?^!0fohA)H6w zjvX!LhgvxertGHpJDi}&0Q(F8Xr7#)zMP<*PdddwOi}zrqr1SOgtC%rh#bO&MJBU; zp&MFgKITN{l>xVq8khMLci9;^zT!`(Se3j|^0~rqY)}7pl*Vk7{VYG zBoX{veN}f%i$d_Z5`MXaAJ5I_BbRP{CX!F&p_(jUIu3dIPa6kv>&7Wb`~`|)zO>NY z0BBz5ZXkIN5m4T8uK(69*I!T#3` zU*>oFK4iEduwO-oY0(>Cv=u+lD|fY#>NpY%CBjfco-wpP%4j6cOq3xbYJ+g#2uhaJ z34c=Y$B^*PlE=*FpOymRKzREqAreFf2S=+ly>g3iLIz7z!DGl087ak>J+Y&d?Pj(| zqm6lsYOhR5d7iQCYBm4BdrF$*+A2IKhZIg)bexA@^6Hg4;F!_*OR}xs_32~*mG;5{ z*sv$l&*v^_I22BQv3;IrEaXQvEn48Hpa7FPh-_qwcc9Mc9@rhv67jKCfXQURRHQz@ z&5{!pKDiO!%I(8fXHbl}l<~-nNpK5hU<>W)_}QsFPhv;vb5~>PzV??H3q6M07ahr& zEYmANP!*7I0EEZs{hN#tB*Cu4j!qmd{wz2|sw37crrG1BLJ>(@oKM=~a^m8uXhT2>Yyk#aF#vwAF#){ zS$rQzFdc(&fU=8C4zo{_HaK5Sd6sC0#ZnKSgNL$8g_cTD7tygqR>;(D0k=893k|N! zlK8naxb%gLMdL+eJ4&%G%yTncwtX&u#h9)>T7Y}Z1Vjw%tF4^m_u^Ss`&^ryR5{H) zcMK(^J>do|daspFc^;mf!k%yyf>Z-N(6OX_?m5)3?Fk+$)p=FG?jy^a#jo0xHBxsSB<)Z}}sDH5suP{5UK!3MUC!n)&)F zuy3i9`TAT|zCJCf@J{CI(|ZF_X$tQvL8zIcuY$_%QTR`3sJhNP_fNLAJ0Bk?#ET;H zY)lh44QH3OXqB$79{ZPA6}s42|II_WIeuh_aQcT!JJh8p!d=wwF}M7_kGc$=egYWt z;Fy=|#2m_Xq=%6ifCqKFAW3km-HUncf&@9JnGV%~xX(m_^VoV9BW)517;i!k<1lI0 z`Ny+U&A&W4(`o+ygc`~D7pI*62`FmMe-W-R|IqI=bhp*>>dapP`uPFM%qF4bJUl$q znniWRCIB*YtjC=9;nek^=txn&X|UN=2ad&GgLWt@zq49pFDjW)JnU>cdYAJCgaGE` zr<5w#g792re*KIIupK=K-_IIzTQ6yodhCME?S^5}FfZ-W?qWPXMZ5Q<+Pw?yB6L1o zLt;Ci-hoX@<{MiF7@Ke7YaBAoO>&w14j0^%v-CRj-M(JUZv2l``geHIIueWO7aa6M z)>DBZ@@WtkjSJ8Nb0NOn_#0l$!Ij>-@o`??!lx;)Xy75K?vua{CG|$ z(JRmLXIwkfe+d7_`8yWbmAW}afHP_fCfCi!T>*B<1Wp()&riD*Ph7I z?~hA9;vO_dBM(st{mkLdk%v(9^U5qtCuSmUV_1>#!$QSGibx)lOr)?g6Iq~`$d6OV zBL7uZ-z_5^h|yNnt&)QNrr+vWOmNe$OV9*%}+EikaT|1 z-{vPW5{AW3+%`XX07N*Ql>8d^nWW^mcm_s_bpn46x9#tdBqeG5q!_)+EXHTB7zG^S}HxTlLR$F?~cZDEM!yk!13dg zFq*AcFXHx6xH%zw`u_s{E@I z$GMms2jM}WI6oa_$Z^2Q6vz1sE~E;b%DdPgB1|SOkRYey0wdgI;=&JD@#MIWf}rFt zXW*lm_^=c$oB|*AIlAD(Hq@~3VJU*KNdF%SVVz}*MJQI>RGc6+QnsojM%c&@sA_Z* z5rW;a9g5YwRh)>QDc5^CzxiyW}F6#yFVtKZ^gsT?cRjPr)W19&tk<^r%K-11{pdkpDvYhhAtXTkEX9lOOzz_!hRem=-XjQ5y2(HEvUWqSqLg zN~kXm_JXM z274<1)=u`jLe}1gK86~V36HA0UZF;ISeopx9fNr6TDq7fz3}Uu7vF5T_>|G^N?gP0 zw0J$sfd=)$J;QESQ1Ok(+aDdrA(uL+8oC@t$LRv&843O`;WPV!1a%+VKa&2e!&CmO z3+`oqQv`>n3EuJlkx^tHy&`bXc)0_g&s0!|2;mD&^E8TQyDlADlPD!8FVaPlI2N`M!Ky z;*2pZULB%FNtsU^OoM5JC*$JJiY?}IvM`Z%As3xQWoK*_u1OS9axG*kDYQermGLj@ zW&ZYj2E z|EBi)8CLyoYQHbE>gP-Qe;3<*CYp3naQ(mckH7G&bVdXJ_(nYVyZ-UHDaO+Cc$S@r zFC?Ar&3S0y-{c?vcj%wQ;rK?Q>YMw={~h`JwtIkp-`qdGkk!-tFsw~|sIsV>|F zHYI6Anp27GgOG3eAf(0n&h&5Dbhlg|FWbcDU-ds&V6|t{AL$&Tur$4ckCT{5e}+eK zbqarmLaV($pqTWJVEL}?t;SQ;-m|zmMSE}GrN;Xtiplm^zDs*u{Uhv!CH1X1^#U^H zl@dK%4X({yT@ipy`U9u&Eqz1>6X?u!i8p$_;y-P=vwk_^PmQmuckKP5+r%2!)s7Dt zraR{P)`Z?YYdF%{y$`$Dt!pdrte|yzR{gETvh+&GqwxzCAO8e9h`Hyj**^Lvk1E^! z*0m;{p=@^jG+B1~%@gCgIE1=4t9vJN)R`X?QM=zIAO-usct3%Na|+nsPGUr&Udspb zIH`ERM zg_IkjvK=L4Jp`6eL66qVM=@lnW

nxbT&mQ*IF>eyvV^Idw8Q2*%!CY!neP@Soup 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

FGhHUij1-L}|C3MVDF*2`MZ1nfQ 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

MNZ)38XeUjCbt51|mSCKHZ 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 M`Tzg@U*Eug0=z;>82|tP literal 437248 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≀hmmOS4fj-9{GZaD4=j}0umY(3(ybNGWF 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 xE6Zcc= Date: Thu, 30 Jan 2025 12:37:39 +0100 Subject: [PATCH 1031/3587] SCM - update list aria label for the input (#239203) --- .../browser/accessibilityConfiguration.ts | 6 +++++- .../contrib/scm/browser/scmViewPane.ts | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 3edbf376c5a1..d9d4cb3e2233 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -63,7 +63,7 @@ export const enum AccessibilityVerbositySettingId { DiffEditorActive = 'accessibility.verbosity.diffEditorActive', Debug = 'accessibility.verbosity.debug', Walkthrough = 'accessibility.verbosity.walkthrough', - SourceControl = 'accessibility.verbosity.scm' + SourceControl = 'accessibility.verbosity.sourceControl' } const baseVerbosityProperty: IConfigurationPropertySchema = { @@ -189,6 +189,10 @@ const configuration: IConfigurationNode = { type: 'boolean', default: true }, + [AccessibilityVerbositySettingId.SourceControl]: { + description: localize('verbosity.scm', 'Provide information about how to access the source control accessibility help menu when the input is focused.'), + ...baseVerbosityProperty + }, 'accessibility.signalOptions.volume': { 'description': localize('accessibility.signalOptions.volume', "The volume of the sounds in percent (0-100)."), 'type': 'number', diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index a786b73aab1a..3a4253497b90 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -105,6 +105,9 @@ import { ITextModel } from '../../../../editor/common/model.js'; import { autorun } from '../../../../base/common/observable.js'; import { PlaceholderTextContribution } from '../../../../editor/contrib/placeholderText/browser/placeholderTextContribution.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; +import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; +import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js'; type TreeElement = ISCMRepository | ISCMInput | ISCMActionButton | ISCMResourceGroup | ISCMResource | IResourceNode; @@ -900,6 +903,9 @@ class SCMResourceIdentityProvider implements IIdentityProvider { export class SCMAccessibilityProvider implements IListAccessibilityProvider { constructor( + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IKeybindingService private readonly keybindingService: IKeybindingService, @ILabelService private readonly labelService: ILabelService ) { } @@ -913,7 +919,16 @@ export class SCMAccessibilityProvider implements IListAccessibilityProvider(AccessibilityVerbositySettingId.SourceControl) === true; + + if (!verbosity || !this.accessibilityService.isScreenReaderOptimized()) { + return localize('scmInput', "Source Control Input"); + } + + const kbLabel = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); + return kbLabel + ? localize('scmInput.accessibilityHelp', "Source Control Input, Use {0} to open Source Control Accessibility Help.", kbLabel) + : localize('scmInput.accessibilityHelpNoKb', "Source Control Input, Run the Open Accessibility Help command for more information."); } else if (isSCMActionButton(element)) { return element.button?.command.title ?? ''; } else if (isSCMResourceGroup(element)) { From 2c907832051d50bd38357078e22a673fa292f052 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:38:51 +0100 Subject: [PATCH 1032/3587] Fix rounded corners in line replacement view (#239204) rounded corners end of line in replacement view --- .../browser/view/inlineEdits/wordReplacementView.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts index 2eccc056602d..309af28f4aae 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts @@ -303,9 +303,10 @@ export class LineReplacementView extends Disposable implements IInlineEditsView const decorations = []; for (const modified of modifiedBubbles.filter(b => b.startLineNumber === lineNumber)) { - decorations.push(new InlineDecoration(new Range(1, modified.startColumn, 1, modified.endColumn), 'inlineCompletions-modified-bubble', InlineDecorationType.Regular)); + const validatedEndColumn = Math.min(modified.endColumn, modLine.length + 1); + decorations.push(new InlineDecoration(new Range(1, modified.startColumn, 1, validatedEndColumn), 'inlineCompletions-modified-bubble', InlineDecorationType.Regular)); decorations.push(new InlineDecoration(new Range(1, modified.startColumn, 1, modified.startColumn + 1), 'start', InlineDecorationType.Regular)); - decorations.push(new InlineDecoration(new Range(1, modified.endColumn - 1, 1, modified.endColumn), 'end', InlineDecorationType.Regular)); + decorations.push(new InlineDecoration(new Range(1, validatedEndColumn - 1, 1, validatedEndColumn), 'end', InlineDecorationType.Regular)); } const result = renderLines(new LineSource([tokens]), RenderOptions.fromEditor(this._editor.editor).withSetWidth(false), decorations, line, true); From 76bfc91e201cbf95f81ba5d09f9cf68bd6bb0770 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:55:51 +0100 Subject: [PATCH 1033/3587] Jumping to suggestion should reveal the entire view (#239205) * fixes https://github.com/microsoft/vscode-copilot/issues/12422 * :lipstick: --- .../browser/model/inlineCompletionsModel.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index f824698d7b00..cbced1a0c702 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -768,7 +768,16 @@ export class InlineCompletionsModel extends Disposable { this._jumpedTo.set(true, tx); this.dontRefetchSignal.trigger(tx); this._editor.setPosition(s.inlineEdit.range.getStartPosition(), 'inlineCompletions.jump'); - this._editor.revealPosition(s.inlineEdit.range.getStartPosition()); + + // TODO: consider using view information to reveal it + const isSingleLineChange = s.inlineEdit.range.startLineNumber === s.inlineEdit.range.endLineNumber && !s.inlineEdit.text.includes('\n'); + if (isSingleLineChange) { + this._editor.revealPosition(s.inlineEdit.range.getStartPosition()); + } else { + const revealRange = new Range(s.inlineEdit.range.startLineNumber - 1, 1, s.inlineEdit.range.endLineNumber + 1, 1); + this._editor.revealRange(revealRange, ScrollType.Immediate); + } + this._editor.focus(); }); } From e1526ec5db01c43633d5df6061c4e60b50696683 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 30 Jan 2025 13:34:59 +0100 Subject: [PATCH 1034/3587] Handle no modification scenario in chat editing (#239207) * Remove/dispose modified files if they turn out to not contain modifications * Only start only accept flow when done streaming, reset when no edits come along fixes https://github.com/microsoft/vscode-copilot/issues/10413 --- .../chatEditingModifiedFileEntry.ts | 16 +++++----- .../browser/chatEditing/chatEditingSession.ts | 19 ++++++++++-- .../chat/browser/chatEditorController.ts | 29 ++++++++++--------- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 149317c127dd..46068b2e8179 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -293,16 +293,12 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie acceptStreamingEditsStart(tx: ITransaction) { this._resetEditsState(tx); + this._autoAcceptCtrl.get()?.cancel(); } - acceptStreamingEditsEnd(tx: ITransaction) { + async acceptStreamingEditsEnd(tx: ITransaction) { this._resetEditsState(tx); - } - - private _resetEditsState(tx: ITransaction): void { - this._isCurrentlyBeingModifiedObs.set(false, tx); - this._rewriteRatioObs.set(0, tx); - this._clearCurrentEditLineDecoration(); + await this._diffOperation; // AUTO accept mode if (!this.reviewMode.get() && !this._autoAcceptCtrl.get()) { @@ -333,6 +329,12 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie } } + private _resetEditsState(tx: ITransaction): void { + this._isCurrentlyBeingModifiedObs.set(false, tx); + this._rewriteRatioObs.set(0, tx); + this._clearCurrentEditLineDecoration(); + } + private _mirrorEdits(event: IModelContentChangedEvent) { const edit = OffsetEdits.fromContentChanges(event.changes); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index a389cb51270c..36c7bdcfa033 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -11,7 +11,7 @@ import { StringSHA1 } from '../../../../../base/common/hash.js'; import { Disposable, DisposableMap, DisposableStore, dispose } from '../../../../../base/common/lifecycle.js'; import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js'; import { Schemas } from '../../../../../base/common/network.js'; -import { autorun, derived, IObservable, IReader, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { asyncTransaction, autorun, derived, IObservable, IReader, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; import { autorunDelta, autorunIterableDelta } from '../../../../../base/common/observableInternal/autorun.js'; import { isEqual, joinPath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -735,9 +735,22 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } private async _resolve(): Promise { - transaction((tx) => { + await asyncTransaction(async (tx) => { + + const entriesWithoutChange = new ResourceMap(); + for (const entry of this._entriesObs.get()) { - entry.acceptStreamingEditsEnd(tx); + await entry.acceptStreamingEditsEnd(tx); + if (entry.diffInfo.get().identical) { + entriesWithoutChange.set(entry.modifiedURI, entry); + } + } + + if (entriesWithoutChange.size > 0) { + const newEntries = this._entriesObs.get().filter(e => !entriesWithoutChange.has(e.modifiedURI)); + this._entriesObs.set(newEntries, tx); + + dispose(entriesWithoutChange.values()); } this._state.set(ChatEditingSessionState.Idle, tx); }); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index 77d55bbbf1c0..a3f4439acb08 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -37,6 +37,7 @@ import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; import { EditorsOrder, IEditorIdentifier, isDiffEditorInput } from '../../../common/editor.js'; import { ChatEditorOverlayController } from './chatEditorOverlay.js'; import { IChatService } from '../common/chatService.js'; +import { StableEditorScrollState } from '../../../../editor/browser/stableEditorScroll.js'; export const ctxIsGlobalEditingSession = new RawContextKey('chat.isGlobalEditingSession', undefined, localize('chat.ctxEditSessionIsGlobal', "The current editor is part of the global edit session")); export const ctxHasEditorModification = new RawContextKey('chat.hasEditorModifications', undefined, localize('chat.hasEditorModifications', "The current editor contains chat modifications")); @@ -114,9 +115,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut for (const session of _chatEditingService.editingSessionsObs.read(r)) { const entries = session.entries.read(r); - const idx = model?.uri - ? entries.findIndex(e => isEqual(e.modifiedURI, model.uri)) - : -1; + const idx = entries.findIndex(e => isEqual(e.modifiedURI, model.uri)); const chatModel = chatService.getSession(session.chatSessionId); @@ -129,7 +128,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut }); - let didReval = false; + let scrollState: StableEditorScrollState | undefined = undefined; this._register(autorunWithStore((r, store) => { @@ -138,7 +137,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut if (!currentEditorEntry) { this._ctxIsGlobalEditsSession.reset(); this._clear(); - didReval = false; + scrollState = undefined; return; } @@ -149,10 +148,10 @@ export class ChatEditorController extends Disposable implements IEditorContribut const { session, chatModel, entries, idx, entry } = currentEditorEntry; - store.add(chatModel.onDidChange(e => { - if (e.kind === 'addRequest') { - didReval = false; - } + const lastRequestSignal = observableFromEvent(this, chatModel.onDidChange, () => chatModel.getRequests().at(-1)); + store.add(autorun(r => { + lastRequestSignal.read(r); + scrollState ??= StableEditorScrollState.capture(this._editor); })); this._ctxIsGlobalEditsSession.set(session.isGlobalEditingSession); @@ -193,7 +192,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut fontInfoObs.read(r); lineHeightObs.read(r); - const diff = entry?.diffInfo.read(r); + const diff = entry.diffInfo.read(r); // Add line decorations (just markers, no UI) for diff navigation this._updateDiffLineDecorations(diff); @@ -207,9 +206,13 @@ export class ChatEditorController extends Disposable implements IEditorContribut this._clearDiffRendering(); } - if (!didReval && !diff.identical) { - didReval = true; - this._reveal(true, false, ScrollType.Immediate); + if (lastRequestSignal.read(r)?.response?.isComplete) { + if (!diff.identical) { + this._reveal(true, false, ScrollType.Immediate); + } else { + scrollState?.restore(_editor); + scrollState = undefined; + } } } })); From 90234e7bf9c824469c1cb12321b7f6c7d925e0c7 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 30 Jan 2025 13:36:27 +0100 Subject: [PATCH 1035/3587] pick where code blocks without existing code uri are applied (#239208) --- .../browser/actions/chatCodeblockActions.ts | 2 +- .../browser/actions/codeBlockOperations.ts | 126 ++++++++++++------ 2 files changed, 86 insertions(+), 42 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 2be07929199b..247fc3082701 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -108,7 +108,7 @@ export class CodeBlockActionRendering extends Disposable implements IWorkbenchCo const context = this._context; if (isCodeBlockActionContext(context) && context.codemapperUri) { const label = labelService.getUriLabel(context.codemapperUri, { relative: true }); - return localize('interactive.applyInEditorWithURL.label', "Apply in {0}", label); + return localize('interactive.applyInEditorWithURL.label', "Apply to {0}", label); } return super.getTooltip(); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index 00bad0fa2c35..d25ce27f456c 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -32,6 +32,8 @@ import { ICodeMapperCodeBlock, ICodeMapperRequest, ICodeMapperResponse, ICodeMap import { ChatUserAction, IChatService } from '../../common/chatService.js'; import { isResponseVM } from '../../common/chatViewModel.js'; import { ICodeBlockActionContext } from '../codeBlockPart.js'; +import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; export class InsertCodeBlockOperation { constructor( @@ -104,48 +106,49 @@ export class ApplyCodeBlockOperation { constructor( @IEditorService private readonly editorService: IEditorService, @ITextFileService private readonly textFileService: ITextFileService, - @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @IChatService private readonly chatService: IChatService, @ILanguageService private readonly languageService: ILanguageService, @IFileService private readonly fileService: IFileService, @IDialogService private readonly dialogService: IDialogService, @ILogService private readonly logService: ILogService, @ICodeMapperService private readonly codeMapperService: ICodeMapperService, - @IProgressService private readonly progressService: IProgressService + @IProgressService private readonly progressService: IProgressService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @ILabelService private readonly labelService: ILabelService, ) { } public async run(context: ICodeBlockActionContext): Promise { let activeEditorControl = getEditableActiveCodeEditor(this.editorService); - if (context.codemapperUri && !isEqual(activeEditorControl?.getModel().uri, context.codemapperUri)) { - // If the code block is from a code mapper, first reveal the target file - try { - // If the file doesn't exist yet, create it - if (!(await this.fileService.exists(context.codemapperUri))) { - // TODO: try to find the file in the workspace + const codemapperUri = await this.evaluateURIToUse(context.codemapperUri, activeEditorControl); + if (!codemapperUri) { + return; + } - await this.fileService.writeFile(context.codemapperUri, VSBuffer.fromString('')); - } - await this.editorService.openEditor({ resource: context.codemapperUri }); + if (codemapperUri && !isEqual(activeEditorControl?.getModel().uri, codemapperUri)) { + // reveal the target file + try { + await this.editorService.openEditor({ resource: codemapperUri }); activeEditorControl = getEditableActiveCodeEditor(this.editorService); if (activeEditorControl) { this.tryToRevealCodeBlock(activeEditorControl, context.code); } } catch (e) { - this.logService.info('[ApplyCodeBlockOperation] error opening code mapper file', context.codemapperUri, e); + this.logService.info('[ApplyCodeBlockOperation] error opening code mapper file', codemapperUri, e); + return; } } let result: IComputeEditsResult | undefined = undefined; if (activeEditorControl) { - result = await this.handleTextEditor(activeEditorControl, context); + result = await this.handleTextEditor(activeEditorControl, context.code); } else { const activeNotebookEditor = getActiveNotebookEditor(this.editorService); if (activeNotebookEditor) { - result = await this.handleNotebookEditor(activeNotebookEditor, context); + result = await this.handleNotebookEditor(activeNotebookEditor, context.code); } else { this.notify(localize('applyCodeBlock.noActiveEditor', "To apply this code block, open a code or notebook editor.")); } @@ -159,26 +162,71 @@ export class ApplyCodeBlockOperation { }); } - private async handleNotebookEditor(notebookEditor: IActiveNotebookEditor, codeBlockContext: ICodeBlockActionContext): Promise { + private async evaluateURIToUse(resource: URI | undefined, activeEditorControl: IActiveCodeEditor | undefined): Promise { + if (resource && await this.fileService.exists(resource)) { + return resource; + } + + const activeEditorOption = activeEditorControl?.getModel().uri ? { label: localize('activeEditor', "Active editor '{0}'", this.labelService.getUriLabel(activeEditorControl.getModel().uri, { relative: true })), id: 'activeEditor' } : undefined; + const untitledEditorOption = { label: localize('newUntitledFile', "New untitled editor"), id: 'newUntitledFile' }; + + const options = []; + if (resource) { + // code block had an URI, but it doesn't exist + options.push({ label: localize('createFile', "New file '{0}'", this.labelService.getUriLabel(resource, { relative: true })), id: 'createFile' }); + options.push(untitledEditorOption); + if (activeEditorOption) { + options.push(activeEditorOption); + } + } else { + // code block had no URI + if (activeEditorOption) { + options.push(activeEditorOption); + } + options.push(untitledEditorOption); + } + + const selected = options.length > 1 ? await this.quickInputService.pick(options, { placeHolder: localize('selectOption', "Select where to apply the code block") }) : options[0]; + if (selected) { + switch (selected.id) { + case 'createFile': + if (resource) { + try { + await this.fileService.writeFile(resource, VSBuffer.fromString('')); + } catch (error) { + this.notify(localize('applyCodeBlock.fileWriteError', "Failed to create file: {0}", error.message)); + return URI.from({ scheme: 'untitled', path: resource.path }); + } + } + return resource; + case 'newUntitledFile': + return URI.from({ scheme: 'untitled', path: resource ? resource.path : 'Untitled-1' }); + case 'activeEditor': + return activeEditorControl?.getModel().uri; + } + } + return undefined; + } + + private async handleNotebookEditor(notebookEditor: IActiveNotebookEditor, code: string): Promise { if (notebookEditor.isReadOnly) { this.notify(localize('applyCodeBlock.readonlyNotebook', "Cannot apply code block to read-only notebook editor.")); return undefined; } const focusRange = notebookEditor.getFocus(); const next = Math.max(focusRange.end - 1, 0); - insertCell(this.languageService, notebookEditor, next, CellKind.Code, 'below', codeBlockContext.code, true); + insertCell(this.languageService, notebookEditor, next, CellKind.Code, 'below', code, true); return undefined; } - private async handleTextEditor(codeEditor: IActiveCodeEditor, codeBlockContext: ICodeBlockActionContext): Promise { + private async handleTextEditor(codeEditor: IActiveCodeEditor, code: string): Promise { const activeModel = codeEditor.getModel(); if (isReadOnly(activeModel, this.textFileService)) { this.notify(localize('applyCodeBlock.readonly', "Cannot apply code block to read-only file.")); return undefined; } - const resource = codeBlockContext.codemapperUri ?? activeModel.uri; - const codeBlock = { code: codeBlockContext.code, resource, markdownBeforeBlock: undefined }; + const codeBlock = { code, resource: activeModel.uri, markdownBeforeBlock: undefined }; const codeMapper = this.codeMapperService.providers[0]?.displayName; if (!codeMapper) { @@ -186,30 +234,26 @@ export class ApplyCodeBlockOperation { return undefined; } let editsProposed = false; - - const editorToApply = await this.codeEditorService.openCodeEditor({ resource }, codeEditor); - if (editorToApply && editorToApply.hasModel()) { - - const cancellationTokenSource = new CancellationTokenSource(); - try { - const iterable = await this.progressService.withProgress>( - { location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true }, - async progress => { - progress.report({ message: localize('applyCodeBlock.progress', "Applying code block using {0}...", codeMapper) }); - const editsIterable = this.getEdits(codeBlock, cancellationTokenSource.token); - return await this.waitForFirstElement(editsIterable); - }, - () => cancellationTokenSource.cancel() - ); - editsProposed = await this.applyWithInlinePreview(iterable, editorToApply, cancellationTokenSource); - } catch (e) { - if (!isCancellationError(e)) { - this.notify(localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message)); - } - } finally { - cancellationTokenSource.dispose(); + const cancellationTokenSource = new CancellationTokenSource(); + try { + const iterable = await this.progressService.withProgress>( + { location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true }, + async progress => { + progress.report({ message: localize('applyCodeBlock.progress', "Applying code block using {0}...", codeMapper) }); + const editsIterable = this.getEdits(codeBlock, cancellationTokenSource.token); + return await this.waitForFirstElement(editsIterable); + }, + () => cancellationTokenSource.cancel() + ); + editsProposed = await this.applyWithInlinePreview(iterable, codeEditor, cancellationTokenSource); + } catch (e) { + if (!isCancellationError(e)) { + this.notify(localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message)); } + } finally { + cancellationTokenSource.dispose(); } + return { editsProposed, codeMapper From a65e2d66e8eb085947b3f1b35bb16262977e2e89 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 30 Jan 2025 14:31:54 +0100 Subject: [PATCH 1036/3587] NES: Injected text fix using css (#239216) fixes https://github.com/microsoft/vscode-copilot/issues/12426 --- .../view/inlineEdits/inlineDiffView.ts | 39 ++++++++++++------- .../browser/view/inlineEdits/view.css | 27 +++++++++---- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts index aa4a49831e79..0aa398876e37 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts @@ -214,21 +214,30 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE } if (useInlineDiff) { const insertedText = modified.getValueOfRange(i.modifiedRange); - originalDecorations.push({ - range: Range.fromPositions(i.originalRange.getEndPosition()), - options: { - description: 'inserted-text', - before: { - content: insertedText, - inlineClassName: classNames( - 'inlineCompletions-char-insert', - i.modifiedRange.isSingleLine() && diff.mode === 'insertionInline' && 'single-line-inline' - ), - }, - zIndex: 2, - showIfCollapsed: true, - } - }); + // when the injected text becomes long, the editor will split it into multiple spans + // to be able to get the border around the start and end of the text, we need to split it into multiple segments + const textSegments = insertedText.length > 3 ? + [{ text: insertedText.slice(0, 1), extraClasses: ['start'] }, { text: insertedText.slice(1, -1), extraClasses: [] }, { text: insertedText.slice(-1), extraClasses: ['end'] }] : + [{ text: insertedText, extraClasses: ['start', 'end'] }]; + + for (const { text, extraClasses } of textSegments) { + originalDecorations.push({ + range: Range.fromPositions(i.originalRange.getEndPosition()), + options: { + description: 'inserted-text', + before: { + content: text, + inlineClassName: classNames( + 'inlineCompletions-char-insert', + i.modifiedRange.isSingleLine() && diff.mode === 'insertionInline' && 'single-line-inline', + ...extraClasses // include extraClasses for additional styling if provided + ), + }, + zIndex: 2, + showIfCollapsed: true, + } + }); + } } } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index 60744b4e378d..af6fcf0f8547 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -173,23 +173,36 @@ border-left: solid var(--vscode-inlineEdit-modifiedChangedTextBackground) 3px; } - .inlineCompletions-char-delete.single-line-inline, - .inlineCompletions-char-insert.single-line-inline { + .inlineCompletions-char-delete.single-line-inline { /* Editor Decoration */ border-radius: 4px; border: 1px solid var(--vscode-editorHoverWidget-border); + margin: -2px 0 0 -2px; padding: 2px; } + .inlineCompletions-char-insert.single-line-inline { /* Inline Decoration */ + padding: 2px 0; + border-top: 1px solid var(--vscode-editorHoverWidget-border); + border-bottom: 1px solid var(--vscode-editorHoverWidget-border); + } + .inlineCompletions-char-insert.single-line-inline.start { + padding-left: 2px; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + border-left: 1px solid var(--vscode-editorHoverWidget-border); + } + .inlineCompletions-char-insert.single-line-inline.end { + padding-right: 2px; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + border-right: 1px solid var(--vscode-editorHoverWidget-border); + } + .inlineCompletions-char-delete.single-line-inline.empty, .inlineCompletions-char-insert.single-line-inline.empty { display: none; } - /* Adjustments due to being a decoration */ - .inlineCompletions-char-delete.single-line-inline { - margin: -2px 0 0 -2px; - } - .inlineCompletions.strike-through { text-decoration-thickness: 1px; } From 8d04e1b1c874ac2d28ffa81ad855f96aa210ceb2 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 30 Jan 2025 15:01:59 +0100 Subject: [PATCH 1037/3587] do not close on clicking link - fix #238988 (#239195) * do not close on clicking link - fix #238988 --- .../extensionManagement/common/extensionManagementService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index e0420986b016..0b672bc527ac 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -906,7 +906,8 @@ export class ExtensionManagementService extends Disposable implements IWorkbench if (verifiedPublishers.length || unverfiiedPublishers.length === 1) { for (const publisher of verifiedPublishers) { customMessage.appendText('\n'); - const publisherVerifiedMessage = localize('verifiedPublisherWithName', "{0} has verified ownership of {1}.", getPublisherLink(publisher), `[${URI.parse(publisher.publisherDomain!.link).authority}](${publisher.publisherDomain!.link})`); + const publisherDomainLink = URI.parse(publisher.publisherDomain!.link); + const publisherVerifiedMessage = localize('verifiedPublisherWithName', "{0} has verified ownership of {1}.", getPublisherLink(publisher), `${publisherDomainLink.authority}${publisherDomainLink.path === '/' ? '' : publisherDomainLink.path}`); customMessage.appendMarkdown(`$(${verifiedPublisherIcon.id}) ${publisherVerifiedMessage}`); } if (unverfiiedPublishers.length) { @@ -941,7 +942,6 @@ export class ExtensionManagementService extends Disposable implements IWorkbench }, custom: { markdownDetails: [{ markdown: customMessage, classes: ['extensions-management-publisher-trust-dialog'] }], - closeOnLinkClick: true, } }); From 921db9eb579bfa0476767574fc2d21ad49f2570c Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 30 Jan 2025 15:24:05 +0100 Subject: [PATCH 1038/3587] reduce the settings sync debounce to 3s (#239219) --- .../userDataSync/common/userDataAutoSyncService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index 685053d0186a..123b26551274 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -333,7 +333,7 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto return this.syncTriggerDelayer.cancel(); } - if (options?.skipIfSyncedRecently && this.lastSyncTriggerTime && new Date().getTime() - this.lastSyncTriggerTime < 15_000) { + if (options?.skipIfSyncedRecently && this.lastSyncTriggerTime && new Date().getTime() - this.lastSyncTriggerTime < 10_000) { this.logService.debug('[AutoSync] Skipping because sync was triggered recently.', sources); return; } @@ -352,11 +352,11 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto } protected getSyncTriggerDelayTime(): number { - if (this.lastSyncTriggerTime && new Date().getTime() - this.lastSyncTriggerTime > 15_000) { - this.logService.debug('[AutoSync] Sync immediately because last sync was triggered more than 15 seconds ago.'); + if (this.lastSyncTriggerTime && new Date().getTime() - this.lastSyncTriggerTime > 10_000) { + this.logService.debug('[AutoSync] Sync immediately because last sync was triggered more than 10 seconds ago.'); return 0; } - return 10_000; /* Debounce for 10 seconds if there are no failures */ + return 3_000; /* Debounce for 3 seconds if there are no failures */ } } From 405f21fb2ced1b91c31eea0bd49b74f0eeb4588d Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 30 Jan 2025 15:26:12 +0100 Subject: [PATCH 1039/3587] feedback on publisher trust dialog (#239220) --- .../common/extensionManagementService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 0b672bc527ac..33ff44eee87e 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -907,7 +907,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench for (const publisher of verifiedPublishers) { customMessage.appendText('\n'); const publisherDomainLink = URI.parse(publisher.publisherDomain!.link); - const publisherVerifiedMessage = localize('verifiedPublisherWithName', "{0} has verified ownership of {1}.", getPublisherLink(publisher), `${publisherDomainLink.authority}${publisherDomainLink.path === '/' ? '' : publisherDomainLink.path}`); + const publisherVerifiedMessage = localize('verifiedPublisherWithName', "{0} has verified ownership of `{1}`.", getPublisherLink(publisher), `${publisherDomainLink.authority}${publisherDomainLink.path === '/' ? '' : publisherDomainLink.path}`); customMessage.appendMarkdown(`$(${verifiedPublisherIcon.id}) ${publisherVerifiedMessage}`); } if (unverfiiedPublishers.length) { @@ -925,9 +925,9 @@ export class ExtensionManagementService extends Disposable implements IWorkbench customMessage.appendText('\n'); if (allPublishers.length > 1) { - customMessage.appendMarkdown(localize('message4', "{0} has no control over the behavior of third-party extensions, including how they manage your personal data. Please proceed only if you trust the publishers.", this.productService.nameLong)); + customMessage.appendMarkdown(localize('message4', "{0} has no control over the behavior of third-party extensions, including how they manage your personal data. Proceed only if you trust the publishers.", this.productService.nameLong)); } else { - customMessage.appendMarkdown(localize('message2', "{0} has no control over the behavior of third-party extensions, including how they manage your personal data. Please proceed only if you trust the publisher.", this.productService.nameLong)); + customMessage.appendMarkdown(localize('message2', "{0} has no control over the behavior of third-party extensions, including how they manage your personal data. Proceed only if you trust the publisher.", this.productService.nameLong)); } await this.dialogService.prompt({ From 060de16f9d9a002d73a0173124c19cfdd4aac479 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Thu, 30 Jan 2025 15:53:12 +0100 Subject: [PATCH 1040/3587] Directly accepting editor selection when using text area edit context (#239211) * allowing two offsets * changing to true --- test/automation/src/settings.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/automation/src/settings.ts b/test/automation/src/settings.ts index f61ff0dc36c6..2ade8b4a9afa 100644 --- a/test/automation/src/settings.ts +++ b/test/automation/src/settings.ts @@ -25,8 +25,7 @@ export class SettingsEditor { await this.openUserSettingsFile(); await this.code.dispatchKeybinding('right'); - const selectionOffset = this._editContextSelectionOffset(); - await this.editor.waitForEditorSelection('settings.json', s => s.selectionStart === selectionOffset && s.selectionEnd === selectionOffset); + await this.editor.waitForEditorSelection('settings.json', (s) => this._acceptEditorSelection(this.code.quality, s)); await this.editor.waitForTypeInEditor('settings.json', `"${setting}": ${value},`); await this.editors.saveOpenedFile(); } @@ -41,8 +40,7 @@ export class SettingsEditor { await this.openUserSettingsFile(); await this.code.dispatchKeybinding('right'); - const selectionOffset = this._editContextSelectionOffset(); - await this.editor.waitForEditorSelection('settings.json', (s) => s.selectionStart === selectionOffset && s.selectionEnd === selectionOffset); + await this.editor.waitForEditorSelection('settings.json', (s) => this._acceptEditorSelection(this.code.quality, s)); await this.editor.waitForTypeInEditor('settings.json', settings.map(v => `"${v[0]}": ${v[1]},`).join('')); await this.editors.saveOpenedFile(); } @@ -86,7 +84,10 @@ export class SettingsEditor { return this.code.quality === Quality.Stable ? SEARCH_BOX_TEXTAREA : SEARCH_BOX_NATIVE_EDIT_CONTEXT; } - private _editContextSelectionOffset(): number { - return this.code.quality === Quality.Stable ? 0 : 1; + private _acceptEditorSelection(quality: Quality, s: { selectionStart: number; selectionEnd: number }): boolean { + if (quality === Quality.Stable) { + return true; + } + return s.selectionStart === 1 && s.selectionEnd === 1; } } From eb6527cadfc55ccb30e80f84dd356464a1b732aa Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 30 Jan 2025 16:04:04 +0100 Subject: [PATCH 1041/3587] Cannot apply code snippet into editor when chat is opened as editor (#239223) --- .../browser/actions/codeBlockOperations.ts | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index d25ce27f456c..03de808a6428 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -10,7 +10,7 @@ import { isCancellationError } from '../../../../../base/common/errors.js'; import { isEqual } from '../../../../../base/common/resources.js'; import * as strings from '../../../../../base/common/strings.js'; import { URI } from '../../../../../base/common/uri.js'; -import { IActiveCodeEditor, isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; +import { getCodeEditor, IActiveCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js'; import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; import { Range } from '../../../../../editor/common/core/range.js'; @@ -129,11 +129,14 @@ export class ApplyCodeBlockOperation { if (codemapperUri && !isEqual(activeEditorControl?.getModel().uri, codemapperUri)) { // reveal the target file try { - await this.editorService.openEditor({ resource: codemapperUri }); - - activeEditorControl = getEditableActiveCodeEditor(this.editorService); - if (activeEditorControl) { - this.tryToRevealCodeBlock(activeEditorControl, context.code); + const editorPane = await this.editorService.openEditor({ resource: codemapperUri }); + const codeEditor = getCodeEditor(editorPane?.getControl()); + if (codeEditor && codeEditor.hasModel()) { + this.tryToRevealCodeBlock(codeEditor, context.code); + activeEditorControl = codeEditor; + } else { + this.notify(localize('applyCodeBlock.errorOpeningFile', "Failed to open {0} in a code editor.", codemapperUri.toString())); + return; } } catch (e) { this.logService.info('[ApplyCodeBlockOperation] error opening code mapper file', codemapperUri, e); @@ -354,19 +357,20 @@ function getEditableActiveCodeEditor(editorService: IEditorService): IActiveCode return activeCodeEditorInNotebook; } - let activeEditorControl = editorService.activeTextEditorControl; - if (isDiffEditor(activeEditorControl)) { - activeEditorControl = activeEditorControl.getOriginalEditor().hasTextFocus() ? activeEditorControl.getOriginalEditor() : activeEditorControl.getModifiedEditor(); - } - - if (!isCodeEditor(activeEditorControl)) { - return undefined; + let codeEditor = getCodeEditor(editorService.activeTextEditorControl); + if (!codeEditor) { + for (const editor of editorService.visibleTextEditorControls) { + codeEditor = getCodeEditor(editor); + if (codeEditor) { + break; + } + } } - if (!activeEditorControl.hasModel()) { + if (!codeEditor || !codeEditor.hasModel()) { return undefined; } - return activeEditorControl; + return codeEditor; } function isReadOnly(model: ITextModel, textFileService: ITextFileService): boolean { From 727ebc06d873b4f7c3d6c2ee18563c3bc8960bda Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:07:13 +0100 Subject: [PATCH 1042/3587] SCM - add aria label to the SCMInputWidget (#239222) --- .../contrib/scm/browser/scmViewPane.ts | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 3a4253497b90..a5c029c986ff 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -102,7 +102,7 @@ import { OpenScmGroupAction } from '../../multiDiffEditor/browser/scmMultiDiffSo import { ContentHoverController } from '../../../../editor/contrib/hover/browser/contentHoverController.js'; import { GlyphHoverController } from '../../../../editor/contrib/hover/browser/glyphHoverController.js'; import { ITextModel } from '../../../../editor/common/model.js'; -import { autorun } from '../../../../base/common/observable.js'; +import { autorun, runOnChange } from '../../../../base/common/observable.js'; import { PlaceholderTextContribution } from '../../../../editor/contrib/placeholderText/browser/placeholderTextContribution.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; @@ -927,8 +927,8 @@ export class SCMAccessibilityProvider implements IListAccessibilityProvider { + // Aria label & placeholder text + const accessibilityVerbosityConfig = observableConfigValue( + AccessibilityVerbositySettingId.SourceControl, true, this.configurationService); + + const getAriaLabel = (placeholder: string, verbosity?: boolean) => { + verbosity = verbosity ?? accessibilityVerbosityConfig.get(); + + if (!verbosity || !this.accessibilityService.isScreenReaderOptimized()) { + return placeholder; + } + + const kbLabel = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); + return kbLabel + ? localize('scmInput.accessibilityHelp', "{0}, Use {1} to open Source Control Accessibility Help.", placeholder, kbLabel) + : localize('scmInput.accessibilityHelpNoKb', "{0}, Run the Open Accessibility Help command for more information.", placeholder); + }; + + const getPlaceholderText = (): string => { const binding = this.keybindingService.lookupKeybinding('scm.acceptInput'); const label = binding ? binding.getLabel() : (platform.isMacintosh ? 'Cmd+Enter' : 'Ctrl+Enter'); - const placeholderText = format(input.placeholder, label); + return format(input.placeholder, label); + }; - this.inputEditor.updateOptions({ placeholder: placeholderText }); + const updatePlaceholderText = () => { + const placeholder = getPlaceholderText(); + const ariaLabel = getAriaLabel(placeholder); + + this.inputEditor.updateOptions({ ariaLabel, placeholder }); }; + this.repositoryDisposables.add(input.onDidChangePlaceholder(updatePlaceholderText)); this.repositoryDisposables.add(this.keybindingService.onDidUpdateKeybindings(updatePlaceholderText)); + + this.repositoryDisposables.add(runOnChange(accessibilityVerbosityConfig, verbosity => { + const placeholder = getPlaceholderText(); + const ariaLabel = getAriaLabel(placeholder, verbosity); + + this.inputEditor.updateOptions({ ariaLabel }); + })); + updatePlaceholderText(); // Update input template @@ -1769,6 +1799,7 @@ class SCMInputWidget { @ISCMViewService private readonly scmViewService: ISCMViewService, @IContextViewService private readonly contextViewService: IContextViewService, @IOpenerService private readonly openerService: IOpenerService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService ) { this.element = append(container, $('.scm-editor')); this.editorContainer = append(this.element, $('.scm-editor-container')); From 45212e10fc21b386332bd6489b3b9d8c514626ea Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:52:17 +0100 Subject: [PATCH 1043/3587] Git - use base branch to calculate unpublished commits (#239226) --- extensions/git/src/repository.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 3f7ccb765f3e..e791e3d500d5 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -2770,8 +2770,25 @@ export class Repository implements Disposable { return this.unpublishedCommits; } - if (this.HEAD && this.HEAD.name && this.HEAD.upstream && this.HEAD.ahead && this.HEAD.ahead > 0) { - const ref1 = `${this.HEAD.upstream.remote}/${this.HEAD.upstream.name}`; + if (!this.HEAD?.name) { + this.unpublishedCommits = new Set(); + return this.unpublishedCommits; + } + + if (this.HEAD.upstream) { + // Upstream + if (this.HEAD.ahead === 0) { + this.unpublishedCommits = new Set(); + } else { + const ref1 = `${this.HEAD.upstream.remote}/${this.HEAD.upstream.name}`; + const ref2 = this.HEAD.name; + + const revList = await this.repository.revList(ref1, ref2); + this.unpublishedCommits = new Set(revList); + } + } else if (this.historyProvider.currentHistoryItemBaseRef) { + // Base + const ref1 = this.historyProvider.currentHistoryItemBaseRef.id; const ref2 = this.HEAD.name; const revList = await this.repository.revList(ref1, ref2); From 1a800134fb0141b90612febd432c2f030f04a6cc Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 30 Jan 2025 17:28:46 +0100 Subject: [PATCH 1044/3587] Use consistent diff algorithm for model and view (#239231) use same diff algorithm across model and view --- .../browser/model/inlineCompletionsModel.ts | 13 ++- .../browser/model/inlineCompletionsSource.ts | 50 ++++++++--- .../view/inlineEdits/viewAndDiffProducer.ts | 88 ++++--------------- 3 files changed, 61 insertions(+), 90 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index cbced1a0c702..d701eedd8120 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -368,10 +368,6 @@ export class InlineCompletionsModel extends Disposable { const item = this._inlineCompletionItems.read(reader); const inlineEditResult = item?.inlineEdit; if (inlineEditResult) { - if (inlineEditResult.inlineEdit.read(reader) === null) { - return undefined; - } - let edit = inlineEditResult.toSingleTextEdit(reader); edit = singleTextRemoveCommonPrefix(edit, model); @@ -767,14 +763,15 @@ export class InlineCompletionsModel extends Disposable { transaction(tx => { this._jumpedTo.set(true, tx); this.dontRefetchSignal.trigger(tx); - this._editor.setPosition(s.inlineEdit.range.getStartPosition(), 'inlineCompletions.jump'); + const edit = s.inlineCompletion.toSingleTextEdit(undefined); + this._editor.setPosition(edit.range.getStartPosition(), 'inlineCompletions.jump'); // TODO: consider using view information to reveal it - const isSingleLineChange = s.inlineEdit.range.startLineNumber === s.inlineEdit.range.endLineNumber && !s.inlineEdit.text.includes('\n'); + const isSingleLineChange = edit.range.startLineNumber === edit.range.endLineNumber && !edit.text.includes('\n'); if (isSingleLineChange) { - this._editor.revealPosition(s.inlineEdit.range.getStartPosition()); + this._editor.revealPosition(edit.range.getStartPosition()); } else { - const revealRange = new Range(s.inlineEdit.range.startLineNumber - 1, 1, s.inlineEdit.range.endLineNumber + 1, 1); + const revealRange = new Range(edit.range.startLineNumber - 1, 1, edit.range.endLineNumber + 1, 1); this._editor.revealRange(revealRange, ScrollType.Immediate); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index d1d476f5e7de..96cb605003a9 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -8,6 +8,7 @@ import { equalsIfDefined, itemEquals } from '../../../../../base/common/equals.j import { matchesSubString } from '../../../../../base/common/filters.js'; import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { IObservable, IReader, ISettableObservable, ITransaction, derivedOpts, disposableObservableValue, observableFromEvent, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { splitLines } from '../../../../../base/common/strings.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; @@ -18,15 +19,15 @@ import { OffsetEdit, SingleOffsetEdit } from '../../../../common/core/offsetEdit import { OffsetRange } from '../../../../common/core/offsetRange.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; -import { SingleTextEdit } from '../../../../common/core/textEdit.js'; +import { SingleTextEdit, StringText } from '../../../../common/core/textEdit.js'; import { TextLength } from '../../../../common/core/textLength.js'; +import { linesDiffComputers } from '../../../../common/diff/linesDiffComputers.js'; import { InlineCompletionContext, InlineCompletionTriggerKind } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; import { EndOfLinePreference, ITextModel } from '../../../../common/model.js'; import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js'; import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js'; import { IModelContentChange, IModelContentChangedEvent } from '../../../../common/textModelEvents.js'; -import { smartDiff } from './computeGhostText.js'; import { InlineCompletionItem, InlineCompletionProviderResult, provideInlineCompletions } from './provideInlineCompletions.js'; import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; @@ -376,21 +377,44 @@ export class InlineCompletionWithUpdatedRange { } } - private _toIndividualEdits(range: Range, _replaceText: string): OffsetEdit { - const originalText = this._textModel.getValueInRange(range); - const replaceText = _replaceText.replace(/\r\n|\r|\n/g, this._textModel.getEOL()); - const diffs = smartDiff(originalText, replaceText, false); - const startOffset = this._textModel.getOffsetAt(range.getStartPosition()); - if (!diffs || diffs.length === 0) { + private _toIndividualEdits(editRange: Range, _replaceText: string): OffsetEdit { + const editOriginalText = this._textModel.getValueInRange(editRange); + const editReplaceText = _replaceText.replace(/\r\n|\r|\n/g, this._textModel.getEOL()); + + const diffAlgorithm = linesDiffComputers.getDefault(); + const lineDiffs = diffAlgorithm.computeDiff( + splitLines(editOriginalText), + splitLines(editReplaceText), + { + ignoreTrimWhitespace: false, + computeMoves: false, + extendToSubwords: true, + maxComputationTimeMs: 500, + } + ); + + const innerChanges = lineDiffs.changes.flatMap(c => c.innerChanges ?? []); + if (innerChanges.length === 0) { + const startOffset = this._textModel.getOffsetAt(editRange.getStartPosition()); return new OffsetEdit( - [new SingleOffsetEdit(OffsetRange.ofStartAndLength(startOffset, originalText.length), replaceText)] + [new SingleOffsetEdit(OffsetRange.ofStartAndLength(startOffset, editOriginalText.length), editReplaceText)] ); } + + function addRangeToPos(pos: Position, range: Range): Range { + const start = TextLength.fromPosition(range.getStartPosition()); + return TextLength.ofRange(range).createRange(start.addToPosition(pos)); + } + + const modifiedText = new StringText(editReplaceText); + return new OffsetEdit( - diffs.map(diff => { - const originalRange = OffsetRange.ofStartAndLength(startOffset + diff.originalStart, diff.originalLength); - const modifiedText = replaceText.substring(diff.modifiedStart, diff.modifiedStart + diff.modifiedLength); - return new SingleOffsetEdit(originalRange, modifiedText); + innerChanges.map(c => { + const range = addRangeToPos(editRange.getStartPosition(), c.originalRange); + const startOffset = this._textModel.getOffsetAt(range.getStartPosition()); + const endOffset = this._textModel.getOffsetAt(range.getEndPosition()); + const originalRange = OffsetRange.ofStartAndLength(startOffset, endOffset - startOffset); + return new SingleOffsetEdit(originalRange, modifiedText.getValueOfRange(c.modifiedRange)); }) ); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts index a0e9d4d1268a..11c86a03b3f0 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts @@ -3,106 +3,56 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LRUCachedFunction } from '../../../../../../base/common/cache.js'; -import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { equalsIfDefined, itemEquals } from '../../../../../../base/common/equals.js'; import { createHotClass } from '../../../../../../base/common/hotReloadHelpers.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { derivedDisposable, ObservablePromise, derived, IObservable, derivedOpts, ISettableObservable } from '../../../../../../base/common/observable.js'; +import { derived, IObservable, ISettableObservable } from '../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; -import { IDiffProviderFactoryService } from '../../../../../browser/widget/diffEditor/diffProviderFactoryService.js'; import { SingleLineEdit } from '../../../../../common/core/lineEdit.js'; import { Position } from '../../../../../common/core/position.js'; import { Range } from '../../../../../common/core/range.js'; import { SingleTextEdit, TextEdit, AbstractText } from '../../../../../common/core/textEdit.js'; -import { TextLength } from '../../../../../common/core/textLength.js'; import { Command } from '../../../../../common/languages.js'; import { TextModelText } from '../../../../../common/model/textModelText.js'; -import { IModelService } from '../../../../../common/services/model.js'; import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; import { InlineEdit } from '../../model/inlineEdit.js'; import { InlineCompletionItem } from '../../model/provideInlineCompletions.js'; import { InlineEditsView } from './view.js'; -import { UniqueUriGenerator } from './utils.js'; -export class InlineEditsViewAndDiffProducer extends Disposable { +export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This class is no longer a diff producer. Rename it or get rid of it public static readonly hot = createHotClass(InlineEditsViewAndDiffProducer); - private readonly _modelUriGenerator = new UniqueUriGenerator('inline-edits'); - - private readonly _originalModel = derivedDisposable(() => this._modelService.createModel( - '', null, this._modelUriGenerator.getUniqueUri())).keepObserved(this._store); - private readonly _modifiedModel = derivedDisposable(() => this._modelService.createModel( - '', null, this._modelUriGenerator.getUniqueUri())).keepObserved(this._store); - - private readonly _differ = new LRUCachedFunction({ getCacheKey: JSON.stringify }, (arg: { original: string; modified: string }) => { - this._originalModel.get().setValue(arg.original); - this._modifiedModel.get().setValue(arg.modified); - - const diffAlgo = this._diffProviderFactoryService.createDiffProvider({ diffAlgorithm: 'advanced' }); - - return ObservablePromise.fromFn(async () => { - const result = await diffAlgo.computeDiff(this._originalModel.get(), this._modifiedModel.get(), { - computeMoves: false, - ignoreTrimWhitespace: false, - maxComputationTimeMs: 1000, - extendToSubwords: true, - }, CancellationToken.None); - return result; - }); - }); - - private readonly _inlineEditPromise = derived | undefined>(this, (reader) => { + private readonly _inlineEdit = derived(this, (reader) => { const model = this._model.read(reader); if (!model) { return undefined; } const inlineEdit = this._edit.read(reader); if (!inlineEdit) { return undefined; } + const textModel = this._editor.getModel(); + if (!textModel) { return undefined; } + + const editOffset = model.inlineEditState.get()?.inlineCompletion.inlineEdit.read(reader); + if (!editOffset) { return undefined; } + + const edits = editOffset.edits.map(e => { + const innerEditRange = Range.fromPositions( + textModel.getPositionAt(e.replaceRange.start), + textModel.getPositionAt(e.replaceRange.endExclusive) + ); + return new SingleTextEdit(innerEditRange, e.newText); + }); - //if (inlineEdit.text.trim() === '') { return undefined; } - const text = new TextModelText(this._editor.getModel()!); - const edit = inlineEdit.edit.extendToFullLine(text); - - const diffResult = this._differ.get({ original: this._editor.getModel()!.getValueInRange(edit.range), modified: edit.text }); - - return diffResult.promiseResult.map(p => { - if (!p || !p.data) { - return undefined; - } - const result = p.data; - - const rangeStartPos = edit.range.getStartPosition(); - const innerChanges = result.changes.flatMap(c => c.innerChanges!); - if (innerChanges.length === 0) { - // there are no changes - return undefined; - } - - function addRangeToPos(pos: Position, range: Range): Range { - const start = TextLength.fromPosition(range.getStartPosition()); - return TextLength.ofRange(range).createRange(start.addToPosition(pos)); - } - - const edits = innerChanges.map(c => new SingleTextEdit( - addRangeToPos(rangeStartPos, c.originalRange), - this._modifiedModel.get()!.getValueInRange(c.modifiedRange) - )); - const diffEdits = new TextEdit(edits); + const diffEdits = new TextEdit(edits); + const text = new TextModelText(textModel); - return new InlineEditWithChanges(text, diffEdits, inlineEdit.isCollapsed, model.primaryPosition.get(), inlineEdit.renderExplicitly, inlineEdit.commands, inlineEdit.inlineCompletion); //inlineEdit.showInlineIfPossible); - }); + return new InlineEditWithChanges(text, diffEdits, inlineEdit.isCollapsed, model.primaryPosition.get(), inlineEdit.renderExplicitly, inlineEdit.commands, inlineEdit.inlineCompletion); }); - private readonly _inlineEdit = derivedOpts({ owner: this, equalsFn: equalsIfDefined(itemEquals()) }, reader => this._inlineEditPromise.read(reader)?.read(reader)); - constructor( private readonly _editor: ICodeEditor, private readonly _edit: IObservable, private readonly _model: IObservable, private readonly _focusIsInMenu: ISettableObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IDiffProviderFactoryService private readonly _diffProviderFactoryService: IDiffProviderFactoryService, - @IModelService private readonly _modelService: IModelService ) { super(); From 10b7647d4576773bc1b6332bbe8d23b7e0f7639a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 30 Jan 2025 17:29:21 +0100 Subject: [PATCH 1045/3587] update codicons (#239232) --- .../browser/ui/codicons/codicon/codicon.ttf | Bin 82548 -> 82640 bytes src/vs/base/common/codiconsLibrary.ts | 1 + 2 files changed, 1 insertion(+) diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index f60b260e0402715b055fd642ceac4c8143bb9161..afd02d556bbcda6383f6c8f9613deb8a01d2ee89 100644 GIT binary patch delta 4618 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 Vot0aj-?t)n>WPD1a%d2H-WTsxzGBdI= zBePO-%hXbH$;?chK2GDurp7VuJ7$ilIWs!Hr{|CF{oQl#doSlb=iJNlUU;td-Q~5{ z_VNt9J#3#y{V0((QPT?-={314hbbg) zW=UB{>Ek~{;1#KxTsSK$b6eqco(owjQgi3b%(B@v?qM0vx8mmmi!x{CIQI{Z5_G!G zV-IG}n(JG#;mN^E1YN!t@!zuUaIo)X0Y6J2QELmiPow84k+jl3_P<6W$j zwRiE-jQ=eqnZ59AN|uRMkTsDV1Diw5vTLo`Kmv_MPv zqa6a!0YT`9PUwL?cnW>-H2Pxz!Vr!K48jl$MGRsQhhZ3w5g3Jdj70(xk%aL`!33mY zBBmk}(~yO11MlH| ze1Ki}2p?lN_Fyj#;1E8=VI0LVe2(Kdfv<25U*kN!!FRYM59L4dKY1kCk|QM&Ci5f@ z8)dG{mpL*;Je{Qr8p(RuB_$X$eB5?)0wIf{JDKo=}QZ$wE3zK|IBi6f^_ z8-a+HZs>{5(q9J1D7=QxupggbpL~KeOh$W|jSIMlZ}BF!;Vrz4cd#9m(pV;-n{2}w zDU!c<>B!n&AlA%D?4j`G-`? zP5DK<@-O*Sev>=$Pq~5D*+vdBa|{MEa}BmIryD%U%rltBoY71-{KK001`C~rjaqg*%hpT-y}Za^ z4s({lY-ePnVDxi}8U_2;;;~XA(aiYWWcao8yuwz6fnN85!As251}`$TP5SbxUTdJa&R`RBy@94<_!5xn z4Mt8dUpCNQY!s=Z8*JF~(C>}fd`XDg)bMp7E=N-F#UXBU!&iv7Eeu~K;$H;VK)Q4kPA553{?$Q%pTa=*#SB z@HDfRL4Rg%g8@vvz7LlW!nmV1AcQmZ280Nv-heQO+0S4IQ*TBX%G8??VweLBVwrkh zLL5`?OBlw~`x1sTJ$hrp2qqspg;C5%gLr0?!B}RrK>|}dLP%t4M+iww?FeB!Q#(RP zVQNPR6PVf&@*tHv+7rS=rgnrdmB|NAA(N?{B1~hBHppUX2MO8CF$TFz?I59m$#;%I zAyYd`n8{4i`Dc$Yi;ZN1Vy5<-P{P!n6Uvy{bHW0qcAQYo)Q%GtG1CkdGt&*8VNNo5 zmO0sAIp^QaFnEqR#b6avM}Y7=Q%8VM!PF5TtY+#65Y{kt1PFXRxH$$6Q%8gFGIP4Y zR%RY~xWOCTnPISlnQ!nOQ-_c6K2wK}@BveYkFbkbWbhGFN0RU{bGE^5<{X1POdVmu zUVZ-OvOze&)Zr!^V$L)8lv!$Um^t6zC{w3|aEz%_Lin7iQ$je-Txf8DsnbLFimB7H znDdWw+*xAqHB+aAaGtr;;2Y*LgYTF+pTrtOT%Av1H6reE!@5LVolatfBJK*qT1DKI zhE%+!G)R&U}SGonjWR}A3-liw{@H@L{gaf5G}Ck%9yPa5bb ze`%nje9Aya`LuzK@)-ji<+BErPLKA%{yGr4&Peoby3Pn^oP3`Bi7j1i#7a_JU2P;k zI@{aN#}p?Za2UEfxq-o+nmlvUNHgXy2Kx1@4XaCWUBfz4T(4opDek`vYfo`+8CId< z{%Tl{ihJ9zG8OkX!x~lGJ33_Cuv`^aZ%iy)#nnYjEMdj{-LRMy_nu){EAD+GH<%9$ zOI>mQFv73v>Vrjybc#9*^Jh1_Ff4(^Tf@jL=X{4?-zV6rV^Gg|)FH`}!&XBh@ytdB zc}zdUVp+V6ja*?iG0<#kP{riXD%RBEZEj>ZvxQ-uE#8(!^fRqQoC`sT_7v@h)pt99`P7}o9Lo}0DFvH&o;tepX#HMT))SZqn``q(S6 zkK⪙*Ja(FzlJ(xx=>(zc8ZTh_n$qM%)}Za^$v=*G7emDjRisbj;|Dqj!$}AwDQR zExtH@bNrz(rDLv)^%=Wi?45+XFoI6KhONndsR)(VG^UmYB98?O3`` zdS3eG^aJU4CoP>+Jvn>w*^F)(l^NepX)>j7%Jr$8rmoLyoq0X;(X_T%K3Nf2iCJY? zUu4(I?vkCBy)~ytPC`ya&e@z>xvg^}a*L- diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index c578d85ee4e0..42fd1c802709 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -586,4 +586,5 @@ export const codiconsLibrary = { python: register('python', 0xec39), copilotLarge: register('copilot-large', 0xec3a), copilotWarningLarge: register('copilot-warning-large', 0xec3b), + keyboardTab: register('keyboard-tab', 0xec3c), } as const; From 66772c062cd97c7718ebb49d0c153e03e36539f7 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 30 Jan 2025 17:59:50 +0100 Subject: [PATCH 1046/3587] fix #239234 (#239235) --- .../contrib/extensions/browser/extensionsWorkbenchService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index ecb4f7da5738..41bcd531cfaf 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1922,9 +1922,9 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension let targetPlatform = galleryExtension.properties.targetPlatform; const options = []; for (const targetPlatform of galleryExtension.allTargetPlatforms) { - if (targetPlatform !== TargetPlatform.UNDEFINED && targetPlatform !== TargetPlatform.UNKNOWN && targetPlatform !== TargetPlatform.UNIVERSAL) { + if (targetPlatform !== TargetPlatform.UNKNOWN && targetPlatform !== TargetPlatform.UNIVERSAL) { options.push({ - label: TargetPlatformToString(targetPlatform), + label: targetPlatform === TargetPlatform.UNDEFINED ? nls.localize('allplatforms', "All Platforms") : TargetPlatformToString(targetPlatform), id: targetPlatform }); } From 5475dfe231ddba0c1e56dabe61e4f114dc697a2c Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 30 Jan 2025 11:54:35 -0600 Subject: [PATCH 1047/3587] create cwd detection and update it when command detection is deserialized (#239238) --- .../terminal/common/xterm/shellIntegrationAddon.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index 92b1851c275c..49ed23c11037 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -659,7 +659,12 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati if (!this._terminal) { throw new Error('Cannot restore commands before addon is activated'); } - this._createOrGetCommandDetection(this._terminal).deserialize(serialized); + const commandDetection = this._createOrGetCommandDetection(this._terminal); + commandDetection.deserialize(serialized); + if (commandDetection.cwd) { + // Cwd gets set when the command is deserialized, so we need to update it here + this._updateCwd(commandDetection.cwd); + } } protected _createOrGetCwdDetection(): ICwdDetectionCapability { From 0446f81c980b2d3c29159091853b9a7ed487772b Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 30 Jan 2025 10:12:50 -0800 Subject: [PATCH 1048/3587] chat: disable agentic pause for stable 1.97 (#239243) --- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 58480d70b207..8858806e069b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -30,6 +30,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { WorkbenchObjectTree } from '../../../../platform/list/browser/listService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground } from '../../../../platform/theme/common/colorRegistry.js'; @@ -234,6 +235,7 @@ export class ChatWidget extends Disposable implements IChatWidget { @IChatEditingService private readonly chatEditingService: IChatEditingService, @IStorageService private readonly storageService: IStorageService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IProductService private readonly productService: IProductService, ) { super(); @@ -895,7 +897,10 @@ export class ChatWidget extends Disposable implements IChatWidget { this.requestInProgress.set(this.viewModel.requestInProgress); this.isRequestPaused.set(this.viewModel.requestPausibility === ChatPauseState.Paused); - this.canRequestBePaused.set(this.viewModel.requestPausibility !== ChatPauseState.NotPausable); + // todo@connor4312: disabled for stable 1.97 release. + if (this.productService.quality !== 'stable') { + this.canRequestBePaused.set(this.viewModel.requestPausibility !== ChatPauseState.NotPausable); + } this.onDidChangeItems(); if (events.some(e => e?.kind === 'addRequest') && this.visible) { From 0c1ff56516b9e4db41cd29240daeb44eaa807b75 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 30 Jan 2025 19:13:42 +0100 Subject: [PATCH 1049/3587] Support colorization of injected text with NES (#239236) fix https://github.com/microsoft/vscode-copilot/issues/12426 --- .../view/inlineEdits/inlineDiffView.ts | 26 ++++++++++++++----- .../browser/view/inlineEdits/view.css | 5 +--- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts index 0aa398876e37..fd7441e4bc46 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts @@ -12,6 +12,7 @@ import { LineSource, renderLines, RenderOptions } from '../../../../../browser/w import { diffAddDecoration } from '../../../../../browser/widget/diffEditor/registrations.contribution.js'; import { applyViewZones, IObservableViewZone } from '../../../../../browser/widget/diffEditor/utils.js'; import { EditorOption } from '../../../../../common/config/editorOptions.js'; +import { OffsetRange } from '../../../../../common/core/offsetRange.js'; import { Range } from '../../../../../common/core/range.js'; import { AbstractText } from '../../../../../common/core/textEdit.js'; import { DetailedLineRangeMapping } from '../../../../../common/diff/rangeMapping.js'; @@ -36,6 +37,8 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE readonly isHovered = constObservable(false); + private readonly _tokenizationFinished = modelTokenizationFinished(this._modifiedTextModel); + constructor( private readonly _originalEditor: ICodeEditor, private readonly _state: IObservable, @@ -55,8 +58,6 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE const editor = observableCodeEditor(this._originalEditor); - const tokenizationFinished = modelTokenizationFinished(_modifiedTextModel); - const originalViewZones = derived(this, (reader) => { const originalModel = editor.model.read(reader); if (!originalModel) { return []; } @@ -73,7 +74,7 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE continue; } - tokenizationFinished.read(reader); // Update view-zones once tokenization completes + this._tokenizationFinished.read(reader); // Update view-zones once tokenization completes const source = new LineSource(diff.modified.mapToLineArray(l => this._modifiedTextModel.tokenization.getLineTokens(l))); @@ -217,15 +218,26 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE // when the injected text becomes long, the editor will split it into multiple spans // to be able to get the border around the start and end of the text, we need to split it into multiple segments const textSegments = insertedText.length > 3 ? - [{ text: insertedText.slice(0, 1), extraClasses: ['start'] }, { text: insertedText.slice(1, -1), extraClasses: [] }, { text: insertedText.slice(-1), extraClasses: ['end'] }] : - [{ text: insertedText, extraClasses: ['start', 'end'] }]; - - for (const { text, extraClasses } of textSegments) { + [ + { text: insertedText.slice(0, 1), extraClasses: ['start'], offsetRange: new OffsetRange(i.modifiedRange.startColumn - 1, i.modifiedRange.startColumn) }, + { text: insertedText.slice(1, -1), extraClasses: [], offsetRange: new OffsetRange(i.modifiedRange.startColumn, i.modifiedRange.endColumn - 2) }, + { text: insertedText.slice(-1), extraClasses: ['end'], offsetRange: new OffsetRange(i.modifiedRange.endColumn - 2, i.modifiedRange.endColumn - 1) } + ] : + [ + { text: insertedText, extraClasses: ['start', 'end'], offsetRange: new OffsetRange(i.modifiedRange.startColumn - 1, i.modifiedRange.endColumn) } + ]; + + // Tokenization + this._tokenizationFinished.read(reader); // reconsider when tokenization is finished + const lineTokens = this._modifiedTextModel.tokenization.getLineTokens(i.modifiedRange.startLineNumber); + + for (const { text, extraClasses, offsetRange } of textSegments) { originalDecorations.push({ range: Range.fromPositions(i.originalRange.getEndPosition()), options: { description: 'inserted-text', before: { + tokens: lineTokens.getTokensInRange(offsetRange), content: text, inlineClassName: classNames( 'inlineCompletions-char-insert', diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index af6fcf0f8547..ac15ee3dd2d1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -177,22 +177,19 @@ border-radius: 4px; border: 1px solid var(--vscode-editorHoverWidget-border); margin: -2px 0 0 -2px; - padding: 2px; } .inlineCompletions-char-insert.single-line-inline { /* Inline Decoration */ - padding: 2px 0; + padding: 1px 0; border-top: 1px solid var(--vscode-editorHoverWidget-border); border-bottom: 1px solid var(--vscode-editorHoverWidget-border); } .inlineCompletions-char-insert.single-line-inline.start { - padding-left: 2px; border-top-left-radius: 4px; border-bottom-left-radius: 4px; border-left: 1px solid var(--vscode-editorHoverWidget-border); } .inlineCompletions-char-insert.single-line-inline.end { - padding-right: 2px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-right: 1px solid var(--vscode-editorHoverWidget-border); From 77afd22ccd3ce7c4da8a615036bf274bb0d1004e Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 30 Jan 2025 19:14:04 +0100 Subject: [PATCH 1050/3587] Comments panel: Line numbers too far to the right (#239241) Fixes #239221 --- .../comments/browser/commentsTreeViewer.ts | 27 +++++++++++++------ .../contrib/comments/browser/media/panel.css | 3 ++- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 37c11e7e11eb..770d4fc7af7c 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -62,7 +62,7 @@ interface ICommentThreadTemplateData { timestamp: TimestampWidget; separator: HTMLElement; commentPreview: HTMLSpanElement; - range: HTMLSpanElement; + range: HTMLElement; }; repliesMetadata: { container: HTMLElement; @@ -193,14 +193,25 @@ export class CommentNodeRenderer implements IListRenderer const threadContainer = dom.append(container, dom.$('.comment-thread-container')); const metadataContainer = dom.append(threadContainer, dom.$('.comment-metadata-container')); const metadata = dom.append(metadataContainer, dom.$('.comment-metadata')); + + const icon = dom.append(metadata, dom.$('.icon')); + const userNames = dom.append(metadata, dom.$('.user')); + const timestamp = new TimestampWidget(this.configurationService, this.hoverService, dom.append(metadata, dom.$('.timestamp-container'))); + const relevance = dom.append(metadata, dom.$('.relevance')); + const separator = dom.append(metadata, dom.$('.separator')); + const commentPreview = dom.append(metadata, dom.$('.text')); + const rangeContainer = dom.append(metadata, dom.$('.range')); + const range = dom.$('p'); + rangeContainer.appendChild(range); + const threadMetadata = { - icon: dom.append(metadata, dom.$('.icon')), - userNames: dom.append(metadata, dom.$('.user')), - timestamp: new TimestampWidget(this.configurationService, this.hoverService, dom.append(metadata, dom.$('.timestamp-container'))), - relevance: dom.append(metadata, dom.$('.relevance')), - separator: dom.append(metadata, dom.$('.separator')), - commentPreview: dom.append(metadata, dom.$('.text')), - range: dom.append(metadata, dom.$('.range')) + icon, + userNames, + timestamp, + relevance, + separator, + commentPreview, + range }; threadMetadata.separator.innerText = '\u00b7'; diff --git a/src/vs/workbench/contrib/comments/browser/media/panel.css b/src/vs/workbench/contrib/comments/browser/media/panel.css index 37930e9119f4..867d5aba90cc 100644 --- a/src/vs/workbench/contrib/comments/browser/media/panel.css +++ b/src/vs/workbench/contrib/comments/browser/media/panel.css @@ -87,7 +87,8 @@ opacity: 0.8; } -.comments-panel .comments-panel-container .tree-container .comment-thread-container .text * { +.comments-panel .comments-panel-container .tree-container .comment-thread-container .text *, +.comments-panel .comments-panel-container .tree-container .comment-thread-container .range * { margin: 0; text-overflow: ellipsis; overflow: hidden; From ec485c892928752712ea4a09033cc38ad2541295 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Thu, 30 Jan 2025 11:09:06 -0800 Subject: [PATCH 1051/3587] [prompts][config]: fix source location resolution for top-level folders of a workspace (#239245) [config]: fix source location resolution for top-level folders of a workspace --- .../chatInstructionsFileLocator.ts | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts index 577e38c7c816..74e2292ad9e2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts @@ -63,21 +63,26 @@ export class ChatInstructionsFileLocator { // otherwise for each folder provided in the configuration, create // a URI per each folder in the current workspace const { folders } = this.workspaceService.getWorkspace(); + const workspaceRootUri = dirname(folders[0].uri); for (const folder of folders) { for (const sourceFolderName of sourceLocations) { - const folderUri = extUri.resolvePath(folder.uri, sourceFolderName); - result.push(folderUri); - } - } + // create the source path as a path relative to the workspace + // folder, or as an absolute path if the absolute value is provided + const sourceFolderUri = extUri.resolvePath(folder.uri, sourceFolderName); + result.push(sourceFolderUri); - // if inside a workspace, add the specified source locations inside the workspace - // root too, to allow users to use `.copilot/prompts` folder (or whatever they - // specify in the setting) in the workspace root - if (folders.length > 1) { - const workspaceRootUri = dirname(folders[0].uri); - for (const sourceFolderName of sourceLocations) { - const folderUri = extUri.resolvePath(workspaceRootUri, sourceFolderName); - result.push(folderUri); + // if not inside a workspace, we are done + if (folders.length <= 1) { + continue; + } + + // if inside a workspace, consider the specified source location inside + // the workspace root, to allow users to use some (e.g., `.github/prompts`) + // folder as a top-level folder in the workspace + const workspaceFolderUri = extUri.resolvePath(workspaceRootUri, sourceFolderName); + if (workspaceFolderUri.fsPath.startsWith(folder.uri.fsPath)) { + result.push(workspaceFolderUri); + } } } From a0a4332e9a6f06ea94d8a893b35d14b6d444ab86 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 30 Jan 2025 13:33:32 -0600 Subject: [PATCH 1052/3587] only return options if no args are provided (#239247) fix #237598 --- .../terminal-suggest/src/terminalSuggestMain.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 6824fbb2e242..3cd5854ca74c 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -368,12 +368,14 @@ export async function getCompletionItemsFromSpecs( filesRequested ||= argsCompletionResult.filesRequested; foldersRequested ||= argsCompletionResult.foldersRequested; } - - const optionsCompletionResult = handleOptions(specLabel, spec, terminalContext, precedingText, prefix); - if (optionsCompletionResult) { - items.push(...optionsCompletionResult.items); - filesRequested ||= optionsCompletionResult.filesRequested; - foldersRequested ||= optionsCompletionResult.foldersRequested; + if (!argsCompletionResult?.items.length) { + // Arg completions are more specific, only get options if those are not provided. + const optionsCompletionResult = handleOptions(specLabel, spec, terminalContext, precedingText, prefix); + if (optionsCompletionResult) { + items.push(...optionsCompletionResult.items); + filesRequested ||= optionsCompletionResult.filesRequested; + foldersRequested ||= optionsCompletionResult.foldersRequested; + } } } } From 4e144a1115118c37816f891db92c2b4d708619ca Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 30 Jan 2025 13:42:57 -0600 Subject: [PATCH 1053/3587] tweak terminal suggest sorting for files/folders (#239145) --- .../recordings/windows11_pwsh_single_char.ts | 88 ------------------- .../terminalSuggestAddon.integrationTest.ts | 2 - .../suggest/browser/simpleCompletionItem.ts | 29 +++++- .../suggest/browser/simpleCompletionModel.ts | 25 +++++- 4 files changed, 49 insertions(+), 95 deletions(-) delete mode 100644 src/vs/workbench/contrib/terminalContrib/suggest/test/browser/recordings/windows11_pwsh_single_char.ts diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/recordings/windows11_pwsh_single_char.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/recordings/windows11_pwsh_single_char.ts deleted file mode 100644 index a3f5904b0d8b..000000000000 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/recordings/windows11_pwsh_single_char.ts +++ /dev/null @@ -1,88 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -/* eslint-disable */ - -// Type "w" and complete "w32tm.exe" ("32tm.exe"). -// This test case is special in in that it accepts a completion immediately after requesting them -// which is a case we want to cover in the tests. -export const events = [ - { - "type": "resize", - "cols": 175, - "rows": 17 - }, - { - "type": "output", - "data": "\u001b[?9001h\u001b[?1004h\u001b[?25l\u001b[2J\u001b[m\u001b[H\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[H\u001b]0;C:\\Program Files\\WindowsApps\\Microsoft.PowerShell_7.4.6.0_x64__8wekyb3d8bbwe\\pwsh.exe\u0007\u001b[?25h" - }, - { - "type": "input", - "data": "\u001b[I" - }, - { - "type": "output", - "data": "\u001b[?25l\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\u001b[H\u001b[?25h" - }, - { - "type": "output", - "data": "\u001b]633;P;IsWindows=True\u0007" - }, - { - "type": "output", - "data": "\u001b]633;P;ContinuationPrompt=\\x1b[38\\x3b5\\x3b8m∙\\x1b[0m \u0007" - }, - { - "type": "output", - "data": "\u001b]633;A\u0007\u001b]633;P;Cwd=C:\\x5cGithub\\x5cTyriar\\x5cxterm.js\u0007\u001b[34m\r\n\u001b[38;2;17;17;17m\u001b[44m11:12:08 \u001b[34m\u001b[41m \u001b[38;2;17;17;17mxterm.js \u001b[31m\u001b[43m \u001b[38;2;17;17;17m master \u001b[33m\u001b[46m \u001b[38;2;17;17;17m$ \u001b[36m\u001b[49m \u001b[mis \u001b[38;5;208m\u001b[1m v5.5.0\u001b[m via \u001b[32m\u001b[1m v20.18.0 \r\n❯\u001b[m \u001b]633;P;Prompt=\\x0a\\x1b[34m\\x1b[44\\x3b38\\x3b2\\x3b17\\x3b17\\x3b17m11:12:08\\x1b[0m\\x1b[44m \\x1b[41\\x3b34m\\x1b[0m\\x1b[41m \\x1b[38\\x3b2\\x3b17\\x3b17\\x3b17mxterm.js\\x1b[0m\\x1b[41m \\x1b[43\\x3b31m\\x1b[38\\x3b2\\x3b17\\x3b17\\x3b17m  master \\x1b[46\\x3b33m\\x1b[38\\x3b2\\x3b17\\x3b17\\x3b17m $ \\x1b[0m\\x1b[36m\\x1b[0m is \\x1b[1\\x3b38\\x3b5\\x3b208m v5.5.0\\x1b[0m via \\x1b[1\\x3b32m v20.18.0 \\x1b[0m\\x0a\\x1b[1\\x3b32m❯\\x1b[0m \u0007\u001b]633;B\u0007" - }, - { - "type": "promptInputChange", - "data": "|" - }, - { - "type": "input", - "data": "w" - }, - { - "type": "output", - "data": "\u001b[?25l" - }, - { - "type": "output", - "data": "\u001b[93mw\u001b[?25h" - }, - { - "type": "promptInputChange", - "data": "w|" - }, - { - "type": "sendText", - "data": "\u001b[24~e" - }, - { - "type": "output", - "data": "\u001b[m\u001b]633;Completions;;0;1;[[\".\\\\webpack.config.headless.js\",3,\"C:\\\\Github\\\\Tyriar\\\\xterm.js\\\\webpack.config.headless.js\"],[\".\\\\webpack.config.js\",3,\"C:\\\\Github\\\\Tyriar\\\\xterm.js\\\\webpack.config.js\"],[\"${$}\",9,\"$\"],[\"$?\",9,\"?\"],[\"$args\",9,\"args\"],[\"$commandLine\",9,\"commandLine\"],[\"$completionPrefix\",9,\"completionPrefix\"],[\"$ConfirmPreference\",9,\"ConfirmPreference\"],[\"$ContinuationPrompt\",9,\"ContinuationPrompt\"],[\"$cursorIndex\",9,\"cursorIndex\"],[\"$DebugPreference\",9,\"DebugPreference\"],[\"$EnabledExperimentalFeatures\",9,\"EnabledExperimentalFeatures\"],[\"$Error\",9,\"Error\"],[\"$ErrorActionPreference\",9,\"ErrorActionPreference\"],[\"$ErrorView\",9,\"ErrorView\"],[\"$ExecutionContext\",9,\"ExecutionContext\"],[\"$false\",9,\"false\"],[\"$FormatEnumerationLimit\",9,\"FormatEnumerationLimit\"],[\"$HOME\",9,\"HOME\"],[\"$Host\",9,\"Host\"],[\"$InformationPreference\",9,\"InformationPreference\"],[\"$input\",9,\"input\"],[\"$IsCoreCLR\",9,\"IsCoreCLR\"],[\"$IsLinux\",9,\"IsLinux\"],[\"$IsMacOS\",9,\"IsMacOS\"],[\"$isStable\",9,\"isStable\"],[\"$IsWindows\",9,\"IsWindows\"],[\"$isWindows10\",9,\"isWindows10\"],[\"$LASTEXITCODE\",9,\"LASTEXITCODE\"],[\"$MaximumHistoryCount\",9,\"MaximumHistoryCount\"],[\"$MyInvocation\",9,\"MyInvocation\"],[\"$NestedPromptLevel\",9,\"NestedPromptLevel\"],[\"$Nonce\",9,\"Nonce\"],[\"$null\",9,\"null\"],[\"$osVersion\",9,\"osVersion\"],[\"$OutputEncoding\",9,\"OutputEncoding\"],[\"$PID\",9,\"PID\"],[\"$prefixCursorDelta\",9,\"prefixCursorDelta\"],[\"$PROFILE\",9,\"PROFILE\"],[\"$ProgressPreference\",9,\"ProgressPreference\"],[\"$PSBoundParameters\",9,\"PSBoundParameters\"],[\"$PSCommandPath\",9,\"PSCommandPath\"],[\"$PSCulture\",9,\"PSCulture\"],[\"$PSDefaultParameterValues\",9,\"PSDefaultParameterValues\"],[\"$PSEdition\",9,\"PSEdition\"],[\"$PSEmailServer\",9,\"PSEmailServer\"],[\"$PSHOME\",9,\"PSHOME\"],[\"$PSNativeCommandArgumentPassing\",9,\"PSNativeCommandArgumentPassing\"],[\"$PSNativeCommandUseErrorActionPreference\",9,\"PSNativeCommandUseErrorActionPreference\"],[\"$PSScriptRoot\",9,\"PSScriptRoot\"],[\"$PSSessionApplicationName\",9,\"PSSessionApplicationName\"],[\"$PSSessionConfigurationName\",9,\"PSSessionConfigurationName\"],[\"$PSSessionOption\",9,\"PSSessionOption\"],[\"$PSStyle\",9,\"PSStyle\"],[\"$PSUICulture\",9,\"PSUICulture\"],[\"$PSVersionTable\",9,\"PSVersionTable\"],[\"$PWD\",9,\"PWD\"],[\"$result\",9,\"result\"],[\"$ShellId\",9,\"ShellId\"],[\"$StackTrace\",9,\"StackTrace\"],[\"$true\",9,\"true\"],[\"$vBinPath\",9,\"vBinPath\"],[\"$VerbosePreference\",9,\"VerbosePreference\"],[\"$vParentPath\",9,\"vParentPath\"],[\"$vResolvedPath\",9,\"vResolvedPath\"],[\"$WarningPreference\",9,\"WarningPreference\"],[\"$WhatIfPreference\",9,\"WhatIfPreference\"],[\"${^}\",9,\"^\"],[\"$__LastHistoryId\",9,\"__LastHistoryId\"],[\"$__VSCodeOriginalPrompt\",9,\"__VSCodeOriginalPrompt\"],[\"$__VSCodeOriginalPSConsoleHostReadLine\",9,\"__VSCodeOriginalPSConsoleHostReadLine\"],[\"$env:ALLUSERSPROFILE\",9,\"[string]env:ALLUSERSPROFILE\"],[\"$env:APPDATA\",9,\"[string]env:APPDATA\"],[\"$env:ChocolateyInstall\",9,\"[string]env:ChocolateyInstall\"],[\"$env:ChocolateyLastPathUpdate\",9,\"[string]env:ChocolateyLastPathUpdate\"],[\"$env:CHROME_CRASHPAD_PIPE_NAME\",9,\"[string]env:CHROME_CRASHPAD_PIPE_NAME\"],[\"$env:CMDER_ROOT\",9,\"[string]env:CMDER_ROOT\"],[\"$env:CODE\",9,\"[string]env:CODE\"],[\"$env:COLORTERM\",9,\"[string]env:COLORTERM\"],[\"$env:CommonProgramFiles\",9,\"[string]env:CommonProgramFiles\"],[\"${env:CommonProgramFiles(x86)}\",9,\"[string]env:CommonProgramFiles(x86)\"],[\"$env:CommonProgramW6432\",9,\"[string]env:CommonProgramW6432\"],[\"$env:COMPUTERNAME\",9,\"[string]env:COMPUTERNAME\"],[\"$env:ComSpec\",9,\"[string]env:ComSpec\"],[\"$env:DISABLE_TEST_EXTENSION\",9,\"[string]env:DISABLE_TEST_EXTENSION\"],[\"$env:DriverData\",9,\"[string]env:DriverData\"],[\"$env:EFC_11408\",9,\"[string]env:EFC_11408\"],[\"$env:FPS_BROWSER_APP_PROFILE_STRING\",9,\"[string]env:FPS_BROWSER_APP_PROFILE_STRING\"],[\"$env:FPS_BROWSER_USER_PROFILE_STRING\",9,\"[string]env:FPS_BROWSER_USER_PROFILE_STRING\"],[\"$env:GIT_ASKPASS\",9,\"[string]env:GIT_ASKPASS\"],[\"$env:GIT_LFS_PATH\",9,\"[string]env:GIT_LFS_PATH\"],[\"$env:HOMEDRIVE\",9,\"[string]env:HOMEDRIVE\"],[\"$env:HOMEPATH\",9,\"[string]env:HOMEPATH\"],[\"$env:JAVA_HOME\",9,\"[string]env:JAVA_HOME\"],[\"$env:LANG\",9,\"[string]env:LANG\"],[\"$env:LOCALAPPDATA\",9,\"[string]env:LOCALAPPDATA\"],[\"$env:LOGONSERVER\",9,\"[string]env:LOGONSERVER\"],[\"$env:NAMESHORT\",9,\"[string]env:NAMESHORT\"],[\"$env:NODE_ENV\",9,\"[string]env:NODE_ENV\"],[\"$env:NODE_INCLUDE_PATH\",9,\"[string]env:NODE_INCLUDE_PATH\"],[\"$env:NUMBER_OF_PROCESSORS\",9,\"[string]env:NUMBER_OF_PROCESSORS\"],[\"$env:OneDrive\",9,\"[string]env:OneDrive\"],[\"$env:OneDriveCommercial\",9,\"[string]env:OneDriveCommercial\"],[\"$env:OneDriveConsumer\",9,\"[string]env:OneDriveConsumer\"],[\"$env:OPEN_SOURCE_CONTRIBUTOR\",9,\"[string]env:OPEN_SOURCE_CONTRIBUTOR\"],[\"$env:ORIGINAL_XDG_CURRENT_DESKTOP\",9,\"[string]env:ORIGINAL_XDG_CURRENT_DESKTOP\"],[\"$env:OS\",9,\"[string]env:OS\"],[\"$env:Path\",9,\"[string]env:Path\"],[\"$env:PATHEXT\",9,\"[string]env:PATHEXT\"],[\"$env:POSH_INSTALLER\",9,\"[string]env:POSH_INSTALLER\"],[\"$env:POSH_THEMES_PATH\",9,\"[string]env:POSH_THEMES_PATH\"],[\"$env:PROCESSOR_ARCHITECTURE\",9,\"[string]env:PROCESSOR_ARCHITECTURE\"],[\"$env:PROCESSOR_IDENTIFIER\",9,\"[string]env:PROCESSOR_IDENTIFIER\"],[\"$env:PROCESSOR_LEVEL\",9,\"[string]env:PROCESSOR_LEVEL\"],[\"$env:PROCESSOR_REVISION\",9,\"[string]env:PROCESSOR_REVISION\"],[\"$env:ProgramData\",9,\"[string]env:ProgramData\"],[\"$env:ProgramFiles\",9,\"[string]env:ProgramFiles\"],[\"${env:ProgramFiles(x86)}\",9,\"[string]env:ProgramFiles(x86)\"],[\"$env:ProgramW6432\",9,\"[string]env:ProgramW6432\"],[\"$env:PROMPT\",9,\"[string]env:PROMPT\"],[\"$env:PSModulePath\",9,\"[string]env:PSModulePath\"],[\"$env:PUBLIC\",9,\"[string]env:PUBLIC\"],[\"$env:PYTHONSTARTUP\",9,\"[string]env:PYTHONSTARTUP\"],[\"$env:SESSIONNAME\",9,\"[string]env:SESSIONNAME\"],[\"$env:STARSHIP_SESSION_KEY\",9,\"[string]env:STARSHIP_SESSION_KEY\"],[\"$env:STARSHIP_SHELL\",9,\"[string]env:STARSHIP_SHELL\"],[\"$env:SystemDrive\",9,\"[string]env:SystemDrive\"],[\"$env:SystemRoot\",9,\"[string]env:SystemRoot\"],[\"$env:TEMP\",9,\"[string]env:TEMP\"],[\"$env:TERM_PROGRAM\",9,\"[string]env:TERM_PROGRAM\"],[\"$env:TERM_PROGRAM_VERSION\",9,\"[string]env:TERM_PROGRAM_VERSION\"],[\"$env:TMP\",9,\"[string]env:TMP\"],[\"$env:UATDATA\",9,\"[string]env:UATDATA\"],[\"$env:USERDOMAIN\",9,\"[string]env:USERDOMAIN\"],[\"$env:USERDOMAIN_ROAMINGPROFILE\",9,\"[string]env:USERDOMAIN_ROAMINGPROFILE\"],[\"$env:USERNAME\",9,\"[string]env:USERNAME\"],[\"$env:USERPROFILE\",9,\"[string]env:USERPROFILE\"],[\"$env:VIRTUAL_ENV_DISABLE_PROMPT\",9,\"[string]env:VIRTUAL_ENV_DISABLE_PROMPT\"],[\"$env:VSCODE_GIT_ASKPASS_EXTRA_ARGS\",9,\"[string]env:VSCODE_GIT_ASKPASS_EXTRA_ARGS\"],[\"$env:VSCODE_GIT_ASKPASS_MAIN\",9,\"[string]env:VSCODE_GIT_ASKPASS_MAIN\"],[\"$env:VSCODE_GIT_ASKPASS_NODE\",9,\"[string]env:VSCODE_GIT_ASKPASS_NODE\"],[\"$env:VSCODE_GIT_IPC_HANDLE\",9,\"[string]env:VSCODE_GIT_IPC_HANDLE\"],[\"$env:VSCODE_INJECTION\",9,\"[string]env:VSCODE_INJECTION\"],[\"$env:windir\",9,\"[string]env:windir\"],[\"$CurrentlyExecutingCommand\",9,\"CurrentlyExecutingCommand\"],[\"$foreach\",9,\"foreach\"],[\"$LogCommandHealthEvent\",9,\"LogCommandHealthEvent\"],[\"$LogCommandLifecycleEvent\",9,\"LogCommandLifecycleEvent\"],[\"$LogEngineHealthEvent\",9,\"LogEngineHealthEvent\"],[\"$LogEngineLifecycleEvent\",9,\"LogEngineLifecycleEvent\"],[\"$LogProviderHealthEvent\",9,\"LogProviderHealthEvent\"],[\"$LogProviderLifecycleEvent\",9,\"LogProviderLifecycleEvent\"],[\"$LogSettingsEvent\",9,\"LogSettingsEvent\"],[\"$Matches\",9,\"Matches\"],[\"$OFS\",9,\"OFS\"],[\"$PSCmdlet\",9,\"PSCmdlet\"],[\"$PSDebugContext\",9,\"PSDebugContext\"],[\"$PSItem\",9,\"PSItem\"],[\"$PSLogUserData\",9,\"PSLogUserData\"],[\"$PSModuleAutoLoadingPreference\",9,\"PSModuleAutoLoadingPreference\"],[\"$PSSenderInfo\",9,\"PSSenderInfo\"],[\"$switch\",9,\"switch\"],[\"$this\",9,\"this\"],[\"$VerboseHelpErrors\",9,\"VerboseHelpErrors\"],[\"$_\",9,\"_\"],[\"$Alias:\",9,\"Drive that contains a view of the aliases stored in a session state\"],[\"$Env:\",9,\"Drive that contains a view of the environment variables for the process\"],[\"$Function:\",9,\"Drive that contains a view of the functions stored in a session state\"],[\"$Temp:\",9,\"Drive that maps to the temporary directory path for the current user\"],[\"$Variable:\",9,\"Drive that contains a view of those variables stored in a session state\"],[\"$Global:\",9,\"Global:\"],[\"$Local:\",9,\"Local:\"],[\"$Script:\",9,\"Script:\"],[\"$Private:\",9,\"Private:\"]]\u0007" - }, - { - "type": "command", - "id": "workbench.action.terminal.acceptSelectedSuggestion" - }, - { - "type": "sendText", - "data": "32tm.exe" - }, - { - "type": "output", - "data": "\u001b[?25l" - }, - { - "type": "output", - "data": "\u001b[93m\bw32tm.exe\u001b[?25h" - }, - { - "type": "promptInputChange", - "data": "w32tm.exe|" - } -]; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts index b3ee142148d2..fe48125d1b8e 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts @@ -35,7 +35,6 @@ import { events as windows11_pwsh_filename_same_case_change_forward_slash } from import { events as windows11_pwsh_getcontent_delete_ghost } from './recordings/windows11_pwsh_getcontent_delete_ghost.js'; import { events as windows11_pwsh_input_ls_complete_ls } from './recordings/windows11_pwsh_input_ls_complete_ls.js'; import { events as windows11_pwsh_namespace_same_prefix } from './recordings/windows11_pwsh_namespace_same_prefix.js'; -import { events as windows11_pwsh_single_char } from './recordings/windows11_pwsh_single_char.js'; import { events as windows11_pwsh_type_before_prompt } from './recordings/windows11_pwsh_type_before_prompt.js'; import { events as windows11_pwsh_writehost_multiline } from './recordings/windows11_pwsh_writehost_multiline.js'; import { events as windows11_pwsh_writehost_multiline_nav_up } from './recordings/windows11_pwsh_writehost_multiline_nav_up.js'; @@ -60,7 +59,6 @@ const recordedTestCases: { name: string; events: RecordedSessionEvent[] }[] = [ { name: 'windows11_pwsh_getcontent_delete_ghost', events: windows11_pwsh_getcontent_delete_ghost as any as RecordedSessionEvent[] }, { name: 'windows11_pwsh_input_ls_complete_ls', events: windows11_pwsh_input_ls_complete_ls as any as RecordedSessionEvent[] }, { name: 'windows11_pwsh_namespace_same_prefix', events: windows11_pwsh_namespace_same_prefix as any as RecordedSessionEvent[] }, - { name: 'windows11_pwsh_single_char', events: windows11_pwsh_single_char as any as RecordedSessionEvent[] }, { name: 'windows11_pwsh_type_before_prompt', events: windows11_pwsh_type_before_prompt as any as RecordedSessionEvent[] }, { name: 'windows11_pwsh_writehost_multiline_nav_up', events: windows11_pwsh_writehost_multiline_nav_up as any as RecordedSessionEvent[] }, { name: 'windows11_pwsh_writehost_multiline', events: windows11_pwsh_writehost_multiline as any as RecordedSessionEvent[] }, diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts index 9af7f683e6bb..a9b69068baab 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts @@ -57,9 +57,25 @@ export interface ISimpleCompletion { } export class SimpleCompletionItem { - // perf + /** + * The lowercase label, normalized to `\` path separators on Windows. + */ readonly labelLow: string; + + /** + * {@link labelLow} without the file extension. + */ readonly labelLowExcludeFileExt: string; + + /** + * The lowercase label, when the completion is a file or directory this has normalized path + * separators (/) on Windows and no trailing separator for directories. + */ + readonly labelLowNormalizedPath: string; + + /** + * The file extension part from {@link labelLow}. + */ readonly fileExtLow: string = ''; // sorting, filtering @@ -73,6 +89,8 @@ export class SimpleCompletionItem { // ensure lower-variants (perf) this.labelLow = this.completion.label.toLowerCase(); this.labelLowExcludeFileExt = this.labelLow; + this.labelLowNormalizedPath = this.labelLow; + if (completion.isFile) { if (isWindows) { this.labelLow = this.labelLow.replaceAll('/', '\\'); @@ -83,5 +101,14 @@ export class SimpleCompletionItem { this.fileExtLow = this.labelLow.substring(extIndex + 1); } } + + if (completion.isFile || completion.isDirectory) { + if (isWindows) { + this.labelLowNormalizedPath = this.labelLow.replaceAll('\\', '/'); + } + if (completion.isDirectory) { + this.labelLowNormalizedPath = this.labelLowNormalizedPath.replace(/\/$/, ''); + } + } } } diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts index df8b119efba2..7cc99b0936e1 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts @@ -8,6 +8,7 @@ import { quickSelect } from '../../../../base/common/arrays.js'; import { CharCode } from '../../../../base/common/charCode.js'; import { FuzzyScore, fuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScoreOptions, FuzzyScorer } from '../../../../base/common/filters.js'; import { isWindows } from '../../../../base/common/platform.js'; +import { count } from '../../../../base/common/strings.js'; export interface ISimpleCompletionStats { pLabelLen: number; @@ -207,12 +208,28 @@ export class SimpleCompletionModel { } // Then by file extension length ascending score = a.fileExtLow.length - b.fileExtLow.length; + if (score !== 0) { + return score; + } } - if (score === 0 || fileExtScore(a.fileExtLow) === 0 && fileExtScore(b.fileExtLow) === 0) { - // both files or directories, sort alphabetically - score = a.completion.label.localeCompare(b.completion.label); + if (a.labelLowNormalizedPath && b.labelLowNormalizedPath) { + // Directories + // Count depth of path (number of / or \ occurrences) + score = count(a.labelLowNormalizedPath, '/') - count(b.labelLowNormalizedPath, '/'); + if (score !== 0) { + return score; + } + + // Ensure shorter prefixes appear first + if (b.labelLowNormalizedPath.startsWith(a.labelLowNormalizedPath)) { + return -1; // `a` is a prefix of `b`, so `a` should come first + } + if (a.labelLowNormalizedPath.startsWith(b.labelLowNormalizedPath)) { + return 1; // `b` is a prefix of `a`, so `b` should come first + } } - return score; + // Sort alphabetically + return a.labelLow.localeCompare(b.labelLow); }); this._refilterKind = Refilter.Nothing; From c894de741b54342ec51818f793f47d90dec52ae6 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Thu, 30 Jan 2025 11:46:02 -0800 Subject: [PATCH 1054/3587] support the "object" variant for the prompt files configuration setting value (#239242) [config]: support the "object" variant for the setting value --- .../contrib/chat/browser/chat.contribution.ts | 2 +- .../chatInstructionsFileLocator.ts | 2 +- .../chat/common/promptSyntax/config.ts | 133 ++++++++++++++---- 3 files changed, 105 insertions(+), 32 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 2e98a848f065..cce081c70ae4 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -154,7 +154,7 @@ configurationRegistry.registerConfiguration({ default: true }, [PromptFilesConfig.CONFIG_KEY]: { - type: ['string', 'array', 'boolean', 'null'], + type: ['string', 'array', 'object', 'boolean', 'null'], title: nls.localize('chat.promptFiles.setting.title', "Prompt Files"), markdownDescription: nls.localize( 'chat.promptFiles.setting.markdownDescription', diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts index 74e2292ad9e2..99d0cf7dd3bc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from '../../../../../base/common/uri.js'; +import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; import { dirname, extUri } from '../../../../../base/common/resources.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; -import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js'; import { PROMPT_SNIPPET_FILE_EXTENSION } from '../../common/promptSyntax/contentProviders/promptContentsProviderBase.js'; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts index e8222202db51..1fa52b8fea9b 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts @@ -35,14 +35,39 @@ import { IConfigurationService } from '../../../../../platform/configuration/com * Enable the feature, specifying multiple prompt files source folder location: * ```json * { + * "chat.experimental.promptSnippets": { + * ".github/prompts" : true, + * ".copilot/prompts" : false, + * "/Users/legomushroom/repos/prompts" : true, + * }, + * } + * ``` + * + * Enable the feature, specifying multiple prompt files source folder location: + * ```json + * { * "chat.experimental.promptSnippets": [ - * '.github/prompts', - * '.copilot/prompts', - * '/Users/legomushroom/repos/prompts', + * ".github/prompts", + * ".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 is provides + * flexibility - the settings are combined into a single object. + * + * Enable the feature, using defaults for prompt files source folder locations + * (see {@link DEFAULT_LOCATION}): + * ```json + * { + * "chat.experimental.promptSnippets": {}, + * } + * ``` + * * See the next section for details on how we treat the config value. * * ### Possible Values @@ -54,13 +79,24 @@ import { IConfigurationService } from '../../../../../platform/configuration/com * - `false`: feature is disabled * - `string`: * - values that can be mapped to `boolean`(`"true"`, `"FALSE", "TrUe"`, etc.) - * are treated as `boolean` above + * 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 + * - `empty` string value is treated the same as the `undefined`/`null` case above + * - `object`: + * - expects the { "string": `boolean` } pairs, where the `string` is a path and the `boolean` + * is a flag that defines if the source folder location is enable or disabled + * - 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 + * folder locations fallback to {@link DEFAULT_LOCATION} * - `array`: * - `string` items in the array are treated as prompt files source folder paths * - all `non-string` items in the array are `ignored` - * - if the resulting array is empty, the feature is considered `enabled`, prompt files source - * folder locations fallback to defaults (see {@linkcode DEFAULT_LOCATION}) + * - if the resulting array is empty, the feature is considered `enabled`, prompt files + * source folder locations fallback to {@link DEFAULT_LOCATION} * * ### File Paths Resolution * @@ -87,7 +123,7 @@ export namespace PromptFilesConfig { /** * Default prompt instructions source folder paths. */ - const DEFAULT_LOCATION = ['.github/prompts']; + const DEFAULT_LOCATION = Object.freeze(['.github/prompts']); /** * Get value of the `prompt files` configuration setting. @@ -95,39 +131,51 @@ export namespace PromptFilesConfig { export const getValue = ( configService: IConfigurationService, ): string | readonly string[] | boolean | undefined => { - const value = configService.getValue(CONFIG_KEY); + const configValue = configService.getValue(CONFIG_KEY); - if (value === undefined || value === null) { + if (configValue === undefined || configValue === null) { return undefined; } - if (typeof value === 'string') { - const cleanValue = value.trim().toLowerCase(); - if (cleanValue === 'true') { - return true; - } - - if (cleanValue === 'false') { - return false; - } + if (typeof configValue === 'string') { + const cleanValue = configValue.trim().toLowerCase(); if (!cleanValue) { return undefined; } - return value; + if (asBoolean(cleanValue) !== undefined) { + return asBoolean(cleanValue); + } + + return cleanValue; } - if (typeof value === 'boolean') { - return value; + if (typeof configValue === 'boolean') { + return configValue; } - if (Array.isArray(value)) { - return value.filter((item) => { + if (Array.isArray(configValue)) { + return configValue.filter((item) => { return typeof item === 'string'; }); } + // note! this would be also true for `null` and `array`, + // but those cases are already handled above + if (typeof configValue === 'object') { + const paths: string[] = []; + + for (const [path, value] of Object.entries(configValue)) { + const cleanPath = path.trim(); + if (asBoolean(value) && cleanPath) { + paths.push(cleanPath); + } + } + + return Object.freeze(paths); + } + return undefined; }; @@ -156,17 +204,42 @@ export namespace PromptFilesConfig { } if (typeof value === 'string') { - return [value]; + return Object.freeze([value]); } - if (Array.isArray(value)) { - if (value.length !== 0) { - return value; - } - - return DEFAULT_LOCATION; + if (Array.isArray(value) && value.length !== 0) { + return value; } return DEFAULT_LOCATION; }; } + +/** + * Helper to parse an input value of `any` type into a boolean. + * + * @param value - input value to parse + * @returns `true` if the value is a boolean `true` 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; +} From db8e3d291301dbde1c40221203e5e4255d9fdbec Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 30 Jan 2025 11:59:22 -0800 Subject: [PATCH 1055/3587] Fix EditFileTool in remote windows (#239250) Map the filePath to a URI in the EH, then uriTransformer runs, tool gets a proper URI. --- .../api/common/extHostLanguageModelTools.ts | 9 ++- .../contrib/chat/browser/chat.contribution.ts | 2 +- .../tools.ts => common/tools/editFileTool.ts} | 69 ++++++++++--------- .../contrib/chat/common/tools/tools.ts | 30 ++++++++ 4 files changed, 76 insertions(+), 34 deletions(-) rename src/vs/workbench/contrib/chat/{browser/tools/tools.ts => common/tools/editFileTool.ts} (80%) create mode 100644 src/vs/workbench/contrib/chat/common/tools/tools.ts diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index d07093176f2f..294c549a0ed0 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -15,6 +15,8 @@ import { IPreparedToolInvocation, isToolInvocationContext, IToolInvocation, IToo import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js'; import * as typeConvert from './extHostTypeConverters.js'; +import { IToolInputProcessor } from '../../contrib/chat/common/tools/tools.js'; +import { EditToolData, EditToolInputProcessor } from '../../contrib/chat/common/tools/editFileTool.js'; export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape { /** A map of tools that were registered in this EH */ @@ -25,6 +27,8 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape /** A map of all known tools, from other EHs or registered in vscode core */ private readonly _allTools = new Map(); + private readonly _toolInputProcessors = new Map(); + constructor(mainContext: IMainContext) { this._proxy = mainContext.getProxy(MainContext.MainThreadLanguageModelTools); @@ -33,6 +37,8 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape this._allTools.set(tool.id, revive(tool)); } }); + + this._toolInputProcessors.set(EditToolData.id, new EditToolInputProcessor()); } async $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise { @@ -60,10 +66,11 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape } // Making the round trip here because not all tools were necessarily registered in this EH + const processedInput = this._toolInputProcessors.get(toolId)?.processInput(options.input) ?? options.input; const result = await this._proxy.$invokeTool({ toolId, callId, - parameters: options.input, + parameters: processedInput, tokenBudget: options.tokenizationOptions?.tokenBudget, context: options.toolInvocationToken as IToolInvocationContext | undefined, chatRequestId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatRequestId : undefined, diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index cce081c70ae4..6b6889506b52 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -79,11 +79,11 @@ import { ChatGettingStartedContribution } from './actions/chatGettingStarted.js' import { Extensions, IConfigurationMigrationRegistry } from '../../../common/configuration.js'; import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesContrib.js'; import { ChatQuotasService, ChatQuotasStatusBarEntry, IChatQuotasService } from './chatQuotasService.js'; -import { BuiltinToolsContribution } from './tools/tools.js'; import { ChatSetupContribution } from './chatSetup.js'; import { ChatEditorOverlayController } from './chatEditorOverlay.js'; import '../common/promptSyntax/languageFeatures/promptLinkProvider.js'; import { PromptFilesConfig } from '../common/promptSyntax/config.js'; +import { BuiltinToolsContribution } from '../common/tools/tools.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); diff --git a/src/vs/workbench/contrib/chat/browser/tools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts similarity index 80% rename from src/vs/workbench/contrib/chat/browser/tools/tools.ts rename to src/vs/workbench/contrib/chat/common/tools/editFileTool.ts index 259e9ecc71ea..77181a7d7dc9 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts @@ -5,13 +5,11 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../base/common/observable.js'; -import { URI } from '../../../../../base/common/uri.js'; +import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { SaveReason } from '../../../../common/editor.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { ICodeMapperService } from '../../common/chatCodeMapperService.js'; @@ -19,29 +17,8 @@ import { IChatEditingService } from '../../common/chatEditingService.js'; import { ChatModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js'; -import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; - -export class BuiltinToolsContribution extends Disposable implements IWorkbenchContribution { - - static readonly ID = 'chat.builtinTools'; - - constructor( - @ILanguageModelToolsService toolsService: ILanguageModelToolsService, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - - const editTool = instantiationService.createInstance(EditTool); - this._register(toolsService.registerToolData(editToolData)); - this._register(toolsService.registerToolImplementation(editToolData.id, editTool)); - } -} - -interface EditToolParams { - filePath: string; - explanation: string; - code: string; -} +import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; +import { IToolInputProcessor } from './tools.js'; const codeInstructions = ` The user is very smart and can understand how to apply your edits to their files, you just need to provide minimal hints. @@ -63,7 +40,7 @@ class Person { } `; -const editToolData: IToolData = { +export const EditToolData: IToolData = { id: 'vscode_editFile', tags: ['vscode_editing'], displayName: localize('chat.tools.editFile', "Edit File"), @@ -88,7 +65,7 @@ const editToolData: IToolData = { } }; -class EditTool implements IToolImpl { +export class EditTool implements IToolImpl { constructor( @IChatService private readonly chatService: IChatService, @@ -105,13 +82,13 @@ class EditTool implements IToolImpl { } const parameters = invocation.parameters as EditToolParams; - const uri = URI.file(parameters.filePath); + const uri = URI.revive(parameters.file); // TODO@roblourens do revive in MainThreadLanguageModelTools if (!this.workspaceContextService.isInsideWorkspace(uri)) { - throw new Error(`File ${parameters.filePath} can't be edited because it's not inside the current workspace`); + throw new Error(`File ${uri.fsPath} can't be edited because it's not inside the current workspace`); } if (await this.ignoredFilesService.fileIsIgnored(uri, token)) { - throw new Error(`File ${parameters.filePath} can't be edited because it is configured to be ignored by Copilot`); + throw new Error(`File ${uri.fsPath} can't be edited because it is configured to be ignored by Copilot`); } const model = this.chatService.getSession(invocation.context?.sessionId) as ChatModel; @@ -182,3 +159,31 @@ class EditTool implements IToolImpl { }; } } + +export interface EditToolParams { + file: UriComponents; + explanation: string; + code: string; +} + +export interface EditToolRawParams { + filePath: string; + explanation: string; + code: string; +} + +export class EditToolInputProcessor implements IToolInputProcessor { + processInput(input: EditToolRawParams): EditToolParams { + if (!input.filePath) { + // Tool name collision, or input wasn't properly validated upstream + return input as any; + } + + // Runs in EH, will be mapped + return { + file: URI.file(input.filePath), + explanation: input.explanation, + code: input.code, + }; + } +} diff --git a/src/vs/workbench/contrib/chat/common/tools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/tools.ts new file mode 100644 index 000000000000..cfdcf85f99e2 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/tools/tools.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IWorkbenchContribution } from '../../../../common/contributions.js'; +import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; +import { EditTool, EditToolData } from './editFileTool.js'; + +export class BuiltinToolsContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'chat.builtinTools'; + + constructor( + @ILanguageModelToolsService toolsService: ILanguageModelToolsService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + const editTool = instantiationService.createInstance(EditTool); + this._register(toolsService.registerToolData(EditToolData)); + this._register(toolsService.registerToolImplementation(EditToolData.id, editTool)); + } +} + +export interface IToolInputProcessor { + processInput(input: any): any; +} From 1827bde2ac834682060e76f7d3c9c7a6f3943d99 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:24:07 -0800 Subject: [PATCH 1056/3587] Fix typo in new setting Fixes #238954 --- .../workbench/contrib/terminal/common/terminalConfiguration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 663011266aaa..e261df28b2c8 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -183,7 +183,7 @@ const terminalConfiguration: IConfigurationNode = { [TerminalSettingId.FontLigaturesFeatureSettings]: { markdownDescription: localize('terminal.integrated.fontLigatures.featureSettings', "Controls what font feature settings are used when ligatures are enabled, in the format of the `font-feature-settings` CSS property. Some examples which may be valid depending on the font:") + '\n\n- ' + [ `\`"calt" off, "ss03"\``, - `\`"liga" on"\``, + `\`"liga" on\``, `\`"calt" off, "dlig" on\`` ].join('\n- '), type: 'string', From a5036df868cf607a0b6db87961a4ad4b0d150380 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Thu, 30 Jan 2025 12:26:51 -0800 Subject: [PATCH 1057/3587] =?UTF-8?q?Style:=20Update=20=E2=80=9CGetting=20?= =?UTF-8?q?Started=E2=80=9D=20media=20layout=20in=20the=20editor=20to=20ac?= =?UTF-8?q?commodate=20width=20constraints.=20(#239252)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit style: enhance getting started media layout in the editor --- .../browser/media/gettingStarted.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css index b7db145f2b5b..3edd78f86400 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css @@ -586,6 +586,14 @@ justify-content: center; } +.monaco-workbench .part.editor > .content .gettingStartedContainer.width-semi-constrained .gettingStartedSlideDetails .gettingStartedDetailsContent.video > .getting-started-media { + grid-area: media; + height: inherit; + width: inherit; + display: flex; + justify-content: center; +} + .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent.markdown > .getting-started-media { height: inherit; } From 77ca993739f091f9159bf58e2e9f84bb36bf8de3 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 30 Jan 2025 15:37:59 -0600 Subject: [PATCH 1058/3587] Update cwd for bash initial prompt (#239261) * Update cwd for bash initial prompt Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> * Update src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh --------- Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> --- .../contrib/terminal/common/scripts/shellIntegration-bash.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index 090f40212d2e..d36d7f401a36 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -243,6 +243,7 @@ __vsc_continuation_end() { __vsc_command_complete() { if [[ -z "${__vsc_first_prompt-}" ]]; then + __vsc_update_cwd builtin return fi if [ "$__vsc_current_command" = "" ]; then From f087caa5ace2333db3d8b3864ccaa384cc52499d Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 30 Jan 2025 23:11:04 +0100 Subject: [PATCH 1059/3587] Add render side by side setting (#239188) --- src/vs/editor/common/config/editorOptions.ts | 14 ++++++++++++++ .../browser/view/inlineEdits/view.ts | 7 ++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 89d37bb27ee9..0155c2e67e5f 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -4213,6 +4213,8 @@ export interface IInlineSuggestOptions { edits?: { codeShifting?: boolean; + renderSideBySide?: 'never' | 'auto'; + /** * @internal */ @@ -4262,6 +4264,7 @@ class InlineEditorSuggest extends BaseEditorOption s.edits.useMixedLinesDiff); private readonly _useInterleavedLinesDiff = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.useInterleavedLinesDiff); private readonly _useCodeShifting = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.codeShifting); + private readonly _renderSideBySide = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.renderSideBySide); private readonly _useMultiLineGhostText = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.useMultiLineGhostText); private _previousView: { @@ -282,7 +283,11 @@ export class InlineEditsView extends Disposable { } } - if (numOriginalLines > 0 && numModifiedLines > 0 && !InlineEditsSideBySideDiff.fitsInsideViewport(this._editor, edit, reader)) { + if (numOriginalLines > 0 && numModifiedLines > 0) { + if (this._renderSideBySide.read(reader) !== 'never' && InlineEditsSideBySideDiff.fitsInsideViewport(this._editor, edit, reader)) { + return 'sideBySide'; + } + return 'lineReplacement'; } From df32d7a95e1ed8a9fec0d0db0f9abeb2e8e2bdc4 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 30 Jan 2025 23:11:31 +0100 Subject: [PATCH 1060/3587] Fixes microsoft/vscode-copilot#11613 (#239263) fixes https://github.com/microsoft/vscode-copilot/issues/11613 --- .../browser/view/inlineEdits/gutterIndicatorView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index ec949f05a51d..72d5bf09400a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -284,7 +284,7 @@ export class InlineEditsGutterIndicator extends Disposable { transition: 'rotate 0.2s ease-in-out', } }, [ - renderIcon(Codicon.arrowRight) // TODO: allow setting css here, is this already supported? + this._tabAction.map(v => v === 'accept' ? renderIcon(Codicon.keyboardTab) : renderIcon(Codicon.arrowRight)) ]) ]), ])).keepUpdated(this._store); From faf7e649aa8edea7435f8defe93f424b080857f9 Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Thu, 30 Jan 2025 14:12:01 -0800 Subject: [PATCH 1061/3587] Reassign event ownership from 'sbatten' to 'benibenj' (#239264) reassigning some events --- src/vs/workbench/browser/layout.ts | 2 +- src/vs/workbench/browser/parts/paneCompositeBar.ts | 2 +- .../workbench/services/views/browser/viewDescriptorService.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 1d362b33fb53..1c566bf0f45e 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -2479,7 +2479,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi }; type StartupLayoutEventClassification = { - owner: 'sbatten'; + owner: 'benibenj'; comment: 'Information about the layout of the workbench during statup'; activityBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the activity bar is visible' }; sideBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the primary side bar is visible' }; diff --git a/src/vs/workbench/browser/parts/paneCompositeBar.ts b/src/vs/workbench/browser/parts/paneCompositeBar.ts index 89d07a87bc03..26693cb4be95 100644 --- a/src/vs/workbench/browser/parts/paneCompositeBar.ts +++ b/src/vs/workbench/browser/parts/paneCompositeBar.ts @@ -843,7 +843,7 @@ class ViewContainerActivityAction extends CompositeBarAction { private logAction(action: string) { type ActivityBarActionClassification = { - owner: 'sbatten'; + owner: 'benibenj'; comment: 'Event logged when an activity bar action is triggered.'; viewletId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The view in the activity bar for which the action was performed.' }; action: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The action that was performed. e.g. "hide", "show", or "refocus"' }; diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index c528489f5a01..df6f972bef51 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -425,7 +425,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor } type ViewDescriptorServiceMoveViewsClassification = { - owner: 'sbatten'; + owner: 'benibenj'; comment: 'Logged when views are moved from one view container to another'; viewCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of views moved' }; fromContainer: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The starting view container of the moved views' }; From 2095ace2fc0f26b74c348b10e5101bf1ebc599ad Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Thu, 30 Jan 2025 14:48:28 -0800 Subject: [PATCH 1062/3587] fix #238931 (#239265) --- .../contrib/preferences/common/preferencesContribution.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts index 2e03281aa959..7231d92bcc9e 100644 --- a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts @@ -129,9 +129,9 @@ registry.registerConfiguration({ 'workbench.settings.useWeightedKeySearch': { 'type': 'boolean', 'default': false, - 'description': nls.localize('useWeightedKeySearch', "Controls whether to use a new weight calculation algorithm to order certain search results in the Settings editor. The only search results that will be affected are those where the search query has been determined to match the setting key, and the weights will be calculated in a way that places settings with more matched words and shorter names to the top of the search results."), + 'description': nls.localize('useWeightedKeySearch', "Controls whether to use an experimental ranking algorithm for search results in the Settings editor. The newer algorithm is still in development and aims to show fewer and more relevant results."), 'scope': ConfigurationScope.WINDOW, - 'tags': ['preview'] + 'tags': ['experimental'] } } }); From bc75335dd88c46836772bb8cdbe426f26a968493 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Thu, 30 Jan 2025 14:49:52 -0800 Subject: [PATCH 1063/3587] fix: Remove video play event telemetry and update video element attributes for autoplay and layout handling (#239266) --- .../welcomeGettingStarted/browser/gettingStarted.ts | 11 ----------- .../browser/gettingStartedDetailsRenderer.ts | 12 +++--------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 7719d18b7a5f..6346f20f11f9 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -736,17 +736,6 @@ export class GettingStartedPage extends EditorPane { this.webview.setHtml(body); } })); - - this.stepDisposables.add(this.webview.onMessage(async e => { - const message: string = e.message as string; - if (message === 'playVideo') { - this.telemetryService.publicLog2('gettingStarted.ActionExecuted', { - command: 'playVideo', - walkthroughId: this.currentWalkthrough?.id, - argument: stepId, - }); - } - })); } } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts index 6755eb5d760a..8fc561417acb 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts @@ -225,19 +225,13 @@ export class GettingStartedDetailsRenderer { - `; } From f9c927cf7a29a59b896b6cdac2d8b5d2d43afea5 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 30 Jan 2025 15:36:53 -0800 Subject: [PATCH 1064/3587] eng: rev to 1.98 (#239269) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4963a5eb7444..9f29e0386f33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-oss-dev", - "version": "1.97.0", + "version": "1.98.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-oss-dev", - "version": "1.97.0", + "version": "1.98.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 53c91131ccf9..b45b2fe3a699 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "code-oss-dev", - "version": "1.97.0", + "version": "1.98.0", "distro": "4e8c47ac95d95aa744f78305db0f163dcd407126", "author": { "name": "Microsoft Corporation" From f2b55975952542f9c4b92cedd42153c62a9bc4cd Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 31 Jan 2025 09:18:46 +0100 Subject: [PATCH 1065/3587] Ben/probable-bobcat (#239286) * Should not change values in environmentService (fix #171707) * dialogs - remove `closeOnLinkClick` option Closing a dialog from anything but a button click is rather unexpected and there is usage of this anymore. * Code duplication for text input actions (fix #239187) --- src/vs/base/browser/ui/dialog/dialog.ts | 15 ---- src/vs/platform/dialogs/common/dialogs.ts | 1 - .../environment/common/environmentService.ts | 2 +- .../browser/actions/textInputActions.ts | 86 +++++++++---------- .../browser/parts/dialogs/dialogHandler.ts | 1 - .../find/browser/terminalFindWidget.ts | 13 ++- .../find/browser/textInputContextMenu.ts | 63 -------------- .../environment/browser/environmentService.ts | 3 +- 8 files changed, 57 insertions(+), 127 deletions(-) delete mode 100644 src/vs/workbench/contrib/terminalContrib/find/browser/textInputContextMenu.ts diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index 2ed8d24fadf7..a26806c6e435 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -37,7 +37,6 @@ export interface IDialogOptions { readonly icon?: ThemeIcon; readonly buttonDetails?: string[]; readonly disableCloseAction?: boolean; - readonly closeOnLinkClick?: boolean; readonly disableDefaultAction?: boolean; readonly buttonStyles: IButtonStyles; readonly checkboxStyles: ICheckboxStyles; @@ -212,20 +211,6 @@ export class Dialog extends Disposable { return; }; - if (this.options.closeOnLinkClick) { - for (const el of this.messageContainer.getElementsByTagName('a')) { - this._register(addDisposableListener(el, EventType.CLICK, () => { - setTimeout(close); // HACK to ensure the link action is triggered before the dialog is closed - })); - this._register(addDisposableListener(el, EventType.KEY_DOWN, (e: KeyboardEvent) => { - const evt = new StandardKeyboardEvent(e); - if (evt.equals(KeyCode.Enter)) { - setTimeout(close); // HACK to ensure the link action is triggered before the dialog is closed - } - })); - } - } - const buttonBar = this.buttonBar = this._register(new ButtonBar(this.buttonsContainer)); const buttonMap = this.rearrangeButtons(this.buttons, this.options.cancelId); diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index 96259d6807ef..dc1c785356e8 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -278,7 +278,6 @@ export interface ICustomDialogOptions { readonly classes?: string[]; readonly icon?: ThemeIcon; readonly disableCloseAction?: boolean; - readonly closeOnLinkClick?: boolean; } export interface ICustomDialogMarkdown { diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index 1984abb76c8d..7004dd4252bf 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -252,7 +252,7 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron return undefined; } - editSessionId: string | undefined = this.args['editSessionId']; + get editSessionId(): string | undefined { return this.args['editSessionId']; } get continueOn(): string | undefined { return this.args['continueOn']; diff --git a/src/vs/workbench/browser/actions/textInputActions.ts b/src/vs/workbench/browser/actions/textInputActions.ts index 20b277dee3c0..0c0d7d1386fd 100644 --- a/src/vs/workbench/browser/actions/textInputActions.ts +++ b/src/vs/workbench/browser/actions/textInputActions.ts @@ -16,11 +16,53 @@ import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js'; import { Event as BaseEvent } from '../../../base/common/event.js'; import { Lazy } from '../../../base/common/lazy.js'; +export function createTextInputActions(clipboardService: IClipboardService): IAction[] { + return [ + + // Undo/Redo + new Action('undo', localize('undo', "Undo"), undefined, true, async () => getActiveDocument().execCommand('undo')), + new Action('redo', localize('redo', "Redo"), undefined, true, async () => getActiveDocument().execCommand('redo')), + new Separator(), + + // Cut / Copy / Paste + new Action('editor.action.clipboardCutAction', localize('cut', "Cut"), undefined, true, async () => getActiveDocument().execCommand('cut')), + new Action('editor.action.clipboardCopyAction', localize('copy', "Copy"), undefined, true, async () => getActiveDocument().execCommand('copy')), + new Action('editor.action.clipboardPasteAction', localize('paste', "Paste"), undefined, true, async element => { + + // Native: paste is supported + if (isNative) { + getActiveDocument().execCommand('paste'); + } + + // Web: paste is not supported due to security reasons + else { + const clipboardText = await clipboardService.readText(); + if ( + isHTMLTextAreaElement(element) || + isHTMLInputElement(element) + ) { + const selectionStart = element.selectionStart || 0; + const selectionEnd = element.selectionEnd || 0; + + element.value = `${element.value.substring(0, selectionStart)}${clipboardText}${element.value.substring(selectionEnd, element.value.length)}`; + element.selectionStart = selectionStart + clipboardText.length; + element.selectionEnd = element.selectionStart; + element.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); + } + } + }), + new Separator(), + + // Select All + new Action('editor.action.selectAll', localize('selectAll', "Select All"), undefined, true, async () => getActiveDocument().execCommand('selectAll')) + ]; +} + export class TextInputActionsProvider extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.textInputActionsProvider'; - private readonly textInputActions = new Lazy(() => this.createActions()); + private readonly textInputActions = new Lazy(() => createTextInputActions(this.clipboardService)); constructor( @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @@ -32,48 +74,6 @@ export class TextInputActionsProvider extends Disposable implements IWorkbenchCo this.registerListeners(); } - private createActions(): IAction[] { - return [ - - // Undo/Redo - new Action('undo', localize('undo', "Undo"), undefined, true, async () => getActiveDocument().execCommand('undo')), - new Action('redo', localize('redo', "Redo"), undefined, true, async () => getActiveDocument().execCommand('redo')), - new Separator(), - - // Cut / Copy / Paste - new Action('editor.action.clipboardCutAction', localize('cut', "Cut"), undefined, true, async () => getActiveDocument().execCommand('cut')), - new Action('editor.action.clipboardCopyAction', localize('copy', "Copy"), undefined, true, async () => getActiveDocument().execCommand('copy')), - new Action('editor.action.clipboardPasteAction', localize('paste', "Paste"), undefined, true, async element => { - - // Native: paste is supported - if (isNative) { - getActiveDocument().execCommand('paste'); - } - - // Web: paste is not supported due to security reasons - else { - const clipboardText = await this.clipboardService.readText(); - if ( - isHTMLTextAreaElement(element) || - isHTMLInputElement(element) - ) { - const selectionStart = element.selectionStart || 0; - const selectionEnd = element.selectionEnd || 0; - - element.value = `${element.value.substring(0, selectionStart)}${clipboardText}${element.value.substring(selectionEnd, element.value.length)}`; - element.selectionStart = selectionStart + clipboardText.length; - element.selectionEnd = element.selectionStart; - element.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); - } - } - }), - new Separator(), - - // Select All - new Action('editor.action.selectAll', localize('selectAll', "Select All"), undefined, true, async () => getActiveDocument().execCommand('selectAll')) - ]; - } - private registerListeners(): void { // Context menu support in input/textarea diff --git a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index b80950387c84..21f62b646c29 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts @@ -137,7 +137,6 @@ export class BrowserDialogHandler extends AbstractDialogHandler { renderBody, icon: customOptions?.icon, disableCloseAction: customOptions?.disableCloseAction, - closeOnLinkClick: customOptions?.closeOnLinkClick, buttonDetails: customOptions?.buttonDetails, checkboxLabel: checkbox?.label, checkboxChecked: checkbox?.checked, diff --git a/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts index ba91e210b468..9c079280d62e 100644 --- a/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts @@ -15,11 +15,12 @@ import { IKeybindingService } from '../../../../../platform/keybinding/common/ke import { Event } from '../../../../../base/common/event.js'; import type { ISearchOptions } from '@xterm/addon-search'; import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; -import { openContextMenu } from './textInputContextMenu.js'; import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { TerminalFindCommandId } from '../common/terminal.find.js'; import { TerminalClipboardContribution } from '../../clipboard/browser/terminal.clipboard.contribution.js'; +import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; +import { createTextInputActions } from '../../../../browser/actions/textInputActions.js'; const TERMINAL_FIND_WIDGET_INITIAL_WIDTH = 419; @@ -74,7 +75,15 @@ export class TerminalFindWidget extends SimpleFindWidget { } const findInputDomNode = this.getFindInputDomNode(); this._register(dom.addDisposableListener(findInputDomNode, 'contextmenu', (event) => { - openContextMenu(dom.getWindow(findInputDomNode), event, clipboardService, contextMenuService); + const targetWindow = dom.getWindow(findInputDomNode); + const standardEvent = new StandardMouseEvent(targetWindow, event); + const actions = createTextInputActions(clipboardService); + + contextMenuService.showContextMenu({ + getAnchor: () => standardEvent, + getActions: () => actions, + getActionsContext: () => event.target, + }); event.stopPropagation(); })); this._register(themeService.onDidColorThemeChange(() => { diff --git a/src/vs/workbench/contrib/terminalContrib/find/browser/textInputContextMenu.ts b/src/vs/workbench/contrib/terminalContrib/find/browser/textInputContextMenu.ts deleted file mode 100644 index d0ba875c3a74..000000000000 --- a/src/vs/workbench/contrib/terminalContrib/find/browser/textInputContextMenu.ts +++ /dev/null @@ -1,63 +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 { getActiveWindow, isHTMLInputElement, isHTMLTextAreaElement } from '../../../../../base/browser/dom.js'; -import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; -import { Action, IAction, Separator } from '../../../../../base/common/actions.js'; -import { isNative } from '../../../../../base/common/platform.js'; -import { localize } from '../../../../../nls.js'; -import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; -import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; - -export function openContextMenu(targetWindow: Window, event: MouseEvent, clipboardService: IClipboardService, contextMenuService: IContextMenuService): void { - const standardEvent = new StandardMouseEvent(targetWindow, event); - - // Actions from workbench/browser/actions/textInputActions - const actions: IAction[] = []; - actions.push( - - // Undo/Redo - new Action('undo', localize('undo', "Undo"), undefined, true, async () => getActiveWindow().document.execCommand('undo')), - new Action('redo', localize('redo', "Redo"), undefined, true, async () => getActiveWindow().document.execCommand('redo')), - new Separator(), - - // Cut / Copy / Paste - new Action('editor.action.clipboardCutAction', localize('cut', "Cut"), undefined, true, async () => getActiveWindow().document.execCommand('cut')), - new Action('editor.action.clipboardCopyAction', localize('copy', "Copy"), undefined, true, async () => getActiveWindow().document.execCommand('copy')), - new Action('editor.action.clipboardPasteAction', localize('paste', "Paste"), undefined, true, async element => { - - // Native: paste is supported - if (isNative) { - getActiveWindow().document.execCommand('paste'); - } - - // Web: paste is not supported due to security reasons - else { - const clipboardText = await clipboardService.readText(); - if ( - isHTMLTextAreaElement(element) || - isHTMLInputElement(element) - ) { - const selectionStart = element.selectionStart || 0; - const selectionEnd = element.selectionEnd || 0; - - element.value = `${element.value.substring(0, selectionStart)}${clipboardText}${element.value.substring(selectionEnd, element.value.length)}`; - element.selectionStart = selectionStart + clipboardText.length; - element.selectionEnd = element.selectionStart; - } - } - }), - new Separator(), - - // Select All - new Action('editor.action.selectAll', localize('selectAll', "Select All"), undefined, true, async () => getActiveWindow().document.execCommand('selectAll')) - ); - - contextMenuService.showContextMenu({ - getAnchor: () => standardEvent, - getActions: () => actions, - getActionsContext: () => event.target, - }); -} diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 2ac401d7357b..03cfdb54aa88 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -259,7 +259,8 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi @memoize get profile(): string | undefined { return this.payload?.get('profile'); } - editSessionId: string | undefined = this.options.editSessionId; + @memoize + get editSessionId(): string | undefined { return this.options.editSessionId; } private payload: Map | undefined; From 9df979cc9a6acf6678c0ccb680f08cb29f567be0 Mon Sep 17 00:00:00 2001 From: Robo Date: Fri, 31 Jan 2025 18:27:39 +0900 Subject: [PATCH 1066/3587] ci: fix installation of build dependencies (#239290) * ci: fix installation of build dependencies * ci: add missing quality parameter --- build/azure-pipelines/alpine/cli-build-alpine.yml | 8 +++++--- .../darwin/product-build-darwin-cli-sign.yml | 12 ++++++++++++ .../darwin/product-build-darwin-universal.yml | 2 ++ build/azure-pipelines/linux/cli-build-linux.yml | 2 ++ .../linux/product-build-linux-legacy-server.yml | 2 ++ build/azure-pipelines/linux/product-build-linux.yml | 2 ++ build/azure-pipelines/product-build.yml | 2 ++ build/azure-pipelines/product-publish.yml | 2 ++ .../win32/product-build-win32-cli-sign.yml | 12 ++++++++++++ 9 files changed, 41 insertions(+), 3 deletions(-) diff --git a/build/azure-pipelines/alpine/cli-build-alpine.yml b/build/azure-pipelines/alpine/cli-build-alpine.yml index 07321ebcd97b..145133481f26 100644 --- a/build/azure-pipelines/alpine/cli-build-alpine.yml +++ b/build/azure-pipelines/alpine/cli-build-alpine.yml @@ -19,13 +19,15 @@ steps: nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - template: ../cli/cli-apply-patches.yml@self + - script: | set -e npm ci workingDirectory: build - displayName: Install pipeline build - - - template: ../cli/cli-apply-patches.yml@self + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Install build dependencies - task: Npm@1 displayName: Download openssl prebuilt diff --git a/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml b/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml index 32615c584637..b3d01ca7ff16 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml @@ -3,6 +3,8 @@ parameters: type: boolean - name: VSCODE_BUILD_MACOS_ARM64 type: boolean + - name: VSCODE_QUALITY + type: string steps: - task: NodeTool@0 @@ -11,6 +13,14 @@ steps: versionFilePath: .nvmrc nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - task: AzureKeyVault@2 + displayName: "Azure Key Vault: Get Secrets" + inputs: + azureSubscription: vscode + KeyVaultName: vscode-build-secrets + SecretsFilter: "github-distro-mixin-password" + - script: node build/setup-npm-registry.js $NPM_REGISTRY build condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry @@ -43,6 +53,8 @@ steps: echo "Npm install failed $i, trying again..." done workingDirectory: build + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Install build dependencies - template: ../cli/cli-darwin-sign.yml@self diff --git a/build/azure-pipelines/darwin/product-build-darwin-universal.yml b/build/azure-pipelines/darwin/product-build-darwin-universal.yml index 27408f71432e..3bb62e154034 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-universal.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-universal.yml @@ -46,6 +46,8 @@ steps: echo "Npm install failed $i, trying again..." done workingDirectory: build + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Install build dependencies - download: current diff --git a/build/azure-pipelines/linux/cli-build-linux.yml b/build/azure-pipelines/linux/cli-build-linux.yml index 89bc8a39e24f..dba949395de3 100644 --- a/build/azure-pipelines/linux/cli-build-linux.yml +++ b/build/azure-pipelines/linux/cli-build-linux.yml @@ -72,6 +72,8 @@ steps: echo "Npm install failed $i, trying again..." done workingDirectory: build + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Install build dependencies - script: | diff --git a/build/azure-pipelines/linux/product-build-linux-legacy-server.yml b/build/azure-pipelines/linux/product-build-linux-legacy-server.yml index 6e4022b40649..4c26baf2f999 100644 --- a/build/azure-pipelines/linux/product-build-linux-legacy-server.yml +++ b/build/azure-pipelines/linux/product-build-linux-legacy-server.yml @@ -97,6 +97,8 @@ steps: echo "Npm install failed $i, trying again..." done workingDirectory: build + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Install build dependencies - script: | diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index b87a82b9fb3e..b9300b1ccba0 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -111,6 +111,8 @@ steps: echo "Npm install failed $i, trying again..." done workingDirectory: build + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Install build dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 39075d822831..ee7dcb99eeab 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -406,6 +406,7 @@ extends: steps: - template: build/azure-pipelines/win32/product-build-win32-cli-sign.yml@self parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_BUILD_WIN32: ${{ parameters.VSCODE_BUILD_WIN32 }} VSCODE_BUILD_WIN32_ARM64: ${{ parameters.VSCODE_BUILD_WIN32_ARM64 }} @@ -706,6 +707,7 @@ extends: steps: - template: build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml@self parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_BUILD_MACOS: ${{ parameters.VSCODE_BUILD_MACOS }} VSCODE_BUILD_MACOS_ARM64: ${{ parameters.VSCODE_BUILD_MACOS_ARM64 }} diff --git a/build/azure-pipelines/product-publish.yml b/build/azure-pipelines/product-publish.yml index c9728a2a113c..8ecf5e6238ef 100644 --- a/build/azure-pipelines/product-publish.yml +++ b/build/azure-pipelines/product-publish.yml @@ -26,6 +26,8 @@ steps: - pwsh: | npm ci workingDirectory: build + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Install build dependencies - download: current diff --git a/build/azure-pipelines/win32/product-build-win32-cli-sign.yml b/build/azure-pipelines/win32/product-build-win32-cli-sign.yml index 9d9af45d4747..c7f4b0a0a127 100644 --- a/build/azure-pipelines/win32/product-build-win32-cli-sign.yml +++ b/build/azure-pipelines/win32/product-build-win32-cli-sign.yml @@ -3,6 +3,8 @@ parameters: type: boolean - name: VSCODE_BUILD_WIN32_ARM64 type: boolean + - name: VSCODE_QUALITY + type: string steps: - task: NodeTool@0 @@ -12,6 +14,14 @@ steps: versionFilePath: .nvmrc nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - task: AzureKeyVault@2 + displayName: "Azure Key Vault: Get Secrets" + inputs: + azureSubscription: vscode + KeyVaultName: vscode-build-secrets + SecretsFilter: "github-distro-mixin-password" + - powershell: node build/setup-npm-registry.js $env:NPM_REGISTRY build condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry @@ -39,6 +49,8 @@ steps: $ErrorActionPreference = "Stop" exec { npm ci } workingDirectory: build + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" retryCountOnTaskFailure: 5 displayName: Install build dependencies From da823db05042dba140c71c12d0d5f3f7a2613914 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 31 Jan 2025 10:35:52 +0100 Subject: [PATCH 1067/3587] Migrate from unsupported paste execCommand in desktop (#239228) (#239293) --- .../browser/actions/textInputActions.ts | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/browser/actions/textInputActions.ts b/src/vs/workbench/browser/actions/textInputActions.ts index 0c0d7d1386fd..144544e2032d 100644 --- a/src/vs/workbench/browser/actions/textInputActions.ts +++ b/src/vs/workbench/browser/actions/textInputActions.ts @@ -8,9 +8,8 @@ import { localize } from '../../../nls.js'; import { IWorkbenchLayoutService } from '../../services/layout/browser/layoutService.js'; import { IContextMenuService } from '../../../platform/contextview/browser/contextView.js'; import { Disposable } from '../../../base/common/lifecycle.js'; -import { EventHelper, addDisposableListener, getActiveDocument, getWindow, isHTMLElement, isHTMLInputElement, isHTMLTextAreaElement } from '../../../base/browser/dom.js'; +import { EventHelper, addDisposableListener, getActiveDocument, getWindow, isHTMLInputElement, isHTMLTextAreaElement } from '../../../base/browser/dom.js'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../common/contributions.js'; -import { isNative } from '../../../base/common/platform.js'; import { IClipboardService } from '../../../platform/clipboard/common/clipboardService.js'; import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js'; import { Event as BaseEvent } from '../../../base/common/event.js'; @@ -28,27 +27,15 @@ export function createTextInputActions(clipboardService: IClipboardService): IAc new Action('editor.action.clipboardCutAction', localize('cut', "Cut"), undefined, true, async () => getActiveDocument().execCommand('cut')), new Action('editor.action.clipboardCopyAction', localize('copy', "Copy"), undefined, true, async () => getActiveDocument().execCommand('copy')), new Action('editor.action.clipboardPasteAction', localize('paste', "Paste"), undefined, true, async element => { - - // Native: paste is supported - if (isNative) { - getActiveDocument().execCommand('paste'); - } - - // Web: paste is not supported due to security reasons - else { - const clipboardText = await clipboardService.readText(); - if ( - isHTMLTextAreaElement(element) || - isHTMLInputElement(element) - ) { - const selectionStart = element.selectionStart || 0; - const selectionEnd = element.selectionEnd || 0; - - element.value = `${element.value.substring(0, selectionStart)}${clipboardText}${element.value.substring(selectionEnd, element.value.length)}`; - element.selectionStart = selectionStart + clipboardText.length; - element.selectionEnd = element.selectionStart; - element.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); - } + const clipboardText = await clipboardService.readText(); + if (isHTMLTextAreaElement(element) || isHTMLInputElement(element)) { + const selectionStart = element.selectionStart || 0; + const selectionEnd = element.selectionEnd || 0; + + element.value = `${element.value.substring(0, selectionStart)}${clipboardText}${element.value.substring(selectionEnd, element.value.length)}`; + element.selectionStart = selectionStart + clipboardText.length; + element.selectionEnd = element.selectionStart; + element.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); } }), new Separator(), @@ -88,7 +75,7 @@ export class TextInputActionsProvider extends Disposable implements IWorkbenchCo } const target = e.target; - if (!(isHTMLElement(target)) || (target.nodeName.toLowerCase() !== 'input' && target.nodeName.toLowerCase() !== 'textarea')) { + if (!isHTMLTextAreaElement(target) && !isHTMLInputElement(target)) { return; // only for inputs or textareas } From 9d43b0751c91c909eee74ea96f765b1765487d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Fri, 31 Jan 2025 10:49:12 +0100 Subject: [PATCH 1068/3587] remove svgz from default file types (#239180) * remove svgz from default file types fixes #231021 * push missing compilation --- build/lib/electron.js | 2 +- build/lib/electron.ts | 2 +- build/win32/code.iss | 8 -------- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/build/lib/electron.js b/build/lib/electron.js index f0eb583f2cba..56992d8a7f71 100644 --- a/build/lib/electron.js +++ b/build/lib/electron.js @@ -195,7 +195,7 @@ exports.config = { 'F# source code': 'fs', 'F# signature file': 'fsi', 'F# script': ['fsx', 'fsscript'], - 'SVG document': ['svg', 'svgz'], + 'SVG document': ['svg'], 'TOML document': 'toml', 'Swift source code': 'swift', }, 'default'), diff --git a/build/lib/electron.ts b/build/lib/electron.ts index 57b27022df86..3bb047dfceeb 100644 --- a/build/lib/electron.ts +++ b/build/lib/electron.ts @@ -176,7 +176,7 @@ export const config = { 'F# source code': 'fs', 'F# signature file': 'fsi', 'F# script': ['fsx', 'fsscript'], - 'SVG document': ['svg', 'svgz'], + 'SVG document': ['svg'], 'TOML document': 'toml', 'Swift source code': 'swift', }, 'default'), diff --git a/build/win32/code.iss b/build/win32/code.iss index a59d7c047f5a..cad6e399f66b 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -1120,14 +1120,6 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg\D Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.svgz\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.svgz\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.svgz"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svgz"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SVGZ}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svgz"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svgz\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svgz\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svgz\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.t\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.t\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.t"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles From 1b5d712dda6c12a4f570af5c720e2c11d8661254 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 31 Jan 2025 11:21:51 +0100 Subject: [PATCH 1069/3587] :up: `@parcel/watcher@2.5.1` (#239291) --- extensions/package-lock.json | 112 +++++++++--------- extensions/package.json | 2 +- package-lock.json | 112 +++++++++--------- package.json | 2 +- remote/package-lock.json | 112 +++++++++--------- remote/package.json | 2 +- .../node/watcher/parcel/parcelWatcher.ts | 40 +++++-- .../files/test/node/parcelWatcher.test.ts | 8 +- 8 files changed, 203 insertions(+), 187 deletions(-) diff --git a/extensions/package-lock.json b/extensions/package-lock.json index c27ce93440e7..500d78417624 100644 --- a/extensions/package-lock.json +++ b/extensions/package-lock.json @@ -13,7 +13,7 @@ "typescript": "^5.7.3" }, "devDependencies": { - "@parcel/watcher": "2.5.0", + "@parcel/watcher": "2.5.1", "esbuild": "0.23.0", "vscode-grammar-updater": "^1.1.0" } @@ -403,9 +403,9 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", - "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -423,25 +423,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.0", - "@parcel/watcher-darwin-arm64": "2.5.0", - "@parcel/watcher-darwin-x64": "2.5.0", - "@parcel/watcher-freebsd-x64": "2.5.0", - "@parcel/watcher-linux-arm-glibc": "2.5.0", - "@parcel/watcher-linux-arm-musl": "2.5.0", - "@parcel/watcher-linux-arm64-glibc": "2.5.0", - "@parcel/watcher-linux-arm64-musl": "2.5.0", - "@parcel/watcher-linux-x64-glibc": "2.5.0", - "@parcel/watcher-linux-x64-musl": "2.5.0", - "@parcel/watcher-win32-arm64": "2.5.0", - "@parcel/watcher-win32-ia32": "2.5.0", - "@parcel/watcher-win32-x64": "2.5.0" + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", - "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", "cpu": [ "arm64" ], @@ -460,9 +460,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", - "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", "cpu": [ "arm64" ], @@ -481,9 +481,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", - "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", "cpu": [ "x64" ], @@ -502,9 +502,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", - "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", "cpu": [ "x64" ], @@ -523,9 +523,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", - "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", "cpu": [ "arm" ], @@ -544,9 +544,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", - "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", "cpu": [ "arm" ], @@ -565,9 +565,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", - "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", "cpu": [ "arm64" ], @@ -586,9 +586,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", - "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", "cpu": [ "arm64" ], @@ -607,9 +607,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", - "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", "cpu": [ "x64" ], @@ -628,9 +628,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", - "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", "cpu": [ "x64" ], @@ -649,9 +649,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", - "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", "cpu": [ "arm64" ], @@ -670,9 +670,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", - "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", "cpu": [ "ia32" ], @@ -691,9 +691,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", - "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", "cpu": [ "x64" ], diff --git a/extensions/package.json b/extensions/package.json index 128832816339..f756f6a42f91 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -10,7 +10,7 @@ "postinstall": "node ./postinstall.mjs" }, "devDependencies": { - "@parcel/watcher": "2.5.0", + "@parcel/watcher": "2.5.1", "esbuild": "0.23.0", "vscode-grammar-updater": "^1.1.0" }, diff --git a/package-lock.json b/package-lock.json index 9f29e0386f33..220b5d9879b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@parcel/watcher": "2.5.0", + "@parcel/watcher": "2.5.1", "@types/semver": "^7.5.8", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", @@ -1728,9 +1728,9 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", - "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1747,25 +1747,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.0", - "@parcel/watcher-darwin-arm64": "2.5.0", - "@parcel/watcher-darwin-x64": "2.5.0", - "@parcel/watcher-freebsd-x64": "2.5.0", - "@parcel/watcher-linux-arm-glibc": "2.5.0", - "@parcel/watcher-linux-arm-musl": "2.5.0", - "@parcel/watcher-linux-arm64-glibc": "2.5.0", - "@parcel/watcher-linux-arm64-musl": "2.5.0", - "@parcel/watcher-linux-x64-glibc": "2.5.0", - "@parcel/watcher-linux-x64-musl": "2.5.0", - "@parcel/watcher-win32-arm64": "2.5.0", - "@parcel/watcher-win32-ia32": "2.5.0", - "@parcel/watcher-win32-x64": "2.5.0" + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", - "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", "cpu": [ "arm64" ], @@ -1783,9 +1783,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", - "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", "cpu": [ "arm64" ], @@ -1803,9 +1803,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", - "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", "cpu": [ "x64" ], @@ -1823,9 +1823,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", - "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", "cpu": [ "x64" ], @@ -1843,9 +1843,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", - "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", "cpu": [ "arm" ], @@ -1863,9 +1863,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", - "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", "cpu": [ "arm" ], @@ -1883,9 +1883,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", - "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", "cpu": [ "arm64" ], @@ -1903,9 +1903,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", - "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", "cpu": [ "arm64" ], @@ -1923,9 +1923,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", - "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", "cpu": [ "x64" ], @@ -1943,9 +1943,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", - "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", "cpu": [ "x64" ], @@ -1963,9 +1963,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", - "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", "cpu": [ "arm64" ], @@ -1983,9 +1983,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", - "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", "cpu": [ "ia32" ], @@ -2003,9 +2003,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", - "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index b45b2fe3a699..3f97b4a3d115 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@parcel/watcher": "2.5.0", + "@parcel/watcher": "2.5.1", "@types/semver": "^7.5.8", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", diff --git a/remote/package-lock.json b/remote/package-lock.json index e16b93eee5ea..787c3c9ba96a 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@parcel/watcher": "2.5.0", + "@parcel/watcher": "2.5.1", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/proxy-agent": "^0.31.0", @@ -89,9 +89,9 @@ "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@parcel/watcher": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", - "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -108,25 +108,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.0", - "@parcel/watcher-darwin-arm64": "2.5.0", - "@parcel/watcher-darwin-x64": "2.5.0", - "@parcel/watcher-freebsd-x64": "2.5.0", - "@parcel/watcher-linux-arm-glibc": "2.5.0", - "@parcel/watcher-linux-arm-musl": "2.5.0", - "@parcel/watcher-linux-arm64-glibc": "2.5.0", - "@parcel/watcher-linux-arm64-musl": "2.5.0", - "@parcel/watcher-linux-x64-glibc": "2.5.0", - "@parcel/watcher-linux-x64-musl": "2.5.0", - "@parcel/watcher-win32-arm64": "2.5.0", - "@parcel/watcher-win32-ia32": "2.5.0", - "@parcel/watcher-win32-x64": "2.5.0" + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", - "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", "cpu": [ "arm64" ], @@ -144,9 +144,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", - "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", "cpu": [ "arm64" ], @@ -164,9 +164,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", - "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", "cpu": [ "x64" ], @@ -184,9 +184,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", - "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", "cpu": [ "x64" ], @@ -204,9 +204,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", - "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", "cpu": [ "arm" ], @@ -224,9 +224,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", - "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", "cpu": [ "arm" ], @@ -244,9 +244,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", - "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", "cpu": [ "arm64" ], @@ -264,9 +264,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", - "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", "cpu": [ "arm64" ], @@ -284,9 +284,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", - "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", "cpu": [ "x64" ], @@ -304,9 +304,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", - "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", "cpu": [ "x64" ], @@ -324,9 +324,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", - "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", "cpu": [ "arm64" ], @@ -344,9 +344,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", - "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", "cpu": [ "ia32" ], @@ -364,9 +364,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", - "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", "cpu": [ "x64" ], diff --git a/remote/package.json b/remote/package.json index b381cd45b106..5cc1ff194680 100644 --- a/remote/package.json +++ b/remote/package.json @@ -5,7 +5,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@parcel/watcher": "2.5.0", + "@parcel/watcher": "2.5.1", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/proxy-agent": "^0.31.0", diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index c778b998140b..a24ef066a5f7 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -192,10 +192,16 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS } private registerListeners(): void { + const onUncaughtException = (error: unknown) => this.onUnexpectedError(error); + const onUnhandledRejection = (error: unknown) => this.onUnexpectedError(error); - // Error handling on process - process.on('uncaughtException', error => this.onUnexpectedError(error)); - process.on('unhandledRejection', error => this.onUnexpectedError(error)); + process.on('uncaughtException', onUncaughtException); + process.on('unhandledRejection', onUnhandledRejection); + + this._register(toDisposable(() => { + process.off('uncaughtException', onUncaughtException); + process.off('unhandledRejection', onUnhandledRejection); + })); } protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise { @@ -289,20 +295,24 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS // We already ran before, check for events since const parcelWatcherLib = parcelWatcher; - if (counter > 1) { - const parcelEvents = await parcelWatcherLib.getEventsSince(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND }); + try { + if (counter > 1) { + const parcelEvents = await parcelWatcherLib.getEventsSince(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND }); - if (cts.token.isCancellationRequested) { - return; + if (cts.token.isCancellationRequested) { + return; + } + + // Handle & emit events + this.onParcelEvents(parcelEvents, watcher, realPathDiffers, realPathLength); } - // Handle & emit events - this.onParcelEvents(parcelEvents, watcher, realPathDiffers, realPathLength); + // Store a snapshot of files to the snapshot file + await parcelWatcherLib.writeSnapshot(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND }); + } catch (error) { + this.onUnexpectedError(error, request); } - // Store a snapshot of files to the snapshot file - await parcelWatcherLib.writeSnapshot(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND }); - // Signal we are ready now when the first snapshot was written if (counter === 1) { instance.complete(); @@ -578,6 +588,12 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS } } + // Version 2.5.1 introduces 3 new errors on macOS + // via https://github.dev/parcel-bundler/watcher/pull/196 + else if (msg.indexOf('File system must be re-scanned') !== -1) { + this.error(msg, request); + } + // Any other error is unexpected and we should try to // restart the watcher as a result to get into healthy // state again if possible and if not attempted too much diff --git a/src/vs/platform/files/test/node/parcelWatcher.test.ts b/src/vs/platform/files/test/node/parcelWatcher.test.ts index e2038a17f50e..40dfc38b49cf 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.test.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.test.ts @@ -748,11 +748,11 @@ suite.skip('File Watcher (parcel)', function () { assert.strictEqual(instance.failed, true); }); - (isWindows /* Windows: times out for some reason */ ? test.skip : test)('watch requests support suspend/resume (folder, does not exist in beginning, not reusing watcher)', async () => { + (!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('watch requests support suspend/resume (folder, does not exist in beginning, not reusing watcher)', async () => { await testWatchFolderDoesNotExist(false); }); - (!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('watch requests support suspend/resume (folder, does not exist in beginning, reusing watcher)', async () => { + test('watch requests support suspend/resume (folder, does not exist in beginning, reusing watcher)', async () => { await testWatchFolderDoesNotExist(true); }); @@ -805,11 +805,11 @@ suite.skip('File Watcher (parcel)', function () { } } - (isWindows /* Windows: times out for some reason */ ? test.skip : test)('watch requests support suspend/resume (folder, exist in beginning, not reusing watcher)', async () => { + (!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('watch requests support suspend/resume (folder, exist in beginning, not reusing watcher)', async () => { await testWatchFolderExists(false); }); - (!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('watch requests support suspend/resume (folder, exist in beginning, reusing watcher)', async () => { + test('watch requests support suspend/resume (folder, exist in beginning, reusing watcher)', async () => { await testWatchFolderExists(true); }); From 8bc418429f00fed6824c6afda5ba038494526300 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 31 Jan 2025 11:56:15 +0100 Subject: [PATCH 1070/3587] Fix end of line trimming in inline completions (#239299) fix end of line trimming --- .../browser/model/inlineCompletionsSource.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index 96cb605003a9..e7e579204e5f 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -378,8 +378,9 @@ export class InlineCompletionWithUpdatedRange { } private _toIndividualEdits(editRange: Range, _replaceText: string): OffsetEdit { + const eol = this._textModel.getEOL(); const editOriginalText = this._textModel.getValueInRange(editRange); - const editReplaceText = _replaceText.replace(/\r\n|\r|\n/g, this._textModel.getEOL()); + const editReplaceText = _replaceText.replace(/\r\n|\r|\n/g, eol); const diffAlgorithm = linesDiffComputers.getDefault(); const lineDiffs = diffAlgorithm.computeDiff( @@ -414,7 +415,15 @@ export class InlineCompletionWithUpdatedRange { const startOffset = this._textModel.getOffsetAt(range.getStartPosition()); const endOffset = this._textModel.getOffsetAt(range.getEndPosition()); const originalRange = OffsetRange.ofStartAndLength(startOffset, endOffset - startOffset); - return new SingleOffsetEdit(originalRange, modifiedText.getValueOfRange(c.modifiedRange)); + + // TODO: EOL are not properly trimmed by the diffAlgorithm #12680 + const replaceText = modifiedText.getValueOfRange(c.modifiedRange); + const oldText = this._textModel.getValueInRange(range); + if (replaceText.endsWith(eol) && oldText.endsWith(eol)) { + return new SingleOffsetEdit(originalRange.deltaEnd(-eol.length), replaceText.slice(0, -eol.length)); + } + + return new SingleOffsetEdit(originalRange, replaceText); }) ); } From bdbd003442332bddb7ff5e1cb38b4184ae6ce6ec Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 31 Jan 2025 12:17:22 +0100 Subject: [PATCH 1071/3587] make sure to reveal/restore only once per chat request (#239306) https://github.com/microsoft/vscode/issues/239301 --- .../chat/browser/chatEditorController.ts | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index a3f4439acb08..13e98f95ec93 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -6,7 +6,7 @@ import './media/chatEditorController.css'; import { addStandardDisposableListener, getTotalWidth } from '../../../../base/browser/dom.js'; import { Disposable, DisposableStore, dispose, toDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, autorunWithStore, derived, IObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; +import { autorun, autorunWithStore, derived, IObservable, observableFromEvent, observableFromEventOpts, observableValue } from '../../../../base/common/observable.js'; import { themeColorFromId } from '../../../../base/common/themables.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, IViewZone, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; import { LineSource, renderLines, RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; @@ -127,8 +127,24 @@ export class ChatEditorController extends Disposable implements IEditorContribut return undefined; }); + const lastRequest = derived(r => { + const entry = entryForEditor.read(r); + if (!entry) { + return undefined; + } + return observableFromEventOpts( + { equalsFn: (a, b) => a?.id === b?.id }, + entry.chatModel.onDidChange, () => entry.chatModel.getRequests().at(-1) + ).read(r); + }); let scrollState: StableEditorScrollState | undefined = undefined; + let didReveal = false; + this._register(autorun(r => { + const value = lastRequest.read(r); + scrollState = value ? StableEditorScrollState.capture(_editor) : undefined; + didReveal = false; + })); this._register(autorunWithStore((r, store) => { @@ -137,7 +153,6 @@ export class ChatEditorController extends Disposable implements IEditorContribut if (!currentEditorEntry) { this._ctxIsGlobalEditsSession.reset(); this._clear(); - scrollState = undefined; return; } @@ -146,13 +161,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut return; } - const { session, chatModel, entries, idx, entry } = currentEditorEntry; - - const lastRequestSignal = observableFromEvent(this, chatModel.onDidChange, () => chatModel.getRequests().at(-1)); - store.add(autorun(r => { - lastRequestSignal.read(r); - scrollState ??= StableEditorScrollState.capture(this._editor); - })); + const { session, entries, idx, entry } = currentEditorEntry; this._ctxIsGlobalEditsSession.set(session.isGlobalEditingSession); this._ctxReviewModelEnabled.set(entry.reviewMode.read(r)); @@ -206,12 +215,13 @@ export class ChatEditorController extends Disposable implements IEditorContribut this._clearDiffRendering(); } - if (lastRequestSignal.read(r)?.response?.isComplete) { - if (!diff.identical) { - this._reveal(true, false, ScrollType.Immediate); - } else { + if (lastRequest.read(r)?.response?.isComplete) { + if (diff.identical) { scrollState?.restore(_editor); scrollState = undefined; + } else if (!didReveal) { + this._reveal(true, false, ScrollType.Immediate); + didReveal = true; } } } From 4fdaa8ad09611095370d1420bef3309bf69e9868 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:40:46 +0100 Subject: [PATCH 1072/3587] Use model EOL for insertion view new line shifting/trimming (#239313) use model EOL for insertion view new line shifting/trimming --- .../browser/view/inlineEdits/insertionView.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts index be0cc7f963bd..0f545a59f56a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts @@ -30,10 +30,11 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits if (!state) { return undefined; } const textModel = this._editor.getModel()!; + const eol = textModel.getEOL(); - if (state.startColumn === 1 && state.lineNumber > 1 && textModel.getLineLength(state.lineNumber) !== 0 && state.text.endsWith('\n') && !state.text.startsWith('\n')) { + if (state.startColumn === 1 && state.lineNumber > 1 && textModel.getLineLength(state.lineNumber) !== 0 && state.text.endsWith(eol) && !state.text.startsWith(eol)) { const endOfLineColumn = textModel.getLineLength(state.lineNumber - 1) + 1; - return { lineNumber: state.lineNumber - 1, column: endOfLineColumn, text: '\n' + state.text.slice(0, -1) }; + return { lineNumber: state.lineNumber - 1, column: endOfLineColumn, text: eol + state.text.slice(0, -eol.length) }; } return { lineNumber: state.lineNumber, column: state.startColumn, text: state.text }; @@ -88,12 +89,12 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits } this._editorObs.versionId.read(reader); const textModel = this._editor.getModel()!; + const eol = textModel.getEOL(); - const cleanText = state.text.replace('\r\n', '\n'); - const textBeforeInsertion = cleanText.startsWith('\n') ? '' : textModel.getValueInRange(new Range(state.lineNumber, 1, state.lineNumber, state.column)); + const textBeforeInsertion = state.text.startsWith(eol) ? '' : textModel.getValueInRange(new Range(state.lineNumber, 1, state.lineNumber, state.column)); const textAfterInsertion = textModel.getValueInRange(new Range(state.lineNumber, state.column, state.lineNumber, textModel.getLineLength(state.lineNumber) + 1)); - const text = textBeforeInsertion + cleanText + textAfterInsertion; - const lines = text.split('\n'); + const text = textBeforeInsertion + state.text + textAfterInsertion; + const lines = text.split(eol); const renderOptions = RenderOptions.fromEditor(this._editor).withSetWidth(false); const lineWidths = lines.map(line => { @@ -121,15 +122,16 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits // Adjust for leading/trailing newlines const lineHeight = this._editor.getOption(EditorOption.lineHeight); + const eol = this._editor.getModel()!.getEOL(); let topTrim = 0; let bottomTrim = 0; let i = 0; - for (; i < text.length && text[i] === '\n'; i++) { + for (; i < text.length && text.startsWith(eol, i); i += eol.length) { topTrim += lineHeight; } - for (let j = text.length - 1; j > i && text[j] === '\n'; j--) { + for (let j = text.length; j > i && text.endsWith(eol, j); j -= eol.length) { bottomTrim += lineHeight; } From d57350fa13782963c56c07ea645d6fd7cd3c25b2 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 31 Jan 2025 13:03:35 +0100 Subject: [PATCH 1073/3587] TST should handle `undefined` correctly (#239315) re https://github.com/microsoft/vscode/issues/239274 --- src/vs/base/common/ternarySearchTree.ts | 34 +++++++++++++------ .../test/common/ternarySearchtree.test.ts | 21 ++++++++++++ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/vs/base/common/ternarySearchTree.ts b/src/vs/base/common/ternarySearchTree.ts index f31f58e703ce..624b7c933270 100644 --- a/src/vs/base/common/ternarySearchTree.ts +++ b/src/vs/base/common/ternarySearchTree.ts @@ -247,17 +247,31 @@ export class UriIterator implements IKeyIterator { throw new Error(); } } + +abstract class Undef { + + static readonly Val: unique symbol = Symbol('undefined_placeholder'); + + static wrap(value: V | undefined): V | typeof Undef.Val { + return value === undefined ? Undef.Val : value; + } + + static unwrap(value: V | typeof Undef.Val): V | undefined { + return value === Undef.Val ? undefined : value as V; + } +} + class TernarySearchTreeNode { height: number = 1; segment!: string; - value: V | undefined; + value: V | typeof Undef.Val | undefined; key: K | undefined; left: TernarySearchTreeNode | undefined; mid: TernarySearchTreeNode | undefined; right: TernarySearchTreeNode | undefined; isEmpty(): boolean { - return !this.left && !this.mid && !this.right && !this.value; + return !this.left && !this.mid && !this.right && this.value === undefined; } rotateLeft() { @@ -401,8 +415,8 @@ export class TernarySearchTree { } // set value - const oldElement = node.value; - node.value = element; + const oldElement = Undef.unwrap(node.value); + node.value = Undef.wrap(element); node.key = key; // balance @@ -462,7 +476,7 @@ export class TernarySearchTree { } get(key: K): V | undefined { - return this._getNode(key)?.value; + return Undef.unwrap(this._getNode(key)?.value); } private _getNode(key: K) { @@ -644,13 +658,13 @@ export class TernarySearchTree { } else if (iter.hasNext()) { // mid iter.next(); - candidate = node.value || candidate; + candidate = Undef.unwrap(node.value) || candidate; node = node.mid; } else { break; } } - return node && node.value || candidate; + return node && Undef.unwrap(node.value) || candidate; } findSuperstr(key: K): IterableIterator<[K, V]> | undefined { @@ -678,7 +692,7 @@ export class TernarySearchTree { // collect if (!node.mid) { if (allowValue) { - return node.value; + return Undef.unwrap(node.value); } else { return undefined; } @@ -718,8 +732,8 @@ export class TernarySearchTree { if (node.left) { this._dfsEntries(node.left, bucket); } - if (node.value) { - bucket.push([node.key!, node.value]); + if (node.value !== undefined) { + bucket.push([node.key!, Undef.unwrap(node.value)!]); } if (node.mid) { this._dfsEntries(node.mid, bucket); diff --git a/src/vs/base/test/common/ternarySearchtree.test.ts b/src/vs/base/test/common/ternarySearchtree.test.ts index 592ef0a597cc..df36727e2d13 100644 --- a/src/vs/base/test/common/ternarySearchtree.test.ts +++ b/src/vs/base/test/common/ternarySearchtree.test.ts @@ -241,6 +241,27 @@ suite('Ternary Search Tree', () => { ); }); + test('TernarySearchTree - set w/ undefined', function () { + + const trie = TernarySearchTree.forStrings(); + trie.set('foobar', undefined); + trie.set('foobaz', 2); + + assert.strictEqual(trie.get('foobar'), undefined); + assert.strictEqual(trie.get('foobaz'), 2); + assert.strictEqual(trie.get('NOT HERE'), undefined); + + assert.ok(trie.has('foobaz')); + assert.ok(trie.has('foobar')); + assert.ok(!trie.has('NOT HERE')); + + assertTstDfs(trie, ['foobar', undefined], ['foobaz', 2]); // should check for undefined value + + const oldValue = trie.set('foobar', 3); + assert.strictEqual(oldValue, undefined); + assert.strictEqual(trie.get('foobar'), 3); + }); + test('TernarySearchTree - findLongestMatch', function () { const trie = TernarySearchTree.forStrings(); From 8c645ee6e4db8563cd32457b0f776fd4947ded10 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 31 Jan 2025 14:15:48 +0100 Subject: [PATCH 1074/3587] First link in dialog is not first in tab order. (fix #239267) (#239318) --- src/vs/base/browser/ui/dialog/dialog.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index a26806c6e435..0c357a671e78 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -324,10 +324,6 @@ export class Dialog extends Disposable { // Focus next element (with wrapping) if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow)) { - if (focusedIndex === -1) { - focusedIndex = 0; // default to focus first element if none have focus - } - const newFocusedIndex = (focusedIndex + 1) % focusableElements.length; focusableElements[newFocusedIndex].focus(); } From 022ab4de6a9ec79d7225db43f78b0f72b2b58b7f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 05:25:23 -0800 Subject: [PATCH 1075/3587] Improve terminal setting both descriptions Fixes #239304 --- .../electron-sandbox/externalTerminal.contribution.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts b/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts index 21967d3de0c5..79d0c7dbd8f0 100644 --- a/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts +++ b/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts @@ -107,7 +107,7 @@ export class ExternalTerminalContribution implements IWorkbenchContribution { enumDescriptions: [ nls.localize('terminal.explorerKind.integrated', "Use VS Code's integrated terminal."), nls.localize('terminal.explorerKind.external', "Use the configured external terminal."), - nls.localize('terminal.explorerKind.both', "Use the other two together.") + nls.localize('terminal.explorerKind.both', "Show both integrated and external terminal actions.") ], description: nls.localize('explorer.openInTerminalKind', "When opening a file from the Explorer in a terminal, determines what kind of terminal will be launched"), default: 'integrated' @@ -122,7 +122,7 @@ export class ExternalTerminalContribution implements IWorkbenchContribution { enumDescriptions: [ nls.localize('terminal.sourceControlRepositoriesKind.integrated', "Use VS Code's integrated terminal."), nls.localize('terminal.sourceControlRepositoriesKind.external', "Use the configured external terminal."), - nls.localize('terminal.sourceControlRepositoriesKind.both', "Use the other two together.") + nls.localize('terminal.sourceControlRepositoriesKind.both', "Show both integrated and external terminal actions.") ], description: nls.localize('sourceControlRepositories.openInTerminalKind', "When opening a repository from the Source Control Repositories view in a terminal, determines what kind of terminal will be launched"), default: 'integrated' From 6b7338d9d8f9a43d35e729e7e704a4ca05df76d6 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 31 Jan 2025 11:15:59 -0300 Subject: [PATCH 1076/3587] Adds debug descriptions (#239323) --- .../browser/view/inlineEdits/utils.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts index 3c80494c8ba3..b4338882ea4a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts @@ -9,7 +9,7 @@ import { numberComparator } from '../../../../../../base/common/arrays.js'; import { findFirstMin } from '../../../../../../base/common/arraysFind.js'; import { BugIndicatingError } from '../../../../../../base/common/errors.js'; import { DisposableStore, IDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; -import { derived, derivedObservableWithCache, IObservable, IReader, observableValue, transaction } from '../../../../../../base/common/observable.js'; +import { derived, derivedObservableWithCache, derivedOpts, IObservable, IReader, observableValue, transaction } from '../../../../../../base/common/observable.js'; import { OS } from '../../../../../../base/common/platform.js'; import { getIndentationLength, splitLines } from '../../../../../../base/common/strings.js'; import { URI } from '../../../../../../base/common/uri.js'; @@ -392,6 +392,7 @@ export abstract class ObserverNode { if (className) { if (hasObservable(className)) { this._deriveds.push(derived(this, reader => { + /** @description set.class */ setClassName(this._element, getClassName(className, reader)); })); } else { @@ -404,7 +405,7 @@ export abstract class ObserverNode { for (const [cssKey, cssValue] of Object.entries(value)) { const key = camelCaseToHyphenCase(cssKey); if (isObservable(cssValue)) { - this._deriveds.push(derived(this, reader => { + this._deriveds.push(derivedOpts({ owner: this, debugName: () => `set.style.${key}` }, reader => { this._element.style.setProperty(key, convertCssValue(cssValue.read(reader))); })); } else { @@ -414,6 +415,7 @@ export abstract class ObserverNode { } else if (key === 'tabIndex') { if (isObservable(value)) { this._deriveds.push(derived(this, reader => { + /** @description set.tabIndex */ this._element.tabIndex = value.read(reader) as any; })); } else { @@ -423,7 +425,7 @@ export abstract class ObserverNode { (this._element as any)[key] = value; } else { if (isObservable(value)) { - this._deriveds.push(derived(this, reader => { + this._deriveds.push(derivedOpts({ owner: this, debugName: () => `set.${key}` }, reader => { setOrRemoveAttribute(this._element, key, value.read(reader)); })); } else { @@ -453,6 +455,7 @@ export abstract class ObserverNode { } const d = derived(this, reader => { + /** @description set.children */ this._element.replaceChildren(...getChildren(reader, children)); }); this._deriveds.push(d); @@ -470,6 +473,7 @@ export abstract class ObserverNode { keepUpdated(store: DisposableStore): ObserverNodeWithElement { derived(reader => { + /** update */ this.readEffect(reader); }).recomputeInitiallyAndOnChange(store); return this as unknown as ObserverNodeWithElement; @@ -604,7 +608,9 @@ type Falsy = T extends false | undefined | null ? T : never; export function mapOutFalsy(obs: IObservable): IObservable> | Falsy> { const nonUndefinedObs = derivedObservableWithCache(undefined, (reader, lastValue) => obs.read(reader) || lastValue); - return derived(reader => { + return derivedOpts({ + debugName: () => `${obs.debugName}.mapOutFalsy` + }, reader => { nonUndefinedObs.read(reader); const val = obs.read(reader); if (!val) { From 5890706a4814ac77cf213b978fcd03801fe517e7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 06:21:01 -0800 Subject: [PATCH 1077/3587] Revert: Editor GPU: Pass content width through to scroll bar This was causing the dom and gpu view lines to both update scroll width which caused an infinite loop as that always triggers an update in the current animation frame. Fixes #239321 --- src/vs/editor/browser/gpu/gpu.ts | 6 +----- .../browser/gpu/renderStrategy/baseRenderStrategy.ts | 4 ++-- .../gpu/renderStrategy/fullFileRenderStrategy.ts | 8 +++----- .../gpu/renderStrategy/viewportRenderStrategy.ts | 9 ++++----- src/vs/editor/browser/gpu/viewGpuContext.ts | 2 -- src/vs/editor/browser/view.ts | 1 - .../browser/viewParts/viewLinesGpu/viewLinesGpu.ts | 11 ++--------- 7 files changed, 12 insertions(+), 29 deletions(-) diff --git a/src/vs/editor/browser/gpu/gpu.ts b/src/vs/editor/browser/gpu/gpu.ts index 500b167e59a9..7284d275c769 100644 --- a/src/vs/editor/browser/gpu/gpu.ts +++ b/src/vs/editor/browser/gpu/gpu.ts @@ -36,10 +36,6 @@ export interface IGpuRenderStrategy extends IDisposable { * Resets the render strategy, clearing all data and setting up for a new frame. */ reset(): void; - update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): IGpuRenderStrategyUpdateResult; + update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number; draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void; } - -export interface IGpuRenderStrategyUpdateResult { - localContentWidth: number; -} diff --git a/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts index 59c7a3e3a535..b5d9fc895ccb 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/baseRenderStrategy.ts @@ -7,7 +7,7 @@ import { ViewEventHandler } from '../../../common/viewEventHandler.js'; import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; import type { ViewContext } from '../../../common/viewModel/viewContext.js'; import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions.js'; -import type { IGpuRenderStrategy, IGpuRenderStrategyUpdateResult } from '../gpu.js'; +import type { IGpuRenderStrategy } from '../gpu.js'; import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; import type { ViewGpuContext } from '../viewGpuContext.js'; @@ -31,6 +31,6 @@ export abstract class BaseRenderStrategy extends ViewEventHandler implements IGp } abstract reset(): void; - abstract update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): IGpuRenderStrategyUpdateResult; + abstract update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number; abstract draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void; } diff --git a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index 71f6cc94228f..c41efda089b1 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -16,7 +16,7 @@ import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions. import type { ITextureAtlasPageGlyph } from '../atlas/atlas.js'; import { createContentSegmenter, type IContentSegmenter } from '../contentSegmenter.js'; import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js'; -import { BindingId, type IGpuRenderStrategyUpdateResult } from '../gpu.js'; +import { BindingId } from '../gpu.js'; import { GPULifecycle } from '../gpuDisposable.js'; import { quadVertices } from '../gpuUtils.js'; import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; @@ -232,7 +232,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { this._finalRenderedLine = 0; } - update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): IGpuRenderStrategyUpdateResult { + update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number { // IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the // loop. This is done so we don't need to trust the JIT compiler to do this optimization to // avoid potential additional blocking time in garbage collector which is a common cause of @@ -501,9 +501,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { this._visibleObjectCount = visibleObjectCount; - return { - localContentWidth: absoluteOffsetX - }; + return visibleObjectCount; } draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void { diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts index b71aaff05bc6..f35ccc85edcf 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -16,7 +16,7 @@ import type { ViewContext } from '../../../common/viewModel/viewContext.js'; import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions.js'; import type { ITextureAtlasPageGlyph } from '../atlas/atlas.js'; import { createContentSegmenter, type IContentSegmenter } from '../contentSegmenter.js'; -import { BindingId, type IGpuRenderStrategyUpdateResult } from '../gpu.js'; +import { BindingId } from '../gpu.js'; import { GPULifecycle } from '../gpuDisposable.js'; import { quadVertices } from '../gpuUtils.js'; import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; @@ -184,7 +184,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { } } - update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): IGpuRenderStrategyUpdateResult { + update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number { // IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the // loop. This is done so we don't need to trust the JIT compiler to do this optimization to // avoid potential additional blocking time in garbage collector which is a common cause of @@ -394,9 +394,8 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1; this._visibleObjectCount = visibleObjectCount; - return { - localContentWidth: absoluteOffsetX - }; + + return visibleObjectCount; } draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void { diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 3cf8238c6c60..181c468f12c5 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -33,7 +33,6 @@ export class ViewGpuContext extends Disposable { readonly maxGpuCols = ViewportRenderStrategy.maxSupportedColumns; readonly canvas: FastDomNode; - readonly scrollWidthElement: FastDomNode; readonly ctx: GPUCanvasContext; static device: Promise; @@ -88,7 +87,6 @@ export class ViewGpuContext extends Disposable { this.canvas = createFastDomNode(document.createElement('canvas')); this.canvas.setClassName('editorCanvas'); - this.scrollWidthElement = createFastDomNode(document.createElement('div')); // Adjust the canvas size to avoid drawing under the scroll bar this._register(Event.runAndSubscribe(configurationService.onDidChangeConfiguration, e => { diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index e383a7c9b2cb..1e883bfcd2b2 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -256,7 +256,6 @@ export class View extends ViewEventHandler { this._overflowGuardContainer.appendChild(this._scrollbar.getDomNode()); if (this._viewGpuContext) { this._overflowGuardContainer.appendChild(this._viewGpuContext.canvas); - this._linesContent.appendChild(this._viewGpuContext.scrollWidthElement); } this._overflowGuardContainer.appendChild(scrollDecoration.getDomNode()); this._overflowGuardContainer.appendChild(this._overlayWidgets.getDomNode()); diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index f7485a2f2b7b..a6cabd559512 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -49,7 +49,6 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { private _initViewportData?: ViewportData[]; private _lastViewportData?: ViewportData; private _lastViewLineOptions?: ViewLineOptions; - private _maxLocalContentWidthSoFar = 0; private _device!: GPUDevice; private _renderPassDescriptor!: GPURenderPassDescriptor; @@ -430,6 +429,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { return true; } override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { return true; } override onFlushed(e: viewEvents.ViewFlushedEvent): boolean { return true; } + override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean { return true; } override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean { return true; } override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean { return true; } @@ -473,14 +473,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { const options = new ViewLineOptions(this._context.configuration, this._context.theme.type); - const { localContentWidth } = this._renderStrategy.value!.update(viewportData, options); - - // Track the largest local content width so far in this session and use it as the scroll - // width. This is how the DOM renderer works as well, so you may not be able to scroll to - // the right in a file with long lines until you scroll down. - this._maxLocalContentWidthSoFar = Math.max(this._maxLocalContentWidthSoFar, localContentWidth / this._viewGpuContext.devicePixelRatio.get()); - this._context.viewModel.viewLayout.setMaxLineWidth(this._maxLocalContentWidthSoFar); - this._viewGpuContext.scrollWidthElement.setWidth(this._context.viewLayout.getScrollWidth()); + this._renderStrategy.value!.update(viewportData, options); this._updateAtlasStorageBufferAndTexture(); From 180efc38de0d5605c820ff70576f6a9fb9206fff Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 06:32:29 -0800 Subject: [PATCH 1078/3587] Tweak wording and share config between kind settings --- .../externalTerminal.contribution.ts | 42 ++++++++----------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts b/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts index 79d0c7dbd8f0..572445bbec5a 100644 --- a/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts +++ b/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts @@ -11,7 +11,7 @@ import { KeyMod, KeyCode } from '../../../../base/common/keyCodes.js'; import { IHistoryService } from '../../../services/history/common/history.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { Schemas } from '../../../../base/common/network.js'; -import { IConfigurationRegistry, Extensions, ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { IConfigurationRegistry, Extensions, ConfigurationScope, type IConfigurationPropertySchema } from '../../../../platform/configuration/common/configurationRegistry.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../common/contributions.js'; import { IExternalTerminalService } from '../../../../platform/externalTerminal/electron-sandbox/externalTerminalService.js'; @@ -91,6 +91,20 @@ export class ExternalTerminalContribution implements IWorkbenchContribution { private async _updateConfiguration(): Promise { const terminals = await this._externalTerminalService.getDefaultTerminalForPlatforms(); const configurationRegistry = Registry.as(Extensions.Configuration); + const terminalKindProperties: Partial = { + type: 'string', + enum: [ + 'integrated', + 'external', + 'both' + ], + enumDescriptions: [ + nls.localize('terminal.kind.integrated', "Show the integrated terminal action."), + nls.localize('terminal.kind.external', "Show the external terminal action."), + nls.localize('terminal.kind.both', "Show both integrated and external terminal actions.") + ], + default: 'integrated' + }; configurationRegistry.registerConfiguration({ id: 'externalTerminal', order: 100, @@ -98,34 +112,12 @@ export class ExternalTerminalContribution implements IWorkbenchContribution { type: 'object', properties: { 'terminal.explorerKind': { - type: 'string', - enum: [ - 'integrated', - 'external', - 'both' - ], - enumDescriptions: [ - nls.localize('terminal.explorerKind.integrated', "Use VS Code's integrated terminal."), - nls.localize('terminal.explorerKind.external', "Use the configured external terminal."), - nls.localize('terminal.explorerKind.both', "Show both integrated and external terminal actions.") - ], + ...terminalKindProperties, description: nls.localize('explorer.openInTerminalKind', "When opening a file from the Explorer in a terminal, determines what kind of terminal will be launched"), - default: 'integrated' }, 'terminal.sourceControlRepositoriesKind': { - type: 'string', - enum: [ - 'integrated', - 'external', - 'both' - ], - enumDescriptions: [ - nls.localize('terminal.sourceControlRepositoriesKind.integrated', "Use VS Code's integrated terminal."), - nls.localize('terminal.sourceControlRepositoriesKind.external', "Use the configured external terminal."), - nls.localize('terminal.sourceControlRepositoriesKind.both', "Show both integrated and external terminal actions.") - ], + ...terminalKindProperties, description: nls.localize('sourceControlRepositories.openInTerminalKind', "When opening a repository from the Source Control Repositories view in a terminal, determines what kind of terminal will be launched"), - default: 'integrated' }, 'terminal.external.windowsExec': { type: 'string', From f6a4de812d9d2c4e5c1395c2ec1313ae9e45e6eb Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:50:54 +0100 Subject: [PATCH 1079/3587] Center gutter indicator icon in NES view (#239331) fixes https://github.com/microsoft/vscode-copilot/issues/12694 --- .../browser/view/inlineEdits/gutterIndicatorView.ts | 3 +++ .../inlineCompletions/browser/view/inlineEdits/view.css | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index 72d5bf09400a..b1da2472750a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -282,6 +282,9 @@ export class InlineEditsGutterIndicator extends Disposable { } }), transition: 'rotate 0.2s ease-in-out', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', } }, [ this._tabAction.map(v => v === 'accept' ? renderIcon(Codicon.keyboardTab) : renderIcon(Codicon.arrowRight)) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index ac15ee3dd2d1..5e98709fda84 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -263,10 +263,6 @@ } } -.inline-edits-view-gutter-indicator .codicon { - margin-top: 1px; /* TODO: Move into gutter DOM initialization */ -} - @keyframes wiggle { 0% { transform: rotate(0) scale(1); From 5c5cb3dc1fcb130cfed29d1c76588cd243d3548a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 06:46:50 -0800 Subject: [PATCH 1080/3587] Turn local echo off by default and downgrade to preview This feature has been a pain point for many remote users for a long time. Something critical to the feature is that it must maintain consistency and heal "mistakes", but unfortunately this isn't true and it can lead to buffer corruption and therefore could lead the user to run a command they didn't mean to. Additionally we don't disable the feature after it fails (#119103) or respect the ECHO termios mode and can therefore echo password characters to the user (#130821). My plan is to keep it off as a preview for the foreseeable future as we don't have plans to prioritize this work any time soon. Part of #126209 --- .../typeAhead/common/terminalTypeAheadConfiguration.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/typeAhead/common/terminalTypeAheadConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/typeAhead/common/terminalTypeAheadConfiguration.ts index 4f32102b09cc..789e4ed39c3b 100644 --- a/src/vs/workbench/contrib/terminalContrib/typeAhead/common/terminalTypeAheadConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/typeAhead/common/terminalTypeAheadConfiguration.ts @@ -29,6 +29,7 @@ export const terminalTypeAheadConfiguration: IStringDictionary Date: Fri, 31 Jan 2025 16:12:15 +0100 Subject: [PATCH 1081/3587] Copilot Edits: voice recording icon jumps to the right when activated (fix microsoft/vscode-copilot#12679) (#239333) --- .../actions/voiceChatActions.ts | 89 ++++++------------- 1 file changed, 26 insertions(+), 63 deletions(-) diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index f3fe9985e72c..c7eb41e1b303 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -19,7 +19,7 @@ import { Action2, IAction2Options, MenuId } from '../../../../../platform/action import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { Extensions, IConfigurationRegistry } from '../../../../../platform/configuration/common/configurationRegistry.js'; -import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, ContextKeyExpression, IContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; @@ -533,17 +533,28 @@ export class QuickVoiceChatAction extends VoiceChatWithHoldModeAction { } } +const primaryVoiceActionMenu = (when: ContextKeyExpression | undefined) => { + return [ + { + id: MenuId.ChatInput, + when: ContextKeyExpr.and(ContextKeyExpr.or(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)), when), + group: 'navigation', + order: 3 + }, + { + id: MenuId.ChatExecute, + when: ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel).negate(), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession).negate(), when), + group: 'navigation', + order: 2 + } + ]; +}; + export class StartVoiceChatAction extends Action2 { static readonly ID = 'workbench.action.chat.startVoiceChat'; constructor() { - const menuCondition = ContextKeyExpr.and( - HasSpeechProvider, - ScopedChatSynthesisInProgress.negate(), // hide when text to speech is in progress - AnyScopedVoiceChatInProgress?.negate(), // hide when voice chat is in progress - ); - super({ id: StartVoiceChatAction.ID, title: localize2('workbench.action.chat.startVoiceChat.label', "Start Voice Chat"), @@ -565,20 +576,11 @@ export class StartVoiceChatAction extends Action2 { AnyChatRequestInProgress?.negate(), // disable when any chat request is in progress SpeechToTextInProgress.negate() // disable when speech to text is in progress ), - menu: [ - { - id: MenuId.ChatInput, - when: ContextKeyExpr.and(ContextKeyExpr.or(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)), menuCondition), - group: 'navigation', - order: 3 - }, - { - id: MenuId.ChatExecute, - when: ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel).negate(), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession).negate(), menuCondition), - group: 'navigation', - order: 2 - } - ] + menu: primaryVoiceActionMenu(ContextKeyExpr.and( + HasSpeechProvider, + ScopedChatSynthesisInProgress.negate(), // hide when text to speech is in progress + AnyScopedVoiceChatInProgress?.negate(), // hide when voice chat is in progress + )) }); } @@ -613,20 +615,7 @@ export class StopListeningAction extends Action2 { }, icon: spinningLoading, precondition: GlobalVoiceChatInProgress, // need global context here because of `f1: true` - menu: [ - { - id: MenuId.ChatInput, - when: ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), AnyScopedVoiceChatInProgress), - group: 'navigation', - order: 3 - }, - { - id: MenuId.ChatExecute, - when: ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel).negate(), AnyScopedVoiceChatInProgress), - group: 'navigation', - order: 2 - } - ] + menu: primaryVoiceActionMenu(AnyScopedVoiceChatInProgress) }); } @@ -960,20 +949,7 @@ export class StopReadAloud extends Action2 { primary: KeyCode.Escape, when: ScopedChatSynthesisInProgress }, - menu: [ - { - id: MenuId.ChatInput, - when: ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ScopedChatSynthesisInProgress), - group: 'navigation', - order: 3 - }, - { - id: MenuId.ChatExecute, - when: ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel).negate(), ScopedChatSynthesisInProgress), - group: 'navigation', - order: 2 - } - ] + menu: primaryVoiceActionMenu(ScopedChatSynthesisInProgress) }); } @@ -1307,20 +1283,7 @@ export class InstallSpeechProviderForVoiceChatAction extends BaseInstallSpeechPr title: localize2('workbench.action.chat.installProviderForVoiceChat.label', "Start Voice Chat"), icon: Codicon.mic, precondition: InstallingSpeechProvider.negate(), - menu: [ - { - id: MenuId.ChatInput, - when: ContextKeyExpr.and(HasSpeechProvider.negate(), ContextKeyExpr.or(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession))), - group: 'navigation', - order: 3 - }, - { - id: MenuId.ChatExecute, - when: ContextKeyExpr.and(HasSpeechProvider.negate(), ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel).negate(), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession).negate()), - group: 'navigation', - order: 2 - } - ] + menu: primaryVoiceActionMenu(HasSpeechProvider.negate()) }); } From d04d2d3bb0f8c165fc70229ca7c97222016379f1 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 31 Jan 2025 16:50:17 +0100 Subject: [PATCH 1082/3587] edits context key cleanup (#239337) * debt - remove `isApplyingChatEdits` context key, it is the same as "requestInProgress" https://github.com/microsoft/vscode/issues/238742 * rename to xyzServiceImpl * move edits context keys into widget, not inside service * fix init order bug --- .../browser/actions/chatExecuteActions.ts | 19 +- .../contrib/chat/browser/chat.contribution.ts | 2 +- ...ngService.ts => chatEditingServiceImpl.ts} | 202 +++++++----------- .../contrib/chat/browser/chatEditorActions.ts | 10 +- .../contrib/chat/browser/chatWidget.ts | 67 ++++-- .../contrib/chat/common/chatEditingService.ts | 7 +- 6 files changed, 146 insertions(+), 161 deletions(-) rename src/vs/workbench/contrib/chat/browser/chatEditing/{chatEditingService.ts => chatEditingServiceImpl.ts} (75%) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index cc5339b263cb..8bf302ae260e 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -16,7 +16,7 @@ import { KeybindingWeight } from '../../../../../platform/keybinding/common/keyb import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { applyingChatEditsContextKey, IChatEditingService, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { IChatEditingService, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { chatAgentLeader, extractAgentAndCommand } from '../../common/chatParserTypes.js'; import { IChatService } from '../../common/chatService.js'; import { EditsViewId, IChatWidget, IChatWidgetService } from '../chat.js'; @@ -218,7 +218,7 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { // if the input has prompt instructions attached, allow submitting requests even // without text present - having instructions is enough context for a request ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), - applyingChatEditsContextKey.toNegated(), + ChatContextKeys.requestInProgress.negate(), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), ); @@ -238,13 +238,13 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { { id: MenuId.ChatExecuteSecondary, group: 'group_1', - when: ContextKeyExpr.and(ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey.toNegated()), + when: ContextKeyExpr.and(ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)), order: 1 }, { id: MenuId.ChatExecute, order: 4, - when: ContextKeyExpr.and(ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey.toNegated()), + when: ContextKeyExpr.and(ContextKeyExpr.or(ChatContextKeys.isRequestPaused, ChatContextKeys.requestInProgress.negate()), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)), group: 'navigation', }, ] @@ -517,10 +517,7 @@ export class CancelAction extends Action2 { icon: Codicon.stopCircle, menu: { id: MenuId.ChatExecute, - when: ContextKeyExpr.or( - ContextKeyExpr.and(ChatContextKeys.isRequestPaused.negate(), ChatContextKeys.requestInProgress), - ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), applyingChatEditsContextKey) - ), + when: ContextKeyExpr.and(ChatContextKeys.isRequestPaused.negate(), ChatContextKeys.requestInProgress), order: 4, group: 'navigation', }, @@ -545,12 +542,6 @@ export class CancelAction extends Action2 { if (widget.viewModel) { chatService.cancelCurrentRequestForSession(widget.viewModel.sessionId); } - - const chatEditingService = accessor.get(IChatEditingService); - const currentEditingSession = chatEditingService.currentEditingSession; - if (currentEditingSession && currentEditingSession?.chatSessionId === widget.viewModel?.sessionId) { - chatEditingService.currentAutoApplyOperation?.cancel(); - } } } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 6b6889506b52..cf1d9b2ebf81 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -54,7 +54,7 @@ import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatW import { ChatAccessibilityService } from './chatAccessibilityService.js'; import './chatAttachmentModel.js'; import { ChatMarkdownAnchorService, IChatMarkdownAnchorService } from './chatContentParts/chatMarkdownAnchorService.js'; -import { ChatEditingService } from './chatEditing/chatEditingService.js'; +import { ChatEditingService } from './chatEditing/chatEditingServiceImpl.js'; import { ChatEditor, IChatEditorOptions } from './chatEditor.js'; import { registerChatEditorActions } from './chatEditorActions.js'; import { ChatEditorController } from './chatEditorController.js'; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts similarity index 75% rename from src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts rename to src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index 1aba7d7c8cdb..3da10d9ae3c8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts @@ -5,7 +5,7 @@ import { coalesce, compareBy, delta } from '../../../../../base/common/arrays.js'; import { AsyncIterableSource } from '../../../../../base/common/async.js'; -import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { BugIndicatingError, ErrorNoTelemetry } from '../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; @@ -22,11 +22,10 @@ import { URI } from '../../../../../base/common/uri.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { localize } from '../../../../../nls.js'; -import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; -import { bindContextKey } from '../../../../../platform/observable/common/platformObservableUtils.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; @@ -37,8 +36,7 @@ import { ILifecycleService } from '../../../../services/lifecycle/common/lifecyc import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from '../../../multiDiffEditor/browser/multiDiffSourceResolverService.js'; import { CellUri } from '../../../notebook/common/notebookCommon.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; -import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { applyingChatEditsContextKey, applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingAgentSupportsReadonlyReferencesContextKey, chatEditingMaxFileAssignmentName, chatEditingResourceContextKey, ChatEditingSessionState, decidedChatEditingResourceContextKey, defaultChatEditingMaxFileLimit, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, IChatEditingSessionStream, IChatRelatedFile, IChatRelatedFilesProvider, IModifiedFileEntry, inChatEditingSessionContextKey, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingAgentSupportsReadonlyReferencesContextKey, chatEditingMaxFileAssignmentName, chatEditingResourceContextKey, ChatEditingSessionState, defaultChatEditingMaxFileLimit, IChatEditingService, IChatEditingSession, IChatEditingSessionStream, IChatRelatedFile, IChatRelatedFilesProvider, IModifiedFileEntry, inChatEditingSessionContextKey, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel, IChatTextEditGroup } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { ChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js'; @@ -66,11 +64,6 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return result; }); - private readonly _currentAutoApplyOperationObs = observableValue(this, null); - get currentAutoApplyOperation(): CancellationTokenSource | null { - return this._currentAutoApplyOperationObs.get(); - } - get currentEditingSession(): IChatEditingSession | null { return this._currentSessionObs.get(); } @@ -91,8 +84,6 @@ export class ChatEditingService extends Disposable implements IChatEditingServic private _restoringEditingSession: Promise | undefined; - private _applyingChatEditsFailedContextKey: IContextKey; - private _chatRelatedFilesProviders = new Map(); constructor( @@ -113,54 +104,14 @@ export class ChatEditingService extends Disposable implements IChatEditingServic @IProductService productService: IProductService, ) { super(); - this._applyingChatEditsFailedContextKey = applyingChatEditsFailedContextKey.bindTo(contextKeyService); - this._applyingChatEditsFailedContextKey.set(false); this._register(decorationsService.registerDecorationsProvider(_instantiationService.createInstance(ChatDecorationsProvider, this._currentSessionObs))); this._register(multiDiffSourceResolverService.registerResolver(_instantiationService.createInstance(ChatEditingMultiDiffSourceResolver, this._currentSessionObs))); this._register(textModelService.registerTextModelContentProvider(ChatEditingTextModelContentProvider.scheme, _instantiationService.createInstance(ChatEditingTextModelContentProvider, this._currentSessionObs))); this._register(textModelService.registerTextModelContentProvider(ChatEditingSnapshotTextModelContentProvider.scheme, _instantiationService.createInstance(ChatEditingSnapshotTextModelContentProvider, this._currentSessionObs))); - this._register(bindContextKey(decidedChatEditingResourceContextKey, contextKeyService, (reader) => { - const currentSession = this._currentSessionObs.read(reader); - if (!currentSession) { - return; - } - const entries = currentSession.entries.read(reader); - const decidedEntries = entries.filter(entry => entry.state.read(reader) !== WorkingSetEntryState.Modified); - return decidedEntries.map(entry => entry.entryId); - })); - this._register(bindContextKey(hasUndecidedChatEditingResourceContextKey, contextKeyService, (reader) => { - for (const session of this.editingSessionsObs.read(reader)) { - const entries = session.entries.read(reader); - const decidedEntries = entries.filter(entry => entry.state.read(reader) === WorkingSetEntryState.Modified); - return decidedEntries.length > 0; - } - return false; - })); - this._register(bindContextKey(hasAppliedChatEditsContextKey, contextKeyService, (reader) => { - const currentSession = this._currentSessionObs.read(reader); - if (!currentSession) { - return false; - } - const entries = currentSession.entries.read(reader); - return entries.length > 0; - })); - this._register(bindContextKey(inChatEditingSessionContextKey, contextKeyService, (reader) => { - return this._currentSessionObs.read(reader) !== null; - })); - this._register(bindContextKey(applyingChatEditsContextKey, contextKeyService, (reader) => { - return this._currentAutoApplyOperationObs.read(reader) !== null; - })); - this._register(bindContextKey(ChatContextKeys.chatEditingCanUndo, contextKeyService, (r) => { - return this._currentSessionObs.read(r)?.canUndo.read(r) || false; - })); - this._register(bindContextKey(ChatContextKeys.chatEditingCanRedo, contextKeyService, (r) => { - return this._currentSessionObs.read(r)?.canRedo.read(r) || false; - })); this._register(this._chatService.onDidDisposeSession((e) => { if (e.reason === 'cleared' && this._currentSessionObs.get()?.chatSessionId === e.sessionId) { - this._applyingChatEditsFailedContextKey.set(false); void this._currentSessionObs.get()?.stop(); } })); @@ -306,7 +257,6 @@ export class ChatEditingService extends Disposable implements IChatEditingServic if (responseModel.result?.errorDetails && !responseModel.result.errorDetails.responseIsIncomplete) { // Roll back everything session.restoreSnapshot(responseModel.requestId); - this._applyingChatEditsFailedContextKey.set(true); } editsSource?.resolve(); @@ -317,94 +267,98 @@ export class ChatEditingService extends Disposable implements IChatEditingServic const handleResponseParts = async (responseModel: IChatResponseModel) => { + + if (responseModel.isCanceled) { + return; + } + for (const part of responseModel.response.value) { - if (part.kind === 'codeblockUri' || part.kind === 'textEditGroup') { - // ensure editor is open asap - if (!editedFilesExist.get(part.uri)) { - const uri = part.uri.scheme === Schemas.vscodeNotebookCell ? CellUri.parse(part.uri)?.notebook ?? part.uri : part.uri; - editedFilesExist.set(part.uri, this._fileService.exists(uri).then((e) => { - if (e) { - this._editorService.openEditor({ resource: uri, options: { inactive: true, preserveFocus: true, pinned: true } }); - } - return e; - })); - } + if (part.kind !== 'codeblockUri' && part.kind !== 'textEditGroup') { + continue; + } + // ensure editor is open asap + if (!editedFilesExist.get(part.uri)) { + const uri = part.uri.scheme === Schemas.vscodeNotebookCell ? CellUri.parse(part.uri)?.notebook ?? part.uri : part.uri; + editedFilesExist.set(part.uri, this._fileService.exists(uri).then((e) => { + if (e) { + this._editorService.openEditor({ resource: uri, options: { inactive: true, preserveFocus: true, pinned: true } }); + } + return e; + })); + } - // get new edits and start editing session - const first = editsSeen.size === 0; - let entry = editsSeen.get(part.uri); - if (!entry) { - entry = { seen: 0 }; - editsSeen.set(part.uri, entry); - } + // get new edits and start editing session + const first = editsSeen.size === 0; + let entry = editsSeen.get(part.uri); + if (!entry) { + entry = { seen: 0 }; + editsSeen.set(part.uri, entry); + } - const allEdits: TextEdit[][] = part.kind === 'textEditGroup' ? part.edits : []; - const newEdits = allEdits.slice(entry.seen); - entry.seen += newEdits.length; + const allEdits: TextEdit[][] = part.kind === 'textEditGroup' ? part.edits : []; + const newEdits = allEdits.slice(entry.seen); + entry.seen += newEdits.length; - if (newEdits.length > 0 || entry.seen === 0) { - // only allow empty edits when having just started, ignore otherwise to avoid unneccessary work - editsSource ??= new AsyncIterableSource(); - editsSource.emitOne({ uri: part.uri, edits: newEdits, kind: 'textEditGroup', done: part.kind === 'textEditGroup' && part.done }); - } + if (newEdits.length > 0 || entry.seen === 0) { + // only allow empty edits when having just started, ignore otherwise to avoid unneccessary work + editsSource ??= new AsyncIterableSource(); + editsSource.emitOne({ uri: part.uri, edits: newEdits, kind: 'textEditGroup', done: part.kind === 'textEditGroup' && part.done }); + } + + if (first) { - if (first) { - - await editsPromise; - - editsPromise = this._continueEditingSession(session, async (builder, token) => { - for await (const item of editsSource!.asyncIterable) { - if (responseModel.isCanceled) { - break; - } - if (token.isCancellationRequested) { - break; - } - if (item.edits.length === 0) { - // EMPTY edit, just signal via empty edits that work is starting - builder.textEdits(item.uri, [], item.done ?? false, responseModel); - continue; - } - for (let i = 0; i < item.edits.length; i++) { - const group = item.edits[i]; - const isLastGroup = i === item.edits.length - 1; - builder.textEdits(item.uri, group, isLastGroup && (item.done ?? false), responseModel); - } + await editsPromise; + + editsPromise = this._continueEditingSession(session, async (builder) => { + for await (const item of editsSource!.asyncIterable) { + if (responseModel.isCanceled) { + break; } - }).finally(() => { - editsPromise = undefined; - }); - } + if (item.edits.length === 0) { + // EMPTY edit, just signal via empty edits that work is starting + builder.textEdits(item.uri, [], item.done ?? false, responseModel); + continue; + } + for (let i = 0; i < item.edits.length; i++) { + const group = item.edits[i]; + const isLastGroup = i === item.edits.length - 1; + builder.textEdits(item.uri, group, isLastGroup && (item.done ?? false), responseModel); + } + } + }).finally(() => { + editsPromise = undefined; + }); } } }; observerDisposables.add(chatModel.onDidChange(async e => { - if (e.kind === 'addRequest') { - session.createSnapshot(e.request.id); - this._applyingChatEditsFailedContextKey.set(false); - const responseModel = e.request.response; - if (responseModel) { + if (e.kind !== 'addRequest') { + return; + } + session.createSnapshot(e.request.id); + const responseModel = e.request.response; + if (!responseModel) { + return; + } + if (responseModel.isComplete) { + await handleResponseParts(responseModel); + onResponseComplete(responseModel); + } else { + const disposable = observerDisposables.add(responseModel.onDidChange(async () => { + await handleResponseParts(responseModel); if (responseModel.isComplete) { - await handleResponseParts(responseModel); onResponseComplete(responseModel); - } else { - const disposable = responseModel.onDidChange(async () => { - await handleResponseParts(responseModel); - if (responseModel.isComplete) { - onResponseComplete(responseModel); - disposable.dispose(); - } - }); + observerDisposables.delete(disposable); } - } + })); } })); observerDisposables.add(chatModel.onDidDispose(() => observerDisposables.dispose())); return observerDisposables; } - private async _continueEditingSession(session: ChatEditingSession, builder: (stream: IChatEditingSessionStream, token: CancellationToken) => Promise): Promise { + private async _continueEditingSession(session: ChatEditingSession, builder: (stream: IChatEditingSessionStream) => Promise): Promise { if (session.state.get() === ChatEditingSessionState.StreamingEdits) { throw new BugIndicatingError('Cannot continue session that is still streaming'); } @@ -415,13 +369,9 @@ export class ChatEditingService extends Disposable implements IChatEditingServic } }; session.acceptStreamingEditsStart(); - const cancellationTokenSource = new CancellationTokenSource(); - this._currentAutoApplyOperationObs.set(cancellationTokenSource, undefined); try { - await builder(stream, cancellationTokenSource.token); + await builder(stream); } finally { - cancellationTokenSource.dispose(); - this._currentAutoApplyOperationObs.set(null, undefined); session.resolve(); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts index 94e831bd1adf..681e61fb730c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts @@ -14,7 +14,7 @@ import { ChatEditorController, ctxHasEditorModification, ctxReviewModeEnabled } import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js'; -import { hasUndecidedChatEditingResourceContextKey, IChatEditingService } from '../common/chatEditingService.js'; +import { IChatEditingService } from '../common/chatEditingService.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { isEqual } from '../../../../base/common/resources.js'; import { Range } from '../../../../editor/common/core/range.js'; @@ -134,7 +134,7 @@ abstract class AcceptDiscardAction extends Action2 { ? localize2('accept2', 'Accept') : localize2('discard2', 'Discard'), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(ctxHasEditorModification, hasUndecidedChatEditingResourceContextKey), + precondition: ContextKeyExpr.and(ctxHasEditorModification), icon: accept ? Codicon.check : Codicon.discard, @@ -212,7 +212,7 @@ class RejectHunkAction extends EditorAction2 { id: 'chatEditor.action.undoHunk', title: localize2('undo', 'Discard this Change'), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(ctxHasEditorModification, ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), + precondition: ContextKeyExpr.and(ctxHasEditorModification, ChatContextKeys.requestInProgress.negate()), icon: Codicon.discard, f1: true, keybinding: { @@ -238,7 +238,7 @@ class AcceptHunkAction extends EditorAction2 { id: 'chatEditor.action.acceptHunk', title: localize2('acceptHunk', 'Accept this Change'), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(ctxHasEditorModification, ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), + precondition: ContextKeyExpr.and(ctxHasEditorModification, ChatContextKeys.requestInProgress.negate()), icon: Codicon.check, f1: true, keybinding: { @@ -268,7 +268,7 @@ class OpenDiffAction extends EditorAction2 { condition: EditorContextKeys.inDiffEditor, icon: Codicon.goToFile, }, - precondition: ContextKeyExpr.and(ctxHasEditorModification, ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), + precondition: ContextKeyExpr.and(ctxHasEditorModification, ChatContextKeys.requestInProgress.negate()), icon: Codicon.diffSingle, keybinding: { when: EditorContextKeys.focus, diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 8858806e069b..d614ffcf1be8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -15,7 +15,7 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, combinedDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../base/common/map.js'; import { Schemas } from '../../../../base/common/network.js'; -import { autorunWithStore, observableFromEvent } from '../../../../base/common/observable.js'; +import { autorunWithStore, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { extUri, isEqual } from '../../../../base/common/resources.js'; import { isDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; @@ -30,6 +30,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { WorkbenchObjectTree } from '../../../../platform/list/browser/listService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; @@ -38,7 +39,7 @@ import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService, IChatWelcomeMessageContent, isChatWelcomeMessageContent } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { IChatEditingService, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../common/chatEditingService.js'; +import { applyingChatEditsFailedContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../common/chatEditingService.js'; import { ChatPauseState, IChatModel, IChatRequestVariableEntry, IChatResponseModel } from '../common/chatModel.js'; import { ChatRequestAgentPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, formatChatQuestion } from '../common/chatParserTypes.js'; import { ChatRequestParser } from '../common/chatRequestParser.js'; @@ -190,7 +191,7 @@ export class ChatWidget extends Disposable implements IChatWidget { return this._viewModel; } - private _editingSession: IChatEditingSession | undefined; + private readonly _editingSession = observableValue(this, undefined); private parsedChatRequest: IParsedChatRequest | undefined; get parsedInput() { @@ -241,6 +242,8 @@ export class ChatWidget extends Disposable implements IChatWidget { this.viewContext = _viewContext ?? {}; + const viewModelObs = observableFromEvent(this, this.onDidChangeViewModel, () => this.viewModel); + if (typeof location === 'object') { this._location = location; } else { @@ -255,10 +258,50 @@ export class ChatWidget extends Disposable implements IChatWidget { this.isRequestPaused = ChatContextKeys.isRequestPaused.bindTo(contextKeyService); this.canRequestBePaused = ChatContextKeys.canRequestBePaused.bindTo(contextKeyService); - this._codeBlockModelCollection = this._register(instantiationService.createInstance(CodeBlockModelCollection)); + this._register(bindContextKey(decidedChatEditingResourceContextKey, contextKeyService, (reader) => { + const currentSession = this._editingSession.read(reader); + if (!currentSession) { + return; + } + const entries = currentSession.entries.read(reader); + const decidedEntries = entries.filter(entry => entry.state.read(reader) !== WorkingSetEntryState.Modified); + return decidedEntries.map(entry => entry.entryId); + })); + this._register(bindContextKey(hasUndecidedChatEditingResourceContextKey, contextKeyService, (reader) => { + const currentSession = this._editingSession.read(reader); + const entries = currentSession?.entries.read(reader) ?? []; // using currentSession here + const decidedEntries = entries.filter(entry => entry.state.read(reader) === WorkingSetEntryState.Modified); + return decidedEntries.length > 0; + })); + this._register(bindContextKey(hasAppliedChatEditsContextKey, contextKeyService, (reader) => { + const currentSession = this._editingSession.read(reader); + if (!currentSession) { + return false; + } + const entries = currentSession.entries.read(reader); + return entries.length > 0; + })); + this._register(bindContextKey(inChatEditingSessionContextKey, contextKeyService, (reader) => { + return this._editingSession.read(reader) !== null; + })); + this._register(bindContextKey(ChatContextKeys.chatEditingCanUndo, contextKeyService, (r) => { + return this._editingSession.read(r)?.canUndo.read(r) || false; + })); + this._register(bindContextKey(ChatContextKeys.chatEditingCanRedo, contextKeyService, (r) => { + return this._editingSession.read(r)?.canRedo.read(r) || false; + })); + this._register(bindContextKey(applyingChatEditsFailedContextKey, contextKeyService, (r) => { + const chatModel = viewModelObs.read(r)?.model; + const editingSession = this._editingSession.read(r); + if (!editingSession || !chatModel) { + return false; + } + const lastResponse = observableFromEvent(this, chatModel.onDidChange, () => chatModel.getRequests().at(-1)?.response).read(r); + return lastResponse?.result?.errorDetails && !lastResponse?.result?.errorDetails.responseIsIncomplete; + })); + this._codeBlockModelCollection = this._register(instantiationService.createInstance(CodeBlockModelCollection)); - const viewModelObs = observableFromEvent(this, this.onDidChangeViewModel, () => this.viewModel); this._register(autorunWithStore((r, store) => { @@ -266,7 +309,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const sessions = chatEditingService.editingSessionsObs.read(r); const session = sessions.find(candidate => candidate.chatSessionId === viewModel?.sessionId); - this._editingSession = undefined; + this._editingSession.set(undefined, undefined); this.renderChatEditingSessionState(); // this is necessary to make sure we dispose previous buttons, etc. if (!session) { @@ -274,13 +317,13 @@ export class ChatWidget extends Disposable implements IChatWidget { return; } - this._editingSession = session; + this._editingSession.set(session, undefined); store.add(session.onDidChange(() => { this.renderChatEditingSessionState(); })); store.add(session.onDidDispose(() => { - this._editingSession = undefined; + this._editingSession.set(undefined, undefined); this.renderChatEditingSessionState(); })); store.add(this.onDidChangeParsedInput(() => { @@ -602,7 +645,7 @@ export class ChatWidget extends Disposable implements IChatWidget { if (!this.inputPart) { return; } - this.inputPart.renderChatEditingSessionState(this._editingSession ?? null, this); + this.inputPart.renderChatEditingSessionState(this._editingSession.get() ?? null, this); if (this.bodyDimension) { this.layout(this.bodyDimension.height, this.bodyDimension.width); @@ -1063,7 +1106,7 @@ export class ChatWidget extends Disposable implements IChatWidget { // This should never exceed the maximum file entries limit above. for (const { uri, isMarkedReadonly } of this.inputPart.chatEditWorkingSetFiles) { // Skip over any suggested files that haven't been confirmed yet in the working set - if (currentEditingSession?.workingSet.get(uri)?.state === WorkingSetEntryState.Suggested) { + if (currentEditingSession.get()?.workingSet.get(uri)?.state === WorkingSetEntryState.Suggested) { unconfirmedSuggestions.add(uri); } else { uniqueWorkingSetEntries.add(uri); @@ -1090,7 +1133,7 @@ export class ChatWidget extends Disposable implements IChatWidget { // Make sure that any files that we sent are part of the working set // but do not permanently add file variables from previous requests to the working set // since the user may subsequently edit the chat history - currentEditingSession?.addFileToWorkingSet(file); + currentEditingSession.get()?.addFileToWorkingSet(file); } // Collect file variables from previous requests before sending the request @@ -1121,7 +1164,7 @@ export class ChatWidget extends Disposable implements IChatWidget { actualSize: number; }; this.telemetryService.publicLog2('chatEditing/workingSetSize', { originalSize: this.inputPart.attemptedWorkingSetEntriesCount, actualSize: uniqueWorkingSetEntries.size }); - currentEditingSession?.remove(WorkingSetEntryRemovalReason.User, ...unconfirmedSuggestions); + currentEditingSession.get()?.remove(WorkingSetEntryRemovalReason.User, ...unconfirmedSuggestions); } this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionId); diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 23d97adc4113..21560cda2cb2 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Event } from '../../../../base/common/event.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../base/common/map.js'; @@ -27,7 +27,6 @@ export interface IChatEditingService { readonly currentEditingSessionObs: IObservable; readonly currentEditingSession: IChatEditingSession | null; - readonly currentAutoApplyOperation: CancellationTokenSource | null; readonly editingSessionFileLimit: number; @@ -101,6 +100,8 @@ export interface IChatEditingSession { */ stop(clearState?: boolean): Promise; + readonly canUndo: IObservable; + readonly canRedo: IObservable; undoInteraction(): Promise; redoInteraction(): Promise; } @@ -126,6 +127,7 @@ export const enum ChatEditingSessionChangeType { } export interface IModifiedFileEntry { + readonly entryId: string; readonly originalURI: URI; readonly originalModel: ITextModel; readonly modifiedURI: URI; @@ -164,7 +166,6 @@ export const chatEditingAgentSupportsReadonlyReferencesContextKey = new RawConte export const decidedChatEditingResourceContextKey = new RawContextKey('decidedChatEditingResource', []); export const chatEditingResourceContextKey = new RawContextKey('chatEditingResource', undefined); export const inChatEditingSessionContextKey = new RawContextKey('inChatEditingSession', undefined); -export const applyingChatEditsContextKey = new RawContextKey('isApplyingChatEdits', undefined); export const hasUndecidedChatEditingResourceContextKey = new RawContextKey('hasUndecidedChatEditingResource', false); export const hasAppliedChatEditsContextKey = new RawContextKey('hasAppliedChatEdits', false); export const applyingChatEditsFailedContextKey = new RawContextKey('applyingChatEditsFailed', false); From 3bcb9a5d9ddd8a55dbaa984ede7e42680dae87e3 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 31 Jan 2025 16:56:13 +0100 Subject: [PATCH 1083/3587] EditContext : Synchornize clipboard.ts and view.ts EditContext information (#239341) adding code to synchornize clipboard.ts and view.ts --- src/vs/editor/browser/view.ts | 20 ++++++++++++------- .../contrib/clipboard/browser/clipboard.ts | 7 ++++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 1e883bfcd2b2..e1edfb83b10d 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -42,7 +42,7 @@ import { ViewCursors } from './viewParts/viewCursors/viewCursors.js'; import { ViewZones } from './viewParts/viewZones/viewZones.js'; import { WhitespaceOverlay } from './viewParts/whitespace/whitespace.js'; import { IEditorConfiguration } from '../common/config/editorConfiguration.js'; -import { EditorOption } from '../common/config/editorOptions.js'; +import { EditorOption, IComputedEditorOptions } from '../common/config/editorOptions.js'; import { Position } from '../common/core/position.js'; import { Range } from '../common/core/range.js'; import { Selection } from '../common/core/selection.js'; @@ -148,7 +148,7 @@ export class View extends ViewEventHandler { // Keyboard handler this._experimentalEditContextEnabled = this._context.configuration.options.get(EditorOption.experimentalEditContextEnabled); this._accessibilitySupport = this._context.configuration.options.get(EditorOption.accessibilitySupport); - this._editContext = this._instantiateEditContext(this._experimentalEditContextEnabled, this._accessibilitySupport); + this._editContext = this._instantiateEditContext(); this._viewParts.push(this._editContext); @@ -277,10 +277,9 @@ export class View extends ViewEventHandler { this._pointerHandler = this._register(new PointerHandler(this._context, this._viewController, this._createPointerHandlerHelper())); } - private _instantiateEditContext(experimentalEditContextEnabled: boolean, accessibilitySupport: AccessibilitySupport): AbstractEditContext { - const domNode = dom.getWindow(this._overflowGuardContainer.domNode); - const isEditContextSupported = EditContext.supported(domNode); - if (experimentalEditContextEnabled && isEditContextSupported && accessibilitySupport !== AccessibilitySupport.Enabled) { + private _instantiateEditContext(): AbstractEditContext { + const usingExperimentalEditContext = useExperimentalEditContext(dom.getWindow(this._overflowGuardContainer.domNode), this._context.configuration.options); + if (usingExperimentalEditContext) { return this._instantiationService.createInstance(NativeEditContext, this._ownerID, this._context, this._overflowGuardContainer, this._viewController, this._createTextAreaHandlerHelper()); } else { return this._instantiationService.createInstance(TextAreaEditContext, this._context, this._overflowGuardContainer, this._viewController, this._createTextAreaHandlerHelper()); @@ -298,7 +297,7 @@ export class View extends ViewEventHandler { const isEditContextFocused = this._editContext.isFocused(); const indexOfEditContext = this._viewParts.indexOf(this._editContext); this._editContext.dispose(); - this._editContext = this._instantiateEditContext(experimentalEditContextEnabled, accessibilitySupport); + this._editContext = this._instantiateEditContext(); if (isEditContextFocused) { this._editContext.focus(); } @@ -852,3 +851,10 @@ class EditorRenderingCoordinator { } } } + +export function useExperimentalEditContext(activeWindow: CodeWindow, options: IComputedEditorOptions): boolean { + const isEditContextSupported = EditContext.supported(activeWindow); + const experimentalEditContextEnabled = options.get(EditorOption.experimentalEditContextEnabled); + const accessibilitySupport = options.get(EditorOption.accessibilitySupport); + return experimentalEditContextEnabled && isEditContextSupported && accessibilitySupport !== AccessibilitySupport.Enabled; +} diff --git a/src/vs/editor/contrib/clipboard/browser/clipboard.ts b/src/vs/editor/contrib/clipboard/browser/clipboard.ts index 15fc67e770b4..17484a5b1e4a 100644 --- a/src/vs/editor/contrib/clipboard/browser/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/browser/clipboard.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as browser from '../../../../base/browser/browser.js'; -import { getActiveDocument } from '../../../../base/browser/dom.js'; +import { getActiveDocument, getWindow } from '../../../../base/browser/dom.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import * as platform from '../../../../base/common/platform.js'; import * as nls from '../../../../nls.js'; @@ -18,6 +18,7 @@ import { NativeEditContextRegistry } from '../../../browser/controller/editConte import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { Command, EditorAction, MultiCommand, registerEditorAction } from '../../../browser/editorExtensions.js'; import { ICodeEditorService } from '../../../browser/services/codeEditorService.js'; +import { useExperimentalEditContext } from '../../../browser/view.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; import { Handler } from '../../../common/editorCommon.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; @@ -235,8 +236,8 @@ if (PasteAction) { if (focusedEditor && focusedEditor.hasModel() && focusedEditor.hasTextFocus()) { // execCommand(paste) does not work with edit context let result: boolean; - const experimentalEditContextEnabled = focusedEditor.getOption(EditorOption.experimentalEditContextEnabled); - if (experimentalEditContextEnabled) { + const usingExperimentalEditContext = useExperimentalEditContext(getWindow(focusedEditor.getDomNode()), focusedEditor.getOptions()); + if (usingExperimentalEditContext) { // Since we can not call execCommand('paste') on a dom node with edit context set // we added a hidden text area that receives the paste execution // see nativeEditContext.ts for more details From 9add868cda75ab3d669f24c15d2f3b5b81a13afe Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 08:30:51 -0800 Subject: [PATCH 1084/3587] Add very basic tokenizer for command line Fixes #239018 --- .../src/terminalSuggestMain.ts | 35 ++++--------- .../terminal-suggest/src/test/tokens.test.ts | 52 +++++++++++++++++++ extensions/terminal-suggest/src/tokens.ts | 33 ++++++++++++ 3 files changed, 94 insertions(+), 26 deletions(-) create mode 100644 extensions/terminal-suggest/src/test/tokens.test.ts create mode 100644 extensions/terminal-suggest/src/tokens.ts diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 3cd5854ca74c..db52610be6c9 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -12,6 +12,7 @@ import cdSpec from './completions/cd'; import codeInsidersCompletionSpec from './completions/code-insiders'; import { osIsWindows } from './helpers/os'; import { isExecutable } from './helpers/executable'; +import { getTokenType, TokenType } from './tokens'; const enum PwshCommandType { Alias = 1 @@ -136,7 +137,8 @@ export async function activate(context: vscode.ExtensionContext) { const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition); const pathSeparator = isWindows ? '\\' : '/'; - const result = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, terminal.shellIntegration?.cwd, token); + const tokenType = getTokenType(terminalContext, shellType); + const result = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, tokenType, terminal.shellIntegration?.cwd, token); if (terminal.shellIntegration?.env) { const homeDirCompletion = result.items.find(i => i.label === '~'); if (homeDirCompletion && terminal.shellIntegration.env.HOME) { @@ -324,6 +326,7 @@ export async function getCompletionItemsFromSpecs( terminalContext: { commandLine: string; cursorPosition: number }, availableCommands: ICompletionResource[], prefix: string, + tokenType: TokenType, shellIntegrationCwd?: vscode.Uri, token?: vscode.CancellationToken ): Promise<{ items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; cwd?: vscode.Uri }> { @@ -331,7 +334,6 @@ export async function getCompletionItemsFromSpecs( let filesRequested = false; let foldersRequested = false; - const firstCommand = getFirstCommand(terminalContext.commandLine); const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1); for (const spec of specs) { @@ -347,14 +349,10 @@ export async function getCompletionItemsFromSpecs( continue; } - if ( - // If the prompt is empty - !terminalContext.commandLine - // or the first command matches the command - || !!firstCommand && specLabel.startsWith(firstCommand) - ) { - // push it to the completion items + // push it to the completion items + if (tokenType === TokenType.Command) { items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: specLabel }, getDescription(spec), availableCommand.detail)); + continue; } if (!terminalContext.commandLine.startsWith(specLabel)) { @@ -385,9 +383,7 @@ export async function getCompletionItemsFromSpecs( !filesRequested && !foldersRequested; - const shouldShowCommands = !terminalContext.commandLine.substring(0, terminalContext.cursorPosition).trimStart().includes(' '); - - if (shouldShowCommands && !filesRequested && !foldersRequested) { + if (tokenType === TokenType.Command) { // Include builitin/available commands in the results const labels = new Set(items.map((i) => i.label)); for (const command of availableCommands) { @@ -536,19 +532,6 @@ function getCompletionItemsFromArgs(args: Fig.SingleOrArray | undefined return { items, filesRequested, foldersRequested }; } - - -function getFirstCommand(commandLine: string): string | undefined { - const wordsOnLine = commandLine.split(' '); - let firstCommand: string | undefined = wordsOnLine[0]; - if (wordsOnLine.length > 1) { - firstCommand = undefined; - } else if (wordsOnLine.length === 0) { - firstCommand = commandLine; - } - return firstCommand; -} - function getFriendlyResourcePath(uri: vscode.Uri, pathSeparator: string, kind?: vscode.TerminalCompletionItemKind): string { let path = uri.fsPath; // Ensure drive is capitalized on Windows @@ -564,7 +547,7 @@ function getFriendlyResourcePath(uri: vscode.Uri, pathSeparator: string, kind?: } // TODO: remove once API is finalized -export enum TerminalShellType { +export const enum TerminalShellType { Sh = 1, Bash = 2, Fish = 3, diff --git a/extensions/terminal-suggest/src/test/tokens.test.ts b/extensions/terminal-suggest/src/test/tokens.test.ts new file mode 100644 index 000000000000..621a9b653883 --- /dev/null +++ b/extensions/terminal-suggest/src/test/tokens.test.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import { strictEqual } from 'node:assert'; +import { getTokenType, TokenType } from '../tokens'; +import { TerminalShellType } from '../terminalSuggestMain'; + +suite('Terminal Suggest', () => { + test('simple command', () => { + strictEqual(getTokenType({ commandLine: 'echo', cursorPosition: 'echo'.length }, undefined), TokenType.Command); + }); + test('simple argument', () => { + strictEqual(getTokenType({ commandLine: 'echo hello', cursorPosition: 'echo hello'.length }, undefined), TokenType.Argument); + }); + test('simple command, cursor mid text', () => { + strictEqual(getTokenType({ commandLine: 'echo hello', cursorPosition: 'echo'.length }, undefined), TokenType.Command); + }); + test('simple argument, cursor mid text', () => { + strictEqual(getTokenType({ commandLine: 'echo hello', cursorPosition: 'echo hel'.length }, undefined), TokenType.Argument); + }); + suite('reset to command', () => { + test('|', () => { + strictEqual(getTokenType({ commandLine: 'echo hello | ', cursorPosition: 'echo hello | '.length }, undefined), TokenType.Command); + }); + test(';', () => { + strictEqual(getTokenType({ commandLine: 'echo hello; ', cursorPosition: 'echo hello; '.length }, undefined), TokenType.Command); + }); + test('&&', () => { + strictEqual(getTokenType({ commandLine: 'echo hello && ', cursorPosition: 'echo hello && '.length }, undefined), TokenType.Command); + }); + test('||', () => { + strictEqual(getTokenType({ commandLine: 'echo hello || ', cursorPosition: 'echo hello || '.length }, undefined), TokenType.Command); + }); + }); + suite('pwsh', () => { + test('simple command', () => { + strictEqual(getTokenType({ commandLine: 'Write-Host', cursorPosition: 'Write-Host'.length }, TerminalShellType.PowerShell), TokenType.Command); + }); + test('simple argument', () => { + strictEqual(getTokenType({ commandLine: 'Write-Host hello', cursorPosition: 'Write-Host hello'.length }, TerminalShellType.PowerShell), TokenType.Argument); + }); + test('reset char', () => { + strictEqual(getTokenType({ commandLine: `Write-Host hello -and `, cursorPosition: `Write-Host hello -and `.length }, TerminalShellType.PowerShell), TokenType.Command); + }); + test('arguments after reset char', () => { + strictEqual(getTokenType({ commandLine: `Write-Host hello -and $true `, cursorPosition: `Write-Host hello -and $true `.length }, TerminalShellType.PowerShell), TokenType.Argument); + }); + }); +}); diff --git a/extensions/terminal-suggest/src/tokens.ts b/extensions/terminal-suggest/src/tokens.ts new file mode 100644 index 000000000000..9712d2b0bfee --- /dev/null +++ b/extensions/terminal-suggest/src/tokens.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TerminalShellType } from './terminalSuggestMain'; + +export const enum TokenType { + Command, + Argument, +} + +const shellTypeResetChars: { [key: number]: string[] | undefined } = { + [TerminalShellType.Bash]: ['>', '>>', '<', '2>', '2>>', '&>', '&>>', '|', '|&', '&&', '||', '&', ';', '(', '{', '<<'], + [TerminalShellType.Zsh]: ['>', '>>', '<', '2>', '2>>', '&>', '&>>', '<>', '|', '|&', '&&', '||', '&', ';', '(', '{', '<<', '<<<', '<('], + [TerminalShellType.PowerShell]: ['>', '>>', '<', '2>', '2>>', '*>', '*>>', '|', '-and', '-or', '-not', '!', '&', '-eq', '-ne', '-gt', '-lt', '-ge', '-le', '-like', '-notlike', '-match', '-notmatch', '-contains', '-notcontains', '-in', '-notin'] +}; + +const defaultShellTypeResetChars = shellTypeResetChars[TerminalShellType.Bash]!; + +export function getTokenType(ctx: { commandLine: string; cursorPosition: number }, shellType: TerminalShellType | undefined): TokenType { + const spaceIndex = ctx.commandLine.substring(0, ctx.cursorPosition).lastIndexOf(' '); + if (spaceIndex === -1) { + return TokenType.Command; + } + const tokenIndex = spaceIndex === -1 ? 0 : spaceIndex + 1; + const previousTokens = ctx.commandLine.substring(0, tokenIndex).trim(); + const commandResetChars = shellType === undefined ? defaultShellTypeResetChars : shellTypeResetChars[shellType] ?? defaultShellTypeResetChars; + if (commandResetChars.some(e => previousTokens.endsWith(e))) { + return TokenType.Command; + } + return TokenType.Argument; +} From 63c8c1557178f7b3d0fd923039cacf35e9d38e1f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 09:04:43 -0800 Subject: [PATCH 1085/3587] Pass token type into getCompletionItemsFromSpecs tests --- .../terminal-suggest/src/test/terminalSuggestMain.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index d72996b8f702..030e9d0db995 100644 --- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -12,6 +12,7 @@ import codeCompletionSpec from '../completions/code'; import codeInsidersCompletionSpec from '../completions/code-insiders'; import type { Uri } from 'vscode'; import { basename } from 'path'; +import { getTokenType } from '../tokens'; const fixtureDir = vscode.Uri.joinPath(vscode.Uri.file(__dirname), '../../testWorkspace'); const testCwdParent = vscode.Uri.joinPath(fixtureDir, 'parent'); @@ -150,7 +151,8 @@ suite('Terminal Suggest', () => { const prefix = commandLine.slice(0, cursorPosition).split(' ').at(-1) || ''; const filesRequested = testSpec.expectedResourceRequests?.type === 'files' || testSpec.expectedResourceRequests?.type === 'both'; const foldersRequested = testSpec.expectedResourceRequests?.type === 'folders' || testSpec.expectedResourceRequests?.type === 'both'; - const result = await getCompletionItemsFromSpecs(completionSpecs, { commandLine, cursorPosition }, availableCommands.map(c => { return { label: c }; }), prefix, testCwd); + const terminalContext = { commandLine, cursorPosition }; + const result = await getCompletionItemsFromSpecs(completionSpecs, terminalContext, availableCommands.map(c => { return { label: c }; }), prefix, getTokenType(terminalContext, undefined), testCwd); deepStrictEqual(result.items.map(i => i.label).sort(), (testSpec.expectedCompletions ?? []).sort()); strictEqual(result.filesRequested, filesRequested); strictEqual(result.foldersRequested, foldersRequested); From 999cf4704bb66546de23b5b8165f09178083557e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 09:56:24 -0800 Subject: [PATCH 1086/3587] Pull in bash aliases, add alias kind Part of #239028 --- .../src/terminalSuggestMain.ts | 27 ++++++++++++++++--- .../common/xterm/shellIntegrationAddon.ts | 4 +++ src/vs/workbench/api/common/extHostTypes.ts | 3 ++- .../common/scripts/shellIntegration-bash.sh | 5 ++++ .../browser/terminalCompletionService.ts | 3 ++- .../suggest/browser/terminalSuggestAddon.ts | 3 ++- ...e.proposed.terminalCompletionProvider.d.ts | 3 ++- 7 files changed, 40 insertions(+), 8 deletions(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 3cd5854ca74c..6ef58066d42e 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs/promises'; import * as path from 'path'; -import { exec, ExecOptionsWithStringEncoding, execSync } from 'child_process'; +import { exec, ExecOptionsWithStringEncoding, execSync, spawnSync } from 'child_process'; import { upstreamSpecs } from './constants'; import codeCompletionSpec from './completions/code'; import cdSpec from './completions/cd'; @@ -45,11 +45,27 @@ async function getBuiltinCommands(shellType: TerminalShellType, existingCommands return; } const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell }; - let commands: string[] | undefined; + let commands: (string | ICompletionResource)[] | undefined; switch (shellType) { case TerminalShellType.Bash: { const bashOutput = execSync('compgen -b', options); commands = bashOutput.split('\n').filter(filter); + + // This must be run with interactive, otherwise there's a good chance aliases won't + // be set up + const bashOutput2 = spawnSync('bash', ['-ic', 'alias'], options).stdout; + for (const line of bashOutput2.split('\n')) { + const match = line.match(/^alias (?[a-zA-Z]+)='(?.+)'$/); + if (!match?.groups) { + continue; + } + commands.push({ + label: match.groups.alias, + detail: match.groups.resolved, + kind: vscode.TerminalCompletionItemKind.Alias, + }); + } + break; } case TerminalShellType.Zsh: { @@ -89,6 +105,7 @@ async function getBuiltinCommands(shellType: TerminalShellType, existingCommands return { label: e.Name, detail: e.DisplayName, + kind: vscode.TerminalCompletionItemKind.Alias, }; } default: { @@ -104,7 +121,7 @@ async function getBuiltinCommands(shellType: TerminalShellType, existingCommands } } - const commandResources = commands?.map(command => ({ label: command })); + const commandResources = commands?.map(command => typeof command === 'string' ? ({ label: command }) : command); cachedBuiltinCommands.set(shellType, commandResources); return commandResources; @@ -236,14 +253,16 @@ function createCompletionItem(cursorPosition: number, prefix: string, commandRes documentation, replacementIndex: cursorPosition - lastWord.length, replacementLength: lastWord.length, - kind: kind ?? vscode.TerminalCompletionItemKind.Method + kind: kind ?? commandResource.kind ?? vscode.TerminalCompletionItemKind.Method }; } interface ICompletionResource { label: string; detail?: string; + kind?: vscode.TerminalCompletionItemKind; } + async function getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { const labels: Set = new Set(); let pathValue: string | undefined; diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index 49ed23c11037..9e23b749bfa2 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -508,6 +508,10 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati return true; } switch (key) { + case 'Aliases': { + console.log('aliases', value); + return true; + } case 'ContinuationPrompt': { this._updateContinuationPrompt(removeAnsiEscapeCodesFromPrompt(value)); return true; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 26ae6d8182ea..ad3bfb390e6c 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2140,7 +2140,8 @@ export enum TerminalCompletionItemKind { Folder = 1, Flag = 2, Method = 3, - Argument = 4 + Argument = 4, + Alias = 5 } export class TerminalCompletionItem implements vscode.TerminalCompletionItem { diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index d36d7f401a36..1b0dc51e00d4 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -241,9 +241,14 @@ __vsc_continuation_end() { builtin printf '\e]633;G\a' } +__vsc_report_aliases() { + builtin printf '\e]633;P;Aliases=%s\a' "$(__vsc_escape_value "$(alias -p)")" $__vsc_nonce +} + __vsc_command_complete() { if [[ -z "${__vsc_first_prompt-}" ]]; then __vsc_update_cwd + __vsc_report_aliases builtin return fi if [ "$__vsc_current_command" = "" ]; then diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 7bbc60a4baca..36d4de75fcd4 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -23,7 +23,8 @@ export enum TerminalCompletionItemKind { Folder = 1, Flag = 2, Method = 3, - Argument = 4 + Argument = 4, + Alias = 5, } export interface ITerminalCompletion extends ISimpleCompletion { diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index efc1efa72e5a..1af621de8cf1 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -84,7 +84,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest [TerminalCompletionItemKind.Folder, Codicon.folder], [TerminalCompletionItemKind.Flag, Codicon.symbolProperty], [TerminalCompletionItemKind.Method, Codicon.symbolMethod], - [TerminalCompletionItemKind.Argument, Codicon.symbolVariable] + [TerminalCompletionItemKind.Argument, Codicon.symbolVariable], + [TerminalCompletionItemKind.Alias, Codicon.replace], ]); private _shouldSyncWhenReady: boolean = false; diff --git a/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts b/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts index 90e922f9304e..26a8a22e4843 100644 --- a/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts @@ -61,7 +61,8 @@ declare module 'vscode' { Folder = 1, Flag = 2, Method = 3, - Argument = 4 + Argument = 4, + Alias = 5, } export interface TerminalCompletionContext { From da74a77c39fb755a4ca9b57f42f1e76c13a140c4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:09:30 -0800 Subject: [PATCH 1087/3587] Move bash logic into own file --- extensions/terminal-suggest/src/shell/bash.ts | 64 +++++++++++++++++++ .../src/terminalSuggestMain.ts | 28 ++------ extensions/terminal-suggest/src/types.ts | 12 ++++ 3 files changed, 80 insertions(+), 24 deletions(-) create mode 100644 extensions/terminal-suggest/src/shell/bash.ts create mode 100644 extensions/terminal-suggest/src/types.ts diff --git a/extensions/terminal-suggest/src/shell/bash.ts b/extensions/terminal-suggest/src/shell/bash.ts new file mode 100644 index 000000000000..0a6dfff74b3f --- /dev/null +++ b/extensions/terminal-suggest/src/shell/bash.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import type { ICompletionResource } from '../types'; +import { exec, spawn, type ExecOptionsWithStringEncoding } from 'node:child_process'; + +export async function getBashGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { + return [ + ...await getBashAliases(options), + ...await getBashBuiltins(options, existingCommands) + ]; +} + +async function getBashBuiltins(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { + const compgenOutput = await new Promise((resolve, reject) => { + exec('compgen -b', options, (error, stdout) => { + if (error) { + reject(error); + } else { + resolve(stdout); + } + }); + }); + const filter = (cmd: string) => cmd && !existingCommands?.has(cmd); + return compgenOutput.split('\n').filter(filter); +} + +async function getBashAliases(options: ExecOptionsWithStringEncoding): Promise { + // This must be run with interactive, otherwise there's a good chance aliases won't + // be set up. Note that this could differ from the actual aliases as it's a new bash + // session, for the same reason this would not include aliases that are created + // by simply running `alias ...` in the terminal. + const aliasOutput = await new Promise((resolve, reject) => { + const child = spawn('bash', ['-ic', 'alias'], options); + let stdout = ''; + child.stdout.on('data', (data) => { + stdout += data; + }); + child.on('close', (code) => { + if (code !== 0) { + reject(new Error(`bash process exited with code ${code}`)); + } else { + resolve(stdout); + } + }); + }); + + const result: ICompletionResource[] = []; + for (const line of aliasOutput.split('\n')) { + const match = line.match(/^alias (?[a-zA-Z]+)='(?.+)'$/); + if (!match?.groups) { + continue; + } + result.push({ + label: match.groups.alias, + detail: match.groups.resolved, + kind: vscode.TerminalCompletionItemKind.Alias, + }); + } + return result; +} diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 6ef58066d42e..9f9aa5ab3b01 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -5,13 +5,15 @@ import * as vscode from 'vscode'; import * as fs from 'fs/promises'; import * as path from 'path'; -import { exec, ExecOptionsWithStringEncoding, execSync, spawnSync } from 'child_process'; +import { exec, ExecOptionsWithStringEncoding, execSync } from 'child_process'; import { upstreamSpecs } from './constants'; import codeCompletionSpec from './completions/code'; import cdSpec from './completions/cd'; import codeInsidersCompletionSpec from './completions/code-insiders'; import { osIsWindows } from './helpers/os'; import { isExecutable } from './helpers/executable'; +import type { ICompletionResource } from './types'; +import { getBashGlobals } from './shell/bash'; const enum PwshCommandType { Alias = 1 @@ -48,24 +50,7 @@ async function getBuiltinCommands(shellType: TerminalShellType, existingCommands let commands: (string | ICompletionResource)[] | undefined; switch (shellType) { case TerminalShellType.Bash: { - const bashOutput = execSync('compgen -b', options); - commands = bashOutput.split('\n').filter(filter); - - // This must be run with interactive, otherwise there's a good chance aliases won't - // be set up - const bashOutput2 = spawnSync('bash', ['-ic', 'alias'], options).stdout; - for (const line of bashOutput2.split('\n')) { - const match = line.match(/^alias (?[a-zA-Z]+)='(?.+)'$/); - if (!match?.groups) { - continue; - } - commands.push({ - label: match.groups.alias, - detail: match.groups.resolved, - kind: vscode.TerminalCompletionItemKind.Alias, - }); - } - + commands = await getBashGlobals(options); break; } case TerminalShellType.Zsh: { @@ -257,11 +242,6 @@ function createCompletionItem(cursorPosition: number, prefix: string, commandRes }; } -interface ICompletionResource { - label: string; - detail?: string; - kind?: vscode.TerminalCompletionItemKind; -} async function getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { const labels: Set = new Set(); diff --git a/extensions/terminal-suggest/src/types.ts b/extensions/terminal-suggest/src/types.ts new file mode 100644 index 000000000000..43b86a08a93e --- /dev/null +++ b/extensions/terminal-suggest/src/types.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export interface ICompletionResource { + label: string; + detail?: string; + kind?: vscode.TerminalCompletionItemKind; +} From 61473cc52e08574ed5729aff82c8c5275dad74aa Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:10:24 -0800 Subject: [PATCH 1088/3587] Remove unused experiment --- .../platform/terminal/common/xterm/shellIntegrationAddon.ts | 4 ---- .../contrib/terminal/common/scripts/shellIntegration-bash.sh | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index 9e23b749bfa2..49ed23c11037 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -508,10 +508,6 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati return true; } switch (key) { - case 'Aliases': { - console.log('aliases', value); - return true; - } case 'ContinuationPrompt': { this._updateContinuationPrompt(removeAnsiEscapeCodesFromPrompt(value)); return true; diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index 1b0dc51e00d4..29457d232234 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -241,10 +241,6 @@ __vsc_continuation_end() { builtin printf '\e]633;G\a' } -__vsc_report_aliases() { - builtin printf '\e]633;P;Aliases=%s\a' "$(__vsc_escape_value "$(alias -p)")" $__vsc_nonce -} - __vsc_command_complete() { if [[ -z "${__vsc_first_prompt-}" ]]; then __vsc_update_cwd From 1908c617e840cf613b88bc86def8704a10aa244c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:24:50 -0800 Subject: [PATCH 1089/3587] zsh aliases --- extensions/terminal-suggest/src/shell/bash.ts | 2 +- extensions/terminal-suggest/src/shell/zsh.ts | 64 +++++++++++++++++++ .../src/terminalSuggestMain.ts | 6 +- .../common/scripts/shellIntegration-bash.sh | 1 - 4 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 extensions/terminal-suggest/src/shell/zsh.ts diff --git a/extensions/terminal-suggest/src/shell/bash.ts b/extensions/terminal-suggest/src/shell/bash.ts index 0a6dfff74b3f..93baf3d6e139 100644 --- a/extensions/terminal-suggest/src/shell/bash.ts +++ b/extensions/terminal-suggest/src/shell/bash.ts @@ -50,7 +50,7 @@ async function getBashAliases(options: ExecOptionsWithStringEncoding): Promise[a-zA-Z]+)='(?.+)'$/); + const match = line.match(/^alias (?[a-zA-Z0-9\.:-]+)='(?.+)'$/); if (!match?.groups) { continue; } diff --git a/extensions/terminal-suggest/src/shell/zsh.ts b/extensions/terminal-suggest/src/shell/zsh.ts new file mode 100644 index 000000000000..a19c71677ec1 --- /dev/null +++ b/extensions/terminal-suggest/src/shell/zsh.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import type { ICompletionResource } from '../types'; +import { exec, spawn, type ExecOptionsWithStringEncoding } from 'node:child_process'; + +export async function getZshGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { + return [ + ...await getZshBuiltins(options, existingCommands), + ...await getZshAliases(options), + ]; +} + +async function getZshBuiltins(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { + const compgenOutput = await new Promise((resolve, reject) => { + exec('printf "%s\\n" ${(k)builtins}', options, (error, stdout) => { + if (error) { + reject(error); + } else { + resolve(stdout); + } + }); + }); + const filter = (cmd: string) => cmd && !existingCommands?.has(cmd); + return compgenOutput.split('\n').filter(filter); +} + +async function getZshAliases(options: ExecOptionsWithStringEncoding): Promise { + // This must be run with interactive, otherwise there's a good chance aliases won't + // be set up. Note that this could differ from the actual aliases as it's a new bash + // session, for the same reason this would not include aliases that are created + // by simply running `alias ...` in the terminal. + const aliasOutput = await new Promise((resolve, reject) => { + const child = spawn('zsh', ['-ic', 'alias'], options); + let stdout = ''; + child.stdout.on('data', (data) => { + stdout += data; + }); + child.on('close', (code) => { + if (code !== 0) { + reject(new Error(`zsh process exited with code ${code}`)); + } else { + resolve(stdout); + } + }); + }); + + const result: ICompletionResource[] = []; + for (const line of aliasOutput.split('\n')) { + const match = line.match(/^(?[a-zA-Z0-9\.:-]+)=(?:'(?.+)'|(?.+))$/); + if (!match?.groups) { + continue; + } + result.push({ + label: match.groups.alias, + detail: match.groups.resolved, + kind: vscode.TerminalCompletionItemKind.Alias, + }); + } + return result; +} diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 9f9aa5ab3b01..3b3b7286fd89 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -14,6 +14,7 @@ import { osIsWindows } from './helpers/os'; import { isExecutable } from './helpers/executable'; import type { ICompletionResource } from './types'; import { getBashGlobals } from './shell/bash'; +import { getZshGlobals } from './shell/zsh'; const enum PwshCommandType { Alias = 1 @@ -50,12 +51,11 @@ async function getBuiltinCommands(shellType: TerminalShellType, existingCommands let commands: (string | ICompletionResource)[] | undefined; switch (shellType) { case TerminalShellType.Bash: { - commands = await getBashGlobals(options); + commands = await getBashGlobals(options, existingCommands); break; } case TerminalShellType.Zsh: { - const zshOutput = execSync('printf "%s\\n" ${(k)builtins}', options); - commands = zshOutput.split('\n').filter(filter); + commands = await getZshGlobals(options, existingCommands); break; } case TerminalShellType.Fish: { diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index 29457d232234..d36d7f401a36 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -244,7 +244,6 @@ __vsc_continuation_end() { __vsc_command_complete() { if [[ -z "${__vsc_first_prompt-}" ]]; then __vsc_update_cwd - __vsc_report_aliases builtin return fi if [ "$__vsc_current_command" = "" ]; then From 02734d254f36dd1219b535c1c4917906bb3c5f89 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:29:06 -0800 Subject: [PATCH 1090/3587] Async fish and aliases --- extensions/terminal-suggest/src/shell/bash.ts | 8 +-- extensions/terminal-suggest/src/shell/fish.ts | 64 +++++++++++++++++++ extensions/terminal-suggest/src/shell/zsh.ts | 8 +-- .../src/terminalSuggestMain.ts | 6 +- 4 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 extensions/terminal-suggest/src/shell/fish.ts diff --git a/extensions/terminal-suggest/src/shell/bash.ts b/extensions/terminal-suggest/src/shell/bash.ts index 93baf3d6e139..70a8110eba3c 100644 --- a/extensions/terminal-suggest/src/shell/bash.ts +++ b/extensions/terminal-suggest/src/shell/bash.ts @@ -9,12 +9,12 @@ import { exec, spawn, type ExecOptionsWithStringEncoding } from 'node:child_proc export async function getBashGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { return [ - ...await getBashAliases(options), - ...await getBashBuiltins(options, existingCommands) + ...await getAliases(options), + ...await getBuiltins(options, existingCommands) ]; } -async function getBashBuiltins(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { +async function getBuiltins(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { const compgenOutput = await new Promise((resolve, reject) => { exec('compgen -b', options, (error, stdout) => { if (error) { @@ -28,7 +28,7 @@ async function getBashBuiltins(options: ExecOptionsWithStringEncoding, existingC return compgenOutput.split('\n').filter(filter); } -async function getBashAliases(options: ExecOptionsWithStringEncoding): Promise { +async function getAliases(options: ExecOptionsWithStringEncoding): Promise { // This must be run with interactive, otherwise there's a good chance aliases won't // be set up. Note that this could differ from the actual aliases as it's a new bash // session, for the same reason this would not include aliases that are created diff --git a/extensions/terminal-suggest/src/shell/fish.ts b/extensions/terminal-suggest/src/shell/fish.ts new file mode 100644 index 000000000000..f63ec5adf4db --- /dev/null +++ b/extensions/terminal-suggest/src/shell/fish.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import type { ICompletionResource } from '../types'; +import { exec, spawn, type ExecOptionsWithStringEncoding } from 'node:child_process'; + +export async function getFishGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { + return [ + ...await getAliases(options), + ...await getBuiltins(options, existingCommands), + ]; +} + +async function getBuiltins(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { + const compgenOutput = await new Promise((resolve, reject) => { + exec('functions -n', options, (error, stdout) => { + if (error) { + reject(error); + } else { + resolve(stdout); + } + }); + }); + const filter = (cmd: string) => cmd && !existingCommands?.has(cmd); + return compgenOutput.split(', ').filter(filter); +} + +async function getAliases(options: ExecOptionsWithStringEncoding): Promise { + // This must be run with interactive, otherwise there's a good chance aliases won't + // be set up. Note that this could differ from the actual aliases as it's a new bash + // session, for the same reason this would not include aliases that are created + // by simply running `alias ...` in the terminal. + const aliasOutput = await new Promise((resolve, reject) => { + const child = spawn('fish', ['-ic', 'alias'], options); + let stdout = ''; + child.stdout.on('data', (data) => { + stdout += data; + }); + child.on('close', (code) => { + if (code !== 0) { + reject(new Error(`bash process exited with code ${code}`)); + } else { + resolve(stdout); + } + }); + }); + + const result: ICompletionResource[] = []; + for (const line of aliasOutput.split('\n')) { + const match = line.match(/^alias (?[a-zA-Z0-9\.:-]+) (?.+)$/); + if (!match?.groups) { + continue; + } + result.push({ + label: match.groups.alias, + detail: match.groups.resolved, + kind: vscode.TerminalCompletionItemKind.Alias, + }); + } + return result; +} diff --git a/extensions/terminal-suggest/src/shell/zsh.ts b/extensions/terminal-suggest/src/shell/zsh.ts index a19c71677ec1..d197dba9147e 100644 --- a/extensions/terminal-suggest/src/shell/zsh.ts +++ b/extensions/terminal-suggest/src/shell/zsh.ts @@ -9,12 +9,12 @@ import { exec, spawn, type ExecOptionsWithStringEncoding } from 'node:child_proc export async function getZshGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { return [ - ...await getZshBuiltins(options, existingCommands), - ...await getZshAliases(options), + ...await getAliases(options), + ...await getBuiltins(options, existingCommands), ]; } -async function getZshBuiltins(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { +async function getBuiltins(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { const compgenOutput = await new Promise((resolve, reject) => { exec('printf "%s\\n" ${(k)builtins}', options, (error, stdout) => { if (error) { @@ -28,7 +28,7 @@ async function getZshBuiltins(options: ExecOptionsWithStringEncoding, existingCo return compgenOutput.split('\n').filter(filter); } -async function getZshAliases(options: ExecOptionsWithStringEncoding): Promise { +async function getAliases(options: ExecOptionsWithStringEncoding): Promise { // This must be run with interactive, otherwise there's a good chance aliases won't // be set up. Note that this could differ from the actual aliases as it's a new bash // session, for the same reason this would not include aliases that are created diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 3b3b7286fd89..9e5547d05c1c 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs/promises'; import * as path from 'path'; -import { exec, ExecOptionsWithStringEncoding, execSync } from 'child_process'; +import { exec, ExecOptionsWithStringEncoding } from 'child_process'; import { upstreamSpecs } from './constants'; import codeCompletionSpec from './completions/code'; import cdSpec from './completions/cd'; @@ -15,6 +15,7 @@ import { isExecutable } from './helpers/executable'; import type { ICompletionResource } from './types'; import { getBashGlobals } from './shell/bash'; import { getZshGlobals } from './shell/zsh'; +import { getFishGlobals } from './shell/fish'; const enum PwshCommandType { Alias = 1 @@ -60,8 +61,7 @@ async function getBuiltinCommands(shellType: TerminalShellType, existingCommands } case TerminalShellType.Fish: { // TODO: Ghost text in the command line prevents completions from working ATM for fish - const fishOutput = execSync('functions -n', options); - commands = fishOutput.split(', ').filter(filter); + commands = await getFishGlobals(options, existingCommands); break; } case TerminalShellType.PowerShell: { From 7e1c4edb7604e99970c85b1a42f37f0a24572e3e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:30:20 -0800 Subject: [PATCH 1091/3587] Remove compile warning --- extensions/terminal-suggest/src/terminalSuggestMain.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 9e5547d05c1c..faf868e3a6ce 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -43,7 +43,6 @@ async function getBuiltinCommands(shellType: TerminalShellType, existingCommands if (cachedCommands) { return cachedCommands; } - const filter = (cmd: string) => cmd && !existingCommands?.has(cmd); const shell = getShell(shellType); if (!shell) { return; From a1f8e8d54e62158ecaa66d4fc724d77ef62acdd3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:35:51 -0800 Subject: [PATCH 1092/3587] Use a map over a switch statement --- .../src/terminalSuggestMain.ts | 60 +++++++++---------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index faf868e3a6ce..cd6d1483320c 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -21,6 +21,23 @@ const enum PwshCommandType { Alias = 1 } +// TODO: remove once API is finalized +export enum TerminalShellType { + Sh = 1, + Bash = 2, + Fish = 3, + Csh = 4, + Ksh = 5, + Zsh = 6, + CommandPrompt = 7, + GitBash = 8, + PowerShell = 9, + Python = 10, + Julia = 11, + NuShell = 12, + Node = 13 +} + const isWindows = osIsWindows(); let cachedAvailableCommandsPath: string | undefined; let cachedWindowsExecutableExtensions: { [key: string]: boolean | undefined } | undefined; @@ -37,6 +54,14 @@ for (const spec of upstreamSpecs) { availableSpecs.push(require(`./completions/upstream/${spec}`).default); } +const getShellGlobals: Map) => Promise<(string | ICompletionResource)[]>> = new Map([ + [TerminalShellType.Bash, getBashGlobals], + [TerminalShellType.Zsh, getZshGlobals], + // TODO: Ghost text in the command line prevents completions from working ATM for fish + [TerminalShellType.Fish, getFishGlobals], + // [TerminalShellType.PowerShell]: getPwshGlobals, +]); + async function getBuiltinCommands(shellType: TerminalShellType, existingCommands?: Set): Promise { try { const cachedCommands = cachedBuiltinCommands.get(shellType); @@ -50,19 +75,6 @@ async function getBuiltinCommands(shellType: TerminalShellType, existingCommands const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell }; let commands: (string | ICompletionResource)[] | undefined; switch (shellType) { - case TerminalShellType.Bash: { - commands = await getBashGlobals(options, existingCommands); - break; - } - case TerminalShellType.Zsh: { - commands = await getZshGlobals(options, existingCommands); - break; - } - case TerminalShellType.Fish: { - // TODO: Ghost text in the command line prevents completions from working ATM for fish - commands = await getFishGlobals(options, existingCommands); - break; - } case TerminalShellType.PowerShell: { const output = await new Promise((resolve, reject) => { exec('Get-Command -All | Select-Object Name, CommandType, DisplayName, Definition | ConvertTo-Json', { @@ -103,6 +115,10 @@ async function getBuiltinCommands(shellType: TerminalShellType, existingCommands cachedBuiltinCommands.set(shellType, commandResources); return commandResources; } + default: { + commands = await getShellGlobals.get(shellType)?.(options, existingCommands); + break; + } } const commandResources = commands?.map(command => typeof command === 'string' ? ({ label: command }) : command); @@ -561,24 +577,6 @@ function getFriendlyResourcePath(uri: vscode.Uri, pathSeparator: string, kind?: return path; } -// TODO: remove once API is finalized -export enum TerminalShellType { - Sh = 1, - Bash = 2, - Fish = 3, - Csh = 4, - Ksh = 5, - Zsh = 6, - CommandPrompt = 7, - GitBash = 8, - PowerShell = 9, - Python = 10, - Julia = 11, - NuShell = 12, - Node = 13 -} - - function getShell(shellType: TerminalShellType): string | undefined { switch (shellType) { case TerminalShellType.Bash: From a3b6a78384d8f022cc2a81295b0959678ff2b201 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 31 Jan 2025 21:01:30 +0100 Subject: [PATCH 1093/3587] Opening a file without read permissions does not yield an error (fix #239298) (#239366) --- src/vs/platform/windows/electron-main/windowsMainService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index b47f1a17cb4f..d569ef654779 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -1164,13 +1164,15 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic this.workspacesHistoryMainService.removeRecentlyOpened([fileUri]); // assume this is a file that does not yet exist - if (options.ignoreFileNotFound) { + if (options.ignoreFileNotFound && error.code === 'ENOENT') { return { fileUri, type: FileType.File, exists: false }; } + + this.logService.error(`Invalid path provided: ${path}, ${error.message}`); } return undefined; From 3e048f17b0c29e9ee72aceae94a6c92c72d36609 Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Fri, 31 Jan 2025 12:44:55 -0800 Subject: [PATCH 1094/3587] Provide error message when it looks like node arch does not match the system (#239364) --- build/npm/preinstall.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build/npm/preinstall.js b/build/npm/preinstall.js index 31821ee2393d..41a17b016767 100644 --- a/build/npm/preinstall.js +++ b/build/npm/preinstall.js @@ -23,6 +23,7 @@ if (process.env['npm_execpath'].includes('yarn')) { const path = require('path'); const fs = require('fs'); const cp = require('child_process'); +const os = require('os'); if (process.platform === 'win32') { if (!hasSupportedVisualStudioVersion()) { @@ -32,6 +33,11 @@ if (process.platform === 'win32') { installHeaders(); } +if (process.arch !== os.arch()) { + console.error(`\x1b[1;31m*** ARCHITECTURE MISMATCH: The node.js process is ${process.arch}, but your OS architecture is ${os.arch()}. ***\x1b[0;0m`); + console.error(`\x1b[1;31m*** This can greatly increase the build time of vs code. ***\x1b[0;0m`); +} + function hasSupportedVisualStudioVersion() { const fs = require('fs'); const path = require('path'); From 735fde9116c34acdc335187cb77a925029fdf51a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:52:34 -0800 Subject: [PATCH 1095/3587] Move pwsh logic into own file --- extensions/terminal-suggest/src/shell/pwsh.ts | 58 +++++++++++++++ .../src/terminalSuggestMain.ts | 70 +++---------------- 2 files changed, 68 insertions(+), 60 deletions(-) create mode 100644 extensions/terminal-suggest/src/shell/pwsh.ts diff --git a/extensions/terminal-suggest/src/shell/pwsh.ts b/extensions/terminal-suggest/src/shell/pwsh.ts new file mode 100644 index 000000000000..4daa0926b86c --- /dev/null +++ b/extensions/terminal-suggest/src/shell/pwsh.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import type { ICompletionResource } from '../types'; +import { exec, type ExecOptionsWithStringEncoding } from 'node:child_process'; + +export async function getPwshGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { + return [ + ...await getCommands(options, existingCommands), + ]; +} + +const enum PwshCommandType { + Alias = 1 +} + +async function getCommands(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { + + const output = await new Promise((resolve, reject) => { + exec('Get-Command -All | Select-Object Name, CommandType, DisplayName, Definition | ConvertTo-Json', { + ...options, + maxBuffer: 1024 * 1024 * 100 // This is a lot of content, increase buffer size + }, (error, stdout) => { + if (error) { + reject(error); + return; + } + resolve(stdout); + }); + }); + let json: any; + try { + json = JSON.parse(output); + } catch (e) { + console.error('Error parsing pwsh output:', e); + return []; + } + return (json as any[]).map(e => { + switch (e.CommandType) { + case PwshCommandType.Alias: { + return { + label: e.Name, + detail: e.DisplayName, + kind: vscode.TerminalCompletionItemKind.Alias, + }; + } + default: { + return { + label: e.Name, + detail: e.Definition, + }; + } + } + }); +} diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index cd6d1483320c..6ca278bcfb52 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -16,10 +16,7 @@ import type { ICompletionResource } from './types'; import { getBashGlobals } from './shell/bash'; import { getZshGlobals } from './shell/zsh'; import { getFishGlobals } from './shell/fish'; - -const enum PwshCommandType { - Alias = 1 -} +import { getPwshGlobals } from './shell/pwsh'; // TODO: remove once API is finalized export enum TerminalShellType { @@ -54,15 +51,15 @@ for (const spec of upstreamSpecs) { availableSpecs.push(require(`./completions/upstream/${spec}`).default); } -const getShellGlobals: Map) => Promise<(string | ICompletionResource)[]>> = new Map([ +const getShellSpecificGlobals: Map) => Promise<(string | ICompletionResource)[]>> = new Map([ [TerminalShellType.Bash, getBashGlobals], [TerminalShellType.Zsh, getZshGlobals], // TODO: Ghost text in the command line prevents completions from working ATM for fish [TerminalShellType.Fish, getFishGlobals], - // [TerminalShellType.PowerShell]: getPwshGlobals, + [TerminalShellType.PowerShell, getPwshGlobals], ]); -async function getBuiltinCommands(shellType: TerminalShellType, existingCommands?: Set): Promise { +async function getShellGlobals(shellType: TerminalShellType, existingCommands?: Set): Promise { try { const cachedCommands = cachedBuiltinCommands.get(shellType); if (cachedCommands) { @@ -73,57 +70,10 @@ async function getBuiltinCommands(shellType: TerminalShellType, existingCommands return; } const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell }; - let commands: (string | ICompletionResource)[] | undefined; - switch (shellType) { - case TerminalShellType.PowerShell: { - const output = await new Promise((resolve, reject) => { - exec('Get-Command -All | Select-Object Name, CommandType, DisplayName, Definition | ConvertTo-Json', { - ...options, - maxBuffer: 1024 * 1024 * 100 // This is a lot of content, increase buffer size - }, (error, stdout) => { - if (error) { - reject(error); - return; - } - resolve(stdout); - }); - }); - let json: any; - try { - json = JSON.parse(output); - } catch (e) { - console.error('Error parsing pwsh output:', e); - return []; - } - const commandResources = (json as any[]).map(e => { - switch (e.CommandType) { - case PwshCommandType.Alias: { - return { - label: e.Name, - detail: e.DisplayName, - kind: vscode.TerminalCompletionItemKind.Alias, - }; - } - default: { - return { - label: e.Name, - detail: e.Definition, - }; - } - } - }); - cachedBuiltinCommands.set(shellType, commandResources); - return commandResources; - } - default: { - commands = await getShellGlobals.get(shellType)?.(options, existingCommands); - break; - } - } - - const commandResources = commands?.map(command => typeof command === 'string' ? ({ label: command }) : command); - cachedBuiltinCommands.set(shellType, commandResources); - return commandResources; + const mixedCommands: (string | ICompletionResource)[] | undefined = await getShellSpecificGlobals.get(shellType)?.(options, existingCommands); + const normalizedCommands = mixedCommands?.map(command => typeof command === 'string' ? ({ label: command }) : command); + cachedBuiltinCommands.set(shellType, normalizedCommands); + return normalizedCommands; } catch (error) { console.error('Error fetching builtin commands:', error); @@ -145,11 +95,11 @@ export async function activate(context: vscode.ExtensionContext) { } const commandsInPath = await getCommandsInPath(terminal.shellIntegration?.env); - const builtinCommands = await getBuiltinCommands(shellType, commandsInPath?.labels) ?? []; + const shellGlobals = await getShellGlobals(shellType, commandsInPath?.labels) ?? []; if (!commandsInPath?.completionResources) { return; } - const commands = [...commandsInPath.completionResources, ...builtinCommands]; + const commands = [...commandsInPath.completionResources, ...shellGlobals]; const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition); const pathSeparator = isWindows ? '\\' : '/'; From fee91cbff51248d85d76cfc2bea4a2252ac9da82 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 13:07:11 -0800 Subject: [PATCH 1096/3587] Share exec/spawn code between shells --- extensions/terminal-suggest/src/shell/bash.ts | 29 ++------------ .../terminal-suggest/src/shell/common.ts | 40 +++++++++++++++++++ extensions/terminal-suggest/src/shell/fish.ts | 28 ++----------- extensions/terminal-suggest/src/shell/pwsh.ts | 18 +++------ extensions/terminal-suggest/src/shell/zsh.ts | 29 ++------------ .../src/terminalSuggestMain.ts | 2 +- 6 files changed, 58 insertions(+), 88 deletions(-) create mode 100644 extensions/terminal-suggest/src/shell/common.ts diff --git a/extensions/terminal-suggest/src/shell/bash.ts b/extensions/terminal-suggest/src/shell/bash.ts index 70a8110eba3c..b440af3c8805 100644 --- a/extensions/terminal-suggest/src/shell/bash.ts +++ b/extensions/terminal-suggest/src/shell/bash.ts @@ -5,7 +5,8 @@ import * as vscode from 'vscode'; import type { ICompletionResource } from '../types'; -import { exec, spawn, type ExecOptionsWithStringEncoding } from 'node:child_process'; +import { type ExecOptionsWithStringEncoding } from 'node:child_process'; +import { execHelper, spawnHelper } from './common'; export async function getBashGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { return [ @@ -15,15 +16,7 @@ export async function getBashGlobals(options: ExecOptionsWithStringEncoding, exi } async function getBuiltins(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { - const compgenOutput = await new Promise((resolve, reject) => { - exec('compgen -b', options, (error, stdout) => { - if (error) { - reject(error); - } else { - resolve(stdout); - } - }); - }); + const compgenOutput = await execHelper('compgen -b', options); const filter = (cmd: string) => cmd && !existingCommands?.has(cmd); return compgenOutput.split('\n').filter(filter); } @@ -33,21 +26,7 @@ async function getAliases(options: ExecOptionsWithStringEncoding): Promise((resolve, reject) => { - const child = spawn('bash', ['-ic', 'alias'], options); - let stdout = ''; - child.stdout.on('data', (data) => { - stdout += data; - }); - child.on('close', (code) => { - if (code !== 0) { - reject(new Error(`bash process exited with code ${code}`)); - } else { - resolve(stdout); - } - }); - }); - + const aliasOutput = await spawnHelper('bash', ['-ic', 'alias'], options); const result: ICompletionResource[] = []; for (const line of aliasOutput.split('\n')) { const match = line.match(/^alias (?[a-zA-Z0-9\.:-]+)='(?.+)'$/); diff --git a/extensions/terminal-suggest/src/shell/common.ts b/extensions/terminal-suggest/src/shell/common.ts new file mode 100644 index 000000000000..5bb5c349f2bc --- /dev/null +++ b/extensions/terminal-suggest/src/shell/common.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { exec, spawn, type ExecOptionsWithStringEncoding } from 'node:child_process'; + +export async function spawnHelper(command: string, args: string[], options: ExecOptionsWithStringEncoding): Promise { + // This must be run with interactive, otherwise there's a good chance aliases won't + // be set up. Note that this could differ from the actual aliases as it's a new bash + // session, for the same reason this would not include aliases that are created + // by simply running `alias ...` in the terminal. + return new Promise((resolve, reject) => { + const child = spawn(command, args, options); + let stdout = ''; + child.stdout.on('data', (data) => { + stdout += data; + }); + child.on('close', (code) => { + if (code !== 0) { + reject(new Error(`bash process exited with code ${code}`)); + } else { + resolve(stdout); + } + }); + }); +} + +export async function execHelper(commandLine: string, options: ExecOptionsWithStringEncoding): Promise { + return new Promise((resolve, reject) => { + exec(commandLine, options, (error, stdout) => { + if (error) { + reject(error); + } else { + resolve(stdout); + } + }); + }); +} + diff --git a/extensions/terminal-suggest/src/shell/fish.ts b/extensions/terminal-suggest/src/shell/fish.ts index f63ec5adf4db..eaa472d2b273 100644 --- a/extensions/terminal-suggest/src/shell/fish.ts +++ b/extensions/terminal-suggest/src/shell/fish.ts @@ -5,7 +5,8 @@ import * as vscode from 'vscode'; import type { ICompletionResource } from '../types'; -import { exec, spawn, type ExecOptionsWithStringEncoding } from 'node:child_process'; +import { execHelper, spawnHelper } from './common'; +import { type ExecOptionsWithStringEncoding } from 'node:child_process'; export async function getFishGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { return [ @@ -15,15 +16,7 @@ export async function getFishGlobals(options: ExecOptionsWithStringEncoding, exi } async function getBuiltins(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { - const compgenOutput = await new Promise((resolve, reject) => { - exec('functions -n', options, (error, stdout) => { - if (error) { - reject(error); - } else { - resolve(stdout); - } - }); - }); + const compgenOutput = await execHelper('functions -n', options); const filter = (cmd: string) => cmd && !existingCommands?.has(cmd); return compgenOutput.split(', ').filter(filter); } @@ -33,20 +26,7 @@ async function getAliases(options: ExecOptionsWithStringEncoding): Promise((resolve, reject) => { - const child = spawn('fish', ['-ic', 'alias'], options); - let stdout = ''; - child.stdout.on('data', (data) => { - stdout += data; - }); - child.on('close', (code) => { - if (code !== 0) { - reject(new Error(`bash process exited with code ${code}`)); - } else { - resolve(stdout); - } - }); - }); + const aliasOutput = await spawnHelper('fish', ['-ic', 'alias'], options); const result: ICompletionResource[] = []; for (const line of aliasOutput.split('\n')) { diff --git a/extensions/terminal-suggest/src/shell/pwsh.ts b/extensions/terminal-suggest/src/shell/pwsh.ts index 4daa0926b86c..47b8e7fe7f63 100644 --- a/extensions/terminal-suggest/src/shell/pwsh.ts +++ b/extensions/terminal-suggest/src/shell/pwsh.ts @@ -5,7 +5,8 @@ import * as vscode from 'vscode'; import type { ICompletionResource } from '../types'; -import { exec, type ExecOptionsWithStringEncoding } from 'node:child_process'; +import { type ExecOptionsWithStringEncoding } from 'node:child_process'; +import { execHelper } from './common'; export async function getPwshGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { return [ @@ -18,18 +19,9 @@ const enum PwshCommandType { } async function getCommands(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { - - const output = await new Promise((resolve, reject) => { - exec('Get-Command -All | Select-Object Name, CommandType, DisplayName, Definition | ConvertTo-Json', { - ...options, - maxBuffer: 1024 * 1024 * 100 // This is a lot of content, increase buffer size - }, (error, stdout) => { - if (error) { - reject(error); - return; - } - resolve(stdout); - }); + const output = await execHelper('Get-Command -All | Select-Object Name, CommandType, DisplayName, Definition | ConvertTo-Json', { + ...options, + maxBuffer: 1024 * 1024 * 100 // This is a lot of content, increase buffer size }); let json: any; try { diff --git a/extensions/terminal-suggest/src/shell/zsh.ts b/extensions/terminal-suggest/src/shell/zsh.ts index d197dba9147e..ce00edb900ba 100644 --- a/extensions/terminal-suggest/src/shell/zsh.ts +++ b/extensions/terminal-suggest/src/shell/zsh.ts @@ -5,7 +5,8 @@ import * as vscode from 'vscode'; import type { ICompletionResource } from '../types'; -import { exec, spawn, type ExecOptionsWithStringEncoding } from 'node:child_process'; +import { execHelper, spawnHelper } from './common'; +import { type ExecOptionsWithStringEncoding } from 'node:child_process'; export async function getZshGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { return [ @@ -15,15 +16,7 @@ export async function getZshGlobals(options: ExecOptionsWithStringEncoding, exis } async function getBuiltins(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { - const compgenOutput = await new Promise((resolve, reject) => { - exec('printf "%s\\n" ${(k)builtins}', options, (error, stdout) => { - if (error) { - reject(error); - } else { - resolve(stdout); - } - }); - }); + const compgenOutput = await execHelper('printf "%s\\n" ${(k)builtins}', options); const filter = (cmd: string) => cmd && !existingCommands?.has(cmd); return compgenOutput.split('\n').filter(filter); } @@ -33,21 +26,7 @@ async function getAliases(options: ExecOptionsWithStringEncoding): Promise((resolve, reject) => { - const child = spawn('zsh', ['-ic', 'alias'], options); - let stdout = ''; - child.stdout.on('data', (data) => { - stdout += data; - }); - child.on('close', (code) => { - if (code !== 0) { - reject(new Error(`zsh process exited with code ${code}`)); - } else { - resolve(stdout); - } - }); - }); - + const aliasOutput = await spawnHelper('zsh', ['-ic', 'alias'], options); const result: ICompletionResource[] = []; for (const line of aliasOutput.split('\n')) { const match = line.match(/^(?[a-zA-Z0-9\.:-]+)=(?:'(?.+)'|(?.+))$/); diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 6ca278bcfb52..b348e7a73fc7 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs/promises'; import * as path from 'path'; -import { exec, ExecOptionsWithStringEncoding } from 'child_process'; +import { ExecOptionsWithStringEncoding } from 'child_process'; import { upstreamSpecs } from './constants'; import codeCompletionSpec from './completions/code'; import cdSpec from './completions/cd'; From 6540b40a3d42024b2643d8ba005f103cdebd9b92 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 13:27:26 -0800 Subject: [PATCH 1097/3587] Set kind for all pwsh commands, handle aliases without resolved --- extensions/terminal-suggest/src/shell/pwsh.ts | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/extensions/terminal-suggest/src/shell/pwsh.ts b/extensions/terminal-suggest/src/shell/pwsh.ts index 47b8e7fe7f63..8751b3f2af76 100644 --- a/extensions/terminal-suggest/src/shell/pwsh.ts +++ b/extensions/terminal-suggest/src/shell/pwsh.ts @@ -14,10 +14,46 @@ export async function getPwshGlobals(options: ExecOptionsWithStringEncoding, exi ]; } +/** + * The numeric values associated with CommandType from Get-Command. It appears that this is a + * bitfield based on the values but I think it's actually used as an enum where a CommandType can + * only be a single one of these. + * + * Source: + * + * ``` + * [enum]::GetValues([System.Management.Automation.CommandTypes]) | ForEach-Object { + * [pscustomobject]@{ + * Name = $_ + * Value = [int]$_ + * } + * } + * ``` + */ const enum PwshCommandType { - Alias = 1 + Alias = 1, + Function = 2, + Filter = 4, + Cmdlet = 8, + ExternalScript = 16, + Application = 32, + Script = 64, + Configuration = 256, + // All = 383, } +const pwshCommandTypeToCompletionKind: Map = new Map([ + [PwshCommandType.Alias, vscode.TerminalCompletionItemKind.Alias], + [PwshCommandType.Function, vscode.TerminalCompletionItemKind.Method], + [PwshCommandType.Filter, vscode.TerminalCompletionItemKind.Method], + [PwshCommandType.Cmdlet, vscode.TerminalCompletionItemKind.Method], + [PwshCommandType.ExternalScript, vscode.TerminalCompletionItemKind.Method], + [PwshCommandType.Application, vscode.TerminalCompletionItemKind.Method], + [PwshCommandType.Script, vscode.TerminalCompletionItemKind.Method], + [PwshCommandType.Configuration, vscode.TerminalCompletionItemKind.Argument], +]); + + async function getCommands(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { const output = await execHelper('Get-Command -All | Select-Object Name, CommandType, DisplayName, Definition | ConvertTo-Json', { ...options, @@ -36,13 +72,17 @@ async function getCommands(options: ExecOptionsWithStringEncoding, existingComma return { label: e.Name, detail: e.DisplayName, - kind: vscode.TerminalCompletionItemKind.Alias, + // Aliases sometimes return the same DisplayName, show as a method in this case. + kind: (e.Name === e.DisplayName + ? vscode.TerminalCompletionItemKind.Method + : vscode.TerminalCompletionItemKind.Alias), }; } default: { return { label: e.Name, detail: e.Definition, + kind: pwshCommandTypeToCompletionKind.get(e.CommandType) }; } } From 0baef062361c764793fe714ba850b4ebfffd4d43 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 31 Jan 2025 16:03:21 -0600 Subject: [PATCH 1098/3587] ensure space after command before providing options (#239378) fix #239249 --- extensions/terminal-suggest/src/terminalSuggestMain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index db52610be6c9..69d474247fed 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -355,7 +355,7 @@ export async function getCompletionItemsFromSpecs( continue; } - if (!terminalContext.commandLine.startsWith(specLabel)) { + if (!terminalContext.commandLine.startsWith(`${specLabel} `)) { // the spec label is not the first word in the command line, so do not provide options or args continue; } From 92f0b764f84bb5c4c5495266e12fca80e47bba86 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:04:33 -0800 Subject: [PATCH 1099/3587] Polish format of aliases --- .../terminal-suggest/src/shell/common.ts | 2 +- extensions/terminal-suggest/src/shell/pwsh.ts | 62 ++++++++++++------- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/extensions/terminal-suggest/src/shell/common.ts b/extensions/terminal-suggest/src/shell/common.ts index 5bb5c349f2bc..590195e3600b 100644 --- a/extensions/terminal-suggest/src/shell/common.ts +++ b/extensions/terminal-suggest/src/shell/common.ts @@ -18,7 +18,7 @@ export async function spawnHelper(command: string, args: string[], options: Exec }); child.on('close', (code) => { if (code !== 0) { - reject(new Error(`bash process exited with code ${code}`)); + reject(new Error(`process exited with code ${code}`)); } else { resolve(stdout); } diff --git a/extensions/terminal-suggest/src/shell/pwsh.ts b/extensions/terminal-suggest/src/shell/pwsh.ts index 8751b3f2af76..ed6d9f50d94a 100644 --- a/extensions/terminal-suggest/src/shell/pwsh.ts +++ b/extensions/terminal-suggest/src/shell/pwsh.ts @@ -10,6 +10,7 @@ import { execHelper } from './common'; export async function getPwshGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { return [ + ...await getAliases(options, existingCommands), ...await getCommands(options, existingCommands), ]; } @@ -53,6 +54,37 @@ const pwshCommandTypeToCompletionKind: Map): Promise { + const output = await execHelper('Get-Command -CommandType Alias | Select-Object Name, CommandType, Definition, ModuleName, @{Name="Version";Expression={$_.Version.ToString()}} | ConvertTo-Json', { + ...options, + maxBuffer: 1024 * 1024 * 100 // This is a lot of content, increase buffer size + }); + let json: any; + try { + json = JSON.parse(output); + } catch (e) { + console.error('Error parsing output:', e); + return []; + } + return (json as any[]).map(e => { + const detailParts: string[] = []; + if (e.Definition) { + detailParts.push(e.Definition); + } + if (e.ModuleName && e.Version) { + detailParts.push(`${e.ModuleName} v${e.Version}`); + } + return { + label: e.Name, + detail: detailParts.join('\n\n'), + // Aliases sometimes return the same DisplayName, show as a method in this case. + kind: (e.Name === e.DisplayName + ? vscode.TerminalCompletionItemKind.Method + : vscode.TerminalCompletionItemKind.Alias), + }; + } + ); +} async function getCommands(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { const output = await execHelper('Get-Command -All | Select-Object Name, CommandType, DisplayName, Definition | ConvertTo-Json', { @@ -66,25 +98,13 @@ async function getCommands(options: ExecOptionsWithStringEncoding, existingComma console.error('Error parsing pwsh output:', e); return []; } - return (json as any[]).map(e => { - switch (e.CommandType) { - case PwshCommandType.Alias: { - return { - label: e.Name, - detail: e.DisplayName, - // Aliases sometimes return the same DisplayName, show as a method in this case. - kind: (e.Name === e.DisplayName - ? vscode.TerminalCompletionItemKind.Method - : vscode.TerminalCompletionItemKind.Alias), - }; - } - default: { - return { - label: e.Name, - detail: e.Definition, - kind: pwshCommandTypeToCompletionKind.get(e.CommandType) - }; - } - } - }); + return ( + (json as any[]) + .filter(e => e.CommandType !== PwshCommandType.Alias) + .map(e => ({ + label: e.Name, + detail: e.Definition, + kind: pwshCommandTypeToCompletionKind.get(e.CommandType) + })) + ); } From c69d72c5159982907deca62378bd94f87096972b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:05:57 -0800 Subject: [PATCH 1100/3587] Correct non-definition entries --- extensions/terminal-suggest/src/shell/pwsh.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/extensions/terminal-suggest/src/shell/pwsh.ts b/extensions/terminal-suggest/src/shell/pwsh.ts index ed6d9f50d94a..d7a3a1954acd 100644 --- a/extensions/terminal-suggest/src/shell/pwsh.ts +++ b/extensions/terminal-suggest/src/shell/pwsh.ts @@ -77,10 +77,11 @@ async function getAliases(options: ExecOptionsWithStringEncoding, existingComman return { label: e.Name, detail: detailParts.join('\n\n'), - // Aliases sometimes return the same DisplayName, show as a method in this case. - kind: (e.Name === e.DisplayName - ? vscode.TerminalCompletionItemKind.Method - : vscode.TerminalCompletionItemKind.Alias), + // Aliases sometimes don't have a definition and use the same DisplayName, show them as + // a method in this case. + kind: (!e.Definition + ? vscode.TerminalCompletionItemKind.Alias + : vscode.TerminalCompletionItemKind.Method), }; } ); From 9802b181393a1c82b7115cd9d41f4e4902a5c35e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:35:40 -0800 Subject: [PATCH 1101/3587] Make other command types consistent, polish abnormal alias types --- extensions/terminal-suggest/src/shell/pwsh.ts | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/extensions/terminal-suggest/src/shell/pwsh.ts b/extensions/terminal-suggest/src/shell/pwsh.ts index d7a3a1954acd..f3b0e371573b 100644 --- a/extensions/terminal-suggest/src/shell/pwsh.ts +++ b/extensions/terminal-suggest/src/shell/pwsh.ts @@ -55,7 +55,7 @@ const pwshCommandTypeToCompletionKind: Map): Promise { - const output = await execHelper('Get-Command -CommandType Alias | Select-Object Name, CommandType, Definition, ModuleName, @{Name="Version";Expression={$_.Version.ToString()}} | ConvertTo-Json', { + const output = await execHelper('Get-Command -CommandType Alias | Select-Object Name, CommandType, Definition, DisplayName, ModuleName, @{Name="Version";Expression={$_.Version.ToString()}} | ConvertTo-Json', { ...options, maxBuffer: 1024 * 1024 * 100 // This is a lot of content, increase buffer size }); @@ -67,9 +67,11 @@ async function getAliases(options: ExecOptionsWithStringEncoding, existingComman return []; } return (json as any[]).map(e => { + // Aliases sometimes use the same Name and DisplayName, show them as methods in this case. + const isAlias = e.Name !== e.DisplayName; const detailParts: string[] = []; if (e.Definition) { - detailParts.push(e.Definition); + detailParts.push(isAlias ? `→ ${e.Definition}` : e.Definition); } if (e.ModuleName && e.Version) { detailParts.push(`${e.ModuleName} v${e.Version}`); @@ -77,18 +79,15 @@ async function getAliases(options: ExecOptionsWithStringEncoding, existingComman return { label: e.Name, detail: detailParts.join('\n\n'), - // Aliases sometimes don't have a definition and use the same DisplayName, show them as - // a method in this case. - kind: (!e.Definition + kind: (isAlias ? vscode.TerminalCompletionItemKind.Alias : vscode.TerminalCompletionItemKind.Method), }; - } - ); + }); } async function getCommands(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { - const output = await execHelper('Get-Command -All | Select-Object Name, CommandType, DisplayName, Definition | ConvertTo-Json', { + const output = await execHelper('Get-Command -All | Select-Object Name, CommandType, Definition, ModuleName, @{Name="Version";Expression={$_.Version.ToString()}} | ConvertTo-Json', { ...options, maxBuffer: 1024 * 1024 * 100 // This is a lot of content, increase buffer size }); @@ -102,10 +101,19 @@ async function getCommands(options: ExecOptionsWithStringEncoding, existingComma return ( (json as any[]) .filter(e => e.CommandType !== PwshCommandType.Alias) - .map(e => ({ - label: e.Name, - detail: e.Definition, - kind: pwshCommandTypeToCompletionKind.get(e.CommandType) - })) + .map(e => { + const detailParts: string[] = []; + if (e.Definition) { + detailParts.push(e.Definition.trim()); + } + if (e.ModuleName && e.Version) { + detailParts.push(`${e.ModuleName} v${e.Version}`); + } + return { + label: e.Name, + detail: detailParts.join('\n\n'), + kind: pwshCommandTypeToCompletionKind.get(e.CommandType) + }; + }) ); } From 2aaf62826970e2cef80e9e90e61d8f5a1a89b235 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 31 Jan 2025 16:36:47 -0600 Subject: [PATCH 1102/3587] fix missing/incorrect info in editor and edits a11y help dialogs (#239377) fix #239325 --- src/vs/editor/common/standaloneStrings.ts | 2 +- .../contrib/chat/browser/actions/chatAccessibilityHelp.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/common/standaloneStrings.ts b/src/vs/editor/common/standaloneStrings.ts index f2edfd33f60a..c181573190ba 100644 --- a/src/vs/editor/common/standaloneStrings.ts +++ b/src/vs/editor/common/standaloneStrings.ts @@ -36,7 +36,7 @@ export namespace AccessibilityHelpNLS { export const debugExecuteSelection = nls.localize('debugConsole.executeSelection', "The Debug: Execute Selection command{0} will execute the selected text in the debug console.", ''); export const chatEditorModification = nls.localize('chatEditorModification', "The editor contains pending modifications that have been made by chat."); export const chatEditorRequestInProgress = nls.localize('chatEditorRequestInProgress', "The editor is currently waiting for modifications to be made by chat."); - export const chatEditActions = nls.localize('chatEditing.navigation', 'Navigate between edits in the editor with navigate previous{0} and next{1} and accept{3} and reject the current change{4}.', '', '', '', ''); + export const chatEditActions = nls.localize('chatEditing.navigation', 'Navigate between edits in the editor with navigate previous{0} and next{1} and accept{2}, reject{3} or view the diff{4} for the current change.', '', '', '', '', ''); } export namespace InspectTokensNLS { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index 5ed43c1ba433..bc485108df65 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -79,7 +79,7 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'qui content.push(localize('chatEditing.expectation', 'When a request is made, a progress indicator will play while the edits are being applied.')); content.push(localize('chatEditing.review', 'Once the edits are applied, focus the editor(s) to review, accept, and discard changes.')); content.push(localize('chatEditing.sections', 'Navigate between edits in the editor with navigate previous{0} and next{1}', '', '')); - content.push(localize('chatEditing.acceptHunk', 'In the editor, Accept{0} and Reject the current Change{1}.', '', '')); + content.push(localize('chatEditing.acceptHunk', 'In the editor, Accept{0}, Reject{1}, or Toggle the Diff{2} for the current Change.', '', '', '')); content.push(localize('chatEditing.helpfulCommands', 'When in the edits view, some helpful commands include:')); content.push(localize('workbench.action.chat.undoEdits', '- Undo Edits{0}.', '')); content.push(localize('workbench.action.chat.editing.attachFiles', '- Attach Files{0}.', '')); From 9d7b58991320ff8fa17fc9aca1290980577b2df4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:40:12 -0800 Subject: [PATCH 1103/3587] Remove arrow from alias detail --- extensions/terminal-suggest/src/shell/pwsh.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/terminal-suggest/src/shell/pwsh.ts b/extensions/terminal-suggest/src/shell/pwsh.ts index f3b0e371573b..321e7f5b54b7 100644 --- a/extensions/terminal-suggest/src/shell/pwsh.ts +++ b/extensions/terminal-suggest/src/shell/pwsh.ts @@ -71,7 +71,7 @@ async function getAliases(options: ExecOptionsWithStringEncoding, existingComman const isAlias = e.Name !== e.DisplayName; const detailParts: string[] = []; if (e.Definition) { - detailParts.push(isAlias ? `→ ${e.Definition}` : e.Definition); + detailParts.push(e.Definition); } if (e.ModuleName && e.Version) { detailParts.push(`${e.ModuleName} v${e.Version}`); From 4c32889faf1e3c533dc024b1bfdb2d78b08888e7 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 31 Jan 2025 21:26:18 -0800 Subject: [PATCH 1104/3587] Show InputBox for unsupported clients (#239389) * Show InputBox for unsupported clients Fixes https://github.com/microsoft/vscode/issues/238147 * comment * Add 127.0.0.1 for good measure --- .../src/common/async.ts | 66 +++++++++++ .../src/common/env.ts | 30 +++++ .../src/common/loopbackClientAndOpener.ts | 112 ++++++++++++++++-- 3 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 extensions/microsoft-authentication/src/common/env.ts diff --git a/extensions/microsoft-authentication/src/common/async.ts b/extensions/microsoft-authentication/src/common/async.ts index 3555bb095026..5f02cc5976d6 100644 --- a/extensions/microsoft-authentication/src/common/async.ts +++ b/extensions/microsoft-authentication/src/common/async.ts @@ -111,3 +111,69 @@ function once(event: Event): Event { export function toPromise(event: Event): Promise { return new Promise(resolve => once(event)(resolve)); } + +//#region DeferredPromise + +export type ValueCallback = (value: T | Promise) => void; + +const enum DeferredOutcome { + Resolved, + Rejected +} + +/** + * Creates a promise whose resolution or rejection can be controlled imperatively. + */ +export class DeferredPromise { + + private completeCallback!: ValueCallback; + private errorCallback!: (err: unknown) => void; + private outcome?: { outcome: DeferredOutcome.Rejected; value: any } | { outcome: DeferredOutcome.Resolved; value: T }; + + public get isRejected() { + return this.outcome?.outcome === DeferredOutcome.Rejected; + } + + public get isResolved() { + return this.outcome?.outcome === DeferredOutcome.Resolved; + } + + public get isSettled() { + return !!this.outcome; + } + + public get value() { + return this.outcome?.outcome === DeferredOutcome.Resolved ? this.outcome?.value : undefined; + } + + public readonly p: Promise; + + constructor() { + this.p = new Promise((c, e) => { + this.completeCallback = c; + this.errorCallback = e; + }); + } + + public complete(value: T) { + return new Promise(resolve => { + this.completeCallback(value); + this.outcome = { outcome: DeferredOutcome.Resolved, value }; + resolve(); + }); + } + + public error(err: unknown) { + return new Promise(resolve => { + this.errorCallback(err); + this.outcome = { outcome: DeferredOutcome.Rejected, value: err }; + resolve(); + }); + } + + public cancel() { + return this.error(new CancellationError()); + } +} + +//#endregion diff --git a/extensions/microsoft-authentication/src/common/env.ts b/extensions/microsoft-authentication/src/common/env.ts new file mode 100644 index 000000000000..5d19183e70cb --- /dev/null +++ b/extensions/microsoft-authentication/src/common/env.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Uri } from 'vscode'; + +const VALID_DESKTOP_CALLBACK_SCHEMES = [ + 'vscode', + 'vscode-insiders', + // On Windows, some browsers don't seem to redirect back to OSS properly. + // As a result, you get stuck in the auth flow. We exclude this from the + // list until we can figure out a way to fix this behavior in browsers. + // 'code-oss', + 'vscode-wsl', + 'vscode-exploration' +]; + +export function isSupportedClient(uri: Uri): boolean { + return ( + VALID_DESKTOP_CALLBACK_SCHEMES.includes(uri.scheme) || + // vscode.dev & insiders.vscode.dev + /(?:^|\.)vscode\.dev$/.test(uri.authority) || + // github.dev & codespaces + /(?:^|\.)github\.dev$/.test(uri.authority) || + // localhost + /^localhost:\d+$/.test(uri.authority) || + // 127.0.0.1 + /^127\.0\.0\.1:\d+$/.test(uri.authority) + ); +} diff --git a/extensions/microsoft-authentication/src/common/loopbackClientAndOpener.ts b/extensions/microsoft-authentication/src/common/loopbackClientAndOpener.ts index 3fbb03400379..e68663efe43a 100644 --- a/extensions/microsoft-authentication/src/common/loopbackClientAndOpener.ts +++ b/extensions/microsoft-authentication/src/common/loopbackClientAndOpener.ts @@ -5,14 +5,17 @@ import type { ILoopbackClient, ServerAuthorizationCodeResponse } from '@azure/msal-node'; import type { UriEventHandler } from '../UriEventHandler'; -import { env, LogOutputChannel, Uri } from 'vscode'; -import { toPromise } from './async'; +import { Disposable, env, l10n, LogOutputChannel, Uri, window } from 'vscode'; +import { DeferredPromise, toPromise } from './async'; +import { isSupportedClient } from './env'; export interface ILoopbackClientAndOpener extends ILoopbackClient { openBrowser(url: string): Promise; } export class UriHandlerLoopbackClient implements ILoopbackClientAndOpener { + private _responseDeferred: DeferredPromise | undefined; + constructor( private readonly _uriHandler: UriEventHandler, private readonly _redirectUri: string, @@ -20,17 +23,14 @@ export class UriHandlerLoopbackClient implements ILoopbackClientAndOpener { ) { } async listenForAuthCode(): Promise { - const url = await toPromise(this._uriHandler.event); - this._logger.debug(`Received URL event. Authority: ${url.authority}`); - const result = new URL(url.toString(true)); - - return { - code: result.searchParams.get('code') ?? undefined, - state: result.searchParams.get('state') ?? undefined, - error: result.searchParams.get('error') ?? undefined, - error_description: result.searchParams.get('error_description') ?? undefined, - error_uri: result.searchParams.get('error_uri') ?? undefined, - }; + await this._responseDeferred?.cancel(); + this._responseDeferred = new DeferredPromise(); + const result = await this._responseDeferred.p; + this._responseDeferred = undefined; + if (result) { + return result; + } + throw new Error('No valid response received for authorization code.'); } getRedirectUri(): string { @@ -46,7 +46,93 @@ export class UriHandlerLoopbackClient implements ILoopbackClientAndOpener { async openBrowser(url: string): Promise { const callbackUri = await env.asExternalUri(Uri.parse(`${env.uriScheme}://vscode.microsoft-authentication`)); + if (isSupportedClient(callbackUri)) { + void this._getCodeResponseFromUriHandler(); + } else { + // Unsupported clients will be shown the code in the browser, but it will not redirect back since this + // isn't a supported client. Instead, they will copy that code in the browser and paste it in an input box + // that will be shown to them by the extension. + void this._getCodeResponseFromQuickPick(); + } + const uri = Uri.parse(url + `&state=${encodeURI(callbackUri.toString(true))}`); await env.openExternal(uri); } + + private async _getCodeResponseFromUriHandler(): Promise { + if (!this._responseDeferred) { + throw new Error('No listener for auth code'); + } + const url = await toPromise(this._uriHandler.event); + this._logger.debug(`Received URL event. Authority: ${url.authority}`); + const result = new URL(url.toString(true)); + + this._responseDeferred?.complete({ + code: result.searchParams.get('code') ?? undefined, + state: result.searchParams.get('state') ?? undefined, + error: result.searchParams.get('error') ?? undefined, + error_description: result.searchParams.get('error_description') ?? undefined, + error_uri: result.searchParams.get('error_uri') ?? undefined, + }); + } + + private async _getCodeResponseFromQuickPick(): Promise { + if (!this._responseDeferred) { + throw new Error('No listener for auth code'); + } + const inputBox = window.createInputBox(); + inputBox.ignoreFocusOut = true; + inputBox.title = l10n.t('Microsoft Authentication'); + inputBox.prompt = l10n.t('Provide the authorization code to complete the sign in flow.'); + inputBox.placeholder = l10n.t('Paste authorization code here...'); + inputBox.show(); + const code = await new Promise((resolve) => { + let resolvedValue: string | undefined = undefined; + const disposable = Disposable.from( + inputBox, + inputBox.onDidAccept(async () => { + if (!inputBox.value) { + inputBox.validationMessage = l10n.t('Authorization code is required.'); + return; + } + const code = inputBox.value; + resolvedValue = code; + resolve(code); + inputBox.hide(); + }), + inputBox.onDidChangeValue(() => { + inputBox.validationMessage = undefined; + }), + inputBox.onDidHide(() => { + disposable.dispose(); + if (!resolvedValue) { + resolve(undefined); + } + }) + ); + Promise.allSettled([this._responseDeferred?.p]).then(() => disposable.dispose()); + }); + // Something canceled the original deferred promise, so just return. + if (this._responseDeferred.isSettled) { + return; + } + if (code) { + this._logger.debug('Received auth code from quick pick'); + this._responseDeferred.complete({ + code, + state: undefined, + error: undefined, + error_description: undefined, + error_uri: undefined + }); + return; + } + this._responseDeferred.complete({ + code: undefined, + state: undefined, + error: 'User cancelled', + error_description: 'User cancelled', + error_uri: undefined + }); + } } From d487556d4d46366cba4684ca933df6b9d6824201 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Sat, 1 Feb 2025 01:00:37 -0800 Subject: [PATCH 1105/3587] prompt files config description update (#239279) * [config]: improve setting description and add examples * [config]: fix localization ID compilation issue * [config]: update the `Examples` header level to h4 --- .../contrib/chat/browser/chat.contribution.ts | 11 ++--- .../chat/common/promptSyntax/config.ts | 45 ++++++++++++++++--- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index cf1d9b2ebf81..269b6536eab8 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -154,14 +154,9 @@ configurationRegistry.registerConfiguration({ default: true }, [PromptFilesConfig.CONFIG_KEY]: { - type: ['string', 'array', 'object', 'boolean', 'null'], - title: nls.localize('chat.promptFiles.setting.title', "Prompt Files"), - markdownDescription: nls.localize( - 'chat.promptFiles.setting.markdownDescription', - "Enable support for attaching reusable prompt files (`*{0}`) for Chat, Edits, and Inline Chat sessions. [Learn More]({1}).", - '.prompt.md', - PromptFilesConfig.DOCUMENTATION_URL, - ), + type: ['boolean', 'object', 'array', 'string', 'null'], + title: PromptFilesConfig.CONFIG_TITLE, + markdownDescription: PromptFilesConfig.CONFIG_DESCRIPTION, default: null, tags: ['experimental'], }, diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts index 1fa52b8fea9b..51eb7333f07a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from '../../../../../nls.js'; +import { PROMPT_SNIPPET_FILE_EXTENSION } from './contentProviders/promptContentsProviderBase.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; /** @@ -21,21 +23,21 @@ import { IConfigurationService } from '../../../../../platform/configuration/com * (see {@link DEFAULT_LOCATION}): * ```json * { - * "chat.experimental.promptSnippets": true, + * "chat.promptFiles": true, * } * ``` * * Enable the feature, specifying a single prompt files source folder location: * ```json * { - * "chat.experimental.promptSnippets": '.github/prompts', + * "chat.promptFiles": '.github/prompts', * } * ``` * * Enable the feature, specifying multiple prompt files source folder location: * ```json * { - * "chat.experimental.promptSnippets": { + * "chat.promptFiles": { * ".github/prompts" : true, * ".copilot/prompts" : false, * "/Users/legomushroom/repos/prompts" : true, @@ -46,7 +48,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com * Enable the feature, specifying multiple prompt files source folder location: * ```json * { - * "chat.experimental.promptSnippets": [ + * "chat.promptFiles": [ * ".github/prompts", * ".copilot/prompts", * "/Users/legomushroom/repos/prompts", @@ -64,7 +66,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com * (see {@link DEFAULT_LOCATION}): * ```json * { - * "chat.experimental.promptSnippets": {}, + * "chat.promptFiles": {}, * } * ``` * @@ -213,6 +215,39 @@ export namespace PromptFilesConfig { return DEFAULT_LOCATION; }; + + const usageExample1 = nls.localize( + `chat.promptFiles.config.description.example1`, + "Enable with the default location of the prompt files (`{0}`):\n{1}", + DEFAULT_LOCATION[0], + `\`\`\`json\n{\n "${CONFIG_KEY}": true,\n}\n\`\`\``, + ); + const usageExample2 = nls.localize( + `chat.promptFiles.config.description.example2`, + "Specify custom location(s) of the prompt files:\n{0}", + `\`\`\`json\n{\n "${CONFIG_KEY}": {\n ".github/prompts": true,\n "/Users/vscode/prompts": true,\n}\n\`\`\``, + ); + + /** + * Configuration setting description to use in the settings UI. + */ + export const CONFIG_DESCRIPTION = nls.localize( + 'chat.promptFiles.config.description', + "Enable support for attaching reusable prompt files (`*{0}`) for Chat, Edits, and Inline Chat sessions. [Learn More]({1}).\n\nSet to `true` or use the `{ \"/path/to/folder\": boolean }` notation to specify a different path (or a couple of them). Relative paths are resolved from the root folder(s) of your workspace, and the default value of `{2}` is used if no other paths provided.\n#### Examples\n{3}\n{4}", + PROMPT_SNIPPET_FILE_EXTENSION, + DOCUMENTATION_URL, + DEFAULT_LOCATION[0], + usageExample1, + usageExample2, + ); + + /** + * Configuration setting title to use in the settings UI. + */ + export const CONFIG_TITLE = nls.localize( + `chat.promptFiles.config.title`, + "Prompt Files", + ); } /** From 9f753c77d9d3e49a2d367f965999282f3519b87f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 04:42:12 -0800 Subject: [PATCH 1106/3587] Simplify terminal suggest token variable Follow up on #239344 --- extensions/terminal-suggest/src/tokens.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extensions/terminal-suggest/src/tokens.ts b/extensions/terminal-suggest/src/tokens.ts index 9712d2b0bfee..a967aeb20545 100644 --- a/extensions/terminal-suggest/src/tokens.ts +++ b/extensions/terminal-suggest/src/tokens.ts @@ -23,8 +23,7 @@ export function getTokenType(ctx: { commandLine: string; cursorPosition: number if (spaceIndex === -1) { return TokenType.Command; } - const tokenIndex = spaceIndex === -1 ? 0 : spaceIndex + 1; - const previousTokens = ctx.commandLine.substring(0, tokenIndex).trim(); + const previousTokens = ctx.commandLine.substring(0, spaceIndex + 1).trim(); const commandResetChars = shellType === undefined ? defaultShellTypeResetChars : shellTypeResetChars[shellType] ?? defaultShellTypeResetChars; if (commandResetChars.some(e => previousTokens.endsWith(e))) { return TokenType.Command; From fafd6b594a59f86ce8555547cdf422e324b66d12 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 05:17:57 -0800 Subject: [PATCH 1107/3587] Optimize getCommandsInPath, restore cache Changes: - Fix cache mechanism as pathValue never got stored - Cache labels instead of returning empty set - Walking each directory in separate promise and await via Promise.all - Don't create a promise on Windows when checking exe Fixes #239396 --- .../src/helpers/executable.ts | 6 +- .../src/terminalSuggestMain.ts | 61 +++++++++++++------ 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/extensions/terminal-suggest/src/helpers/executable.ts b/extensions/terminal-suggest/src/helpers/executable.ts index cd8fce4f8dc8..00f56f09cbda 100644 --- a/extensions/terminal-suggest/src/helpers/executable.ts +++ b/extensions/terminal-suggest/src/helpers/executable.ts @@ -6,11 +6,15 @@ import { osIsWindows } from './os'; import * as fs from 'fs/promises'; -export async function isExecutable(filePath: string, configuredWindowsExecutableExtensions?: { [key: string]: boolean | undefined } | undefined): Promise { +export function isExecutable(filePath: string, configuredWindowsExecutableExtensions?: { [key: string]: boolean | undefined } | undefined): Promise | boolean { if (osIsWindows()) { const resolvedWindowsExecutableExtensions = resolveWindowsExecutableExtensions(configuredWindowsExecutableExtensions); return resolvedWindowsExecutableExtensions.find(ext => filePath.endsWith(ext)) !== undefined; } + return isExecutableUnix(filePath); +} + +export async function isExecutableUnix(filePath: string): Promise { try { const stats = await fs.stat(filePath); // On macOS/Linux, check if the executable bit is set diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 82c00c2196a1..8d278d1c002d 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -41,6 +41,7 @@ let cachedAvailableCommandsPath: string | undefined; let cachedWindowsExecutableExtensions: { [key: string]: boolean | undefined } | undefined; const cachedWindowsExecutableExtensionsSettingId = 'terminal.integrated.suggest.windowsExecutableExtensions'; let cachedAvailableCommands: Set | undefined; +let cachedAvailableCommandsLabels: Set | undefined; const cachedBuiltinCommands: Map = new Map(); export const availableSpecs: Fig.Spec[] = [ @@ -211,7 +212,7 @@ function createCompletionItem(cursorPosition: number, prefix: string, commandRes async function getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { - const labels: Set = new Set(); + // Create cache key let pathValue: string | undefined; if (isWindows) { const caseSensitivePathKey = Object.keys(env).find(key => key.toLowerCase() === 'path'); @@ -227,37 +228,59 @@ async function getCommandsInPath(env: { [key: string]: string | undefined } = pr // Check cache if (cachedAvailableCommands && cachedAvailableCommandsPath === pathValue) { - return { completionResources: cachedAvailableCommands, labels }; + return { completionResources: cachedAvailableCommands, labels: cachedAvailableCommandsLabels }; } // Extract executables from PATH const paths = pathValue.split(isWindows ? ';' : ':'); const pathSeparator = isWindows ? '\\' : '/'; - const executables = new Set(); + const promises: Promise | undefined>[] = []; + const labels: Set = new Set(); for (const path of paths) { - try { - const dirExists = await fs.stat(path).then(stat => stat.isDirectory()).catch(() => false); - if (!dirExists) { - continue; - } - const fileResource = vscode.Uri.file(path); - const files = await vscode.workspace.fs.readDirectory(fileResource); - for (const [file, fileType] of files) { - const formattedPath = getFriendlyResourcePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); - if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath, cachedWindowsExecutableExtensions)) { - executables.add({ label: file, detail: formattedPath }); - labels.add(file); - } + promises.push(getFilesInPath(path, pathSeparator, labels)); + } + + // Merge all results + const executables = new Set(); + const resultSets = await Promise.all(promises); + for (const resultSet of resultSets) { + if (resultSet) { + for (const executable of resultSet) { + executables.add(executable); } - } catch (e) { - // Ignore errors for directories that can't be read - continue; } } + + // Return cachedAvailableCommands = executables; + cachedAvailableCommandsLabels = labels; + cachedAvailableCommandsPath = pathValue; return { completionResources: executables, labels }; } +async function getFilesInPath(path: string, pathSeparator: string, labels: Set): Promise | undefined> { + try { + const dirExists = await fs.stat(path).then(stat => stat.isDirectory()).catch(() => false); + if (!dirExists) { + return undefined; + } + const result = new Set(); + const fileResource = vscode.Uri.file(path); + const files = await vscode.workspace.fs.readDirectory(fileResource); + for (const [file, fileType] of files) { + const formattedPath = getFriendlyResourcePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); + if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath, cachedWindowsExecutableExtensions)) { + result.add({ label: file, detail: formattedPath }); + labels.add(file); + } + } + return result; + } catch (e) { + // Ignore errors for directories that can't be read + return undefined; + } +} + function getPrefix(commandLine: string, cursorPosition: number): string { // Return an empty string if the command line is empty after trimming if (commandLine.trim() === '') { From 7a9f62939d601473c38f0d3d9529663df9597198 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 05:45:21 -0800 Subject: [PATCH 1108/3587] Move path exe logic into own file --- .../src/env/pathExecutables.ts | 101 ++++++++++++++++ .../terminal-suggest/src/helpers/uri.ts | 20 ++++ .../src/terminalSuggestMain.ts | 111 +----------------- 3 files changed, 127 insertions(+), 105 deletions(-) create mode 100644 extensions/terminal-suggest/src/env/pathExecutables.ts create mode 100644 extensions/terminal-suggest/src/helpers/uri.ts diff --git a/extensions/terminal-suggest/src/env/pathExecutables.ts b/extensions/terminal-suggest/src/env/pathExecutables.ts new file mode 100644 index 000000000000..98b62d345ea9 --- /dev/null +++ b/extensions/terminal-suggest/src/env/pathExecutables.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs/promises'; +import * as vscode from 'vscode'; +import { isExecutable } from '../helpers/executable'; +import { osIsWindows } from '../helpers/os'; +import type { ICompletionResource } from '../types'; +import { getFriendlyResourcePath } from '../helpers/uri'; + +const isWindows = osIsWindows(); +let cachedAvailableCommandsPath: string | undefined; +let cachedWindowsExecutableExtensions: { [key: string]: boolean | undefined } | undefined; +const cachedWindowsExecutableExtensionsSettingId = 'terminal.integrated.suggest.windowsExecutableExtensions'; +let cachedAvailableCommands: Set | undefined; +let cachedAvailableCommandsLabels: Set | undefined; + +export function activatePathExecutables(context: vscode.ExtensionContext) { + if (isWindows) { + cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration('terminal.integrated.suggest').get('windowsExecutableExtensions'); + context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(cachedWindowsExecutableExtensionsSettingId)) { + cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration('terminal.integrated.suggest').get('windowsExecutableExtensions'); + cachedAvailableCommands = undefined; + cachedAvailableCommandsPath = undefined; + } + })); + } +} + +export async function getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { + // Create cache key + let pathValue: string | undefined; + if (isWindows) { + const caseSensitivePathKey = Object.keys(env).find(key => key.toLowerCase() === 'path'); + if (caseSensitivePathKey) { + pathValue = env[caseSensitivePathKey]; + } + } else { + pathValue = env.PATH; + } + if (pathValue === undefined) { + return; + } + + // Check cache + if (cachedAvailableCommands && cachedAvailableCommandsPath === pathValue) { + return { completionResources: cachedAvailableCommands, labels: cachedAvailableCommandsLabels }; + } + + // Extract executables from PATH + const paths = pathValue.split(isWindows ? ';' : ':'); + const pathSeparator = isWindows ? '\\' : '/'; + const promises: Promise | undefined>[] = []; + const labels: Set = new Set(); + for (const path of paths) { + promises.push(getFilesInPath(path, pathSeparator, labels)); + } + + // Merge all results + const executables = new Set(); + const resultSets = await Promise.all(promises); + for (const resultSet of resultSets) { + if (resultSet) { + for (const executable of resultSet) { + executables.add(executable); + } + } + } + + // Return + cachedAvailableCommands = executables; + cachedAvailableCommandsLabels = labels; + cachedAvailableCommandsPath = pathValue; + return { completionResources: executables, labels }; +} + +async function getFilesInPath(path: string, pathSeparator: string, labels: Set): Promise | undefined> { + try { + const dirExists = await fs.stat(path).then(stat => stat.isDirectory()).catch(() => false); + if (!dirExists) { + return undefined; + } + const result = new Set(); + const fileResource = vscode.Uri.file(path); + const files = await vscode.workspace.fs.readDirectory(fileResource); + for (const [file, fileType] of files) { + const formattedPath = getFriendlyResourcePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); + if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath, cachedWindowsExecutableExtensions)) { + result.add({ label: file, detail: formattedPath }); + labels.add(file); + } + } + return result; + } catch (e) { + // Ignore errors for directories that can't be read + return undefined; + } +} diff --git a/extensions/terminal-suggest/src/helpers/uri.ts b/extensions/terminal-suggest/src/helpers/uri.ts new file mode 100644 index 000000000000..90e09dbc1c29 --- /dev/null +++ b/extensions/terminal-suggest/src/helpers/uri.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export function getFriendlyResourcePath(uri: vscode.Uri, pathSeparator: string, kind?: vscode.TerminalCompletionItemKind): string { + let path = uri.fsPath; + // Ensure drive is capitalized on Windows + if (pathSeparator === '\\' && path.match(/^[a-zA-Z]:\\/)) { + path = `${path[0].toUpperCase()}:${path.slice(2)}`; + } + if (kind === vscode.TerminalCompletionItemKind.Folder) { + if (!path.endsWith(pathSeparator)) { + path += pathSeparator; + } + } + return path; +} diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 8d278d1c002d..e3f85e5d1326 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -11,13 +11,14 @@ import codeCompletionSpec from './completions/code'; import cdSpec from './completions/cd'; import codeInsidersCompletionSpec from './completions/code-insiders'; import { osIsWindows } from './helpers/os'; -import { isExecutable } from './helpers/executable'; import type { ICompletionResource } from './types'; import { getBashGlobals } from './shell/bash'; import { getZshGlobals } from './shell/zsh'; import { getFishGlobals } from './shell/fish'; import { getPwshGlobals } from './shell/pwsh'; import { getTokenType, TokenType } from './tokens'; +import { activatePathExecutables, getCommandsInPath } from './env/pathExecutables'; +import { getFriendlyResourcePath } from './helpers/uri'; // TODO: remove once API is finalized export const enum TerminalShellType { @@ -37,12 +38,7 @@ export const enum TerminalShellType { } const isWindows = osIsWindows(); -let cachedAvailableCommandsPath: string | undefined; -let cachedWindowsExecutableExtensions: { [key: string]: boolean | undefined } | undefined; -const cachedWindowsExecutableExtensionsSettingId = 'terminal.integrated.suggest.windowsExecutableExtensions'; -let cachedAvailableCommands: Set | undefined; -let cachedAvailableCommandsLabels: Set | undefined; -const cachedBuiltinCommands: Map = new Map(); +const cachedGlobals: Map = new Map(); export const availableSpecs: Fig.Spec[] = [ cdSpec, @@ -63,7 +59,7 @@ const getShellSpecificGlobals: Map): Promise { try { - const cachedCommands = cachedBuiltinCommands.get(shellType); + const cachedCommands = cachedGlobals.get(shellType); if (cachedCommands) { return cachedCommands; } @@ -74,7 +70,7 @@ async function getShellGlobals(shellType: TerminalShellType, existingCommands?: const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell }; const mixedCommands: (string | ICompletionResource)[] | undefined = await getShellSpecificGlobals.get(shellType)?.(options, existingCommands); const normalizedCommands = mixedCommands?.map(command => typeof command === 'string' ? ({ label: command }) : command); - cachedBuiltinCommands.set(shellType, normalizedCommands); + cachedGlobals.set(shellType, normalizedCommands); return normalizedCommands; } catch (error) { @@ -121,17 +117,7 @@ export async function activate(context: vscode.ExtensionContext) { return result.items; } }, '/', '\\')); - - if (isWindows) { - cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration('terminal.integrated.suggest').get('windowsExecutableExtensions'); - context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(cachedWindowsExecutableExtensionsSettingId)) { - cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration('terminal.integrated.suggest').get('windowsExecutableExtensions'); - cachedAvailableCommands = undefined; - cachedAvailableCommandsPath = undefined; - } - })); - } + activatePathExecutables(context); } /** @@ -210,77 +196,6 @@ function createCompletionItem(cursorPosition: number, prefix: string, commandRes }; } - -async function getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { - // Create cache key - let pathValue: string | undefined; - if (isWindows) { - const caseSensitivePathKey = Object.keys(env).find(key => key.toLowerCase() === 'path'); - if (caseSensitivePathKey) { - pathValue = env[caseSensitivePathKey]; - } - } else { - pathValue = env.PATH; - } - if (pathValue === undefined) { - return; - } - - // Check cache - if (cachedAvailableCommands && cachedAvailableCommandsPath === pathValue) { - return { completionResources: cachedAvailableCommands, labels: cachedAvailableCommandsLabels }; - } - - // Extract executables from PATH - const paths = pathValue.split(isWindows ? ';' : ':'); - const pathSeparator = isWindows ? '\\' : '/'; - const promises: Promise | undefined>[] = []; - const labels: Set = new Set(); - for (const path of paths) { - promises.push(getFilesInPath(path, pathSeparator, labels)); - } - - // Merge all results - const executables = new Set(); - const resultSets = await Promise.all(promises); - for (const resultSet of resultSets) { - if (resultSet) { - for (const executable of resultSet) { - executables.add(executable); - } - } - } - - // Return - cachedAvailableCommands = executables; - cachedAvailableCommandsLabels = labels; - cachedAvailableCommandsPath = pathValue; - return { completionResources: executables, labels }; -} - -async function getFilesInPath(path: string, pathSeparator: string, labels: Set): Promise | undefined> { - try { - const dirExists = await fs.stat(path).then(stat => stat.isDirectory()).catch(() => false); - if (!dirExists) { - return undefined; - } - const result = new Set(); - const fileResource = vscode.Uri.file(path); - const files = await vscode.workspace.fs.readDirectory(fileResource); - for (const [file, fileType] of files) { - const formattedPath = getFriendlyResourcePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); - if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath, cachedWindowsExecutableExtensions)) { - result.add({ label: file, detail: formattedPath }); - labels.add(file); - } - } - return result; - } catch (e) { - // Ignore errors for directories that can't be read - return undefined; - } -} - function getPrefix(commandLine: string, cursorPosition: number): string { // Return an empty string if the command line is empty after trimming if (commandLine.trim() === '') { @@ -519,20 +434,6 @@ function getCompletionItemsFromArgs(args: Fig.SingleOrArray | undefined return { items, filesRequested, foldersRequested }; } -function getFriendlyResourcePath(uri: vscode.Uri, pathSeparator: string, kind?: vscode.TerminalCompletionItemKind): string { - let path = uri.fsPath; - // Ensure drive is capitalized on Windows - if (pathSeparator === '\\' && path.match(/^[a-zA-Z]:\\/)) { - path = `${path[0].toUpperCase()}:${path.slice(2)}`; - } - if (kind === vscode.TerminalCompletionItemKind.Folder) { - if (!path.endsWith(pathSeparator)) { - path += pathSeparator; - } - } - return path; -} - function getShell(shellType: TerminalShellType): string | undefined { switch (shellType) { case TerminalShellType.Bash: From 9131925b74191d5883f991483924ffc1daac5dd6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 05:56:14 -0800 Subject: [PATCH 1109/3587] Move path exe logic into class --- extensions/terminal-suggest/src/constants.ts | 7 ++ .../src/env/pathExecutableCache.ts | 112 ++++++++++++++++++ .../src/env/pathExecutables.ts | 101 ---------------- .../src/terminalSuggestMain.ts | 9 +- 4 files changed, 125 insertions(+), 104 deletions(-) create mode 100644 extensions/terminal-suggest/src/env/pathExecutableCache.ts delete mode 100644 extensions/terminal-suggest/src/env/pathExecutables.ts diff --git a/extensions/terminal-suggest/src/constants.ts b/extensions/terminal-suggest/src/constants.ts index 37d08189da65..f4ebb9e055d5 100644 --- a/extensions/terminal-suggest/src/constants.ts +++ b/extensions/terminal-suggest/src/constants.ts @@ -11,3 +11,10 @@ export const upstreamSpecs = [ 'rmdir', 'touch', ]; + + +export const enum SettingsIds { + SuggestPrefix = 'terminal.integrated.suggest', + CachedWindowsExecutableExtensions = 'terminal.integrated.suggest.windowsExecutableExtensions', + CachedWindowsExecutableExtensionsSuffixOnly = 'windowsExecutableExtensions', +} diff --git a/extensions/terminal-suggest/src/env/pathExecutableCache.ts b/extensions/terminal-suggest/src/env/pathExecutableCache.ts new file mode 100644 index 000000000000..07ad75b1d5bc --- /dev/null +++ b/extensions/terminal-suggest/src/env/pathExecutableCache.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs/promises'; +import * as vscode from 'vscode'; +import { isExecutable } from '../helpers/executable'; +import { osIsWindows } from '../helpers/os'; +import type { ICompletionResource } from '../types'; +import { getFriendlyResourcePath } from '../helpers/uri'; +import { SettingsIds } from '../constants'; + +const isWindows = osIsWindows(); + +export class PathExecutableCache implements vscode.Disposable { + private _disposables: vscode.Disposable[] = []; + + private _cachedAvailableCommandsPath: string | undefined; + private _cachedWindowsExecutableExtensions: { [key: string]: boolean | undefined } | undefined; + private _cachedAvailableCommands: Set | undefined; + private _cachedAvailableCommandsLabels: Set | undefined; + + constructor() { + if (isWindows) { + this._cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration(SettingsIds.SuggestPrefix).get(SettingsIds.CachedWindowsExecutableExtensionsSuffixOnly); + this._disposables.push(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(SettingsIds.CachedWindowsExecutableExtensions)) { + this._cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration(SettingsIds.SuggestPrefix).get(SettingsIds.CachedWindowsExecutableExtensionsSuffixOnly); + this._cachedAvailableCommands = undefined; + this._cachedAvailableCommandsPath = undefined; + } + })); + } + } + + dispose() { + for (const d of this._disposables) { + d.dispose(); + } + } + + async getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { + // Create cache key + let pathValue: string | undefined; + if (isWindows) { + const caseSensitivePathKey = Object.keys(env).find(key => key.toLowerCase() === 'path'); + if (caseSensitivePathKey) { + pathValue = env[caseSensitivePathKey]; + } + } else { + pathValue = env.PATH; + } + if (pathValue === undefined) { + return; + } + + // Check cache + if (this._cachedAvailableCommands && this._cachedAvailableCommandsPath === pathValue) { + return { completionResources: this._cachedAvailableCommands, labels: this._cachedAvailableCommandsLabels }; + } + + // Extract executables from PATH + const paths = pathValue.split(isWindows ? ';' : ':'); + const pathSeparator = isWindows ? '\\' : '/'; + const promises: Promise | undefined>[] = []; + const labels: Set = new Set(); + for (const path of paths) { + promises.push(this._getFilesInPath(path, pathSeparator, labels)); + } + + // Merge all results + const executables = new Set(); + const resultSets = await Promise.all(promises); + for (const resultSet of resultSets) { + if (resultSet) { + for (const executable of resultSet) { + executables.add(executable); + } + } + } + + // Return + this._cachedAvailableCommands = executables; + this._cachedAvailableCommandsLabels = labels; + this._cachedAvailableCommandsPath = pathValue; + return { completionResources: executables, labels }; + } + + private async _getFilesInPath(path: string, pathSeparator: string, labels: Set): Promise | undefined> { + try { + const dirExists = await fs.stat(path).then(stat => stat.isDirectory()).catch(() => false); + if (!dirExists) { + return undefined; + } + const result = new Set(); + const fileResource = vscode.Uri.file(path); + const files = await vscode.workspace.fs.readDirectory(fileResource); + for (const [file, fileType] of files) { + const formattedPath = getFriendlyResourcePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); + if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath, this._cachedWindowsExecutableExtensions)) { + result.add({ label: file, detail: formattedPath }); + labels.add(file); + } + } + return result; + } catch (e) { + // Ignore errors for directories that can't be read + return undefined; + } + } +} diff --git a/extensions/terminal-suggest/src/env/pathExecutables.ts b/extensions/terminal-suggest/src/env/pathExecutables.ts deleted file mode 100644 index 98b62d345ea9..000000000000 --- a/extensions/terminal-suggest/src/env/pathExecutables.ts +++ /dev/null @@ -1,101 +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 fs from 'fs/promises'; -import * as vscode from 'vscode'; -import { isExecutable } from '../helpers/executable'; -import { osIsWindows } from '../helpers/os'; -import type { ICompletionResource } from '../types'; -import { getFriendlyResourcePath } from '../helpers/uri'; - -const isWindows = osIsWindows(); -let cachedAvailableCommandsPath: string | undefined; -let cachedWindowsExecutableExtensions: { [key: string]: boolean | undefined } | undefined; -const cachedWindowsExecutableExtensionsSettingId = 'terminal.integrated.suggest.windowsExecutableExtensions'; -let cachedAvailableCommands: Set | undefined; -let cachedAvailableCommandsLabels: Set | undefined; - -export function activatePathExecutables(context: vscode.ExtensionContext) { - if (isWindows) { - cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration('terminal.integrated.suggest').get('windowsExecutableExtensions'); - context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(cachedWindowsExecutableExtensionsSettingId)) { - cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration('terminal.integrated.suggest').get('windowsExecutableExtensions'); - cachedAvailableCommands = undefined; - cachedAvailableCommandsPath = undefined; - } - })); - } -} - -export async function getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { - // Create cache key - let pathValue: string | undefined; - if (isWindows) { - const caseSensitivePathKey = Object.keys(env).find(key => key.toLowerCase() === 'path'); - if (caseSensitivePathKey) { - pathValue = env[caseSensitivePathKey]; - } - } else { - pathValue = env.PATH; - } - if (pathValue === undefined) { - return; - } - - // Check cache - if (cachedAvailableCommands && cachedAvailableCommandsPath === pathValue) { - return { completionResources: cachedAvailableCommands, labels: cachedAvailableCommandsLabels }; - } - - // Extract executables from PATH - const paths = pathValue.split(isWindows ? ';' : ':'); - const pathSeparator = isWindows ? '\\' : '/'; - const promises: Promise | undefined>[] = []; - const labels: Set = new Set(); - for (const path of paths) { - promises.push(getFilesInPath(path, pathSeparator, labels)); - } - - // Merge all results - const executables = new Set(); - const resultSets = await Promise.all(promises); - for (const resultSet of resultSets) { - if (resultSet) { - for (const executable of resultSet) { - executables.add(executable); - } - } - } - - // Return - cachedAvailableCommands = executables; - cachedAvailableCommandsLabels = labels; - cachedAvailableCommandsPath = pathValue; - return { completionResources: executables, labels }; -} - -async function getFilesInPath(path: string, pathSeparator: string, labels: Set): Promise | undefined> { - try { - const dirExists = await fs.stat(path).then(stat => stat.isDirectory()).catch(() => false); - if (!dirExists) { - return undefined; - } - const result = new Set(); - const fileResource = vscode.Uri.file(path); - const files = await vscode.workspace.fs.readDirectory(fileResource); - for (const [file, fileType] of files) { - const formattedPath = getFriendlyResourcePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); - if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath, cachedWindowsExecutableExtensions)) { - result.add({ label: file, detail: formattedPath }); - labels.add(file); - } - } - return result; - } catch (e) { - // Ignore errors for directories that can't be read - return undefined; - } -} diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index e3f85e5d1326..808e26a9c6b7 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -17,7 +17,7 @@ import { getZshGlobals } from './shell/zsh'; import { getFishGlobals } from './shell/fish'; import { getPwshGlobals } from './shell/pwsh'; import { getTokenType, TokenType } from './tokens'; -import { activatePathExecutables, getCommandsInPath } from './env/pathExecutables'; +import { PathExecutableCache } from './env/pathExecutableCache'; import { getFriendlyResourcePath } from './helpers/uri'; // TODO: remove once API is finalized @@ -39,6 +39,7 @@ export const enum TerminalShellType { const isWindows = osIsWindows(); const cachedGlobals: Map = new Map(); +let pathExecutableCache: PathExecutableCache; export const availableSpecs: Fig.Spec[] = [ cdSpec, @@ -80,6 +81,9 @@ async function getShellGlobals(shellType: TerminalShellType, existingCommands?: } export async function activate(context: vscode.ExtensionContext) { + pathExecutableCache = new PathExecutableCache(); + context.subscriptions.push(pathExecutableCache); + context.subscriptions.push(vscode.window.registerTerminalCompletionProvider({ id: 'terminal-suggest', async provideTerminalCompletions(terminal: vscode.Terminal, terminalContext: { commandLine: string; cursorPosition: number }, token: vscode.CancellationToken): Promise { @@ -92,7 +96,7 @@ export async function activate(context: vscode.ExtensionContext) { return; } - const commandsInPath = await getCommandsInPath(terminal.shellIntegration?.env); + const commandsInPath = await pathExecutableCache.getCommandsInPath(terminal.shellIntegration?.env); const shellGlobals = await getShellGlobals(shellType, commandsInPath?.labels) ?? []; if (!commandsInPath?.completionResources) { return; @@ -117,7 +121,6 @@ export async function activate(context: vscode.ExtensionContext) { return result.items; } }, '/', '\\')); - activatePathExecutables(context); } /** From af8504c3ac7f895e1b085e3bdbb9946f0be5cd33 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 06:04:08 -0800 Subject: [PATCH 1110/3587] Add test to prevent future simple cache breakages --- .../src/env/pathExecutableCache.ts | 15 +++++------ .../src/test/env/pathExecutableCache.test.ts | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 extensions/terminal-suggest/src/test/env/pathExecutableCache.test.ts diff --git a/extensions/terminal-suggest/src/env/pathExecutableCache.ts b/extensions/terminal-suggest/src/env/pathExecutableCache.ts index 07ad75b1d5bc..15f1bbcb694e 100644 --- a/extensions/terminal-suggest/src/env/pathExecutableCache.ts +++ b/extensions/terminal-suggest/src/env/pathExecutableCache.ts @@ -18,8 +18,7 @@ export class PathExecutableCache implements vscode.Disposable { private _cachedAvailableCommandsPath: string | undefined; private _cachedWindowsExecutableExtensions: { [key: string]: boolean | undefined } | undefined; - private _cachedAvailableCommands: Set | undefined; - private _cachedAvailableCommandsLabels: Set | undefined; + private _cachedCommandsInPath: { completionResources: Set | undefined; labels: Set | undefined } | undefined; constructor() { if (isWindows) { @@ -27,8 +26,7 @@ export class PathExecutableCache implements vscode.Disposable { this._disposables.push(vscode.workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration(SettingsIds.CachedWindowsExecutableExtensions)) { this._cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration(SettingsIds.SuggestPrefix).get(SettingsIds.CachedWindowsExecutableExtensionsSuffixOnly); - this._cachedAvailableCommands = undefined; - this._cachedAvailableCommandsPath = undefined; + this._cachedCommandsInPath = undefined; } })); } @@ -56,8 +54,8 @@ export class PathExecutableCache implements vscode.Disposable { } // Check cache - if (this._cachedAvailableCommands && this._cachedAvailableCommandsPath === pathValue) { - return { completionResources: this._cachedAvailableCommands, labels: this._cachedAvailableCommandsLabels }; + if (this._cachedCommandsInPath && this._cachedAvailableCommandsPath === pathValue) { + return this._cachedCommandsInPath; } // Extract executables from PATH @@ -81,10 +79,9 @@ export class PathExecutableCache implements vscode.Disposable { } // Return - this._cachedAvailableCommands = executables; - this._cachedAvailableCommandsLabels = labels; this._cachedAvailableCommandsPath = pathValue; - return { completionResources: executables, labels }; + this._cachedCommandsInPath = { completionResources: executables, labels }; + return this._cachedCommandsInPath; } private async _getFilesInPath(path: string, pathSeparator: string, labels: Set): Promise | undefined> { diff --git a/extensions/terminal-suggest/src/test/env/pathExecutableCache.test.ts b/extensions/terminal-suggest/src/test/env/pathExecutableCache.test.ts new file mode 100644 index 000000000000..4ba5451eafc2 --- /dev/null +++ b/extensions/terminal-suggest/src/test/env/pathExecutableCache.test.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import { strictEqual } from 'node:assert'; +import { PathExecutableCache } from '../../env/pathExecutableCache'; + +suite('PathExecutableCache', () => { + test('cache should return empty for empty PATH', async () => { + const cache = new PathExecutableCache(); + const result = await cache.getCommandsInPath({ PATH: '' }); + strictEqual(Array.from(result!.completionResources!).length, 0); + strictEqual(Array.from(result!.labels!).length, 0); + }); + + test('caching is working on successive calls', async () => { + const cache = new PathExecutableCache(); + const env = { PATH: process.env.PATH }; + const result = await cache.getCommandsInPath(env); + const result2 = await cache.getCommandsInPath(env); + strictEqual(result, result2); + }); +}); From 276c0edf9157b9bd08c925a07eb6e31da13f0b43 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 06:14:23 -0800 Subject: [PATCH 1111/3587] Only invalidate terminal suggestions when whitespace is encountered Fixes #239017 --- .../suggest/browser/terminalSuggestAddon.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 1af621de8cf1..9c70271e2ad4 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -322,12 +322,15 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest return; } - // Hide the widget if the cursor moves to the left of the initial position as the - // completions are no longer valid - // to do: get replacement length to be correct, readd this? - if (this._currentPromptInputState && this._currentPromptInputState.cursorIndex <= this._leadingLineContent.length) { - this.hideSuggestWidget(); - return; + // Hide the widget if the cursor moves to the left and invalidates the completions. + // Originally this was to the left of the initial position that the completions were + // requested, but since extensions are expected to allow the client-side to filter, they are + // only invalidated when whitespace is encountered. + if (this._currentPromptInputState && this._currentPromptInputState.cursorIndex < this._leadingLineContent.length) { + if (this._currentPromptInputState.cursorIndex === 0 || this._leadingLineContent[this._currentPromptInputState.cursorIndex - 1].match(/\s/)) { + this.hideSuggestWidget(); + return; + } } if (this._terminalSuggestWidgetVisibleContextKey.get()) { From 863147b23b72c02d5993ae421f2a0ba726f952d7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 06:49:16 -0800 Subject: [PATCH 1112/3587] Penalize files/dirs starting with _, otherwise ignore punctuation Fixes #238085 --- .../suggest/browser/simpleCompletionItem.ts | 4 ++++ .../suggest/browser/simpleCompletionModel.ts | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts index a9b69068baab..5bfb52723431 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts @@ -5,6 +5,7 @@ import { FuzzyScore } from '../../../../base/common/filters.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { basename } from '../../../../base/common/path.js'; import { isWindows } from '../../../../base/common/platform.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; @@ -73,6 +74,8 @@ export class SimpleCompletionItem { */ readonly labelLowNormalizedPath: string; + readonly unscorePenalty: number = 0; + /** * The file extension part from {@link labelLow}. */ @@ -109,6 +112,7 @@ export class SimpleCompletionItem { if (completion.isDirectory) { this.labelLowNormalizedPath = this.labelLowNormalizedPath.replace(/\/$/, ''); } + this.unscorePenalty = basename(this.labelLowNormalizedPath).startsWith('_') ? 1 : 0; } } } diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts index 7cc99b0936e1..4e59fc802e89 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts @@ -188,11 +188,13 @@ export class SimpleCompletionModel { return score; } } + // Sort by the score score = b.score[0] - a.score[0]; if (score !== 0) { return score; } + // Sort files with the same score against each other specially const isArg = leadingLineContent.includes(' '); if (!isArg && a.fileExtLow.length > 0 && b.fileExtLow.length > 0) { @@ -212,6 +214,13 @@ export class SimpleCompletionModel { return score; } } + + // Sort by unscore penalty (eg. `__init__/` should be penalized) + if (a.unscorePenalty !== b.unscorePenalty) { + return a.unscorePenalty - b.unscorePenalty; + } + + // Sort by folder depth (eg. `vscode/` should come before `vscode-.../`) if (a.labelLowNormalizedPath && b.labelLowNormalizedPath) { // Directories // Count depth of path (number of / or \ occurrences) @@ -228,8 +237,10 @@ export class SimpleCompletionModel { return 1; // `b` is a prefix of `a`, so `b` should come first } } - // Sort alphabetically - return a.labelLow.localeCompare(b.labelLow); + + // Sort alphabetically, ignoring punctuation causes dot files to be mixed in rather than + // all at the top + return a.labelLow.localeCompare(b.labelLow, undefined, { ignorePunctuation: true }); }); this._refilterKind = Refilter.Nothing; From 7fe56a59b5ebcc231bdc3cce4efa60a9ccf85bf3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 07:41:41 -0800 Subject: [PATCH 1113/3587] Support CDPATH in terminal suggest Fixes #239401 --- .../browser/terminalCompletionService.ts | 47 +++++++++++++++---- .../suggest/browser/terminalSuggestAddon.ts | 2 +- .../common/terminalSuggestConfiguration.ts | 13 +++++ 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 36d4de75fcd4..18ecd71882b7 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -12,6 +12,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { TerminalCapability, type ITerminalCapabilityStore } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; import { GeneralShellType, TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; import { ISimpleCompletion } from '../../../../services/suggest/browser/simpleCompletionItem.js'; import { TerminalSuggestSettingId } from '../common/terminalSuggestConfiguration.js'; @@ -82,7 +83,7 @@ export interface ITerminalCompletionService { _serviceBrand: undefined; readonly providers: IterableIterator; registerTerminalCompletionProvider(extensionIdentifier: string, id: string, provider: ITerminalCompletionProvider, ...triggerCharacters: string[]): IDisposable; - provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean): Promise; + provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, capabilities: ITerminalCapabilityStore, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean): Promise; } export class TerminalCompletionService extends Disposable implements ITerminalCompletionService { @@ -101,7 +102,8 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo } } - constructor(@IConfigurationService private readonly _configurationService: IConfigurationService, + constructor( + @IConfigurationService private readonly _configurationService: IConfigurationService, @IFileService private readonly _fileService: IFileService ) { super(); @@ -127,7 +129,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo }); } - async provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean): Promise { + async provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, capabilities: ITerminalCapabilityStore, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean): Promise { if (!this._providers || !this._providers.values || cursorPosition < 0) { return undefined; } @@ -153,7 +155,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo if (skipExtensionCompletions) { providers = providers.filter(p => p.isBuiltin); - return this._collectCompletions(providers, shellType, promptValue, cursorPosition, token); + return this._collectCompletions(providers, shellType, promptValue, cursorPosition, capabilities, token); } const providerConfig: { [key: string]: boolean } = this._configurationService.getValue(TerminalSuggestSettingId.Providers); @@ -166,10 +168,10 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return; } - return this._collectCompletions(providers, shellType, promptValue, cursorPosition, token); + return this._collectCompletions(providers, shellType, promptValue, cursorPosition, capabilities, token); } - private async _collectCompletions(providers: ITerminalCompletionProvider[], shellType: TerminalShellType, promptValue: string, cursorPosition: number, token: CancellationToken): Promise { + private async _collectCompletions(providers: ITerminalCompletionProvider[], shellType: TerminalShellType, promptValue: string, cursorPosition: number, capabilities: ITerminalCapabilityStore, token: CancellationToken): Promise { const completionPromises = providers.map(async provider => { if (provider.shellTypes && !provider.shellTypes.includes(shellType)) { return undefined; @@ -193,7 +195,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return completionItems; } if (completions.resourceRequestConfig) { - const resourceCompletions = await this.resolveResources(completions.resourceRequestConfig, promptValue, cursorPosition, provider.id); + const resourceCompletions = await this.resolveResources(completions.resourceRequestConfig, promptValue, cursorPosition, provider.id, capabilities); if (resourceCompletions) { completionItems.push(...resourceCompletions); } @@ -206,7 +208,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return results.filter(result => !!result).flat(); } - async resolveResources(resourceRequestConfig: TerminalResourceRequestConfig, promptValue: string, cursorPosition: number, provider: string): Promise { + async resolveResources(resourceRequestConfig: TerminalResourceRequestConfig, promptValue: string, cursorPosition: number, provider: string, capabilities: ITerminalCapabilityStore): Promise { if (resourceRequestConfig.shouldNormalizePrefix) { // for tests, make sure the right path separator is used promptValue = promptValue.replaceAll(/[\\/]/g, resourceRequestConfig.pathSeparator); @@ -349,6 +351,35 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo } } + // Support $CDPATH specially for the `cd` command only + if (promptValue.startsWith('cd ')) { + const config = this._configurationService.getValue(TerminalSuggestSettingId.CdPath); + if (config === 'absolute' || config === 'relative') { + const cdPath = capabilities.get(TerminalCapability.ShellEnvDetection)?.env?.get('CDPATH'); + if (cdPath) { + const cdPathEntries = cdPath.split(useForwardSlash ? ';' : ':'); + for (const cdPathEntry of cdPathEntries) { + const fileStat = await this._fileService.resolve(URI.file(cdPathEntry), { resolveSingleChildDescendants: true }); + if (fileStat?.children) { + for (const child of fileStat.children) { + const label = config === 'relative' ? basename(child.resource.fsPath) : getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator); + resourceCompletions.push({ + label, + provider, + kind: TerminalCompletionItemKind.Folder, + isDirectory: child.isDirectory, + isFile: child.isFile, + detail: `CDPATH`, + replacementIndex: cursorPosition - lastWord.length, + replacementLength: lastWord.length + }); + } + } + } + } + } + } + // Add parent directory to the bottom of the list because it's not as useful as other suggestions // // For example: diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 1af621de8cf1..599199ad9e87 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -169,7 +169,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest }; this._requestedCompletionsIndex = this._currentPromptInputState.cursorIndex; - const providedCompletions = await this._terminalCompletionService.provideCompletions(this._currentPromptInputState.prefix, this._currentPromptInputState.cursorIndex, this.shellType, token, doNotRequestExtensionCompletions); + const providedCompletions = await this._terminalCompletionService.provideCompletions(this._currentPromptInputState.prefix, this._currentPromptInputState.cursorIndex, this.shellType, this._capabilities, token, doNotRequestExtensionCompletions); if (!providedCompletions?.length || token.isCancellationRequested) { return; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts index e11d8d0587c2..664892c681f1 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts @@ -17,6 +17,7 @@ export const enum TerminalSuggestSettingId { WindowsExecutableExtensions = 'terminal.integrated.suggest.windowsExecutableExtensions', Providers = 'terminal.integrated.suggest.providers', ShowStatusBar = 'terminal.integrated.suggest.showStatusBar', + CdPath = 'terminal.integrated.suggest.cdPath', } export const windowsDefaultExecutableExtensions: string[] = [ @@ -134,6 +135,18 @@ export const terminalSuggestConfiguration: IStringDictionary Date: Sat, 1 Feb 2025 07:46:25 -0800 Subject: [PATCH 1114/3587] Handle errors and improve detail when config is absolute --- .../browser/terminalCompletionService.ts | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 18ecd71882b7..a3bc6a1e3bc9 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -359,22 +359,26 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo if (cdPath) { const cdPathEntries = cdPath.split(useForwardSlash ? ';' : ':'); for (const cdPathEntry of cdPathEntries) { - const fileStat = await this._fileService.resolve(URI.file(cdPathEntry), { resolveSingleChildDescendants: true }); - if (fileStat?.children) { - for (const child of fileStat.children) { - const label = config === 'relative' ? basename(child.resource.fsPath) : getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator); - resourceCompletions.push({ - label, - provider, - kind: TerminalCompletionItemKind.Folder, - isDirectory: child.isDirectory, - isFile: child.isFile, - detail: `CDPATH`, - replacementIndex: cursorPosition - lastWord.length, - replacementLength: lastWord.length - }); + try { + const fileStat = await this._fileService.resolve(URI.file(cdPathEntry), { resolveSingleChildDescendants: true }); + if (fileStat?.children) { + for (const child of fileStat.children) { + const useRelative = config === 'relative'; + const label = useRelative ? basename(child.resource.fsPath) : getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator); + const detail = useRelative ? `CDPATH ${getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator)}` : `CDPATH`; + resourceCompletions.push({ + label, + provider, + kind: TerminalCompletionItemKind.Folder, + isDirectory: child.isDirectory, + isFile: child.isFile, + detail, + replacementIndex: cursorPosition - lastWord.length, + replacementLength: lastWord.length + }); + } } - } + } catch { /* ignore */ } } } } From 732c306210defed1e0ff2489d260821a84a9546d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 07:49:57 -0800 Subject: [PATCH 1115/3587] Fix compile in completion tests --- .../browser/terminalCompletionService.test.ts | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index 7cdac7c3d674..bdc89e39c17c 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -13,6 +13,7 @@ import { TestInstantiationService } from '../../../../../../platform/instantiati import { createFileStat } from '../../../../../test/common/workbenchTestServices.js'; import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { TerminalCapabilityStore } from '../../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js'; const pathSeparator = isWindows ? '\\' : '/'; @@ -77,7 +78,7 @@ suite('TerminalCompletionService', () => { suite('resolveResources should return undefined', () => { test('if cwd is not provided', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, new TerminalCapabilityStore()); assert(!result); }); @@ -87,7 +88,7 @@ suite('TerminalCompletionService', () => { pathSeparator }; validResources = [URI.parse('file:///test')]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, new TerminalCapabilityStore()); assert(!result); }); }); @@ -107,7 +108,7 @@ suite('TerminalCompletionService', () => { foldersRequested: true, pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 1, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 1, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: '.', detail: '/test/' }, @@ -123,7 +124,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 3, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 3, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -139,7 +140,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./', 5, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./', 5, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -154,7 +155,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./f', 6, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./f', 6, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -170,7 +171,7 @@ suite('TerminalCompletionService', () => { shouldNormalizePrefix: true, env: { HOME: '/test/' } }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ~/', 5, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ~/', 5, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: '~/', detail: '/test/' }, @@ -197,7 +198,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -217,7 +218,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './h', 3, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './h', 3, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -245,7 +246,7 @@ suite('TerminalCompletionService', () => { }; validResources = [URI.parse('file:///usr')]; childResources = []; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '/usr/', 5, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '/usr/', 5, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: '/usr/', detail: '/' }, @@ -267,7 +268,7 @@ suite('TerminalCompletionService', () => { { resource: URI.parse('file:///C:/test/anotherFolder/'), isDirectory: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '.\\folder', 8, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '.\\folder', 8, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: '.\\', detail: 'C:\\test\\' }, @@ -290,7 +291,7 @@ suite('TerminalCompletionService', () => { { resource: URI.parse('file:///test/foldera/'), isDirectory: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder', 8, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder', 8, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -313,7 +314,7 @@ suite('TerminalCompletionService', () => { { resource: URI.parse('file:///test/folder1/'), isDirectory: true }, { resource: URI.parse('file:///test/folder2/'), isDirectory: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 0, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 0, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: '.', detail: '/test/' }, @@ -335,7 +336,7 @@ suite('TerminalCompletionService', () => { resource: URI.parse(`file:///test/folder${i}/`), isDirectory: true })); - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider, new TerminalCapabilityStore()); assert(result); // includes the 1000 folders + ./ and ./../ @@ -356,7 +357,7 @@ suite('TerminalCompletionService', () => { { resource: URI.parse('file:///test/folder1/'), isDirectory: true }, { resource: URI.parse('file:///test/folder2/'), isDirectory: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder1', 10, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder1', 10, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: './', detail: '/test/' }, From 74974c9bb3091d3aca8c20999c82e1ba2ccc754f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 08:07:40 -0800 Subject: [PATCH 1116/3587] Add tests for CDPATH feature --- .../browser/terminalCompletionService.ts | 3 + .../browser/terminalCompletionService.test.ts | 89 +++++++++++++++---- 2 files changed, 76 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index a3bc6a1e3bc9..ea42bf6b6f30 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -363,6 +363,9 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo const fileStat = await this._fileService.resolve(URI.file(cdPathEntry), { resolveSingleChildDescendants: true }); if (fileStat?.children) { for (const child of fileStat.children) { + if (!child.isDirectory) { + continue; + } const useRelative = config === 'relative'; const label = useRelative ? basename(child.resource.fsPath) : getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator); const detail = useRelative ? `CDPATH ${getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator)}` : `CDPATH`; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index bdc89e39c17c..92d723eb8d9a 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -14,6 +14,8 @@ import { createFileStat } from '../../../../../test/common/workbenchTestServices import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { TerminalCapabilityStore } from '../../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js'; +import { ShellEnvDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/shellEnvDetectionCapability.js'; +import { TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; const pathSeparator = isWindows ? '\\' : '/'; @@ -50,6 +52,7 @@ suite('TerminalCompletionService', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); let instantiationService: TestInstantiationService; let configurationService: TestConfigurationService; + let capabilities: TerminalCapabilityStore; let validResources: URI[]; let childResources: { resource: URI; isFile?: boolean; isDirectory?: boolean }[]; let terminalCompletionService: TerminalCompletionService; @@ -67,18 +70,20 @@ suite('TerminalCompletionService', () => { return createFileStat(resource); }, async resolve(resource: URI, options: IResolveMetadataFileOptions): Promise { - return createFileStat(resource, undefined, undefined, undefined, childResources); + const children = childResources.filter(e => e.resource.fsPath.startsWith(resource.fsPath)); + return createFileStat(resource, undefined, undefined, undefined, children); }, }); terminalCompletionService = store.add(instantiationService.createInstance(TerminalCompletionService)); validResources = []; childResources = []; + capabilities = store.add(new TerminalCapabilityStore()); }); suite('resolveResources should return undefined', () => { test('if cwd is not provided', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, capabilities); assert(!result); }); @@ -88,7 +93,7 @@ suite('TerminalCompletionService', () => { pathSeparator }; validResources = [URI.parse('file:///test')]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, capabilities); assert(!result); }); }); @@ -108,7 +113,7 @@ suite('TerminalCompletionService', () => { foldersRequested: true, pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 1, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 1, provider, capabilities); assertCompletions(result, [ { label: '.', detail: '/test/' }, @@ -124,7 +129,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 3, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 3, provider, capabilities); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -140,7 +145,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./', 5, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./', 5, provider, capabilities); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -155,7 +160,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./f', 6, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./f', 6, provider, capabilities); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -171,7 +176,7 @@ suite('TerminalCompletionService', () => { shouldNormalizePrefix: true, env: { HOME: '/test/' } }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ~/', 5, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ~/', 5, provider, capabilities); assertCompletions(result, [ { label: '~/', detail: '/test/' }, @@ -198,7 +203,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider, capabilities); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -218,7 +223,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './h', 3, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './h', 3, provider, capabilities); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -246,7 +251,7 @@ suite('TerminalCompletionService', () => { }; validResources = [URI.parse('file:///usr')]; childResources = []; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '/usr/', 5, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '/usr/', 5, provider, capabilities); assertCompletions(result, [ { label: '/usr/', detail: '/' }, @@ -268,7 +273,7 @@ suite('TerminalCompletionService', () => { { resource: URI.parse('file:///C:/test/anotherFolder/'), isDirectory: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '.\\folder', 8, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '.\\folder', 8, provider, capabilities); assertCompletions(result, [ { label: '.\\', detail: 'C:\\test\\' }, @@ -291,7 +296,7 @@ suite('TerminalCompletionService', () => { { resource: URI.parse('file:///test/foldera/'), isDirectory: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder', 8, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder', 8, provider, capabilities); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -314,7 +319,7 @@ suite('TerminalCompletionService', () => { { resource: URI.parse('file:///test/folder1/'), isDirectory: true }, { resource: URI.parse('file:///test/folder2/'), isDirectory: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 0, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 0, provider, capabilities); assertCompletions(result, [ { label: '.', detail: '/test/' }, @@ -336,7 +341,7 @@ suite('TerminalCompletionService', () => { resource: URI.parse(`file:///test/folder${i}/`), isDirectory: true })); - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider, capabilities); assert(result); // includes the 1000 folders + ./ and ./../ @@ -357,7 +362,7 @@ suite('TerminalCompletionService', () => { { resource: URI.parse('file:///test/folder1/'), isDirectory: true }, { resource: URI.parse('file:///test/folder2/'), isDirectory: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder1', 10, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder1', 10, provider, capabilities); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -367,4 +372,56 @@ suite('TerminalCompletionService', () => { ], { replacementIndex: 1, replacementLength: 9 }); }); }); + + suite('cdpath', () => { + let shellEnvDetection: ShellEnvDetectionCapability; + + setup(() => { + validResources = [URI.parse('file:///test')]; + childResources = [ + { resource: URI.parse('file:///cdpath_value/folder1/'), isDirectory: true }, + { resource: URI.parse('file:///cdpath_value/file1.txt'), isFile: true }, + ]; + + shellEnvDetection = store.add(new ShellEnvDetectionCapability()); + shellEnvDetection.setEnvironment({ CDPATH: '/cdpath_value' }, true); + capabilities.add(TerminalCapability.ShellEnvDetection, shellEnvDetection); + }); + + test('cd | should show paths from $CDPATH (relative)', async () => { + configurationService.setUserConfiguration('terminal.integrated.suggest.cdPath', 'relative'); + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///test'), + foldersRequested: true, + filesRequested: true, + pathSeparator, + shouldNormalizePrefix: true + }; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, capabilities); + + assertCompletions(result, [ + { label: '.', detail: '/test/' }, + { label: 'folder1', detail: 'CDPATH /cdpath_value/folder1/' }, + { label: '../', detail: '/' }, + ], { replacementIndex: 3, replacementLength: 0 }); + }); + + test('cd | should show paths from $CDPATH (absolute)', async () => { + configurationService.setUserConfiguration('terminal.integrated.suggest.cdPath', 'absolute'); + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///test'), + foldersRequested: true, + filesRequested: true, + pathSeparator, + shouldNormalizePrefix: true + }; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, capabilities); + + assertCompletions(result, [ + { label: '.', detail: '/test/' }, + { label: '/cdpath_value/folder1/', detail: 'CDPATH' }, + { label: '../', detail: '/' }, + ], { replacementIndex: 3, replacementLength: 0 }); + }); + }); }); From 154cd450456f1820487b4ddd8ac65d14f3bae4dc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 09:03:56 -0800 Subject: [PATCH 1117/3587] Terminal suggest absolute path support Fixes #237979 --- .../browser/terminalCompletionService.ts | 234 +++++++++++------- .../browser/terminalCompletionService.test.ts | 56 ++++- 2 files changed, 199 insertions(+), 91 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index ea42bf6b6f30..4eb91c8ef62b 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -220,11 +220,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return; } - const fileStat = await this._fileService.resolve(cwd, { resolveSingleChildDescendants: true }); - if (!fileStat || !fileStat?.children) { - return; - } - const resourceCompletions: ITerminalCompletion[] = []; const cursorPrefix = promptValue.substring(0, cursorPosition); @@ -271,39 +266,107 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return resourceCompletions; } } - // Add current directory. This should be shown at the top because it will be an exact match - // and therefore highlight the detail, plus it improves the experience when runOnEnter is - // used. - // - // For example: - // - `|` -> `.`, this does not have the trailing `/` intentionally as it's common to - // complete the current working directory and we do not want to complete `./` when - // `runOnEnter` is used. - // - `./src/|` -> `./src/` - if (foldersRequested) { - resourceCompletions.push({ - label: lastWordFolder.length === 0 ? '.' : lastWordFolder, - provider, - kind: TerminalCompletionItemKind.Folder, - isDirectory: true, - isFile: false, - detail: getFriendlyPath(cwd, resourceRequestConfig.pathSeparator), - replacementIndex: cursorPosition - lastWord.length, - replacementLength: lastWord.length - }); - } // Handle absolute paths differently to avoid adding `./` prefixes // TODO: Deal with git bash case const isAbsolutePath = useForwardSlash - ? /^[a-zA-Z]:\\/.test(lastWord) - : lastWord.startsWith(resourceRequestConfig.pathSeparator) && lastWord.endsWith(resourceRequestConfig.pathSeparator); - - // Add all direct children files or folders - // - // For example: - // - `cd ./src/` -> `cd ./src/folder1`, ... - if (!isAbsolutePath) { + ? lastWord.startsWith(resourceRequestConfig.pathSeparator) && lastWord.endsWith(resourceRequestConfig.pathSeparator) + : /^[a-zA-Z]:[\\\/]/.test(lastWord); + + if (isAbsolutePath) { + + const fileStat = await this._fileService.resolve(URI.file(lastWordFolder), { resolveSingleChildDescendants: true }); + if (!fileStat?.children) { + return; + } + + // Add current directory. This should be shown at the top because it will be an exact match + // and therefore highlight the detail, plus it improves the experience when runOnEnter is + // used. + // + // For example: + // - `c:/foo/|` -> `c:/foo/` + if (foldersRequested) { + resourceCompletions.push({ + label: lastWordFolder.length === 0 ? '.' : lastWordFolder, + provider, + kind: TerminalCompletionItemKind.Folder, + isDirectory: true, + isFile: false, + detail: getFriendlyPath(cwd, resourceRequestConfig.pathSeparator), + replacementIndex: cursorPosition - lastWord.length, + replacementLength: lastWord.length + }); + } + + // Add all direct children files or folders + // + // For example: + // - `cd c:/src/` -> `cd c:/src/folder1/`, ... + for (const child of fileStat.children) { + if ( + (child.isDirectory && !foldersRequested) || + (child.isFile && !filesRequested) + ) { + continue; + } + + let label = lastWordFolder; + if (!label.endsWith(resourceRequestConfig.pathSeparator)) { + label += resourceRequestConfig.pathSeparator; + } + label += child.name; + if (child.isDirectory) { + label += resourceRequestConfig.pathSeparator; + } + + const kind = child.isDirectory ? TerminalCompletionItemKind.Folder : TerminalCompletionItemKind.File; + + resourceCompletions.push({ + label, + provider, + kind, + detail: getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator, kind), + isDirectory: child.isDirectory, + isFile: child.isFile, + replacementIndex: cursorPosition - lastWord.length, + replacementLength: lastWord.length + }); + } + + } else { // !isAbsolutePath + + const fileStat = await this._fileService.resolve(cwd, { resolveSingleChildDescendants: true }); + if (!fileStat?.children) { + return; + } + + // Add current directory. This should be shown at the top because it will be an exact match + // and therefore highlight the detail, plus it improves the experience when runOnEnter is + // used. + // + // For example: + // - `|` -> `.`, this does not have the trailing `/` intentionally as it's common to + // complete the current working directory and we do not want to complete `./` when + // `runOnEnter` is used. + // - `./src/|` -> `./src/` + if (foldersRequested) { + resourceCompletions.push({ + label: lastWordFolder.length === 0 ? '.' : lastWordFolder, + provider, + kind: TerminalCompletionItemKind.Folder, + isDirectory: true, + isFile: false, + detail: getFriendlyPath(cwd, resourceRequestConfig.pathSeparator), + replacementIndex: cursorPosition - lastWord.length, + replacementLength: lastWord.length + }); + } + + // Add all direct children files or folders + // + // For example: + // - `cd ./src/` -> `cd ./src/folder1/`, ... for (const stat of fileStat.children) { let kind: TerminalCompletionItemKind | undefined; if (foldersRequested && stat.isDirectory) { @@ -349,64 +412,65 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo replacementLength: lastWord.length }); } - } - // Support $CDPATH specially for the `cd` command only - if (promptValue.startsWith('cd ')) { - const config = this._configurationService.getValue(TerminalSuggestSettingId.CdPath); - if (config === 'absolute' || config === 'relative') { - const cdPath = capabilities.get(TerminalCapability.ShellEnvDetection)?.env?.get('CDPATH'); - if (cdPath) { - const cdPathEntries = cdPath.split(useForwardSlash ? ';' : ':'); - for (const cdPathEntry of cdPathEntries) { - try { - const fileStat = await this._fileService.resolve(URI.file(cdPathEntry), { resolveSingleChildDescendants: true }); - if (fileStat?.children) { - for (const child of fileStat.children) { - if (!child.isDirectory) { - continue; + // Support $CDPATH specially for the `cd` command only + if (promptValue.startsWith('cd ')) { + const config = this._configurationService.getValue(TerminalSuggestSettingId.CdPath); + if (config === 'absolute' || config === 'relative') { + const cdPath = capabilities.get(TerminalCapability.ShellEnvDetection)?.env?.get('CDPATH'); + if (cdPath) { + const cdPathEntries = cdPath.split(useForwardSlash ? ';' : ':'); + for (const cdPathEntry of cdPathEntries) { + try { + const fileStat = await this._fileService.resolve(URI.file(cdPathEntry), { resolveSingleChildDescendants: true }); + if (fileStat?.children) { + for (const child of fileStat.children) { + if (!child.isDirectory) { + continue; + } + const useRelative = config === 'relative'; + const label = useRelative ? basename(child.resource.fsPath) : getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator); + const detail = useRelative ? `CDPATH ${getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator)}` : `CDPATH`; + resourceCompletions.push({ + label, + provider, + kind: TerminalCompletionItemKind.Folder, + isDirectory: child.isDirectory, + isFile: child.isFile, + detail, + replacementIndex: cursorPosition - lastWord.length, + replacementLength: lastWord.length + }); } - const useRelative = config === 'relative'; - const label = useRelative ? basename(child.resource.fsPath) : getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator); - const detail = useRelative ? `CDPATH ${getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator)}` : `CDPATH`; - resourceCompletions.push({ - label, - provider, - kind: TerminalCompletionItemKind.Folder, - isDirectory: child.isDirectory, - isFile: child.isFile, - detail, - replacementIndex: cursorPosition - lastWord.length, - replacementLength: lastWord.length - }); } - } - } catch { /* ignore */ } + } catch { /* ignore */ } + } } } } - } - // Add parent directory to the bottom of the list because it's not as useful as other suggestions - // - // For example: - // - `|` -> `../` - // - `./src/|` -> `./src/../` - // - // On Windows, the path seprators are normalized to `\`: - // - `./src/|` -> `.\src\..\` - if (!isAbsolutePath && foldersRequested) { - const parentDir = URI.joinPath(cwd, '..' + resourceRequestConfig.pathSeparator); - resourceCompletions.push({ - label: lastWordFolder + '..' + resourceRequestConfig.pathSeparator, - provider, - kind: TerminalCompletionItemKind.Folder, - detail: getFriendlyPath(parentDir, resourceRequestConfig.pathSeparator), - isDirectory: true, - isFile: false, - replacementIndex: cursorPosition - lastWord.length, - replacementLength: lastWord.length - }); + // Add parent directory to the bottom of the list because it's not as useful as other suggestions + // + // For example: + // - `|` -> `../` + // - `./src/|` -> `./src/../` + // + // On Windows, the path seprators are normalized to `\`: + // - `./src/|` -> `.\src\..\` + if (foldersRequested) { + const parentDir = URI.joinPath(cwd, '..' + resourceRequestConfig.pathSeparator); + resourceCompletions.push({ + label: lastWordFolder + '..' + resourceRequestConfig.pathSeparator, + provider, + kind: TerminalCompletionItemKind.Folder, + detail: getFriendlyPath(parentDir, resourceRequestConfig.pathSeparator), + isDirectory: true, + isFile: false, + replacementIndex: cursorPosition - lastWord.length, + replacementLength: lastWord.length + }); + } + } return resourceCompletions.length ? resourceCompletions : undefined; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index 92d723eb8d9a..45b3d8af9a47 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -241,23 +241,67 @@ suite('TerminalCompletionService', () => { childResources = []; }); - if (!isWindows) { - test('/usr/| Missing . should show correct results', async () => { + if (isWindows) { + test('C:/Foo/| absolute paths on Windows', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///C:/Foo'), + foldersRequested: true, + pathSeparator, + shouldNormalizePrefix: true, + }; + validResources = [URI.parse('file:///C:/Foo')]; // Updated to reflect new cwd + childResources = [ + { resource: URI.parse('file:///C:/Foo/Bar'), isDirectory: true, isFile: false }, + { resource: URI.parse('file:///C:/Foo/Baz.txt'), isDirectory: false, isFile: true } + ]; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'C:/Foo/', 7, provider, capabilities); + + assertCompletions(result, [ + { label: 'C:/Foo/', detail: 'C:/Foo/' }, + { label: 'C:/Foo/Bar/', detail: 'C:/Foo/Bar/' }, + ], { replacementIndex: 0, replacementLength: 7 }); + }); + test('c:/foo/| case sensitivity on Windows', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///c:/foo'), + foldersRequested: true, + pathSeparator, + shouldNormalizePrefix: true, + }; + validResources = [URI.parse('file:///c:/foo')]; // Updated to reflect new cwd + childResources = [ + { resource: URI.parse('file:///c:/foo/Bar'), isDirectory: true, isFile: false } + ]; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'c:/foo/', 7, provider, capabilities); + + assertCompletions(result, [ + // Note that the detail is normalizes drive letters to capital case intentionally + { label: 'c:/foo/', detail: 'C:/foo/' }, + { label: 'c:/foo/Bar/', detail: 'C:/foo/Bar/' }, + ], { replacementIndex: 0, replacementLength: 7 }); + }); + } else { + test.skip('/Foo/| absolute paths NOT on Windows', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///'), foldersRequested: true, pathSeparator, shouldNormalizePrefix: true }; - validResources = [URI.parse('file:///usr')]; - childResources = []; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '/usr/', 5, provider, capabilities); + validResources = [URI.parse('file:///Foo')]; // Updated to reflect new cwd + childResources = [ + { resource: URI.parse('file:///Foo/Bar'), isDirectory: true, isFile: false }, + { resource: URI.parse('file:///Foo/Baz.txt'), isDirectory: false, isFile: true } + ]; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '/Foo/', 5, provider, capabilities); assertCompletions(result, [ - { label: '/usr/', detail: '/' }, + { label: '/Foo/', detail: '/Foo/' }, + { label: '/Foo/Bar/', detail: '/Foo/Bar/' }, ], { replacementIndex: 0, replacementLength: 5 }); }); } + if (isWindows) { test('.\\folder | Case insensitivity should resolve correctly on Windows', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { From 5446a5a61616ae45d6f40dd14e86b23c8b5caed9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 09:08:09 -0800 Subject: [PATCH 1118/3587] Add case sensitivity test case for non windows --- .../browser/terminalCompletionService.test.ts | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index 45b3d8af9a47..c446aacfdf35 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -261,7 +261,7 @@ suite('TerminalCompletionService', () => { { label: 'C:/Foo/Bar/', detail: 'C:/Foo/Bar/' }, ], { replacementIndex: 0, replacementLength: 7 }); }); - test('c:/foo/| case sensitivity on Windows', async () => { + test('c:/foo/| case insensitivity on Windows', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///c:/foo'), foldersRequested: true, @@ -281,25 +281,40 @@ suite('TerminalCompletionService', () => { ], { replacementIndex: 0, replacementLength: 7 }); }); } else { - test.skip('/Foo/| absolute paths NOT on Windows', async () => { + test('/foo/| absolute paths NOT on Windows', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { cwd: URI.parse('file:///'), foldersRequested: true, pathSeparator, shouldNormalizePrefix: true }; - validResources = [URI.parse('file:///Foo')]; // Updated to reflect new cwd + validResources = [URI.parse('file:///foo')]; // Updated to reflect new cwd childResources = [ - { resource: URI.parse('file:///Foo/Bar'), isDirectory: true, isFile: false }, - { resource: URI.parse('file:///Foo/Baz.txt'), isDirectory: false, isFile: true } + { resource: URI.parse('file:///foo/Bar'), isDirectory: true, isFile: false }, + { resource: URI.parse('file:///foo/Baz.txt'), isDirectory: false, isFile: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '/Foo/', 5, provider, capabilities); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '/foo/', 5, provider, capabilities); assertCompletions(result, [ - { label: '/Foo/', detail: '/Foo/' }, - { label: '/Foo/Bar/', detail: '/Foo/Bar/' }, + { label: '/foo/', detail: '/foo/' }, + { label: '/foo/Bar/', detail: '/foo/Bar/' }, ], { replacementIndex: 0, replacementLength: 5 }); }); + test('/foo/| case insensitivity NOT on Windows', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///foo'), + foldersRequested: true, + pathSeparator, + shouldNormalizePrefix: true, + }; + validResources = [URI.parse('file:///foo')]; // Updated to reflect new cwd + childResources = [ + { resource: URI.parse('file:///foo/Bar'), isDirectory: true, isFile: false } + ]; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '/Foo/', 5, provider, capabilities); + + assertCompletions(result, [], { replacementIndex: 0, replacementLength: 7 }); + }); } if (isWindows) { From 53bfcd14b299465373ecf82d41757941ab12cbca Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 09:13:32 -0800 Subject: [PATCH 1119/3587] Set kind correctly for children Fixes #239408 --- .../suggest/browser/terminalCompletionService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 36d4de75fcd4..84b6e595e73e 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -340,7 +340,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo label, provider, kind, - detail: getFriendlyPath(stat.resource, resourceRequestConfig.pathSeparator, TerminalCompletionItemKind.File), + detail: getFriendlyPath(stat.resource, resourceRequestConfig.pathSeparator, kind), isDirectory, isFile: kind === TerminalCompletionItemKind.File, replacementIndex: cursorPosition - lastWord.length, From 1fec88a0a63df190caf637357740919de96f75cb Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 09:28:16 -0800 Subject: [PATCH 1120/3587] Fix typo, use more specific type --- .../services/suggest/browser/simpleCompletionItem.ts | 7 +++++-- .../services/suggest/browser/simpleCompletionModel.ts | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts index 5bfb52723431..b1301167f1a0 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts @@ -74,7 +74,10 @@ export class SimpleCompletionItem { */ readonly labelLowNormalizedPath: string; - readonly unscorePenalty: number = 0; + /** + * A penalty that applies to files or folders starting with the underscore character. + */ + readonly underscorePenalty: 0 | 1 = 0; /** * The file extension part from {@link labelLow}. @@ -112,7 +115,7 @@ export class SimpleCompletionItem { if (completion.isDirectory) { this.labelLowNormalizedPath = this.labelLowNormalizedPath.replace(/\/$/, ''); } - this.unscorePenalty = basename(this.labelLowNormalizedPath).startsWith('_') ? 1 : 0; + this.underscorePenalty = basename(this.labelLowNormalizedPath).startsWith('_') ? 1 : 0; } } } diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts index 4e59fc802e89..4031bba7bae3 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts @@ -215,9 +215,9 @@ export class SimpleCompletionModel { } } - // Sort by unscore penalty (eg. `__init__/` should be penalized) - if (a.unscorePenalty !== b.unscorePenalty) { - return a.unscorePenalty - b.unscorePenalty; + // Sort by underscore penalty (eg. `__init__/` should be penalized) + if (a.underscorePenalty !== b.underscorePenalty) { + return a.underscorePenalty - b.underscorePenalty; } // Sort by folder depth (eg. `vscode/` should come before `vscode-.../`) From b900a008adefb16db4c692755a5f26cb59317da2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 11:16:34 -0800 Subject: [PATCH 1121/3587] Get absolute paths working on mac --- .../suggest/browser/terminalCompletionService.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 4eb91c8ef62b..58fb4ff10a3b 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -223,7 +223,8 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo const resourceCompletions: ITerminalCompletion[] = []; const cursorPrefix = promptValue.substring(0, cursorPosition); - const useForwardSlash = !resourceRequestConfig.shouldNormalizePrefix && isWindows; + // TODO: This should come in through the resourceRequestConfig + const useBackslash = isWindows; // The last word (or argument). When the cursor is following a space it will be the empty // string @@ -232,7 +233,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo // Get the nearest folder path from the prefix. This ignores everything after the `/` as // they are what triggers changes in the directory. let lastSlashIndex: number; - if (useForwardSlash) { + if (useBackslash) { lastSlashIndex = Math.max(lastWord.lastIndexOf('\\'), lastWord.lastIndexOf('/')); } else { lastSlashIndex = lastWord.lastIndexOf(resourceRequestConfig.pathSeparator); @@ -269,9 +270,9 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo // Handle absolute paths differently to avoid adding `./` prefixes // TODO: Deal with git bash case - const isAbsolutePath = useForwardSlash - ? lastWord.startsWith(resourceRequestConfig.pathSeparator) && lastWord.endsWith(resourceRequestConfig.pathSeparator) - : /^[a-zA-Z]:[\\\/]/.test(lastWord); + const isAbsolutePath = useBackslash + ? /^[a-zA-Z]:[\\\/]/.test(lastWord) + : lastWord.startsWith(resourceRequestConfig.pathSeparator); if (isAbsolutePath) { @@ -397,7 +398,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo // Normalize path separator to `\` on Windows. It should act the exact same as `/` but // suggestions should all use `\` - if (useForwardSlash) { + if (useBackslash) { label = label.replaceAll('/', '\\'); } @@ -419,7 +420,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo if (config === 'absolute' || config === 'relative') { const cdPath = capabilities.get(TerminalCapability.ShellEnvDetection)?.env?.get('CDPATH'); if (cdPath) { - const cdPathEntries = cdPath.split(useForwardSlash ? ';' : ':'); + const cdPathEntries = cdPath.split(useBackslash ? ';' : ':'); for (const cdPathEntry of cdPathEntries) { try { const fileStat = await this._fileService.resolve(URI.file(cdPathEntry), { resolveSingleChildDescendants: true }); From 61f0892200b19b315e248fca8d1ac896715039be Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 11:24:01 -0800 Subject: [PATCH 1122/3587] Fix detail for absolute paths --- .../browser/terminalCompletionService.ts | 5 +++-- .../browser/terminalCompletionService.test.ts | 21 +++---------------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 58fb4ff10a3b..9d1865bc3d78 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -276,7 +276,8 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo if (isAbsolutePath) { - const fileStat = await this._fileService.resolve(URI.file(lastWordFolder), { resolveSingleChildDescendants: true }); + const lastWordResource = URI.file(lastWordFolder); + const fileStat = await this._fileService.resolve(lastWordResource, { resolveSingleChildDescendants: true }); if (!fileStat?.children) { return; } @@ -294,7 +295,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo kind: TerminalCompletionItemKind.Folder, isDirectory: true, isFile: false, - detail: getFriendlyPath(cwd, resourceRequestConfig.pathSeparator), + detail: getFriendlyPath(lastWordResource, resourceRequestConfig.pathSeparator), replacementIndex: cursorPosition - lastWord.length, replacementLength: lastWord.length }); diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index c446aacfdf35..f5dfbda038ad 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -249,7 +249,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true, }; - validResources = [URI.parse('file:///C:/Foo')]; // Updated to reflect new cwd + validResources = [URI.parse('file:///C:/Foo')]; childResources = [ { resource: URI.parse('file:///C:/Foo/Bar'), isDirectory: true, isFile: false }, { resource: URI.parse('file:///C:/Foo/Baz.txt'), isDirectory: false, isFile: true } @@ -268,7 +268,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true, }; - validResources = [URI.parse('file:///c:/foo')]; // Updated to reflect new cwd + validResources = [URI.parse('file:///c:/foo')]; childResources = [ { resource: URI.parse('file:///c:/foo/Bar'), isDirectory: true, isFile: false } ]; @@ -288,7 +288,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - validResources = [URI.parse('file:///foo')]; // Updated to reflect new cwd + validResources = [URI.parse('file:///foo')]; childResources = [ { resource: URI.parse('file:///foo/Bar'), isDirectory: true, isFile: false }, { resource: URI.parse('file:///foo/Baz.txt'), isDirectory: false, isFile: true } @@ -300,21 +300,6 @@ suite('TerminalCompletionService', () => { { label: '/foo/Bar/', detail: '/foo/Bar/' }, ], { replacementIndex: 0, replacementLength: 5 }); }); - test('/foo/| case insensitivity NOT on Windows', async () => { - const resourceRequestConfig: TerminalResourceRequestConfig = { - cwd: URI.parse('file:///foo'), - foldersRequested: true, - pathSeparator, - shouldNormalizePrefix: true, - }; - validResources = [URI.parse('file:///foo')]; // Updated to reflect new cwd - childResources = [ - { resource: URI.parse('file:///foo/Bar'), isDirectory: true, isFile: false } - ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '/Foo/', 5, provider, capabilities); - - assertCompletions(result, [], { replacementIndex: 0, replacementLength: 7 }); - }); } if (isWindows) { From f90911587074f5a78c53391eec973f019cb836d3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 11:26:02 -0800 Subject: [PATCH 1123/3587] Use a different cwd in tests to confirm cwd isn't used in completions --- .../suggest/test/browser/terminalCompletionService.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index f5dfbda038ad..00325ad53699 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -244,7 +244,7 @@ suite('TerminalCompletionService', () => { if (isWindows) { test('C:/Foo/| absolute paths on Windows', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { - cwd: URI.parse('file:///C:/Foo'), + cwd: URI.parse('file:///C:'), foldersRequested: true, pathSeparator, shouldNormalizePrefix: true, @@ -263,7 +263,7 @@ suite('TerminalCompletionService', () => { }); test('c:/foo/| case insensitivity on Windows', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { - cwd: URI.parse('file:///c:/foo'), + cwd: URI.parse('file:///c:'), foldersRequested: true, pathSeparator, shouldNormalizePrefix: true, From 53790287ceb2cf62383e4b5ef5f86b1681c35fc0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 2 Feb 2025 04:27:47 -0800 Subject: [PATCH 1124/3587] Improve names on PathExecutableCache --- .../src/env/pathExecutableCache.ts | 26 +++++++++---------- .../src/terminalSuggestMain.ts | 2 +- .../src/test/env/pathExecutableCache.test.ts | 6 ++--- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/extensions/terminal-suggest/src/env/pathExecutableCache.ts b/extensions/terminal-suggest/src/env/pathExecutableCache.ts index 15f1bbcb694e..4ce8090f088f 100644 --- a/extensions/terminal-suggest/src/env/pathExecutableCache.ts +++ b/extensions/terminal-suggest/src/env/pathExecutableCache.ts @@ -16,17 +16,17 @@ const isWindows = osIsWindows(); export class PathExecutableCache implements vscode.Disposable { private _disposables: vscode.Disposable[] = []; - private _cachedAvailableCommandsPath: string | undefined; - private _cachedWindowsExecutableExtensions: { [key: string]: boolean | undefined } | undefined; - private _cachedCommandsInPath: { completionResources: Set | undefined; labels: Set | undefined } | undefined; + private _cachedPathValue: string | undefined; + private _cachedWindowsExeExtensions: { [key: string]: boolean | undefined } | undefined; + private _cachedExes: { completionResources: Set | undefined; labels: Set | undefined } | undefined; constructor() { if (isWindows) { - this._cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration(SettingsIds.SuggestPrefix).get(SettingsIds.CachedWindowsExecutableExtensionsSuffixOnly); + this._cachedWindowsExeExtensions = vscode.workspace.getConfiguration(SettingsIds.SuggestPrefix).get(SettingsIds.CachedWindowsExecutableExtensionsSuffixOnly); this._disposables.push(vscode.workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration(SettingsIds.CachedWindowsExecutableExtensions)) { - this._cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration(SettingsIds.SuggestPrefix).get(SettingsIds.CachedWindowsExecutableExtensionsSuffixOnly); - this._cachedCommandsInPath = undefined; + this._cachedWindowsExeExtensions = vscode.workspace.getConfiguration(SettingsIds.SuggestPrefix).get(SettingsIds.CachedWindowsExecutableExtensionsSuffixOnly); + this._cachedExes = undefined; } })); } @@ -38,7 +38,7 @@ export class PathExecutableCache implements vscode.Disposable { } } - async getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { + async getExecutablesInPath(env: { [key: string]: string | undefined } = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { // Create cache key let pathValue: string | undefined; if (isWindows) { @@ -54,8 +54,8 @@ export class PathExecutableCache implements vscode.Disposable { } // Check cache - if (this._cachedCommandsInPath && this._cachedAvailableCommandsPath === pathValue) { - return this._cachedCommandsInPath; + if (this._cachedExes && this._cachedPathValue === pathValue) { + return this._cachedExes; } // Extract executables from PATH @@ -79,9 +79,9 @@ export class PathExecutableCache implements vscode.Disposable { } // Return - this._cachedAvailableCommandsPath = pathValue; - this._cachedCommandsInPath = { completionResources: executables, labels }; - return this._cachedCommandsInPath; + this._cachedPathValue = pathValue; + this._cachedExes = { completionResources: executables, labels }; + return this._cachedExes; } private async _getFilesInPath(path: string, pathSeparator: string, labels: Set): Promise | undefined> { @@ -95,7 +95,7 @@ export class PathExecutableCache implements vscode.Disposable { const files = await vscode.workspace.fs.readDirectory(fileResource); for (const [file, fileType] of files) { const formattedPath = getFriendlyResourcePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); - if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath, this._cachedWindowsExecutableExtensions)) { + if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath, this._cachedWindowsExeExtensions)) { result.add({ label: file, detail: formattedPath }); labels.add(file); } diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 808e26a9c6b7..d5119697098e 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -96,7 +96,7 @@ export async function activate(context: vscode.ExtensionContext) { return; } - const commandsInPath = await pathExecutableCache.getCommandsInPath(terminal.shellIntegration?.env); + const commandsInPath = await pathExecutableCache.getExecutablesInPath(terminal.shellIntegration?.env); const shellGlobals = await getShellGlobals(shellType, commandsInPath?.labels) ?? []; if (!commandsInPath?.completionResources) { return; diff --git a/extensions/terminal-suggest/src/test/env/pathExecutableCache.test.ts b/extensions/terminal-suggest/src/test/env/pathExecutableCache.test.ts index 4ba5451eafc2..890aa1010333 100644 --- a/extensions/terminal-suggest/src/test/env/pathExecutableCache.test.ts +++ b/extensions/terminal-suggest/src/test/env/pathExecutableCache.test.ts @@ -10,7 +10,7 @@ import { PathExecutableCache } from '../../env/pathExecutableCache'; suite('PathExecutableCache', () => { test('cache should return empty for empty PATH', async () => { const cache = new PathExecutableCache(); - const result = await cache.getCommandsInPath({ PATH: '' }); + const result = await cache.getExecutablesInPath({ PATH: '' }); strictEqual(Array.from(result!.completionResources!).length, 0); strictEqual(Array.from(result!.labels!).length, 0); }); @@ -18,8 +18,8 @@ suite('PathExecutableCache', () => { test('caching is working on successive calls', async () => { const cache = new PathExecutableCache(); const env = { PATH: process.env.PATH }; - const result = await cache.getCommandsInPath(env); - const result2 = await cache.getCommandsInPath(env); + const result = await cache.getExecutablesInPath(env); + const result2 = await cache.getExecutablesInPath(env); strictEqual(result, result2); }); }); From 521e89838af1a28c39bd119dbb449d76bb73a210 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 2 Feb 2025 04:40:31 -0800 Subject: [PATCH 1125/3587] Don't show fig information for aliases Fixes #239425 --- extensions/terminal-suggest/src/shell/bash.ts | 1 + extensions/terminal-suggest/src/shell/fish.ts | 2 +- extensions/terminal-suggest/src/shell/pwsh.ts | 1 + extensions/terminal-suggest/src/shell/zsh.ts | 1 + extensions/terminal-suggest/src/terminalSuggestMain.ts | 6 ++++-- extensions/terminal-suggest/src/types.ts | 5 +++++ 6 files changed, 13 insertions(+), 3 deletions(-) diff --git a/extensions/terminal-suggest/src/shell/bash.ts b/extensions/terminal-suggest/src/shell/bash.ts index b440af3c8805..96d3b3765b23 100644 --- a/extensions/terminal-suggest/src/shell/bash.ts +++ b/extensions/terminal-suggest/src/shell/bash.ts @@ -37,6 +37,7 @@ async function getAliases(options: ExecOptionsWithStringEncoding): Promise[a-zA-Z0-9\.:-]+) (?.+)$/); @@ -38,6 +37,7 @@ async function getAliases(options: ExecOptionsWithStringEncoding): Promise command.label === specLabel); + const availableCommand = availableCommands.find(command => specLabel === (command.definition ?? command.label)); if (!availableCommand || (token && token.isCancellationRequested)) { continue; } // push it to the completion items if (tokenType === TokenType.Command) { - items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: specLabel }, getDescription(spec), availableCommand.detail)); + if (availableCommand.kind !== vscode.TerminalCompletionItemKind.Alias) { + items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: specLabel }, getDescription(spec), availableCommand.detail)); + } continue; } diff --git a/extensions/terminal-suggest/src/types.ts b/extensions/terminal-suggest/src/types.ts index 43b86a08a93e..6e9ec6c5f140 100644 --- a/extensions/terminal-suggest/src/types.ts +++ b/extensions/terminal-suggest/src/types.ts @@ -7,6 +7,11 @@ import * as vscode from 'vscode'; export interface ICompletionResource { label: string; + /** + * The definition of the completion, this will be the resolved value of an + * alias completion. + */ + definition?: string; detail?: string; kind?: vscode.TerminalCompletionItemKind; } From c4cf1864d2fbea6ac48e0219211cc53c26cd4382 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 2 Feb 2025 05:15:25 -0800 Subject: [PATCH 1126/3587] Resolve fig spec against alias definition command --- extensions/terminal-suggest/src/shell/bash.ts | 8 +++++++- extensions/terminal-suggest/src/shell/fish.ts | 8 +++++++- extensions/terminal-suggest/src/shell/pwsh.ts | 10 +++++++++- extensions/terminal-suggest/src/shell/zsh.ts | 8 +++++++- extensions/terminal-suggest/src/terminalSuggestMain.ts | 5 +++-- extensions/terminal-suggest/src/types.ts | 6 +++--- 6 files changed, 36 insertions(+), 9 deletions(-) diff --git a/extensions/terminal-suggest/src/shell/bash.ts b/extensions/terminal-suggest/src/shell/bash.ts index 96d3b3765b23..c4ce4598cb39 100644 --- a/extensions/terminal-suggest/src/shell/bash.ts +++ b/extensions/terminal-suggest/src/shell/bash.ts @@ -33,11 +33,17 @@ async function getAliases(options: ExecOptionsWithStringEncoding): Promise specLabel === (command.definition ?? command.label)); + const availableCommand = availableCommands.find(command => specLabel === command.label); if (!availableCommand || (token && token.isCancellationRequested)) { continue; } @@ -262,7 +262,8 @@ export async function getCompletionItemsFromSpecs( continue; } - if (!terminalContext.commandLine.startsWith(`${specLabel} `)) { + const commandAndAliases = availableCommands.filter(command => specLabel === (command.definitionCommand ?? command.label)); + if (!commandAndAliases.some(e => terminalContext.commandLine.startsWith(`${e.label} `))) { // the spec label is not the first word in the command line, so do not provide options or args continue; } diff --git a/extensions/terminal-suggest/src/types.ts b/extensions/terminal-suggest/src/types.ts index 6e9ec6c5f140..abc4f4f291b4 100644 --- a/extensions/terminal-suggest/src/types.ts +++ b/extensions/terminal-suggest/src/types.ts @@ -8,10 +8,10 @@ import * as vscode from 'vscode'; export interface ICompletionResource { label: string; /** - * The definition of the completion, this will be the resolved value of an - * alias completion. + * The definition command of the completion, this will be the resolved value of an alias + * completion. */ - definition?: string; + definitionCommand?: string; detail?: string; kind?: vscode.TerminalCompletionItemKind; } From 4f587d78c2ecd5d5d4100aa473ed647ba267331e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 2 Feb 2025 05:18:45 -0800 Subject: [PATCH 1127/3587] Pull alias parsing into helper --- extensions/terminal-suggest/src/shell/bash.ts | 29 ++----------------- .../terminal-suggest/src/shell/common.ts | 29 +++++++++++++++++++ extensions/terminal-suggest/src/shell/fish.ts | 29 ++----------------- extensions/terminal-suggest/src/shell/zsh.ts | 29 ++----------------- 4 files changed, 35 insertions(+), 81 deletions(-) diff --git a/extensions/terminal-suggest/src/shell/bash.ts b/extensions/terminal-suggest/src/shell/bash.ts index c4ce4598cb39..bb7e374bed21 100644 --- a/extensions/terminal-suggest/src/shell/bash.ts +++ b/extensions/terminal-suggest/src/shell/bash.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; import type { ICompletionResource } from '../types'; import { type ExecOptionsWithStringEncoding } from 'node:child_process'; -import { execHelper, spawnHelper } from './common'; +import { execHelper, getAliasesHelper } from './common'; export async function getBashGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { return [ @@ -22,29 +21,5 @@ async function getBuiltins(options: ExecOptionsWithStringEncoding, existingComma } async function getAliases(options: ExecOptionsWithStringEncoding): Promise { - // This must be run with interactive, otherwise there's a good chance aliases won't - // be set up. Note that this could differ from the actual aliases as it's a new bash - // session, for the same reason this would not include aliases that are created - // by simply running `alias ...` in the terminal. - const aliasOutput = await spawnHelper('bash', ['-ic', 'alias'], options); - const result: ICompletionResource[] = []; - for (const line of aliasOutput.split('\n')) { - const match = line.match(/^alias (?[a-zA-Z0-9\.:-]+)='(?.+)'$/); - if (!match?.groups) { - continue; - } - let definitionCommand = ''; - let definitionIndex = match.groups.resolved.indexOf(' '); - if (definitionIndex === -1) { - definitionIndex = match.groups.resolved.length; - } - definitionCommand = match.groups.resolved.substring(0, definitionIndex); - result.push({ - label: match.groups.alias, - detail: match.groups.resolved, - kind: vscode.TerminalCompletionItemKind.Alias, - definitionCommand, - }); - } - return result; + return getAliasesHelper('bash', ['-ic', 'alias'], /^alias (?[a-zA-Z0-9\.:-]+)='(?.+)'$/, options); } diff --git a/extensions/terminal-suggest/src/shell/common.ts b/extensions/terminal-suggest/src/shell/common.ts index 590195e3600b..5ef609002a6c 100644 --- a/extensions/terminal-suggest/src/shell/common.ts +++ b/extensions/terminal-suggest/src/shell/common.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; import { exec, spawn, type ExecOptionsWithStringEncoding } from 'node:child_process'; +import type { ICompletionResource } from '../types'; export async function spawnHelper(command: string, args: string[], options: ExecOptionsWithStringEncoding): Promise { // This must be run with interactive, otherwise there's a good chance aliases won't @@ -38,3 +40,30 @@ export async function execHelper(commandLine: string, options: ExecOptionsWithSt }); } +export async function getAliasesHelper(command: string, args: string[], regex: RegExp, options: ExecOptionsWithStringEncoding): Promise { + // This must be run with interactive, otherwise there's a good chance aliases won't + // be set up. Note that this could differ from the actual aliases as it's a new bash + // session, for the same reason this would not include aliases that are created + // by simply running `alias ...` in the terminal. + const aliasOutput = await spawnHelper(command, args, options); + const result: ICompletionResource[] = []; + for (const line of aliasOutput.split('\n')) { + const match = line.match(regex); + if (!match?.groups) { + continue; + } + let definitionCommand = ''; + let definitionIndex = match.groups.resolved.indexOf(' '); + if (definitionIndex === -1) { + definitionIndex = match.groups.resolved.length; + } + definitionCommand = match.groups.resolved.substring(0, definitionIndex); + result.push({ + label: match.groups.alias, + detail: match.groups.resolved, + kind: vscode.TerminalCompletionItemKind.Alias, + definitionCommand, + }); + } + return result; +} diff --git a/extensions/terminal-suggest/src/shell/fish.ts b/extensions/terminal-suggest/src/shell/fish.ts index 5341a5e5a4f6..cc2fbaff6e0c 100644 --- a/extensions/terminal-suggest/src/shell/fish.ts +++ b/extensions/terminal-suggest/src/shell/fish.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; import type { ICompletionResource } from '../types'; -import { execHelper, spawnHelper } from './common'; +import { execHelper, getAliasesHelper } from './common'; import { type ExecOptionsWithStringEncoding } from 'node:child_process'; export async function getFishGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { @@ -22,29 +21,5 @@ async function getBuiltins(options: ExecOptionsWithStringEncoding, existingComma } async function getAliases(options: ExecOptionsWithStringEncoding): Promise { - // This must be run with interactive, otherwise there's a good chance aliases won't - // be set up. Note that this could differ from the actual aliases as it's a new bash - // session, for the same reason this would not include aliases that are created - // by simply running `alias ...` in the terminal. - const aliasOutput = await spawnHelper('fish', ['-ic', 'alias'], options); - const result: ICompletionResource[] = []; - for (const line of aliasOutput.split('\n')) { - const match = line.match(/^alias (?[a-zA-Z0-9\.:-]+) (?.+)$/); - if (!match?.groups) { - continue; - } - let definitionCommand = ''; - let definitionIndex = match.groups.resolved.indexOf(' '); - if (definitionIndex === -1) { - definitionIndex = match.groups.resolved.length; - } - definitionCommand = match.groups.resolved.substring(0, definitionIndex); - result.push({ - label: match.groups.alias, - detail: match.groups.resolved, - kind: vscode.TerminalCompletionItemKind.Alias, - definitionCommand, - }); - } - return result; + return getAliasesHelper('fish', ['-ic', 'alias'], /^alias (?[a-zA-Z0-9\.:-]+) (?.+)$/, options); } diff --git a/extensions/terminal-suggest/src/shell/zsh.ts b/extensions/terminal-suggest/src/shell/zsh.ts index eb04464be68d..1530c5de5865 100644 --- a/extensions/terminal-suggest/src/shell/zsh.ts +++ b/extensions/terminal-suggest/src/shell/zsh.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; import type { ICompletionResource } from '../types'; -import { execHelper, spawnHelper } from './common'; +import { execHelper, getAliasesHelper } from './common'; import { type ExecOptionsWithStringEncoding } from 'node:child_process'; export async function getZshGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { @@ -22,29 +21,5 @@ async function getBuiltins(options: ExecOptionsWithStringEncoding, existingComma } async function getAliases(options: ExecOptionsWithStringEncoding): Promise { - // This must be run with interactive, otherwise there's a good chance aliases won't - // be set up. Note that this could differ from the actual aliases as it's a new bash - // session, for the same reason this would not include aliases that are created - // by simply running `alias ...` in the terminal. - const aliasOutput = await spawnHelper('zsh', ['-ic', 'alias'], options); - const result: ICompletionResource[] = []; - for (const line of aliasOutput.split('\n')) { - const match = line.match(/^(?[a-zA-Z0-9\.:-]+)=(?:'(?.+)'|(?.+))$/); - if (!match?.groups) { - continue; - } - let definitionCommand = ''; - let definitionIndex = match.groups.resolved.indexOf(' '); - if (definitionIndex === -1) { - definitionIndex = match.groups.resolved.length; - } - definitionCommand = match.groups.resolved.substring(0, definitionIndex); - result.push({ - label: match.groups.alias, - detail: match.groups.resolved, - kind: vscode.TerminalCompletionItemKind.Alias, - definitionCommand, - }); - } - return result; + return getAliasesHelper('zsh', ['-ic', 'alias'], /^(?[a-zA-Z0-9\.:-]+)=(?:'(?.+)'|(?.+))$/, options); } From d1e94453d27b840729cce517bbe99cf1b8d92eaa Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Sun, 2 Feb 2025 14:34:08 -0300 Subject: [PATCH 1128/3587] Refactors observable logging (#239430) --- .../base/common/observableInternal/autorun.ts | 54 +++--- src/vs/base/common/observableInternal/base.ts | 72 +++++--- .../base/common/observableInternal/derived.ts | 25 +-- .../base/common/observableInternal/index.ts | 10 +- .../observableInternal/lazyObservableValue.ts | 6 +- .../consoleObservableLogger.ts} | 169 ++++++++---------- .../observableInternal/logging/logging.ts | 129 +++++++++++++ .../base/common/observableInternal/utils.ts | 10 +- 8 files changed, 300 insertions(+), 175 deletions(-) rename src/vs/base/common/observableInternal/{logging.ts => logging/consoleObservableLogger.ts} (70%) create mode 100644 src/vs/base/common/observableInternal/logging/logging.ts diff --git a/src/vs/base/common/observableInternal/autorun.ts b/src/vs/base/common/observableInternal/autorun.ts index d2425e110121..704353fe6cc9 100644 --- a/src/vs/base/common/observableInternal/autorun.ts +++ b/src/vs/base/common/observableInternal/autorun.ts @@ -6,7 +6,7 @@ import { IChangeContext, IObservable, IObservableWithChange, IObserver, IReader } from './base.js'; import { DebugNameData, IDebugNameData } from './debugName.js'; import { assertFn, BugIndicatingError, DisposableStore, IDisposable, markAsDisposed, onBugIndicatingError, toDisposable, trackDisposable } from './commonFacade/deps.js'; -import { getLogger } from './logging.js'; +import { getLogger } from './logging/logging.js'; /** * Runs immediately and whenever a transaction ends and an observed observable changed. @@ -153,9 +153,7 @@ export function autorunIterableDelta( }); } - - -const enum AutorunState { +export const enum AutorunState { /** * A dependency could have changed. * We need to explicitly ask them if at least one dependency changed. @@ -176,6 +174,7 @@ export class AutorunObserver implements IObserver, IReader private dependencies = new Set>(); private dependenciesToBeRemoved = new Set>(); private changeSummary: TChangeSummary | undefined; + private _isRunning = false; public get debugName(): string { return this._debugNameData.getDebugName(this) ?? '(anonymous)'; @@ -197,10 +196,11 @@ export class AutorunObserver implements IObserver, IReader public dispose(): void { this.disposed = true; for (const o of this.dependencies) { - o.removeObserver(this); + o.removeObserver(this); // Warning: external call! } this.dependencies.clear(); + getLogger()?.handleAutorunDisposed(this); markAsDisposed(this); } @@ -215,29 +215,28 @@ export class AutorunObserver implements IObserver, IReader this.state = AutorunState.upToDate; - const isDisposed = this.disposed; try { - if (!isDisposed) { - getLogger()?.handleAutorunTriggered(this); + if (!this.disposed) { + getLogger()?.handleAutorunStarted(this); const changeSummary = this.changeSummary!; try { - this.changeSummary = this.createChangeSummary?.(); - this._isReaderValid = true; - this._runFn(this, changeSummary); + this.changeSummary = this.createChangeSummary?.(); // Warning: external call! + this._isRunning = true; + this._runFn(this, changeSummary); // Warning: external call! } catch (e) { onBugIndicatingError(e); } finally { - this._isReaderValid = false; + this._isRunning = false; } } } finally { - if (!isDisposed) { + if (!this.disposed) { getLogger()?.handleAutorunFinished(this); } // We don't want our observed observables to think that they are (not even temporarily) not being observed. // Thus, we only unsubscribe from observables that are definitely not read anymore. for (const o of this.dependenciesToBeRemoved) { - o.removeObserver(this); + o.removeObserver(this); // Warning: external call! } this.dependenciesToBeRemoved.clear(); } @@ -248,21 +247,21 @@ export class AutorunObserver implements IObserver, IReader } // IObserver implementation - public beginUpdate(): void { + public beginUpdate(_observable: IObservable): void { if (this.state === AutorunState.upToDate) { this.state = AutorunState.dependenciesMightHaveChanged; } this.updateCount++; } - public endUpdate(): void { + public endUpdate(_observable: IObservable): void { try { if (this.updateCount === 1) { do { if (this.state === AutorunState.dependenciesMightHaveChanged) { this.state = AutorunState.upToDate; for (const d of this.dependencies) { - d.reportChanges(); + d.reportChanges(); // Warning: external call! if (this.state as AutorunState === AutorunState.stale) { // The other dependencies will refresh on demand break; @@ -270,7 +269,7 @@ export class AutorunObserver implements IObserver, IReader } } - this._runIfNeeded(); + this._runIfNeeded(); // Warning: indirect external call! } while (this.state !== AutorunState.upToDate); } } finally { @@ -281,14 +280,16 @@ export class AutorunObserver implements IObserver, IReader } public handlePossibleChange(observable: IObservable): void { - if (this.state === AutorunState.upToDate && this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) { + if (this.state === AutorunState.upToDate && this._isDependency(observable)) { this.state = AutorunState.dependenciesMightHaveChanged; } } public handleChange(observable: IObservableWithChange, change: TChange): void { - if (this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) { + if (this._isDependency(observable)) { + getLogger()?.handleAutorunDependencyChanged(this, observable, change); try { + // Warning: external call! const shouldReact = this._handleChange ? this._handleChange({ changedObservable: observable, change, @@ -303,19 +304,22 @@ export class AutorunObserver implements IObserver, IReader } } + private _isDependency(observable: IObservableWithChange): boolean { + return this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable); + } + // IReader implementation - private _isReaderValid = false; public readObservable(observable: IObservable): T { - if (!this._isReaderValid) { throw new BugIndicatingError('The reader object cannot be used outside its compute function!'); } + if (!this._isRunning) { throw new BugIndicatingError('The reader object cannot be used outside its compute function!'); } // In case the run action disposes the autorun if (this.disposed) { - return observable.get(); + return observable.get(); // warning: external call! } - observable.addObserver(this); - const value = observable.get(); + observable.addObserver(this); // warning: external call! + const value = observable.get(); // warning: external call! this.dependencies.add(observable); this.dependenciesToBeRemoved.delete(observable); return value; diff --git a/src/vs/base/common/observableInternal/base.ts b/src/vs/base/common/observableInternal/base.ts index f2aa0466f783..04378d92552d 100644 --- a/src/vs/base/common/observableInternal/base.ts +++ b/src/vs/base/common/observableInternal/base.ts @@ -6,7 +6,7 @@ import { DebugNameData, DebugOwner, getFunctionName } from './debugName.js'; import { DisposableStore, EqualityComparer, IDisposable, strictEquals } from './commonFacade/deps.js'; import type { derivedOpts } from './derived.js'; -import { getLogger, logObservable } from './logging.js'; +import { getLogger, logObservable } from './logging/logging.js'; import { keepObserved, recomputeInitiallyAndOnChange } from './utils.js'; /** @@ -14,6 +14,8 @@ import { keepObserved, recomputeInitiallyAndOnChange } from './utils.js'; * * @template T The type of the values the observable can hold. */ +// This interface exists so that, for example for string observables, +// typescript renders the type as `IObservable` instead of `IObservable`. export interface IObservable extends IObservableWithChange { } /** @@ -56,6 +58,8 @@ export interface IObservableWithChange { */ removeObserver(observer: IObserver): void; + // #region These members have a standard implementation and are only part of the interface for convenience. + /** * Reads the current value and subscribes the reader to this observable. * @@ -64,6 +68,16 @@ export interface IObservableWithChange { */ read(reader: IReader | undefined): T; + /** + * Makes sure this value is computed eagerly. + */ + recomputeInitiallyAndOnChange(store: DisposableStore, handleValue?: (value: T) => void): IObservable; + + /** + * Makes sure this value is cached. + */ + keepObserved(store: DisposableStore): IObservable; + /** * Creates a derived observable that depends on this observable. * Use the reader to read other observables @@ -80,16 +94,6 @@ export interface IObservableWithChange { */ log(): IObservableWithChange; - /** - * Makes sure this value is computed eagerly. - */ - recomputeInitiallyAndOnChange(store: DisposableStore, handleValue?: (value: T) => void): IObservable; - - /** - * Makes sure this value is cached. - */ - keepObserved(store: DisposableStore): IObservable; - /** * A human-readable name for debugging purposes. */ @@ -99,13 +103,8 @@ export interface IObservableWithChange { * This property captures the type of the change object. Do not use it at runtime! */ readonly TChange: TChange; -} -export interface IReader { - /** - * Reads the value of an observable and subscribes to it. - */ - readObservable(observable: IObservableWithChange): T; + // #endregion } /** @@ -153,6 +152,13 @@ export interface IObserver { handleChange(observable: IObservableWithChange, change: TChange): void; } +export interface IReader { + /** + * Reads the value of an observable and subscribes to it. + */ + readObservable(observable: IObservableWithChange): T; +} + export interface ISettable { /** * Sets the value of the observable. @@ -182,7 +188,6 @@ export function _setKeepObserved(keepObserved: typeof _keepObserved) { _keepObserved = keepObserved; } - let _derived: typeof derivedOpts; /** * @internal @@ -246,10 +251,7 @@ export abstract class ConvenientObservable implements IObservableWit ); } - public log(): IObservableWithChange { - logObservable(this); - return this; - } + public abstract log(): IObservableWithChange; /** * @sealed @@ -290,12 +292,20 @@ export abstract class ConvenientObservable implements IObservableWit export abstract class BaseObservable extends ConvenientObservable { protected readonly observers = new Set(); + constructor() { + super(); + getLogger()?.handleObservableCreated(this); + } + public addObserver(observer: IObserver): void { const len = this.observers.size; this.observers.add(observer); if (len === 0) { this.onFirstObserverAdded(); } + if (len !== this.observers.size) { + getLogger()?.handleOnListenerCountChanged(this, this.observers.size); + } } public removeObserver(observer: IObserver): void { @@ -303,10 +313,22 @@ export abstract class BaseObservable extends ConvenientObserv if (deleted && this.observers.size === 0) { this.onLastObserverRemoved(); } + if (deleted) { + getLogger()?.handleOnListenerCountChanged(this, this.observers.size); + } } protected onFirstObserverAdded(): void { } protected onLastObserverRemoved(): void { } + + public override log(): IObservableWithChange { + const hadLogger = !!getLogger(); + logObservable(this); + if (!hadLogger) { + getLogger()?.handleObservableCreated(this); + } + return this; + } } /** @@ -390,7 +412,7 @@ export class TransactionImpl implements ITransaction { } // Prevent anyone from updating observers from now on. this.updatingObservers = null; - getLogger()?.handleEndTransaction(); + getLogger()?.handleEndTransaction(this); } } @@ -434,6 +456,8 @@ export class ObservableValue ) { super(); this._value = initialValue; + + getLogger()?.handleObservableUpdated(this, { hadValue: false, newValue: initialValue, change: undefined, didChange: true, oldValue: undefined }); } public override get(): T { return this._value; @@ -451,7 +475,7 @@ export class ObservableValue try { const oldValue = this._value; this._setValue(value); - getLogger()?.handleObservableChanged(this, { oldValue, newValue: value, change, didChange: true, hadValue: true }); + getLogger()?.handleObservableUpdated(this, { oldValue, newValue: value, change, didChange: true, hadValue: true }); for (const observer of this.observers) { tx.updateObserver(observer, this); diff --git a/src/vs/base/common/observableInternal/derived.ts b/src/vs/base/common/observableInternal/derived.ts index 54a1e99296db..ce1bbd7878bc 100644 --- a/src/vs/base/common/observableInternal/derived.ts +++ b/src/vs/base/common/observableInternal/derived.ts @@ -6,7 +6,7 @@ import { BaseObservable, IChangeContext, IObservable, IObservableWithChange, IObserver, IReader, ISettableObservable, ITransaction, _setDerivedOpts, } from './base.js'; import { DebugNameData, DebugOwner, IDebugNameData } from './debugName.js'; import { BugIndicatingError, DisposableStore, EqualityComparer, IDisposable, assertFn, onBugIndicatingError, strictEquals } from './commonFacade/deps.js'; -import { getLogger } from './logging.js'; +import { getLogger } from './logging/logging.js'; /** * Creates an observable that is derived from other observables. @@ -164,7 +164,7 @@ export function derivedDisposable(computeFnOr ); } -const enum DerivedState { +export const enum DerivedState { /** Initial state, no previous value, recomputation needed */ initial = 0, @@ -210,7 +210,6 @@ export class Derived extends BaseObservable im ) { super(); this.changeSummary = this.createChangeSummary?.(); - getLogger()?.handleDerivedCreated(this); } protected override onLastObserverRemoved(): void { @@ -230,7 +229,9 @@ export class Derived extends BaseObservable im } public override get(): T { - if (this._isComputing) { + const checkEnabled = false; // TODO set to true + if (this._isComputing && checkEnabled) { + // investigate why this fails in the diff editor! throw new BugIndicatingError('Cyclic deriveds are not supported yet!'); } @@ -291,7 +292,7 @@ export class Derived extends BaseObservable im let didChange = false; - this._isComputing = false; // TODO@hediet: Set to true and investigate diff editor scrolling issues! (also see test.skip('catches cyclic dependencies') + this._isComputing = true; try { const changeSummary = this.changeSummary!; @@ -312,7 +313,7 @@ export class Derived extends BaseObservable im didChange = hadValue && !(this._equalityComparator(oldValue!, this.value)); - getLogger()?.handleDerivedRecomputed(this, { + getLogger()?.handleObservableUpdated(this, { oldValue, newValue: this.value, change: undefined, @@ -399,6 +400,8 @@ export class Derived extends BaseObservable im public handleChange(observable: IObservableWithChange, change: TChange): void { if (this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) { + getLogger()?.handleDerivedDependencyChanged(this, observable, change); + let shouldReact = false; try { shouldReact = this._handleChange ? this._handleChange({ @@ -460,16 +463,6 @@ export class Derived extends BaseObservable im } super.removeObserver(observer); } - - public override log(): IObservableWithChange { - if (!getLogger()) { - super.log(); - getLogger()?.handleDerivedCreated(this); - } else { - super.log(); - } - return this; - } } diff --git a/src/vs/base/common/observableInternal/index.ts b/src/vs/base/common/observableInternal/index.ts index 873cf946171c..85d4cc158f87 100644 --- a/src/vs/base/common/observableInternal/index.ts +++ b/src/vs/base/common/observableInternal/index.ts @@ -14,10 +14,10 @@ export { derivedWithCancellationToken, waitForState } from './utilsCancellation. export { constObservable, debouncedObservable, derivedConstOnceDefined, derivedObservableWithCache, derivedObservableWithWritableCache, keepObserved, latestChangedValue, mapObservableArrayCached, observableFromEvent, observableFromEventOpts, observableFromPromise, observableFromValueWithChangeEvent, observableSignal, observableSignalFromEvent, recomputeInitiallyAndOnChange, runOnChange, runOnChangeWithStore, signalFromObservable, ValueWithChangeEventFromObservable, wasEventTriggeredRecently, type IObservableSignal, } from './utils.js'; export { type DebugOwner } from './debugName.js'; -import { - ConsoleObservableLogger, - setLogger -} from './logging.js'; +import { addLogger, setLogObservableFn } from './logging/logging.js'; +import { ConsoleObservableLogger, logObservableToConsole } from './logging/consoleObservableLogger.js'; + +setLogObservableFn(logObservableToConsole); // Remove "//" in the next line to enable logging const enableLogging = false @@ -25,5 +25,5 @@ const enableLogging = false ; if (enableLogging) { - setLogger(new ConsoleObservableLogger()); + addLogger(new ConsoleObservableLogger()); } diff --git a/src/vs/base/common/observableInternal/lazyObservableValue.ts b/src/vs/base/common/observableInternal/lazyObservableValue.ts index 6c0f85aa8767..7b6697210ef4 100644 --- a/src/vs/base/common/observableInternal/lazyObservableValue.ts +++ b/src/vs/base/common/observableInternal/lazyObservableValue.ts @@ -6,7 +6,7 @@ import { EqualityComparer } from './commonFacade/deps.js'; import { BaseObservable, IObserver, ISettableObservable, ITransaction, TransactionImpl } from './base.js'; import { DebugNameData } from './debugName.js'; -import { getLogger } from './logging.js'; +import { getLogger } from './logging/logging.js'; /** * Holds off updating observers until the value is actually read. @@ -44,14 +44,14 @@ export class LazyObservableValue if (this._deltas.length > 0) { for (const change of this._deltas) { - getLogger()?.handleObservableChanged(this, { change, didChange: true, oldValue: '(unknown)', newValue: this._value, hadValue: true }); + getLogger()?.handleObservableUpdated(this, { change, didChange: true, oldValue: '(unknown)', newValue: this._value, hadValue: true }); for (const observer of this.observers) { observer.handleChange(this, change); } } this._deltas.length = 0; } else { - getLogger()?.handleObservableChanged(this, { change: undefined, didChange: true, oldValue: '(unknown)', newValue: this._value, hadValue: true }); + getLogger()?.handleObservableUpdated(this, { change: undefined, didChange: true, oldValue: '(unknown)', newValue: this._value, hadValue: true }); for (const observer of this.observers) { observer.handleChange(this, undefined); } diff --git a/src/vs/base/common/observableInternal/logging.ts b/src/vs/base/common/observableInternal/logging/consoleObservableLogger.ts similarity index 70% rename from src/vs/base/common/observableInternal/logging.ts rename to src/vs/base/common/observableInternal/logging/consoleObservableLogger.ts index f0bc82170d8e..325787d633d1 100644 --- a/src/vs/base/common/observableInternal/logging.ts +++ b/src/vs/base/common/observableInternal/logging/consoleObservableLogger.ts @@ -3,55 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AutorunObserver } from './autorun.js'; -import { IObservable, TransactionImpl } from './base.js'; -import { Derived } from './derived.js'; -import { FromEventObservable } from './utils.js'; - -let globalObservableLogger: IObservableLogger | undefined; - -export function setLogger(logger: IObservableLogger): void { - globalObservableLogger = logger; -} - -export function getLogger(): IObservableLogger | undefined { - return globalObservableLogger; -} - -export function logObservable(obs: IObservable): void { - if (!globalObservableLogger) { - const l = new ConsoleObservableLogger(); - l.addFilteredObj(obs); - setLogger(l); - } else { - if (globalObservableLogger instanceof ConsoleObservableLogger) { - (globalObservableLogger as ConsoleObservableLogger).addFilteredObj(obs); - } +import { AutorunObserver } from '../autorun.js'; +import { IObservable, TransactionImpl } from '../base.js'; +import { Derived } from '../derived.js'; +import { IObservableLogger, IChangeInformation, addLogger } from './logging.js'; +import { FromEventObservable } from '../utils.js'; + +let consoleObservableLogger: ConsoleObservableLogger | undefined; + +export function logObservableToConsole(obs: IObservable): void { + if (!consoleObservableLogger) { + consoleObservableLogger = new ConsoleObservableLogger(); + addLogger(consoleObservableLogger); } -} - -interface IChangeInformation { - oldValue: unknown; - newValue: unknown; - change: unknown; - didChange: boolean; - hadValue: boolean; -} - -export interface IObservableLogger { - handleObservableChanged(observable: IObservable, info: IChangeInformation): void; - handleFromEventObservableTriggered(observable: FromEventObservable, info: IChangeInformation): void; - - handleAutorunCreated(autorun: AutorunObserver): void; - handleAutorunTriggered(autorun: AutorunObserver): void; - handleAutorunFinished(autorun: AutorunObserver): void; - - handleDerivedCreated(observable: Derived): void; - handleDerivedRecomputed(observable: Derived, info: IChangeInformation): void; - handleDerivedCleared(observable: Derived): void; - - handleBeginTransaction(transaction: TransactionImpl): void; - handleEndTransaction(): void; + consoleObservableLogger.addFilteredObj(obs); } export class ConsoleObservableLogger implements IObservableLogger { @@ -102,8 +67,45 @@ export class ConsoleObservableLogger implements IObservableLogger { : [normalText(` (unchanged)`)]; } - handleObservableChanged(observable: IObservable, info: IChangeInformation): void { + handleObservableCreated(observable: IObservable): void { + if (observable instanceof Derived) { + const derived = observable; + this.changedObservablesSets.set(derived, new Set()); + + const debugTrackUpdating = false; + if (debugTrackUpdating) { + const updating: IObservable[] = []; + (derived as any).__debugUpdating = updating; + + const existingBeginUpdate = derived.beginUpdate; + derived.beginUpdate = (obs) => { + updating.push(obs); + return existingBeginUpdate.apply(derived, [obs]); + }; + + const existingEndUpdate = derived.endUpdate; + derived.endUpdate = (obs) => { + const idx = updating.indexOf(obs); + if (idx === -1) { + console.error('endUpdate called without beginUpdate', derived.debugName, obs.debugName); + } + updating.splice(idx, 1); + return existingEndUpdate.apply(derived, [obs]); + }; + } + } + } + + handleOnListenerCountChanged(observable: IObservable, newCount: number): void { + } + + handleObservableUpdated(observable: IObservable, info: IChangeInformation): void { if (!this._isIncluded(observable)) { return; } + if (observable instanceof Derived) { + this._handleDerivedRecomputed(observable, info); + return; + } + console.log(...this.textToConsoleArgs([ formatKind('observable value changed'), styled(observable.debugName, { color: 'BlueViolet' }), @@ -125,38 +127,13 @@ export class ConsoleObservableLogger implements IObservableLogger { ); } - handleDerivedCreated(derived: Derived): void { - const existingHandleChange = derived.handleChange; - this.changedObservablesSets.set(derived, new Set()); - derived.handleChange = (observable, change) => { - this.changedObservablesSets.get(derived)!.add(observable); - return existingHandleChange.apply(derived, [observable, change]); - }; - - const debugTrackUpdating = false; - if (debugTrackUpdating) { - const updating: IObservable[] = []; - (derived as any).__debugUpdating = updating; - - const existingBeginUpdate = derived.beginUpdate; - derived.beginUpdate = (obs) => { - updating.push(obs); - return existingBeginUpdate.apply(derived, [obs]); - }; - - const existingEndUpdate = derived.endUpdate; - derived.endUpdate = (obs) => { - const idx = updating.indexOf(obs); - if (idx === -1) { - console.error('endUpdate called without beginUpdate', derived.debugName, obs.debugName); - } - updating.splice(idx, 1); - return existingEndUpdate.apply(derived, [obs]); - }; - } + handleDerivedDependencyChanged(derived: Derived, observable: IObservable, change: unknown): void { + if (!this._isIncluded(derived)) { return; } + + this.changedObservablesSets.get(derived)?.add(observable); } - handleDerivedRecomputed(derived: Derived, info: IChangeInformation): void { + _handleDerivedRecomputed(derived: Derived, info: IChangeInformation): void { if (!this._isIncluded(derived)) { return; } const changedObservables = this.changedObservablesSets.get(derived); @@ -194,15 +171,19 @@ export class ConsoleObservableLogger implements IObservableLogger { handleAutorunCreated(autorun: AutorunObserver): void { if (!this._isIncluded(autorun)) { return; } - const existingHandleChange = autorun.handleChange; this.changedObservablesSets.set(autorun, new Set()); - autorun.handleChange = (observable, change) => { - this.changedObservablesSets.get(autorun)!.add(observable); - return existingHandleChange.apply(autorun, [observable, change]); - }; } - handleAutorunTriggered(autorun: AutorunObserver): void { + handleAutorunDisposed(autorun: AutorunObserver): void { + } + + handleAutorunDependencyChanged(autorun: AutorunObserver, observable: IObservable, change: unknown): void { + if (!this._isIncluded(autorun)) { return; } + + this.changedObservablesSets.get(autorun)!.add(observable); + } + + handleAutorunStarted(autorun: AutorunObserver): void { const changedObservables = this.changedObservablesSets.get(autorun); if (!changedObservables) { return; } @@ -241,12 +222,9 @@ export class ConsoleObservableLogger implements IObservableLogger { this.indentation--; } } - -type ConsoleText = - | (ConsoleText | undefined)[] - | { text: string; style: string; data?: unknown[] } - | { data: unknown[] }; - +type ConsoleText = (ConsoleText | undefined)[] | +{ text: string; style: string; data?: unknown[] } | +{ data: unknown[] }; function consoleTextToArgs(text: ConsoleText): unknown[] { const styles = new Array(); const data: unknown[] = []; @@ -276,15 +254,12 @@ function consoleTextToArgs(text: ConsoleText): unknown[] { result.push(...data); return result; } - function normalText(text: string): ConsoleText { return styled(text, { color: 'black' }); } - function formatKind(kind: string): ConsoleText { return styled(padStr(`${kind}: `, 10), { color: 'black', bold: true }); } - function styled( text: string, options: { color: string; strikeThrough?: boolean; bold?: boolean } = { @@ -316,7 +291,7 @@ function styled( }; } -function formatValue(value: unknown, availableLen: number): string { +export function formatValue(value: unknown, availableLen: number): string { switch (typeof value) { case 'number': return '' + value; @@ -346,7 +321,6 @@ function formatValue(value: unknown, availableLen: number): string { return '' + value; } } - function formatArray(value: unknown[], availableLen: number): string { let result = '[ '; let first = true; @@ -364,7 +338,6 @@ function formatArray(value: unknown[], availableLen: number): string { result += ' ]'; return result; } - function formatObject(value: object, availableLen: number): string { if (typeof value.toString === 'function' && value.toString !== Object.prototype.toString) { const val = value.toString(); @@ -390,7 +363,6 @@ function formatObject(value: object, availableLen: number): string { result += ' }'; return result; } - function repeat(str: string, count: number): string { let result = ''; for (let i = 1; i <= count; i++) { @@ -398,7 +370,6 @@ function repeat(str: string, count: number): string { } return result; } - function padStr(str: string, length: number): string { while (str.length < length) { str += ' '; diff --git a/src/vs/base/common/observableInternal/logging/logging.ts b/src/vs/base/common/observableInternal/logging/logging.ts new file mode 100644 index 000000000000..dd6ba48416dc --- /dev/null +++ b/src/vs/base/common/observableInternal/logging/logging.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AutorunObserver } from '../autorun.js'; +import { IObservable, TransactionImpl } from '../base.js'; +import type { Derived } from '../derived.js'; + +let globalObservableLogger: IObservableLogger | undefined; + +export function addLogger(logger: IObservableLogger): void { + if (!globalObservableLogger) { + globalObservableLogger = logger; + } else if (globalObservableLogger instanceof ComposedLogger) { + globalObservableLogger.loggers.push(logger); + } else { + globalObservableLogger = new ComposedLogger([globalObservableLogger, logger]); + } +} + +export function getLogger(): IObservableLogger | undefined { + return globalObservableLogger; +} + +let globalObservableLoggerFn: ((obs: IObservable) => void) | undefined = undefined; +export function setLogObservableFn(fn: (obs: IObservable) => void): void { + globalObservableLoggerFn = fn; +} + +export function logObservable(obs: IObservable): void { + if (globalObservableLoggerFn) { + globalObservableLoggerFn(obs); + } +} + +export interface IChangeInformation { + oldValue: unknown; + newValue: unknown; + change: unknown; + didChange: boolean; + hadValue: boolean; +} + +export interface IObservableLogger { + handleObservableCreated(observable: IObservable): void; + handleOnListenerCountChanged(observable: IObservable, newCount: number): void; + + handleObservableUpdated(observable: IObservable, info: IChangeInformation): void; + + handleAutorunCreated(autorun: AutorunObserver): void; + handleAutorunDisposed(autorun: AutorunObserver): void; + handleAutorunDependencyChanged(autorun: AutorunObserver, observable: IObservable, change: unknown): void; + handleAutorunStarted(autorun: AutorunObserver): void; + handleAutorunFinished(autorun: AutorunObserver): void; + + handleDerivedDependencyChanged(derived: Derived, observable: IObservable, change: unknown): void; + handleDerivedCleared(observable: Derived): void; + + handleBeginTransaction(transaction: TransactionImpl): void; + handleEndTransaction(transaction: TransactionImpl): void; +} + +class ComposedLogger implements IObservableLogger { + constructor( + public readonly loggers: IObservableLogger[], + ) { } + + handleObservableCreated(observable: IObservable): void { + for (const logger of this.loggers) { + logger.handleObservableCreated(observable); + } + } + handleOnListenerCountChanged(observable: IObservable, newCount: number): void { + for (const logger of this.loggers) { + logger.handleOnListenerCountChanged(observable, newCount); + } + } + handleObservableUpdated(observable: IObservable, info: IChangeInformation): void { + for (const logger of this.loggers) { + logger.handleObservableUpdated(observable, info); + } + } + handleAutorunCreated(autorun: AutorunObserver): void { + for (const logger of this.loggers) { + logger.handleAutorunCreated(autorun); + } + } + handleAutorunDisposed(autorun: AutorunObserver): void { + for (const logger of this.loggers) { + logger.handleAutorunDisposed(autorun); + } + } + handleAutorunDependencyChanged(autorun: AutorunObserver, observable: IObservable, change: unknown): void { + for (const logger of this.loggers) { + logger.handleAutorunDependencyChanged(autorun, observable, change); + } + } + handleAutorunStarted(autorun: AutorunObserver): void { + for (const logger of this.loggers) { + logger.handleAutorunStarted(autorun); + } + } + handleAutorunFinished(autorun: AutorunObserver): void { + for (const logger of this.loggers) { + logger.handleAutorunFinished(autorun); + } + } + handleDerivedDependencyChanged(derived: Derived, observable: IObservable, change: unknown): void { + for (const logger of this.loggers) { + logger.handleDerivedDependencyChanged(derived, observable, change); + } + } + handleDerivedCleared(observable: Derived): void { + for (const logger of this.loggers) { + logger.handleDerivedCleared(observable); + } + } + handleBeginTransaction(transaction: TransactionImpl): void { + for (const logger of this.loggers) { + logger.handleBeginTransaction(transaction); + } + } + handleEndTransaction(transaction: TransactionImpl): void { + for (const logger of this.loggers) { + logger.handleEndTransaction(transaction); + } + } +} diff --git a/src/vs/base/common/observableInternal/utils.ts b/src/vs/base/common/observableInternal/utils.ts index 56d3e692b495..9b12679d7c8f 100644 --- a/src/vs/base/common/observableInternal/utils.ts +++ b/src/vs/base/common/observableInternal/utils.ts @@ -8,7 +8,7 @@ import { BaseObservable, ConvenientObservable, IObservable, IObservableWithChang import { DebugNameData, DebugOwner, IDebugNameData, getDebugName, } from './debugName.js'; import { BugIndicatingError, DisposableStore, EqualityComparer, Event, IDisposable, IValueWithChangeEvent, strictEquals, toDisposable } from './commonFacade/deps.js'; import { derived, derivedOpts } from './derived.js'; -import { getLogger } from './logging.js'; +import { getLogger } from './logging/logging.js'; /** * Represents an efficient observable whose value never changes. @@ -36,6 +36,10 @@ class ConstObservable extends ConvenientObservable { // NO OP } + override log(): IObservableWithChange { + return this; + } + override toString(): string { return `Const: ${this.value}`; } @@ -140,7 +144,7 @@ export class FromEventObservable extends BaseObservable { subtransaction( this._getTransaction(), (tx) => { - getLogger()?.handleFromEventObservableTriggered(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue }); + getLogger()?.handleObservableUpdated(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue }); for (const o of this.observers) { tx.updateObserver(o, this); @@ -157,7 +161,7 @@ export class FromEventObservable extends BaseObservable { } if (!didRunTransaction) { - getLogger()?.handleFromEventObservableTriggered(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue }); + getLogger()?.handleObservableUpdated(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue }); } }; From e89638c11854e7ee6854156ac231b7f37b97d61c Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sun, 2 Feb 2025 11:51:22 -0800 Subject: [PATCH 1129/3587] Add new agent setting and experiment (#239439) Add new agent setting and experiment (#239277) * Contribute agent setting from vscode, with experiment to control visibility * Check agent experiment when submitting request, too * log * Fix race in test * Add separate context key mirroring the experiment, so the picker can be hidden even when the user set the setting --- .../contrib/chat/browser/chat.contribution.ts | 64 ++++++++++++++++++- .../contrib/chat/common/chatContextKeys.ts | 1 + .../contrib/chat/common/chatServiceImpl.ts | 14 +++- .../test/browser/inlineChatController.test.ts | 3 +- 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 269b6536eab8..f570cd3641de 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -13,13 +13,13 @@ import { registerEditorFeature } from '../../../../editor/common/editorFeatures. import * as nls from '../../../../nls.js'; import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationNode, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; -import { WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; +import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; import { EditorExtensions, IEditorFactoryRegistry } from '../../../common/editor.js'; import { IEditorResolverService, RegisteredEditorPriority } from '../../../services/editor/common/editorResolverService.js'; import { ChatAgentLocation, ChatAgentNameService, ChatAgentService, IChatAgentNameService, IChatAgentService } from '../common/chatAgents.js'; @@ -84,6 +84,10 @@ import { ChatEditorOverlayController } from './chatEditorOverlay.js'; import '../common/promptSyntax/languageFeatures/promptLinkProvider.js'; import { PromptFilesConfig } from '../common/promptSyntax/config.js'; import { BuiltinToolsContribution } from '../common/tools/tools.js'; +import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ChatContextKeys } from '../common/chatContextKeys.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -212,6 +216,61 @@ class ChatResolverContribution extends Disposable { } } +class ChatAgentSettingContribution implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.chatAgentSetting'; + + private registeredNode: IConfigurationNode | undefined; + + constructor( + @IWorkbenchAssignmentService experimentService: IWorkbenchAssignmentService, + @IProductService private readonly productService: IProductService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + if (this.productService.quality !== 'stable') { + this.registerSetting(); + } + + const expDisabledKey = ChatContextKeys.Editing.agentModeDisallowed.bindTo(contextKeyService); + experimentService.getTreatment('chatAgentEnabled').then(value => { + if (value) { + this.registerSetting(); + } else if (value === false) { + this.deregisterSetting(); + expDisabledKey.set(true); + } + }); + } + + private registerSetting() { + if (this.registeredNode) { + return; + } + + this.registeredNode = { + id: 'chatAgent', + title: nls.localize('interactiveSessionConfigurationTitle', "Chat"), + type: 'object', + properties: { + 'chat.agent.enabled': { + type: 'boolean', + description: nls.localize('chat.agent.enabled.description', "Enable agent mode for {0}. When this is enabled, a dropdown appears in the {0} view to toggle agent mode.", 'Copilot Edits'), + default: this.productService.quality !== 'stable', + tags: ['experimental', 'onExp'], + }, + } + }; + configurationRegistry.registerConfiguration(this.registeredNode); + } + + private deregisterSetting() { + if (this.registeredNode) { + configurationRegistry.deregisterConfigurations([this.registeredNode]); + this.registeredNode = undefined; + } + } +} + AccessibleViewRegistry.register(new ChatResponseAccessibleView()); AccessibleViewRegistry.register(new PanelChatAccessibilityHelp()); AccessibleViewRegistry.register(new QuickChatAccessibilityHelp()); @@ -329,6 +388,7 @@ registerWorkbenchContribution2(ChatGettingStartedContribution.ID, ChatGettingSta registerWorkbenchContribution2(ChatSetupContribution.ID, ChatSetupContribution, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatQuotasStatusBarEntry.ID, ChatQuotasStatusBarEntry, WorkbenchPhase.Eventually); registerWorkbenchContribution2(BuiltinToolsContribution.ID, BuiltinToolsContribution, WorkbenchPhase.Eventually); +registerWorkbenchContribution2(ChatAgentSettingContribution.ID, ChatAgentSettingContribution, WorkbenchPhase.BlockRestore); registerChatActions(); registerChatCopyActions(); diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 7723d67602f1..4a2798b9f0eb 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -83,5 +83,6 @@ export namespace ChatContextKeys { export const Editing = { hasToolsAgent: new RawContextKey('chatHasToolsAgent', false, { type: 'boolean', description: localize('chatEditingHasToolsAgent', "True when a tools agent is registered.") }), agentMode: new RawContextKey('chatAgentMode', false, { type: 'boolean', description: localize('chatEditingAgentMode', "True when edits is in agent mode.") }), + agentModeDisallowed: new RawContextKey('chatAgentModeDisallowed', false, { type: 'boolean', description: localize('chatAgentModeDisallowed', "True when agent mode is not allowed.") }), // experiment-driven disablement }; } diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 6e2741852cf6..f6f352449499 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -23,6 +23,7 @@ import { Progress } from '../../../../platform/progress/common/progress.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from './chatAgents.js'; import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, normalizeSerializableChatData, toChatHistoryContent, updateRanges } from './chatModel.js'; @@ -138,7 +139,8 @@ export class ChatService extends Disposable implements IChatService { @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, @IChatAgentService private readonly chatAgentService: IChatAgentService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, ) { super(); @@ -687,6 +689,7 @@ export class ChatService extends Disposable implements IChatService { const agent = (detectedAgent ?? agentPart?.agent ?? defaultAgent)!; const command = detectedCommand ?? agentSlashCommandPart?.command; await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`); + await this.checkAgentAllowed(agent); // Recompute history in case the agent or command changed const history = this.getHistoryEntriesFromModel(requests, model.sessionId, location, agent.id); @@ -811,6 +814,15 @@ export class ChatService extends Disposable implements IChatService { }; } + private async checkAgentAllowed(agent: IChatAgentData): Promise { + if (agent.isToolsAgent) { + const enabled = await this.experimentService.getTreatment('chatAgentEnabled'); + if (enabled === false) { + throw new Error('Agent is currently disabled'); + } + } + } + private attachmentKindsForTelemetry(variableData: IChatRequestVariableData): string[] { // TODO this shows why attachments still have to be cleaned up somewhat return variableData.variables.map(v => { diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index d8d8ce7467ce..10d38f3cc79a 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -841,8 +841,7 @@ suite('InlineChatController', function () { const newSession = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); assertType(newSession); - await chatService.sendRequest(newSession.chatModel.sessionId, 'Existing', { location: ChatAgentLocation.Editor }); - + await (await chatService.sendRequest(newSession.chatModel.sessionId, 'Existing', { location: ChatAgentLocation.Editor }))?.responseCreatedPromise; assert.strictEqual(newSession.chatModel.requestInProgress, true); From 201b6506c9f63340c906883de5b719136397456c Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sun, 2 Feb 2025 11:52:09 -0800 Subject: [PATCH 1130/3587] Fix showing wrong welcome view content when offline (#239438) Fix showing wrong welcome view content when offline (#239365) Only apply agent content override hack in chatwidget welcome content, not contributed welcome views Fix microsoft/vscode-copilot#12726 --- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 5 ++++- .../chat/browser/viewsWelcome/chatViewWelcomeController.ts | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index d614ffcf1be8..2d4a9c8ce4b6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -630,7 +630,10 @@ export class ChatWidget extends Disposable implements IChatWidget { const welcomePart = this._register(this.instantiationService.createInstance( ChatViewWelcomePart, { ...welcomeContent, tips, }, - { location: this.location } + { + location: this.location, + isWidgetWelcomeViewContent: true + } )); dom.append(this.welcomeMessageContainer, welcomePart.element); } diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index 9fe96562bba2..eb8eadcfa0d3 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -116,6 +116,7 @@ export interface IChatViewWelcomeContent { export interface IChatViewWelcomeRenderOptions { firstLinkToButton?: boolean; location: ChatAgentLocation; + isWidgetWelcomeViewContent?: boolean; } export class ChatViewWelcomePart extends Disposable { @@ -146,7 +147,7 @@ export class ChatViewWelcomePart extends Disposable { title.textContent = content.title; // Preview indicator - if (options?.location === ChatAgentLocation.EditingSession && typeof content.message !== 'function' && chatAgentService.toolsAgentModeEnabled) { + if (options?.location === ChatAgentLocation.EditingSession && typeof content.message !== 'function' && chatAgentService.toolsAgentModeEnabled && options.isWidgetWelcomeViewContent) { // Override welcome message for the agent. Sort of a hack, should it come from the participant? This case is different because the welcome content typically doesn't change per ChatWidget const agentMessage = localize({ key: 'agentMessage', comment: ['{Locked="["}', '{Locked="]({0})"}'] }, "Ask Copilot to edit your files in [agent mode]({0}). Copilot will automatically use multiple requests to pick files to edit, run terminal commands, and iterate on errors.\n\nCopilot is powered by AI, so mistakes are possible. Review output carefully before use.", 'https://aka.ms/vscode-copilot-agent'); content.message = new MarkdownString(agentMessage); From 47b4f5c1dd592783d83c5ae896e26ba6654fb528 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sun, 2 Feb 2025 12:58:50 -0800 Subject: [PATCH 1131/3587] Don't hardcode editFile tool name (#239440) --- src/vs/workbench/api/common/extHostLanguageModelTools.ts | 6 +++--- .../contrib/chat/browser/languageModelToolsService.ts | 3 ++- src/vs/workbench/contrib/chat/common/tools/editFileTool.ts | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index 294c549a0ed0..b5ee1c0dc1c7 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -16,7 +16,7 @@ import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/ex import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js'; import * as typeConvert from './extHostTypeConverters.js'; import { IToolInputProcessor } from '../../contrib/chat/common/tools/tools.js'; -import { EditToolData, EditToolInputProcessor } from '../../contrib/chat/common/tools/editFileTool.js'; +import { EditToolData, EditToolId, EditToolInputProcessor } from '../../contrib/chat/common/tools/editFileTool.js'; export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape { /** A map of tools that were registered in this EH */ @@ -61,7 +61,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape throw new Error(`Invalid tool invocation token`); } - if (toolId === 'vscode_editFile' && !isProposedApiEnabled(extension, 'chatParticipantPrivate')) { + if (toolId === EditToolId && !isProposedApiEnabled(extension, 'chatParticipantPrivate')) { throw new Error(`Invalid tool: ${toolId}`); } @@ -92,7 +92,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape return Array.from(this._allTools.values()) .map(tool => typeConvert.LanguageModelToolDescription.to(tool)) .filter(tool => { - if (tool.name === 'vscode_editFile') { + if (tool.name === EditToolId) { return isProposedApiEnabled(extension, 'chatParticipantPrivate'); } diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index 9e3d66aeca9d..02389c3fc29b 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -20,6 +20,7 @@ import { ChatModel } from '../common/chatModel.js'; import { ChatToolInvocation } from '../common/chatProgressTypes/chatToolInvocation.js'; import { IChatService } from '../common/chatService.js'; import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../common/languageModelToolsService.js'; +import { EditToolId } from '../common/tools/editFileTool.js'; interface IToolEntry { data: IToolData; @@ -186,7 +187,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo const defaultMessage = localize('toolInvocationMessage', "Using {0}", `"${tool.data.displayName}"`); const invocationMessage = prepared?.invocationMessage ?? defaultMessage; - if (tool.data.id !== 'vscode_editFile') { + if (tool.data.id !== EditToolId) { toolInvocation = new ChatToolInvocation(invocationMessage, prepared?.pastTenseMessage, prepared?.tooltip, prepared?.confirmationMessages); model.acceptResponseProgress(request, toolInvocation); if (prepared?.confirmationMessages) { diff --git a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts index 77181a7d7dc9..f8ac80fe994d 100644 --- a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts @@ -40,8 +40,9 @@ class Person { } `; +export const EditToolId = 'vscode_editFile'; export const EditToolData: IToolData = { - id: 'vscode_editFile', + id: EditToolId, tags: ['vscode_editing'], displayName: localize('chat.tools.editFile', "Edit File"), modelDescription: `Edit a file in the workspace. Use this tool once per file that needs to be modified, even if there are multiple changes for a file. Generate the "explanation" property first. ${codeInstructions}`, From 28448cedcfb0841d0948c0c1b960999b92fbc98e Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Sun, 2 Feb 2025 23:17:36 -0300 Subject: [PATCH 1132/3587] Removes debugger scripts, as they don't work with ESM anymore. (#239445) --- .vscode/launch.json | 4 - scripts/debugger-scripts-api.d.ts | 35 -- scripts/hot-reload-injected-script.js | 483 -------------------------- 3 files changed, 522 deletions(-) delete mode 100644 scripts/debugger-scripts-api.d.ts delete mode 100644 scripts/hot-reload-injected-script.js diff --git a/.vscode/launch.json b/.vscode/launch.json index 1a6be10d6c1b..afd57934304f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -275,10 +275,6 @@ "presentation": { "hidden": true, }, - // This is read by the vscode-diagnostic-tools extension - "vscode-diagnostic-tools.debuggerScripts": [ - "${workspaceFolder}/scripts/hot-reload-injected-script.js" - ] }, { "type": "node", diff --git a/scripts/debugger-scripts-api.d.ts b/scripts/debugger-scripts-api.d.ts deleted file mode 100644 index b101855f4d0e..000000000000 --- a/scripts/debugger-scripts-api.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -type RunFunction = - | ((debugSession: IDebugSession, context: Context) => IDisposable) - | ((debugSession: IDebugSession, context: Context) => Promise); - -interface IDebugSession { - name: string; - eval(expression: string): Promise; - evalJs( - bodyFn: (...args: T) => TResult, - ...args: T - ): Promise; -} - -interface Context { - vscode: typeof import('vscode'); -} - -interface IDisposable { - dispose(): void; -} - -interface HotReloadConfig { - mode?: 'patch-prototype' | undefined; -} - -interface GlobalThisAddition { - $hotReload_applyNewExports?(args: { oldExports: Record; newSrc: string; config?: HotReloadConfig }): AcceptNewExportsFn | undefined; -} - -type AcceptNewExportsFn = (newExports: Record) => boolean; diff --git a/scripts/hot-reload-injected-script.js b/scripts/hot-reload-injected-script.js deleted file mode 100644 index 431f11b6a66b..000000000000 --- a/scripts/hot-reload-injected-script.js +++ /dev/null @@ -1,483 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// @ts-check -/// -/// - -const path = require('path'); -const fsPromise = require('fs/promises'); -const parcelWatcher = require('@parcel/watcher'); - -// This file is loaded by the vscode-diagnostic-tools extension and injected into the debugger. - - -/** - * Represents a lazy evaluation container. - * @template T - * @template TArg - */ -class Lazy { - /** - * Creates a new instance of the Lazy class. - * @param {(arg: TArg) => T} _fn - The function to be lazily evaluated. - */ - constructor(_fn) { - this._fn = _fn; - this._value = undefined; - } - - /** - * Gets the lazily evaluated value. - * @param {TArg} arg - The argument passed in to the evaluation function. - * @return {T} - */ - getValue(arg) { - if (!this._value) { - this._value = this._fn(arg); - } - return this._value; - } -} - -/** - * @param {Context['vscode']} vscode - */ -function setupGlobals(vscode) { - /** @type {DisposableStore} */ - const store = globalThis['hot-reload-injected-script-disposables'] ?? (globalThis['hot-reload-injected-script-disposables'] = new DisposableStore()); - store.clear(); - - function getConfig() { - const config = vscode.workspace.getConfiguration('vscode-diagnostic-tools').get('debuggerScriptsConfig', { - 'hotReload.sources': {} - }); - if (!config['hotReload.sources']) { - config['hotReload.sources'] = {}; - } - return config; - } - - /** - * @type {Map void>>} - */ - const enabledRelativePaths = new Map(); - const api = { - /** - * @param {string} relativePath - * @param {() => void} forceReloadFn - */ - reloadFailed: (relativePath, forceReloadFn) => { - const set = enabledRelativePaths.get(relativePath) ?? new Set(); - set.add(forceReloadFn); - enabledRelativePaths.set(relativePath, set); - - update(); - }, - - /** - * @param {string} relativePath - * @returns {HotReloadConfig} - */ - getConfig: (relativePath) => { - const config = getConfig(); - return { mode: config['hotReload.sources'][relativePath] === 'patch-prototype' ? 'patch-prototype' : undefined }; - } - }; - - const item = store.add(vscode.window.createStatusBarItem(undefined, 10000)); - - function update() { - item.hide(); - const e = vscode.window.activeTextEditor; - if (!e) { return; } - - const part = e.document.fileName.replace(/\\/g, '/').replace(/\.ts/, '.js').split('/src/')[1]; - if (!part) { return; } - - const isEnabled = api.getConfig(part)?.mode === 'patch-prototype'; - - if (!enabledRelativePaths.has(part) && !isEnabled) { - return; - } - - if (!isEnabled) { - item.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); - item.text = '$(sync-ignored) hot reload disabled'; - } else { - item.backgroundColor = undefined; - item.text = '$(sync) hot reload enabled'; - } - - item.command = { - command: 'vscode-diagnostic-tools.hotReload.toggle', - title: 'Toggle hot reload', - arguments: [part], - tooltip: 'Toggle hot reload' - }; - item.tooltip = 'Toggle hot reload'; - item.show(); - } - - store.add(vscode.window.onDidChangeActiveTextEditor(e => { - update(); - })); - - store.add(vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('vscode-diagnostic-tools.debuggerScriptsConfig')) { - update(); - } - })); - - update(); - - store.add(vscode.commands.registerCommand('vscode-diagnostic-tools.hotReload.toggle', async (relativePath) => { - let config = getConfig(); - const current = config['hotReload.sources'][relativePath]; - const newValue = current === 'patch-prototype' ? undefined : 'patch-prototype'; - config = { ...config, 'hotReload.sources': { ...config['hotReload.sources'], [relativePath]: newValue } }; - - await vscode.workspace.getConfiguration('vscode-diagnostic-tools').update('debuggerScriptsConfig', config, vscode.ConfigurationTarget.Global); - - if (newValue === 'patch-prototype') { - const reloadFns = enabledRelativePaths.get(relativePath); - console.log(reloadFns); - if (reloadFns) { - for (const fn of reloadFns) { - fn(); - } - } - } - })); - - return api; -} - -const g = new Lazy(setupGlobals); - -/** @type {RunFunction} */ -module.exports.run = async function (debugSession, ctx) { - const store = new DisposableStore(); - - const global = ctx.vscode ? g.getValue(ctx.vscode) : undefined; - - const watcher = store.add(await DirWatcher.watchRecursively(path.join(__dirname, '../out/'))); - - /** - * So that the same file always gets the same reload fn. - * @type {Map void>} - */ - const reloadFns = new Map(); - - store.add(watcher.onDidChange(async changes => { - const supportedChanges = changes - .filter(c => c.path.endsWith('.js') || c.path.endsWith('.css')) - .map(c => { - const relativePath = c.path.replace(/\\/g, '/').split('/out/')[1]; - return { ...c, relativePath, config: global?.getConfig(relativePath) }; - }); - - const result = await debugSession.evalJs(function (changes, debugSessionName) { - // This function is stringified and injected into the debuggee. - - /** @type {{ count: number; originalWindowTitle: any; timeout: any; shouldReload: boolean }} */ - const hotReloadData = globalThis.$hotReloadData || (globalThis.$hotReloadData = { count: 0, messageHideTimeout: undefined, shouldReload: false }); - - /** @type {{ relativePath: string, path: string }[]} */ - const reloadFailedJsFiles = []; - - for (const change of changes) { - handleChange(change.relativePath, change.path, change.newContent, change.config); - } - - return { reloadFailedJsFiles }; - - /** - * @param {string} relativePath - * @param {string} path - * @param {string} newSrc - * @param {HotReloadConfig | undefined} config - */ - function handleChange(relativePath, path, newSrc, config) { - if (relativePath.endsWith('.css')) { - handleCssChange(relativePath); - } else if (relativePath.endsWith('.js')) { - handleJsChange(relativePath, path, newSrc, config); - } - } - - /** - * @param {string} relativePath - */ - function handleCssChange(relativePath) { - if (typeof document === 'undefined') { - return; - } - - const styleSheet = (/** @type {HTMLLinkElement[]} */ ([...document.querySelectorAll(`link[rel='stylesheet']`)])) - .find(l => new URL(l.href, document.location.href).pathname.endsWith(relativePath)); - if (styleSheet) { - setMessage(`reload ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); - console.log(debugSessionName, 'css reloaded', relativePath); - styleSheet.href = styleSheet.href.replace(/\?.*/, '') + '?' + Date.now(); - } else { - setMessage(`could not reload ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); - console.log(debugSessionName, 'ignoring css change, as stylesheet is not loaded', relativePath); - } - } - - /** - * @param {string} relativePath - * @param {string} newSrc - * @param {HotReloadConfig | undefined} config - */ - function handleJsChange(relativePath, path, newSrc, config) { - const moduleIdStr = trimEnd(relativePath, '.js'); - - /** @type {any} */ - const requireFn = globalThis.require; - const moduleManager = requireFn.moduleManager; - if (!moduleManager) { - console.log(debugSessionName, 'ignoring js change, as moduleManager is not available', relativePath); - return; - } - - const moduleId = moduleManager._moduleIdProvider.getModuleId(moduleIdStr); - const oldModule = moduleManager._modules2[moduleId]; - - if (!oldModule) { - console.log(debugSessionName, 'ignoring js change, as module is not loaded', relativePath); - return; - } - - // Check if we can reload - const g = /** @type {GlobalThisAddition} */ (globalThis); - - // A frozen copy of the previous exports - const oldExports = Object.freeze({ ...oldModule.exports }); - const reloadFn = g.$hotReload_applyNewExports?.({ oldExports, newSrc, config }); - - if (!reloadFn) { - console.log(debugSessionName, 'ignoring js change, as module does not support hot-reload', relativePath); - hotReloadData.shouldReload = true; - - reloadFailedJsFiles.push({ relativePath, path }); - - setMessage(`hot reload not supported for ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); - return; - } - - // Eval maintains source maps - function newScript(/* this parameter is used by newSrc */ define) { - // eslint-disable-next-line no-eval - eval(newSrc); // CodeQL [SM01632] This code is only executed during development. It is required for the hot-reload functionality. - } - - newScript(/* define */ function (deps, callback) { - // Evaluating the new code was successful. - - // Redefine the module - delete moduleManager._modules2[moduleId]; - moduleManager.defineModule(moduleIdStr, deps, callback); - const newModule = moduleManager._modules2[moduleId]; - - - // Patch the exports of the old module, so that modules using the old module get the new exports - Object.assign(oldModule.exports, newModule.exports); - // We override the exports so that future reloads still patch the initial exports. - newModule.exports = oldModule.exports; - - const successful = reloadFn(newModule.exports); - if (!successful) { - hotReloadData.shouldReload = true; - setMessage(`hot reload failed ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); - console.log(debugSessionName, 'hot reload was not successful', relativePath); - return; - } - - console.log(debugSessionName, 'hot reloaded', moduleIdStr); - setMessage(`successfully reloaded ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); - }); - } - - /** - * @param {string} message - */ - function setMessage(message) { - const domElem = /** @type {HTMLDivElement | undefined} */ (document.querySelector('.titlebar-center .window-title')); - if (!domElem) { return; } - if (!hotReloadData.timeout) { - hotReloadData.originalWindowTitle = domElem.innerText; - } else { - clearTimeout(hotReloadData.timeout); - } - if (hotReloadData.shouldReload) { - message += ' (manual reload required)'; - } - - domElem.innerText = message; - hotReloadData.timeout = setTimeout(() => { - hotReloadData.timeout = undefined; - // If wanted, we can restore the previous title message - // domElem.replaceChildren(hotReloadData.originalWindowTitle); - }, 5000); - } - - /** - * @param {string} path - * @returns {string} - */ - function formatPath(path) { - const parts = path.split('/'); - parts.reverse(); - let result = parts[0]; - parts.shift(); - for (const p of parts) { - if (result.length + p.length > 40) { - break; - } - result = p + '/' + result; - if (result.length > 20) { - break; - } - } - return result; - } - - function trimEnd(str, suffix) { - if (str.endsWith(suffix)) { - return str.substring(0, str.length - suffix.length); - } - return str; - } - - }, supportedChanges, debugSession.name.substring(0, 25)); - - for (const failedFile of result.reloadFailedJsFiles) { - const reloadFn = reloadFns.get(failedFile.relativePath) ?? (() => { - console.log('force change'); - watcher.forceChange(failedFile.path); - }); - reloadFns.set(failedFile.relativePath, reloadFn); - global?.reloadFailed(failedFile.relativePath, reloadFn); - } - })); - - return store; -}; - -class DirWatcher { - /** - * - * @param {string} dir - * @returns {Promise} - */ - static async watchRecursively(dir) { - /** @type {((changes: { path: string, newContent: string }[]) => void)[]} */ - const listeners = []; - /** @type {Map } */ - const fileContents = new Map(); - /** @type {Map} */ - const changes = new Map(); - /** @type {(handler: (changes: { path: string, newContent: string }[]) => void) => IDisposable} */ - const event = (handler) => { - listeners.push(handler); - return { - dispose: () => { - const idx = listeners.indexOf(handler); - if (idx >= 0) { - listeners.splice(idx, 1); - } - } - }; - }; - const r = parcelWatcher.subscribe(dir, async (err, events) => { - for (const e of events) { - if (e.type === 'update') { - const newContent = await fsPromise.readFile(e.path, 'utf8'); - if (fileContents.get(e.path) !== newContent) { - fileContents.set(e.path, newContent); - changes.set(e.path, { path: e.path, newContent }); - } - } - } - if (changes.size > 0) { - debounce(() => { - const uniqueChanges = Array.from(changes.values()); - changes.clear(); - listeners.forEach(l => l(uniqueChanges)); - })(); - } - }); - const result = await r; - return new DirWatcher(event, () => result.unsubscribe(), path => { - const content = fileContents.get(path); - if (content !== undefined) { - listeners.forEach(l => l([{ path: path, newContent: content }])); - } - }); - } - - /** - * @param {(handler: (changes: { path: string, newContent: string }[]) => void) => IDisposable} onDidChange - * @param {() => void} unsub - * @param {(path: string) => void} forceChange - */ - constructor(onDidChange, unsub, forceChange) { - this.onDidChange = onDidChange; - this.unsub = unsub; - this.forceChange = forceChange; - } - - dispose() { - this.unsub(); - } -} - -/** - * Debounce function calls - * @param {() => void} fn - * @param {number} delay - */ -function debounce(fn, delay = 50) { - let timeoutId; - return function (...args) { - clearTimeout(timeoutId); - timeoutId = setTimeout(() => { - fn.apply(this, args); - }, delay); - }; -} - -class DisposableStore { - constructor() { - this._toDispose = new Set(); - this._isDisposed = false; - } - - - /** - * Adds an item to the collection. - * - * @template T - * @param {T} t - The item to add. - * @returns {T} The added item. - */ - add(t) { - this._toDispose.add(t); - return t; - } - dispose() { - if (this._isDisposed) { - return; - } - this._isDisposed = true; - this.clear(); - } - clear() { - this._toDispose.forEach(item => item.dispose()); - this._toDispose.clear(); - } -} From 0270155f64566b079d714afd4faa7fba753611c0 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 3 Feb 2025 06:43:28 +0100 Subject: [PATCH 1133/3587] SCM - fix action button container height (#239456) --- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index a5c029c986ff..d6f9ee965e85 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -174,7 +174,7 @@ interface ActionButtonTemplate { } export class ActionButtonRenderer implements ICompressibleTreeRenderer { - static readonly DEFAULT_HEIGHT = 30; + static readonly DEFAULT_HEIGHT = 28; static readonly TEMPLATE_ID = 'actionButton'; get templateId(): string { return ActionButtonRenderer.TEMPLATE_ID; } @@ -710,9 +710,10 @@ class ListDelegate implements IListVirtualDelegate { getHeight(element: TreeElement) { if (isSCMInput(element)) { + console.log(this.inputRenderer.getHeight(element)); return this.inputRenderer.getHeight(element); } else if (isSCMActionButton(element)) { - return ActionButtonRenderer.DEFAULT_HEIGHT + 10; + return ActionButtonRenderer.DEFAULT_HEIGHT + 8; } else { return 22; } From f029eb3f18ada5a686368beff3d14ae6982cc52a Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Mon, 3 Feb 2025 13:55:30 +0800 Subject: [PATCH 1134/3587] fix: don't run participant detection when an explicit slash command is assigned (#239458) --- src/vs/workbench/contrib/chat/common/chatServiceImpl.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index f6f352449499..f7365f520e4a 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -670,12 +670,12 @@ export class ChatService extends Disposable implements IChatService { } satisfies IChatAgentRequest; }; - if (this.configurationService.getValue('chat.detectParticipant.enabled') !== false && this.chatAgentService.hasChatParticipantDetectionProviders() && !agentPart && !commandPart && enableCommandDetection) { + if (this.configurationService.getValue('chat.detectParticipant.enabled') !== false && this.chatAgentService.hasChatParticipantDetectionProviders() && !agentPart && !commandPart && !agentSlashCommandPart && enableCommandDetection) { // We have no agent or command to scope history with, pass the full history to the participant detection provider const defaultAgentHistory = this.getHistoryEntriesFromModel(requests, model.sessionId, location, defaultAgent.id); // Prepare the request object that we will send to the participant detection provider - const chatAgentRequest = await prepareChatAgentRequest(defaultAgent, agentSlashCommandPart?.command, enableCommandDetection, undefined, false); + const chatAgentRequest = await prepareChatAgentRequest(defaultAgent, undefined, enableCommandDetection, undefined, false); const result = await this.chatAgentService.detectAgentOrCommand(chatAgentRequest, defaultAgentHistory, { location }, token); if (result && this.chatAgentService.getAgent(result.agent.id)?.locations?.includes(location)) { From ba2aa54fc680be451b6e0186da793068524630d4 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 3 Feb 2025 07:26:32 +0100 Subject: [PATCH 1135/3587] Git - fix post-commit command for multi-repo workspaces (#239462) --- extensions/git/src/model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index f64528275d09..d17d118de1c9 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -873,7 +873,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } if (hint instanceof ApiRepository) { - return this.openRepositories.filter(r => r.repository === hint.repository)[0]; + hint = hint.rootUri; } if (typeof hint === 'string') { From 33945ac8fa388e24acb1b7366908e4776b56f484 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Mon, 3 Feb 2025 15:11:57 +0800 Subject: [PATCH 1136/3587] fix: don't repeat custom icons in icon labels (#239464) --- src/vs/base/browser/ui/iconLabel/iconLabel.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 981933151ccb..986b37920682 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -168,6 +168,10 @@ export class IconLabel extends Disposable { iconNode = existingIconNode; } iconNode.style.backgroundImage = css.asCSSUrl(options?.iconPath); + iconNode.style.backgroundRepeat = 'no-repeat'; + iconNode.style.backgroundPosition = 'center'; + iconNode.style.backgroundSize = 'contain'; + } else if (existingIconNode) { existingIconNode.remove(); } From b6843947b12cebe46d0df4a4441ec840041ce2ea Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 3 Feb 2025 10:05:42 +0100 Subject: [PATCH 1137/3587] fix esbuild warning, https://github.com/microsoft/vscode/pull/235599 (#239469) --- src/vs/workbench/api/common/extHostTypes.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index a3e72727d03c..4130d79f0ab7 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1951,7 +1951,6 @@ export namespace TextEditorSelectionChangeKind { switch (s) { case 'keyboard': return TextEditorSelectionChangeKind.Keyboard; case 'mouse': return TextEditorSelectionChangeKind.Mouse; - case 'api': case TextEditorSelectionSource.PROGRAMMATIC: case TextEditorSelectionSource.JUMP: case TextEditorSelectionSource.NAVIGATION: From 8216e850729eb038d93fcdc77ce2f3af394b7050 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:11:29 +0100 Subject: [PATCH 1138/3587] SCM - fix graph title actions rendering (#239471) --- src/vs/workbench/contrib/scm/browser/media/scm.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 5a6eebc287e1..c11117fde935 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -558,6 +558,8 @@ padding: 2px 4px; } +.monaco-workbench .part.sidebar > .title > .title-actions .action-label.scm-graph-repository-picker, +.monaco-workbench .part.sidebar > .title > .title-actions .action-label.scm-graph-history-item-picker, .monaco-workbench .part.auxiliarybar > .title > .title-actions .action-label.scm-graph-repository-picker, .monaco-workbench .part.auxiliarybar > .title > .title-actions .action-label.scm-graph-history-item-picker, .monaco-workbench .part.panel > .title > .title-actions .action-label.scm-graph-repository-picker, From cda13b71f48e197d8e4ac7538874cc16224fa166 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 3 Feb 2025 10:35:37 +0100 Subject: [PATCH 1139/3587] Add test to assert LanguageModelError (#239474) * add test for https://github.com/microsoft/vscode/issues/235322 * extract options/metadata --- .../src/singlefolder-tests/lm.test.ts | 67 ++++++++++++------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts index 8ab5c5949a15..cc995c08497b 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts @@ -13,6 +13,15 @@ suite('lm', function () { let disposables: vscode.Disposable[] = []; + const testProviderOptions: vscode.ChatResponseProviderMetadata = { + name: 'test-lm', + version: '1.0.0', + family: 'test', + vendor: 'test-lm-vendor', + maxInputTokens: 100, + maxOutputTokens: 100, + }; + setup(function () { disposables = []; }); @@ -37,14 +46,7 @@ suite('lm', function () { async provideTokenCount(_text, _token) { return 1; }, - }, { - name: 'test-lm', - version: '1.0.0', - family: 'test', - vendor: 'test-lm-vendor', - maxInputTokens: 100, - maxOutputTokens: 100, - })); + }, testProviderOptions)); const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); assert.strictEqual(models.length, 1); @@ -88,14 +90,7 @@ suite('lm', function () { async provideTokenCount(_text, _token) { return 1; }, - }, { - name: 'test-lm', - version: '1.0.0', - family: 'test', - vendor: 'test-lm-vendor', - maxInputTokens: 100, - maxOutputTokens: 100, - })); + }, testProviderOptions)); const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); assert.strictEqual(models.length, 1); @@ -118,15 +113,8 @@ suite('lm', function () { }, async provideTokenCount(_text, _token) { return 1; - }, - }, { - name: 'test-lm', - version: '1.0.0', - family: 'test', - vendor: 'test-lm-vendor', - maxInputTokens: 100, - maxOutputTokens: 100, - })); + } + }, testProviderOptions)); const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); assert.strictEqual(models.length, 1); @@ -150,4 +138,33 @@ suite('lm', function () { // assert.ok(error instanceof Error); // todo@jrieken requires one more insiders } }); + + test('LanguageModelError instance is not thrown to extensions#235322', async function () { + + disposables.push(vscode.lm.registerChatModelProvider('test-lm', { + async provideLanguageModelResponse(_messages, _options, _extensionId, _progress, _token) { + throw vscode.LanguageModelError.Blocked('You have been blocked'); + }, + async provideTokenCount(_text, _token) { + return 1; + } + }, testProviderOptions)); + + const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); + assert.strictEqual(models.length, 1); + + let output = ''; + + try { + const response = await models[0].sendRequest([vscode.LanguageModelChatMessage.User('Hello')]); + assert.ok(response); + for await (const thing of response.text) { + output += thing; + } + } catch (error) { + assert.ok(error instanceof vscode.LanguageModelError); + assert.strictEqual(error.message, 'You have been blocked'); + } + assert.strictEqual(output, ''); + }); }); From 9cdf1dbadbfc6002f277dfbfc642a51063faf7a5 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:36:07 +0100 Subject: [PATCH 1140/3587] Add next edit suggestions tag to settings and options (#239475) @tag:nextEditSuggestions --- src/vs/editor/common/config/editorOptions.ts | 2 ++ .../browser/view/inlineEdits/gutterIndicatorMenu.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 0155c2e67e5f..4fda0b457780 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -4315,6 +4315,7 @@ class InlineEditorSuggest extends BaseEditorOption option(createOptionArgs({ id: c.id, title: c.title, icon: Codicon.symbolEvent, commandId: c.id, commandArgs: c.arguments }))), separator() ] : []), - option(createOptionArgs({ id: 'settings', title: localize('settings', "Settings"), icon: Codicon.gear, commandId: 'workbench.action.openSettings', commandArgs: ['inlineSuggest.edits'] })), + option(createOptionArgs({ id: 'settings', title: localize('settings', "Settings"), icon: Codicon.gear, commandId: 'workbench.action.openSettings', commandArgs: ['@tag:nextEditSuggestions'] })), ]); } From a4c8965571d8be877f424bdfb5ff7b27b0549fb3 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:56:37 +0100 Subject: [PATCH 1141/3587] =?UTF-8?q?SCM=20-=20=F0=9F=92=84=20remove=20deb?= =?UTF-8?q?ug=20statement=20(#239478)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index d6f9ee965e85..84bec04b859e 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -710,7 +710,6 @@ class ListDelegate implements IListVirtualDelegate { getHeight(element: TreeElement) { if (isSCMInput(element)) { - console.log(this.inputRenderer.getHeight(element)); return this.inputRenderer.getHeight(element); } else if (isSCMActionButton(element)) { return ActionButtonRenderer.DEFAULT_HEIGHT + 8; From d76dc4f688ab73c1996245c5be2566243ba50e2d Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Mon, 3 Feb 2025 01:59:21 -0800 Subject: [PATCH 1142/3587] fixes https://github.com/microsoft/vscode-copilot-release/issues/3520 (#239359) --- .../actions/browser/dropdownWithPrimaryActionViewItem.ts | 2 +- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts b/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts index dbc781394719..bbdc7e37240e 100644 --- a/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts +++ b/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts @@ -144,7 +144,7 @@ export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem { update(dropdownAction: IAction, dropdownMenuActions: IAction[], dropdownIcon?: string): void { this._dropdown.dispose(); this._dropdown = new DropdownMenuActionViewItem(dropdownAction, dropdownMenuActions, this._contextMenuProvider, { - menuAsChild: true, + menuAsChild: this._options?.menuAsChild ?? true, classNames: ['codicon', dropdownIcon || 'codicon-chevron-down'], actionRunner: this._options?.actionRunner, hoverDelegate: this._options?.hoverDelegate, diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index a6406dff79a8..6fc6b0b821b7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -815,7 +815,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (this.location === ChatAgentLocation.Panel || this.location === ChatAgentLocation.Editor) { if ((action.id === ChatSubmitAction.ID || action.id === CancelAction.ID) && action instanceof MenuItemAction) { const dropdownAction = this.instantiationService.createInstance(MenuItemAction, { id: 'chat.moreExecuteActions', title: localize('notebook.moreExecuteActionsLabel', "More..."), icon: Codicon.chevronDown }, undefined, undefined, undefined, undefined); - return this.instantiationService.createInstance(ChatSubmitDropdownActionItem, action, dropdownAction, options); + return this.instantiationService.createInstance(ChatSubmitDropdownActionItem, action, dropdownAction, { ...options, menuAsChild: false }); } } From 160a5beb8664c25902498226406a405d2d7c787e Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 3 Feb 2025 11:43:12 +0100 Subject: [PATCH 1143/3587] Cleanup line replacement view implementation (#239482) line replacement view cleanup --- ...ReplacementView.ts => replacementViews.ts} | 180 +++++++++++------- .../browser/view/inlineEdits/view.ts | 16 +- 2 files changed, 117 insertions(+), 79 deletions(-) rename src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/{wordReplacementView.ts => replacementViews.ts} (86%) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/replacementViews.ts similarity index 86% rename from src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts rename to src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/replacementViews.ts index 309af28f4aae..5bd2d644c1b5 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/wordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/replacementViews.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; -import { constObservable, derived, mapObservableArrayCached } from '../../../../../../base/common/observable.js'; +import { autorun, autorunDelta, constObservable, derived, mapObservableArrayCached } from '../../../../../../base/common/observable.js'; import { editorHoverStatusBarBackground } from '../../../../../../platform/theme/common/colorRegistry.js'; import { registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; import { ObservableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; @@ -22,12 +22,12 @@ import { getPrefixTrim, mapOutFalsy, n, rectToProps } from './utils.js'; import { localize } from '../../../../../../nls.js'; import { IInlineEditsView } from './sideBySideDiff.js'; import { Range } from '../../../../../common/core/range.js'; -import { LineRange } from '../../../../../common/core/lineRange.js'; import { InlineDecoration, InlineDecorationType } from '../../../../../common/viewModel.js'; import { IModelDecorationOptions, TrackedRangeStickiness } from '../../../../../common/model.js'; import { $ } from '../../../../../../base/browser/dom.js'; -import { observableValue } from '../../../../../../base/common/observableInternal/base.js'; +import { IObservable } from '../../../../../../base/common/observableInternal/base.js'; import { IViewZoneChangeAccessor } from '../../../../../browser/editorBrowser.js'; +import { LineRange } from '../../../../../common/core/lineRange.js'; export const transparentHoverBackground = registerColor( 'inlineEdit.wordReplacementView.background', { @@ -277,21 +277,27 @@ export class LineReplacementView extends Disposable implements IInlineEditsView stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; - private readonly _maxPrefixTrim = getPrefixTrim(this._replacements.flatMap(r => [r.originalRange, r.modifiedRange]), this._originalRange, this._modifiedLines, this._editor.editor); + private readonly _maxPrefixTrim = this._edit.map(e => e ? getPrefixTrim(e.replacements.flatMap(r => [r.originalRange, r.modifiedRange]), e.originalRange, e.modifiedLines, this._editor.editor) : undefined); private readonly _modifiedLineElements = derived(reader => { const lines = []; let requiredWidth = 0; - const maxPrefixTrim = this._maxPrefixTrim.prefixTrim; - const modifiedBubbles = rangesToBubbleRanges(this._replacements.map(r => r.modifiedRange)).map(r => new Range(r.startLineNumber, r.startColumn - maxPrefixTrim, r.endLineNumber, r.endColumn - maxPrefixTrim)); + const prefixTrim = this._maxPrefixTrim.read(reader); + const edit = this._edit.read(reader); + if (!edit || !prefixTrim) { + return undefined; + } + + const maxPrefixTrim = prefixTrim.prefixTrim; + const modifiedBubbles = rangesToBubbleRanges(edit.replacements.map(r => r.modifiedRange)).map(r => new Range(r.startLineNumber, r.startColumn - maxPrefixTrim, r.endLineNumber, r.endColumn - maxPrefixTrim)); const textModel = this._editor.model.get()!; - const startLineNumber = this._modifiedRange.startLineNumber; - for (let i = 0; i < this._modifiedRange.length; i++) { + const startLineNumber = edit.modifiedRange.startLineNumber; + for (let i = 0; i < edit.modifiedRange.length; i++) { const line = document.createElement('div'); const lineNumber = startLineNumber + i; - const modLine = this._modifiedLines[i].replace(/\t\n/g, '').slice(maxPrefixTrim); + const modLine = edit.modifiedLines[i].slice(maxPrefixTrim); const t = textModel.tokenization.tokenizeLinesAt(lineNumber, [modLine])?.[0]; let tokens: LineTokens; @@ -301,6 +307,7 @@ export class LineReplacementView extends Disposable implements IInlineEditsView tokens = LineTokens.createEmpty(modLine, this._languageService.languageIdCodec); } + // Inline decorations are broken down into individual spans. To be able to render rounded corners, we need to set the start and end decorations separately. const decorations = []; for (const modified of modifiedBubbles.filter(b => b.startLineNumber === lineNumber)) { const validatedEndColumn = Math.min(modified.endColumn, modLine.length + 1); @@ -309,7 +316,9 @@ export class LineReplacementView extends Disposable implements IInlineEditsView decorations.push(new InlineDecoration(new Range(1, validatedEndColumn - 1, 1, validatedEndColumn), 'end', InlineDecorationType.Regular)); } + // TODO: All lines should be rendered at once for one dom element const result = renderLines(new LineSource([tokens]), RenderOptions.fromEditor(this._editor.editor).withSetWidth(false), decorations, line, true); + this._editor.getOption(EditorOption.fontInfo).read(reader); // update when font info changes requiredWidth = Math.max(requiredWidth, result.minWidthInPx); @@ -319,10 +328,17 @@ export class LineReplacementView extends Disposable implements IInlineEditsView return { lines, requiredWidth: requiredWidth - 10 }; // TODO: Width is always too large, why? }); - private readonly _viewZoneInfo = observableValue<{ height: number; lineNumber: number } | undefined>('viewZoneInfo', undefined); private readonly _layout = derived(this, reader => { - const { requiredWidth } = this._modifiedLineElements.read(reader); + const modifiedLines = this._modifiedLineElements.read(reader); + const maxPrefixTrim = this._maxPrefixTrim.read(reader); + const edit = this._edit.read(reader); + if (!modifiedLines || !maxPrefixTrim || !edit) { + return undefined; + } + + const { prefixLeftOffset } = maxPrefixTrim; + const { requiredWidth } = modifiedLines; const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); const contentLeft = this._editor.layoutInfoContentLeft.read(reader); @@ -332,13 +348,12 @@ export class LineReplacementView extends Disposable implements IInlineEditsView const PADDING = 4; const textModel = this._editor.editor.getModel()!; - const { prefixLeftOffset } = this._maxPrefixTrim; - const originalLineWidths = this._originalRange.mapToLineArray(line => this._editor.editor.getOffsetForColumn(line, textModel.getLineMaxColumn(line)) - prefixLeftOffset); + const originalLineWidths = edit.originalRange.mapToLineArray(line => this._editor.editor.getOffsetForColumn(line, textModel.getLineMaxColumn(line)) - prefixLeftOffset); const maxLineWidth = Math.max(...originalLineWidths, requiredWidth); - const startLineNumber = this._originalRange.startLineNumber; - const endLineNumber = this._originalRange.endLineNumberExclusive - 1; + const startLineNumber = edit.originalRange.startLineNumber; + const endLineNumber = edit.originalRange.endLineNumberExclusive - 1; const topOfOriginalLines = this._editor.editor.getTopForLineNumber(startLineNumber) - scrollTop; const bottomOfOriginalLines = this._editor.editor.getBottomForLineNumber(endLineNumber) - scrollTop; @@ -353,26 +368,13 @@ export class LineReplacementView extends Disposable implements IInlineEditsView originalLinesOverlay.left, originalLinesOverlay.bottom + PADDING, originalLinesOverlay.width, - this._modifiedRange.length * lineHeight + edit.modifiedRange.length * lineHeight ); const background = Rect.hull([originalLinesOverlay, modifiedLinesOverlay]).withMargin(PADDING); const lowerBackground = background.intersectVertical(new OffsetRange(originalLinesOverlay.bottom, Number.MAX_SAFE_INTEGER)); const lowerText = new Rect(lowerBackground.left + PADDING, lowerBackground.top + PADDING, lowerBackground.right, lowerBackground.bottom); - // Add ViewZone if needed - const shouldShowViewZone = this._editor.editor.getOption(EditorOption.inlineSuggest).edits.codeShifting; - if (shouldShowViewZone) { - const viewZoneHeight = lowerBackground.height + 2 * PADDING; - const viewZoneLineNumber = this._originalRange.endLineNumberExclusive; - const activeViewZone = this._viewZoneInfo.get(); - if (!activeViewZone || activeViewZone.lineNumber !== viewZoneLineNumber || activeViewZone.height !== viewZoneHeight) { - this._viewZoneInfo.set({ height: viewZoneHeight, lineNumber: viewZoneLineNumber }, undefined); - } - } else if (this._viewZoneInfo.get()) { - this._viewZoneInfo.set(undefined, undefined); - } - return { originalLinesOverlay, modifiedLinesOverlay, @@ -384,52 +386,30 @@ export class LineReplacementView extends Disposable implements IInlineEditsView }; }); - private _previousViewZoneInfo: { height: number; lineNumber: number; id: string } | undefined = undefined; - protected readonly _viewZone = derived(this, reader => { - const viewZoneInfo = this._viewZoneInfo.read(reader); - this._editor.editor.changeViewZones((changeAccessor) => { - this.removePreviousViewZone(changeAccessor); - if (!viewZoneInfo) { return; } - this.addViewZone(viewZoneInfo, changeAccessor); - }); - }).recomputeInitiallyAndOnChange(this._store); - - private removePreviousViewZone(changeAccessor: IViewZoneChangeAccessor) { - if (!this._previousViewZoneInfo) { - return; - } - - changeAccessor.removeZone(this._previousViewZoneInfo.id); - - const cursorLineNumber = this._editor.cursorLineNumber.get(); - if (cursorLineNumber !== null && cursorLineNumber >= this._previousViewZoneInfo.lineNumber) { - this._editor.editor.setScrollTop(this._editor.scrollTop.get() - this._previousViewZoneInfo.height); + private readonly _viewZoneInfo = derived<{ height: number; lineNumber: number } | undefined>(reader => { + const shouldShowViewZone = this._editor.getOption(EditorOption.inlineSuggest).map(o => o.edits.codeShifting).read(reader); + if (!shouldShowViewZone) { + return undefined; } - this._previousViewZoneInfo = undefined; - } - - private addViewZone(viewZoneInfo: { height: number; lineNumber: number }, changeAccessor: IViewZoneChangeAccessor) { - const activeViewZone = changeAccessor.addZone({ - afterLineNumber: viewZoneInfo.lineNumber - 1, - heightInPx: viewZoneInfo.height, // move computation to layout? - domNode: $('div'), - }); - - const cursorLineNumber = this._editor.cursorLineNumber.get(); - if (cursorLineNumber !== null && cursorLineNumber >= viewZoneInfo.lineNumber) { - this._editor.editor.setScrollTop(this._editor.scrollTop.get() + viewZoneInfo.height); + const layout = this._layout.read(reader); + const edit = this._edit.read(reader); + if (!layout || !edit) { + return undefined; } - this._previousViewZoneInfo = { height: viewZoneInfo.height, lineNumber: viewZoneInfo.lineNumber, id: activeViewZone }; - } + const viewZoneHeight = layout.lowerBackground.height + 2 * layout.padding; + const viewZoneLineNumber = edit.originalRange.endLineNumberExclusive; + return { height: viewZoneHeight, lineNumber: viewZoneLineNumber }; + }); private readonly _div = n.div({ class: 'line-replacement', }, [ derived(reader => { const layout = mapOutFalsy(this._layout).read(reader); - if (!layout) { + const modifiedLineElements = this._modifiedLineElements.read(reader); + if (!layout || !modifiedLineElements) { return []; } @@ -445,8 +425,7 @@ export class LineReplacementView extends Disposable implements IInlineEditsView } const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader); - const modifiedLines = this._modifiedLineElements.read(reader).lines; - modifiedLines.forEach(l => { + modifiedLineElements.lines.forEach(l => { l.style.width = `${layout.read(reader).lowerText.width}px`; l.style.height = `${lineHeight}px`; l.style.position = 'relative'; @@ -507,7 +486,7 @@ export class LineReplacementView extends Disposable implements IInlineEditsView fontWeight: this._editor.getOption(EditorOption.fontWeight), pointerEvents: 'none', } - }, [...modifiedLines]), + }, [...modifiedLineElements.lines]), n.div({ style: { position: 'absolute', @@ -532,10 +511,12 @@ export class LineReplacementView extends Disposable implements IInlineEditsView constructor( private readonly _editor: ObservableCodeEditor, - private readonly _originalRange: LineRange, - private readonly _modifiedRange: LineRange, - private readonly _modifiedLines: string[], - private readonly _replacements: readonly Replacement[], + private readonly _edit: IObservable<{ + originalRange: LineRange; + modifiedRange: LineRange; + modifiedLines: string[]; + replacements: Replacement[]; + } | undefined>, @ILanguageService private readonly _languageService: ILanguageService, ) { super(); @@ -543,8 +524,25 @@ export class LineReplacementView extends Disposable implements IInlineEditsView this._register(toDisposable(() => this._originalBubblesDecorationCollection.clear())); this._register(toDisposable(() => this._editor.editor.changeViewZones(accessor => this.removePreviousViewZone(accessor)))); - const originalBubbles = rangesToBubbleRanges(this._replacements.map(r => r.originalRange)); - this._originalBubblesDecorationCollection.set(originalBubbles.map(r => ({ range: r, options: this._originalBubblesDecorationOptions }))); + this._register(autorunDelta(this._viewZoneInfo, ({ lastValue, newValue }) => { + if (lastValue === newValue || (lastValue?.height === newValue?.height && lastValue?.lineNumber === newValue?.lineNumber)) { + return; + } + this._editor.editor.changeViewZones((changeAccessor) => { + this.removePreviousViewZone(changeAccessor); + if (!newValue) { return; } + this.addViewZone(newValue, changeAccessor); + }); + })); + + this._register(autorun(reader => { + const edit = this._edit.read(reader); + const originalBubbles = []; + if (edit) { + originalBubbles.push(...rangesToBubbleRanges(edit.replacements.map(r => r.originalRange))); + } + this._originalBubblesDecorationCollection.set(originalBubbles.map(r => ({ range: r, options: this._originalBubblesDecorationOptions }))); + })); this._register(this._editor.createOverlayWidget({ domNode: this._div.element, @@ -555,6 +553,40 @@ export class LineReplacementView extends Disposable implements IInlineEditsView allowEditorOverflow: false, })); } + + // View Zones + + private _previousViewZoneInfo: { height: number; lineNumber: number; id: string } | undefined = undefined; + + private removePreviousViewZone(changeAccessor: IViewZoneChangeAccessor) { + if (!this._previousViewZoneInfo) { + return; + } + + changeAccessor.removeZone(this._previousViewZoneInfo.id); + + const cursorLineNumber = this._editor.cursorLineNumber.get(); + if (cursorLineNumber !== null && cursorLineNumber >= this._previousViewZoneInfo.lineNumber) { + this._editor.editor.setScrollTop(this._editor.scrollTop.get() - this._previousViewZoneInfo.height); + } + + this._previousViewZoneInfo = undefined; + } + + private addViewZone(viewZoneInfo: { height: number; lineNumber: number }, changeAccessor: IViewZoneChangeAccessor) { + const activeViewZone = changeAccessor.addZone({ + afterLineNumber: viewZoneInfo.lineNumber - 1, + heightInPx: viewZoneInfo.height, // move computation to layout? + domNode: $('div'), + }); + + this._previousViewZoneInfo = { height: viewZoneInfo.height, lineNumber: viewZoneInfo.lineNumber, id: activeViewZone }; + + const cursorLineNumber = this._editor.cursorLineNumber.get(); + if (cursorLineNumber !== null && cursorLineNumber >= viewZoneInfo.lineNumber) { + this._editor.editor.setScrollTop(this._editor.scrollTop.get() + viewZoneInfo.height); + } + } } function rangesToBubbleRanges(ranges: Range[]): Range[] { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index f512014774ac..b61c9947186b 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -26,7 +26,7 @@ import { InlineEditsSideBySideDiff } from './sideBySideDiff.js'; import { applyEditToModifiedRangeMappings, createReindentEdit } from './utils.js'; import './view.css'; import { InlineEditWithChanges } from './viewAndDiffProducer.js'; -import { LineReplacementView, WordReplacementView } from './wordReplacementView.js'; +import { LineReplacementView, WordReplacementView } from './replacementViews.js'; export class InlineEditsView extends Disposable { private readonly _editorObs = observableCodeEditor(this._editor); @@ -168,9 +168,15 @@ export class InlineEditsView extends Disposable { return store.add(this._instantiationService.createInstance(WordReplacementView, this._editorObs, e, [e])); }).recomputeInitiallyAndOnChange(this._store); - protected readonly _lineReplacementView = mapObservableArrayCached(this, this._uiState.map(s => s?.state?.kind === 'lineReplacement' ? [s.state] : []), (e, store) => { // TODO: no need for map here, how can this be done with observables - return store.add(this._instantiationService.createInstance(LineReplacementView, this._editorObs, e.originalRange, e.modifiedRange, e.modifiedLines, e.replacements)); - }).recomputeInitiallyAndOnChange(this._store); + protected readonly _lineReplacementView = this._register(this._instantiationService.createInstance(LineReplacementView, + this._editorObs, + this._uiState.map(s => s?.state?.kind === 'lineReplacement' ? ({ + originalRange: s.state.originalRange, + modifiedRange: s.state.modifiedRange, + modifiedLines: s.state.modifiedLines, + replacements: s.state.replacements, + }) : undefined), + )); private readonly _useGutterIndicator = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.useGutterIndicator); @@ -179,7 +185,7 @@ export class InlineEditsView extends Disposable { || this._wordReplacementViews.read(reader).some(v => v.isHovered.read(reader)) || this._deletion.isHovered.read(reader) || this._inlineDiffView.isHovered.read(reader) - || this._lineReplacementView.read(reader).some(v => v.isHovered.read(reader)); + || this._lineReplacementView.isHovered.read(reader); }); private readonly _gutterIndicatorOffset = derived(this, reader => { From d0c29a47d568c456b7852e2cab55c857a15bb1f7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 3 Feb 2025 11:52:37 +0100 Subject: [PATCH 1144/3587] Support clicking untrusted links in the dialog (fix #239193) (#239391) --- src/vs/editor/browser/services/openerService.ts | 15 +++++++++------ .../markdownRenderer/browser/markdownRenderer.ts | 7 ++++--- src/vs/platform/opener/common/opener.ts | 1 + .../parts/dialogs/dialog.web.contribution.ts | 6 ++++-- .../browser/parts/dialogs/dialogHandler.ts | 13 ++++++++++--- .../url/browser/trustedDomainsValidator.ts | 2 +- .../parts/dialogs/dialog.contribution.ts | 6 ++++-- .../common/extensionManagementService.ts | 3 +-- 8 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index 870f27bdec73..64765857710f 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -73,6 +73,7 @@ class EditorOpener implements IOpener { if (typeof target === 'string') { target = URI.parse(target); } + const { selection, uri } = extractSelection(target); target = uri; @@ -169,13 +170,15 @@ export class OpenerService implements IOpenerService { } async open(target: URI | string, options?: OpenOptions): Promise { + // check with contributed validators - const targetURI = typeof target === 'string' ? URI.parse(target) : target; - // validate against the original URI that this URI resolves to, if one exists - const validationTarget = this._resolvedUriTargets.get(targetURI) ?? target; - for (const validator of this._validators) { - if (!(await validator.shouldOpen(validationTarget, options))) { - return false; + if (!options?.skipValidation) { + const targetURI = typeof target === 'string' ? URI.parse(target) : target; + const validationTarget = this._resolvedUriTargets.get(targetURI) ?? target; // validate against the original URI that this URI resolves to, if one exists + for (const validator of this._validators) { + if (!(await validator.shouldOpen(validationTarget, options))) { + return false; + } } } diff --git a/src/vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer.ts b/src/vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer.ts index 4bd0ef15ad0b..d8f157876166 100644 --- a/src/vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer.ts +++ b/src/vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer.ts @@ -60,7 +60,7 @@ export class MarkdownRenderer { }; } - protected _getRenderOptions(markdown: IMarkdownString, disposables: DisposableStore): MarkdownRenderOptions { + private _getRenderOptions(markdown: IMarkdownString, disposables: DisposableStore): MarkdownRenderOptions { return { codeBlockRenderer: async (languageAlias, value) => { // In markdown, @@ -97,7 +97,7 @@ export class MarkdownRenderer { }, actionHandler: { callback: (link) => this.openMarkdownLink(link, markdown), - disposables: disposables + disposables } }; } @@ -107,12 +107,13 @@ export class MarkdownRenderer { } } -export async function openLinkFromMarkdown(openerService: IOpenerService, link: string, isTrusted: boolean | MarkdownStringTrustedOptions | undefined): Promise { +export async function openLinkFromMarkdown(openerService: IOpenerService, link: string, isTrusted: boolean | MarkdownStringTrustedOptions | undefined, skipValidation?: boolean): Promise { try { return await openerService.open(link, { fromUserGesture: true, allowContributedOpeners: true, allowCommands: toAllowCommandsOption(isTrusted), + skipValidation }); } catch (e) { onUnexpectedError(e); diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index eb8074255a63..813228294057 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -43,6 +43,7 @@ export type OpenExternalOptions = { readonly allowTunneling?: boolean; readonly allowContributedOpeners?: boolean | string; readonly fromWorkspace?: boolean; + readonly skipValidation?: boolean; }; export type OpenOptions = OpenInternalOptions & OpenExternalOptions; diff --git a/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts b/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts index b4806127adf1..21c9440b0eb5 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts @@ -16,6 +16,7 @@ import { DialogService } from '../../../services/dialogs/common/dialogService.js import { Disposable } from '../../../../base/common/lifecycle.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { Lazy } from '../../../../base/common/lazy.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; export class DialogHandlerContribution extends Disposable implements IWorkbenchContribution { @@ -33,11 +34,12 @@ export class DialogHandlerContribution extends Disposable implements IWorkbenchC @IKeybindingService keybindingService: IKeybindingService, @IInstantiationService instantiationService: IInstantiationService, @IProductService productService: IProductService, - @IClipboardService clipboardService: IClipboardService + @IClipboardService clipboardService: IClipboardService, + @IOpenerService openerService: IOpenerService ) { super(); - this.impl = new Lazy(() => new BrowserDialogHandler(logService, layoutService, keybindingService, instantiationService, productService, clipboardService)); + this.impl = new Lazy(() => new BrowserDialogHandler(logService, layoutService, keybindingService, instantiationService, productService, clipboardService, openerService)); this.model = (this.dialogService as DialogService).model; diff --git a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index 21f62b646c29..7563b30cd475 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts @@ -17,9 +17,10 @@ import { IProductService } from '../../../../platform/product/common/productServ import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { fromNow } from '../../../../base/common/date.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { MarkdownRenderer, openLinkFromMarkdown } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; import { defaultButtonStyles, defaultCheckboxStyles, defaultDialogStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { ResultKind } from '../../../../platform/keybinding/common/keybindingResolver.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; export class BrowserDialogHandler extends AbstractDialogHandler { @@ -40,7 +41,8 @@ export class BrowserDialogHandler extends AbstractDialogHandler { @IKeybindingService private readonly keybindingService: IKeybindingService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IProductService private readonly productService: IProductService, - @IClipboardService private readonly clipboardService: IClipboardService + @IClipboardService private readonly clipboardService: IClipboardService, + @IOpenerService private readonly openerService: IOpenerService ) { super(); } @@ -111,7 +113,12 @@ export class BrowserDialogHandler extends AbstractDialogHandler { const renderBody = customOptions ? (parent: HTMLElement) => { parent.classList.add(...(customOptions.classes || [])); customOptions.markdownDetails?.forEach(markdownDetail => { - const result = this.markdownRenderer.render(markdownDetail.markdown); + const result = this.markdownRenderer.render(markdownDetail.markdown, { + actionHandler: { + callback: link => openLinkFromMarkdown(this.openerService, link, markdownDetail.markdown.isTrusted, true /* skip URL validation to prevent another dialog from showing which is unsupported */), + disposables: dialogDisposables + } + }); parent.appendChild(result.element); result.element.classList.add(...(markdownDetail.classes || [])); dialogDisposables.add(result); diff --git a/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts index dac333694a59..3a4df27b1185 100644 --- a/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts @@ -58,7 +58,7 @@ export class OpenerValidatorContributions implements IWorkbenchContribution { resourceUri = resource; } - if (await this._trustedDomainService.isValid(resourceUri)) { + if (this._trustedDomainService.isValid(resourceUri)) { return true; } else { const { scheme, authority, path, query, fragment } = resourceUri; diff --git a/src/vs/workbench/electron-sandbox/parts/dialogs/dialog.contribution.ts b/src/vs/workbench/electron-sandbox/parts/dialogs/dialog.contribution.ts index 41859ad5d3cf..305074b7fb01 100644 --- a/src/vs/workbench/electron-sandbox/parts/dialogs/dialog.contribution.ts +++ b/src/vs/workbench/electron-sandbox/parts/dialogs/dialog.contribution.ts @@ -19,6 +19,7 @@ import { DialogService } from '../../../services/dialogs/common/dialogService.js import { Disposable } from '../../../../base/common/lifecycle.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { Lazy } from '../../../../base/common/lazy.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; export class DialogHandlerContribution extends Disposable implements IWorkbenchContribution { @@ -39,11 +40,12 @@ export class DialogHandlerContribution extends Disposable implements IWorkbenchC @IInstantiationService instantiationService: IInstantiationService, @IProductService productService: IProductService, @IClipboardService clipboardService: IClipboardService, - @INativeHostService nativeHostService: INativeHostService + @INativeHostService nativeHostService: INativeHostService, + @IOpenerService openerService: IOpenerService ) { super(); - this.browserImpl = new Lazy(() => new BrowserDialogHandler(logService, layoutService, keybindingService, instantiationService, productService, clipboardService)); + this.browserImpl = new Lazy(() => new BrowserDialogHandler(logService, layoutService, keybindingService, instantiationService, productService, clipboardService, openerService)); this.nativeImpl = new Lazy(() => new NativeDialogHandler(logService, nativeHostService, productService, clipboardService)); this.model = (this.dialogService as DialogService).model; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 33ff44eee87e..067b3a93c4fd 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -906,8 +906,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench if (verifiedPublishers.length || unverfiiedPublishers.length === 1) { for (const publisher of verifiedPublishers) { customMessage.appendText('\n'); - const publisherDomainLink = URI.parse(publisher.publisherDomain!.link); - const publisherVerifiedMessage = localize('verifiedPublisherWithName', "{0} has verified ownership of `{1}`.", getPublisherLink(publisher), `${publisherDomainLink.authority}${publisherDomainLink.path === '/' ? '' : publisherDomainLink.path}`); + const publisherVerifiedMessage = localize('verifiedPublisherWithName', "{0} has verified ownership of {1}.", getPublisherLink(publisher), `[$(link-external) ${URI.parse(publisher.publisherDomain!.link).authority}](${publisher.publisherDomain!.link})`); customMessage.appendMarkdown(`$(${verifiedPublisherIcon.id}) ${publisherVerifiedMessage}`); } if (unverfiiedPublishers.length) { From b3043483c7b09fc7b83d4225ee55cc93c3776031 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 3 Feb 2025 12:11:09 +0100 Subject: [PATCH 1145/3587] debt - more work to move aways from single editing session (#239484) * * removes unused `getOrRestoreEditingSession` * adds `getEditingSession(chatSessionId)` * lipstick * rename `currentEditingSession` to global editing session * add `EditingSessionAction` with shared code to lookup widget and corresponding editing session * use `IChatEditingService.getEditingSession` for retry --- .../chat/browser/actions/chatClearActions.ts | 12 +- .../browser/actions/chatContextActions.ts | 12 +- .../browser/actions/chatExecuteActions.ts | 6 +- .../chat/browser/actions/chatTitleActions.ts | 6 +- .../chatMarkdownContentPart.ts | 2 +- .../browser/chatEditing/chatEditingActions.ts | 113 +++++++++--------- .../chatEditing/chatEditingServiceImpl.ts | 20 ++-- .../contrib/chat/browser/chatWidget.ts | 2 +- .../browser/contrib/chatInputCompletions.ts | 2 +- .../contrib/chatInputRelatedFilesContrib.ts | 6 +- .../contrib/chat/common/chatEditingService.ts | 24 ++-- .../contrib/chat/common/tools/editFileTool.ts | 7 +- .../test/browser/inlineChatController.test.ts | 2 +- .../test/browser/inlineChatSession.test.ts | 2 +- .../chatEdit/notebookCellDecorators.ts | 2 +- .../chatEdit/notebookChatActionsOverlay.ts | 2 +- .../chatEdit/notebookChatEditController.ts | 2 +- .../contrib/chatEdit/notebookSynchronizer.ts | 2 +- .../chatEdit/notebookSynchronizerService.ts | 4 +- 19 files changed, 115 insertions(+), 113 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index 87d33948281a..04ed62ec3f73 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -137,7 +137,7 @@ export function registerNewChatActions() { * @returns false if the user had edits and did not action the dialog to take action on them, true otherwise */ private async _handleCurrentEditingSession(chatEditingService: IChatEditingService, dialogService: IDialogService): Promise { - const currentEditingSession = chatEditingService.currentEditingSessionObs.get(); + const currentEditingSession = chatEditingService.globalEditingSessionObs.get(); const currentEdits = currentEditingSession?.entries.get(); const currentEditCount = currentEdits?.length; @@ -189,7 +189,7 @@ export function registerNewChatActions() { announceChatCleared(accessibilitySignalService); const widget = widgetService.getWidgetBySessionId(context.sessionId); if (widget) { - await chatEditingService.currentEditingSessionObs.get()?.stop(true); + await chatEditingService.globalEditingSessionObs.get()?.stop(true); widget.clear(); widget.attachmentModel.clear(); widget.focusInput(); @@ -200,7 +200,7 @@ export function registerNewChatActions() { const widget = chatView.widget; announceChatCleared(accessibilitySignalService); - await chatEditingService.currentEditingSessionObs.get()?.stop(true); + await chatEditingService.globalEditingSessionObs.get()?.stop(true); widget.clear(); widget.attachmentModel.clear(); widget.focusInput(); @@ -273,7 +273,7 @@ export function registerNewChatActions() { async run(accessor: ServicesAccessor, ...args: any[]) { const chatEditingService = accessor.get(IChatEditingService); - const currentEditingSession = chatEditingService.currentEditingSession; + const currentEditingSession = chatEditingService.globalEditingSession; if (!currentEditingSession) { return; } @@ -301,11 +301,11 @@ export function registerNewChatActions() { async run(accessor: ServicesAccessor, ...args: any[]) { const chatEditingService = accessor.get(IChatEditingService); - const currentEditingSession = chatEditingService.currentEditingSession; + const currentEditingSession = chatEditingService.globalEditingSession; if (!currentEditingSession) { return; } - await chatEditingService.currentEditingSession?.redoInteraction(); + await currentEditingSession.redoInteraction(); } }); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 4123c4b8d347..f4590dbb70d5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -496,7 +496,7 @@ export class AttachContextAction extends Action2 { } else { // file attachment if (chatEditingService) { - chatEditingService.currentEditingSessionObs.get()?.addFileToWorkingSet(pick.resource); + chatEditingService.globalEditingSessionObs.get()?.addFileToWorkingSet(pick.resource); } else { toAttach.push({ id: this._getFileContextId({ resource: pick.resource }), @@ -521,7 +521,7 @@ export class AttachContextAction extends Action2 { const uri = editor instanceof DiffEditorInput ? editor.modified.resource : editor.resource; if (uri) { if (chatEditingService) { - chatEditingService.currentEditingSessionObs.get()?.addFileToWorkingSet(uri); + chatEditingService.globalEditingSessionObs.get()?.addFileToWorkingSet(uri); } else { toAttach.push({ id: this._getFileContextId({ resource: uri }), @@ -537,7 +537,7 @@ export class AttachContextAction extends Action2 { const searchView = viewsService.getViewWithId(SEARCH_VIEW_ID) as SearchView; for (const result of searchView.model.searchResult.matches()) { if (chatEditingService) { - chatEditingService.currentEditingSessionObs.get()?.addFileToWorkingSet(result.resource); + chatEditingService.globalEditingSessionObs.get()?.addFileToWorkingSet(result.resource); } else { toAttach.push({ id: this._getFileContextId({ resource: result.resource }), @@ -576,7 +576,7 @@ export class AttachContextAction extends Action2 { }, [])); const selectedFiles = await quickInputService.pick(itemsPromise, { placeHolder: localize('relatedFiles', 'Add related files to your working set'), canPickMany: true }); for (const file of selectedFiles ?? []) { - chatEditingService?.currentEditingSessionObs.get()?.addFileToWorkingSet(file.value); + chatEditingService?.globalEditingSessionObs.get()?.addFileToWorkingSet(file.value); } } else if (isScreenshotQuickPickItem(pick)) { const blob = await hostService.getScreenshot(); @@ -779,7 +779,7 @@ export class AttachContextAction extends Action2 { }); } } else if (context.showFilesOnly) { - if (chatEditingService?.hasRelatedFilesProviders() && (widget.getInput() || chatEditingService.currentEditingSessionObs.get()?.workingSet.size)) { + if (chatEditingService?.hasRelatedFilesProviders() && (widget.getInput() || chatEditingService.globalEditingSessionObs.get()?.workingSet.size)) { quickPickItems.push({ kind: 'related-files', id: 'related-files', @@ -853,7 +853,7 @@ export class AttachContextAction extends Action2 { // Avoid attaching the same context twice const attachedContext = widget.attachmentModel.getAttachmentIDs(); if (chatEditingService) { - for (const [file, state] of chatEditingService.currentEditingSessionObs.get()?.workingSet.entries() ?? []) { + for (const [file, state] of chatEditingService.globalEditingSessionObs.get()?.workingSet.entries() ?? []) { if (state.state !== WorkingSetEntryState.Suggested) { attachedContext.add(this._getFileContextId({ resource: file })); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 8bf302ae260e..3baa6aa980ae 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -138,7 +138,7 @@ export class ToggleAgentModeAction extends Action2 { const chatService = accessor.get(IChatService); const commandService = accessor.get(ICommandService); const dialogService = accessor.get(IDialogService); - const currentEditingSession = chatEditingService.currentEditingSession; + const currentEditingSession = chatEditingService.globalEditingSession; if (!currentEditingSession) { return; } @@ -427,7 +427,7 @@ class SendToChatEditingAction extends Action2 { const dialogService = accessor.get(IDialogService); const chatEditingService = accessor.get(IChatEditingService); - const currentEditingSession = chatEditingService.currentEditingSessionObs.get(); + const currentEditingSession = chatEditingService.globalEditingSessionObs.get(); const currentEditCount = currentEditingSession?.entries.get().length; if (currentEditCount) { const result = await dialogService.confirm({ @@ -449,7 +449,7 @@ class SendToChatEditingAction extends Action2 { const { widget: editingWidget } = await viewsService.openView(EditsViewId) as ChatViewPane; for (const attachment of widget.attachmentModel.attachments) { if (attachment.isFile && URI.isUri(attachment.value)) { - chatEditingService.currentEditingSessionObs.get()?.addFileToWorkingSet(attachment.value); + chatEditingService.globalEditingSessionObs.get()?.addFileToWorkingSet(attachment.value); } else { editingWidget.attachmentModel.addContext(attachment); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index 10dd97aff86a..3f757e90ed44 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -232,7 +232,7 @@ export function registerChatTitleActions() { const configurationService = accessor.get(IConfigurationService); const dialogService = accessor.get(IDialogService); const chatEditingService = accessor.get(IChatEditingService); - const currentEditingSession = chatEditingService.currentEditingSession; + const currentEditingSession = chatEditingService.getEditingSession(chatModel.sessionId); if (!currentEditingSession) { return; } @@ -463,9 +463,9 @@ export function registerChatTitleActions() { await viewsService.openView(EditsViewId); - let editingSession = chatEditingService.currentEditingSessionObs.get(); + let editingSession = chatEditingService.globalEditingSessionObs.get(); if (!editingSession) { - editingSession = await waitForState(chatEditingService.currentEditingSessionObs); + editingSession = await waitForState(chatEditingService.globalEditingSessionObs); } if (!editingSession) { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index 8d7654540df9..f00b4c0d3e93 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -335,7 +335,7 @@ class CollapsedCodeBlock extends Disposable { this._uri = uri; const iconText = this.labelService.getUriBasenameLabel(uri); - const modifiedEntry = this.chatEditingService.currentEditingSession?.getEntry(uri); + const modifiedEntry = this.chatEditingService.globalEditingSession?.getEntry(uri); const isComplete = !modifiedEntry?.isCurrentlyBeingModified.get(); let iconClasses: string[] = []; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 76f056f72b0c..17573774ae7a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -17,7 +17,7 @@ import { ITextModel } from '../../../../../editor/common/model.js'; import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { localize, localize2 } from '../../../../../nls.js'; -import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { Action2, IAction2Options, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; @@ -35,19 +35,41 @@ import { CHAT_CATEGORY } from '../actions/chatActions.js'; import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js'; import { EditsAttachmentModel } from '../chatAttachmentModel.js'; -abstract class WorkingSetAction extends Action2 { +export abstract class EditingSessionAction extends Action2 { + + constructor(opts: Readonly) { + super({ + category: CHAT_CATEGORY, + ...opts + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { const chatEditingService = accessor.get(IChatEditingService); - const currentEditingSession = chatEditingService.currentEditingSession; - if (!currentEditingSession) { + const chatWidget = accessor.get(IChatWidgetService).lastFocusedWidget; + + if (chatWidget?.location !== ChatAgentLocation.EditingSession || !chatWidget.viewModel) { return; } - const chatWidget = accessor.get(IChatWidgetService).lastFocusedWidget; - if (chatWidget?.location !== ChatAgentLocation.EditingSession) { + const chatSessionId = chatWidget.viewModel.model.sessionId; + const editingSession = chatEditingService.getEditingSession(chatSessionId); + + if (!editingSession) { return; } + return this.runEditingSessionAction(accessor, editingSession, chatWidget, ...args); + } + + abstract runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]): any; +} + + +abstract class WorkingSetAction extends EditingSessionAction { + + runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]) { + const uris: URI[] = []; if (URI.isUri(args[0])) { uris.push(args[0]); @@ -58,10 +80,10 @@ abstract class WorkingSetAction extends Action2 { return; } - return this.runWorkingSetAction(accessor, currentEditingSession, chatWidget, ...uris); + return this.runWorkingSetAction(accessor, editingSession, chatWidget, ...uris); } - abstract runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, chatWidget: IChatWidget | undefined, ...uris: URI[]): any; + abstract runWorkingSetAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget | undefined, ...uris: URI[]): any; } registerAction2(class MarkFileAsReadonly extends WorkingSetAction { @@ -249,7 +271,7 @@ registerAction2(class DiscardAction extends WorkingSetAction { } }); -export class ChatEditingAcceptAllAction extends Action2 { +export class ChatEditingAcceptAllAction extends EditingSessionAction { constructor() { super({ @@ -280,13 +302,8 @@ export class ChatEditingAcceptAllAction extends Action2 { }); } - async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const chatEditingService = accessor.get(IChatEditingService); - const currentEditingSession = chatEditingService.currentEditingSession; - if (!currentEditingSession) { - return; - } - await currentEditingSession.accept(); + override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]) { + await editingSession.accept(); } } registerAction2(ChatEditingAcceptAllAction); @@ -331,7 +348,7 @@ registerAction2(ChatEditingDiscardAllAction); export async function discardAllEditsWithConfirmation(accessor: ServicesAccessor): Promise { const chatEditingService = accessor.get(IChatEditingService); const dialogService = accessor.get(IDialogService); - const currentEditingSession = chatEditingService.currentEditingSession; + const currentEditingSession = chatEditingService.globalEditingSession; if (!currentEditingSession) { return false; } @@ -356,7 +373,7 @@ export async function discardAllEditsWithConfirmation(accessor: ServicesAccessor return true; } -export class ChatEditingRemoveAllFilesAction extends Action2 { +export class ChatEditingRemoveAllFilesAction extends EditingSessionAction { static readonly ID = 'chatEditing.clearWorkingSet'; constructor() { @@ -377,29 +394,20 @@ export class ChatEditingRemoveAllFilesAction extends Action2 { }); } - async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const chatEditingService = accessor.get(IChatEditingService); - const currentEditingSession = chatEditingService.currentEditingSession; - if (!currentEditingSession) { - return; - } - + override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]): Promise { // Remove all files from working set - const chatWidget = accessor.get(IChatWidgetService).getWidgetBySessionId(currentEditingSession.chatSessionId); - const uris = [...currentEditingSession.workingSet.keys()]; - currentEditingSession.remove(WorkingSetEntryRemovalReason.User, ...uris); + const uris = [...editingSession.workingSet.keys()]; + editingSession.remove(WorkingSetEntryRemovalReason.User, ...uris); // Remove all file attachments - const attachmentModel = chatWidget?.attachmentModel as EditsAttachmentModel | undefined; - const fileAttachments = attachmentModel ? [...attachmentModel.excludedFileAttachments, ...attachmentModel.fileAttachments] : []; - + const fileAttachments = chatWidget.attachmentModel ? [...(chatWidget.attachmentModel as EditsAttachmentModel).excludedFileAttachments, ...(chatWidget.attachmentModel as EditsAttachmentModel).fileAttachments] : []; const attachmentIdsToRemove = fileAttachments.map(attachment => (attachment.value as URI).toString()); - chatWidget?.attachmentModel.delete(...attachmentIdsToRemove); + chatWidget.attachmentModel.delete(...attachmentIdsToRemove); } } registerAction2(ChatEditingRemoveAllFilesAction); -export class ChatEditingShowChangesAction extends Action2 { +export class ChatEditingShowChangesAction extends EditingSessionAction { static readonly ID = 'chatEditing.viewChanges'; static readonly LABEL = localize('chatEditing.viewChanges', 'View All Edits'); @@ -422,32 +430,25 @@ export class ChatEditingShowChangesAction extends Action2 { }); } - async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const chatEditingService = accessor.get(IChatEditingService); - const currentEditingSession = chatEditingService.currentEditingSession; - if (!currentEditingSession) { - return; - } - await currentEditingSession.show(); + override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]): Promise { + await editingSession.show(); } } registerAction2(ChatEditingShowChangesAction); -registerAction2(class AddFilesToWorkingSetAction extends Action2 { +registerAction2(class AddFilesToWorkingSetAction extends EditingSessionAction { constructor() { super({ id: 'workbench.action.chat.addSelectedFilesToWorkingSet', title: localize2('workbench.action.chat.addSelectedFilesToWorkingSet.label', "Add Selected Files to Working Set"), icon: Codicon.attach, - category: CHAT_CATEGORY, precondition: ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), f1: true }); } - override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]): Promise { const listService = accessor.get(IListService); - const chatEditingService = accessor.get(IChatEditingService); const editorGroupService = accessor.get(IEditorGroupsService); const uris: URI[] = []; @@ -472,7 +473,7 @@ registerAction2(class AddFilesToWorkingSetAction extends Action2 { } for (const file of uris) { - chatEditingService?.currentEditingSessionObs.get()?.addFileToWorkingSet(file); + editingSession.addFileToWorkingSet(file); } } }); @@ -527,7 +528,7 @@ registerAction2(class RemoveAction extends Action2 { return; } - const session = chatEditingService.currentEditingSession; + const session = chatEditingService.globalEditingSession; if (!session) { return; } @@ -542,7 +543,7 @@ registerAction2(class RemoveAction extends Action2 { const requestsToRemove = chatRequests.slice(itemIndex); const requestIdsToRemove = new Set(requestsToRemove.map(request => request.id)); - const entriesModifiedInRequestsToRemove = chatEditingService.currentEditingSessionObs.get()?.entries.get().filter((entry) => requestIdsToRemove.has(entry.lastModifyingRequestId)) ?? []; + const entriesModifiedInRequestsToRemove = chatEditingService.globalEditingSessionObs.get()?.entries.get().filter((entry) => requestIdsToRemove.has(entry.lastModifyingRequestId)) ?? []; const shouldPrompt = entriesModifiedInRequestsToRemove.length > 0 && configurationService.getValue('chat.editing.confirmEditRequestRemoval') === true; let message: string; @@ -629,7 +630,7 @@ registerAction2(class OpenWorkingSetHistoryAction extends Action2 { } const snapshotRequestId = requests[snapshotRequestIndex]?.id; if (snapshotRequestId) { - const snapshot = chatEditingService.currentEditingSession?.getSnapshotUri(snapshotRequestId, context.uri); + const snapshot = chatEditingService.globalEditingSession?.getSnapshotUri(snapshotRequestId, context.uri); if (snapshot) { const editor = await editorService.openEditor({ resource: snapshot, label: localize('chatEditing.snapshot', '{0} (Snapshot {1})', basename(context.uri), snapshotRequestIndex - 1), options: { transient: true, activation: EditorActivation.ACTIVATE } }); if (isCodeEditor(editor)) { @@ -640,7 +641,7 @@ registerAction2(class OpenWorkingSetHistoryAction extends Action2 { } }); -registerAction2(class ResolveSymbolsContextAction extends Action2 { +registerAction2(class ResolveSymbolsContextAction extends EditingSessionAction { constructor() { super({ id: 'workbench.action.edits.addFilesFromReferences', @@ -656,15 +657,13 @@ registerAction2(class ResolveSymbolsContextAction extends Action2 { }); } - override async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const widgetService = accessor.get(IChatWidgetService); - const textModelService = accessor.get(ITextModelService); - const languageFeaturesService = accessor.get(ILanguageFeaturesService); - const [widget] = widgetService.getWidgetsByLocations(ChatAgentLocation.EditingSession); - if (!widget || args.length === 0 || !isLocation(args[0])) { + override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]): Promise { + if (args.length === 0 || !isLocation(args[0])) { return; } + const textModelService = accessor.get(ITextModelService); + const languageFeaturesService = accessor.get(ILanguageFeaturesService); const symbol = args[0] as Location; const modelReference = await textModelService.createModelReference(symbol.uri); @@ -685,10 +684,10 @@ registerAction2(class ResolveSymbolsContextAction extends Action2 { // how important it is that they make it into the working set as it has limited size const attachments = []; for (const reference of [...definitions, ...implementations, ...references]) { - attachments.push(widget.attachmentModel.asVariableEntry(reference.uri)); + attachments.push(chatWidget.attachmentModel.asVariableEntry(reference.uri)); } - widget.attachmentModel.addContext(...attachments); + chatWidget.attachmentModel.addContext(...attachments); } private async getReferences(position: Position, textModel: ITextModel, languageFeaturesService: ILanguageFeaturesService): Promise { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index 3da10d9ae3c8..95d6d3330fa4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts @@ -64,11 +64,11 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return result; }); - get currentEditingSession(): IChatEditingSession | null { + get globalEditingSession(): IChatEditingSession | null { return this._currentSessionObs.get(); } - get currentEditingSessionObs(): IObservable { + get globalEditingSessionObs(): IObservable { return this._currentSessionObs; } @@ -143,7 +143,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic const sessionIdToRestore = storageService.get(STORAGE_KEY_EDITING_SESSION, StorageScope.WORKSPACE); if (isString(sessionIdToRestore)) { if (this._chatService.getOrRestoreSession(sessionIdToRestore)) { - this._restoringEditingSession = this.startOrContinueEditingSession(sessionIdToRestore); + this._restoringEditingSession = this.startOrContinueGlobalEditingSession(sessionIdToRestore); this._restoringEditingSession.finally(() => { this._restoringEditingSession = undefined; }); @@ -154,19 +154,12 @@ export class ChatEditingService extends Disposable implements IChatEditingServic } } - async getOrRestoreEditingSession(): Promise { - if (this._restoringEditingSession) { - await this._restoringEditingSession; - } - return this.currentEditingSessionObs.get(); - } - override dispose(): void { this._currentSessionObs.get()?.dispose(); super.dispose(); } - async startOrContinueEditingSession(chatSessionId: string): Promise { + async startOrContinueGlobalEditingSession(chatSessionId: string): Promise { await this._restoringEditingSession; const session = this._currentSessionObs.get(); @@ -215,6 +208,11 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return session; } + getEditingSession(chatSessionId: string): IChatEditingSession | undefined { + return this.editingSessionsObs.get() + .find(candidate => candidate.chatSessionId === chatSessionId); + } + async createAdhocEditingSession(chatSessionId: string): Promise { const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, false, this._editingSessionFileLimitPromise, this._lookupEntry.bind(this)); await session.init(); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 2d4a9c8ce4b6..bbce566f01ea 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -344,7 +344,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const sessionId = this._viewModel?.sessionId; if (sessionId) { if (sessionId !== currentEditSession?.chatSessionId) { - currentEditSession = await chatEditingService.startOrContinueEditingSession(sessionId); + currentEditSession = await chatEditingService.startOrContinueGlobalEditingSession(sessionId); } } else { if (currentEditSession) { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 68e436eeeef5..69c400a312c8 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -572,7 +572,7 @@ class BuiltinDynamicCompletions extends Disposable { const len = result.suggestions.length; // RELATED FILES - if (widget.location === ChatAgentLocation.EditingSession && widget.viewModel && this._chatEditingService.currentEditingSessionObs.get()?.chatSessionId === widget.viewModel?.sessionId) { + if (widget.location === ChatAgentLocation.EditingSession && widget.viewModel && this._chatEditingService.globalEditingSessionObs.get()?.chatSessionId === widget.viewModel?.sessionId) { const relatedFiles = (await raceTimeout(this._chatEditingService.getRelatedFiles(widget.viewModel.sessionId, widget.getInput(), token), 200)) ?? []; for (const relatedFileGroup of relatedFiles) { for (const relatedFile of relatedFileGroup.files) { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts index 3f4045b9ce09..2389cf9a3d54 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts @@ -28,7 +28,7 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben this._register(autorun(r => { this.chatEditingSessionDisposables.clear(); - const session = this.chatEditingService.currentEditingSessionObs.read(r); + const session = this.chatEditingService.globalEditingSessionObs.read(r); if (session) { this._handleNewEditingSession(session); } @@ -40,7 +40,7 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben return; } - const currentEditingSession = this.chatEditingService.currentEditingSessionObs.get(); + const currentEditingSession = this.chatEditingService.globalEditingSessionObs.get(); if (!currentEditingSession) { return; } @@ -61,7 +61,7 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben return; } - const currentEditingSession = this.chatEditingService.currentEditingSessionObs.get(); + const currentEditingSession = this.chatEditingService.globalEditingSessionObs.get(); if (!currentEditingSession || currentEditingSession.chatSessionId !== widget.viewModel?.sessionId || currentEditingSession.entries.get().length) { return; // Might have disposed while we were calculating } diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 21560cda2cb2..3a2b40178e2b 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -24,19 +24,13 @@ export interface IChatEditingService { _serviceBrand: undefined; - readonly currentEditingSessionObs: IObservable; + readonly globalEditingSessionObs: IObservable; - readonly currentEditingSession: IChatEditingSession | null; + readonly globalEditingSession: IChatEditingSession | null; - readonly editingSessionFileLimit: number; - - startOrContinueEditingSession(chatSessionId: string): Promise; - getOrRestoreEditingSession(): Promise; + startOrContinueGlobalEditingSession(chatSessionId: string): Promise; - - hasRelatedFilesProviders(): boolean; - registerRelatedFilesProvider(handle: number, provider: IChatRelatedFilesProvider): IDisposable; - getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>; + getEditingSession(chatSessionId: string): IChatEditingSession | undefined; /** * All editing sessions, sorted by recency, e.g the last created session comes first. @@ -47,6 +41,16 @@ export interface IChatEditingService { * Creates a new short lived editing session */ createAdhocEditingSession(chatSessionId: string): Promise; + + readonly editingSessionFileLimit: number; + + //#region related files + + hasRelatedFilesProviders(): boolean; + registerRelatedFilesProvider(handle: number, provider: IChatRelatedFilesProvider): IDisposable; + getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>; + + //#endregion } export interface IChatRequestDraft { diff --git a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts index f8ac80fe994d..4c758db27d31 100644 --- a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts @@ -108,7 +108,8 @@ export class EditTool implements IToolImpl { content: new MarkdownString(parameters.code + '\n````\n') }); - if (this.chatEditingService.currentEditingSession?.chatSessionId !== model.sessionId) { + const editSession = this.chatEditingService.getEditingSession(model.sessionId); + if (!editSession) { throw new Error('This tool must be called from within an editing session'); } @@ -135,8 +136,8 @@ export class EditTool implements IToolImpl { let wasFileBeingModified = false; dispose = autorun((r) => { - const currentEditingSession = this.chatEditingService.currentEditingSessionObs.read(r); - const entries = currentEditingSession?.entries.read(r); + + const entries = editSession.entries.read(r); const currentFile = entries?.find((e) => e.modifiedURI.toString() === uri.toString()); if (currentFile) { if (currentFile.isCurrentlyBeingModified.read(r)) { diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 10d38f3cc79a..1e12c2c3544c 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -163,7 +163,7 @@ suite('InlineChatController', function () { [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], [ICommandService, new SyncDescriptor(TestCommandService)], [IChatEditingService, new class extends mock() { - override currentEditingSessionObs: IObservable = observableValue(this, null); + override globalEditingSessionObs: IObservable = observableValue(this, null); override editingSessionsObs: IObservable = constObservable([]); }], [IEditorProgressService, new class extends mock() { diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts index cfa95c164672..c7a67a79d95b 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -106,7 +106,7 @@ suite('InlineChatSession', function () { } }], [IChatEditingService, new class extends mock() { - override currentEditingSessionObs: IObservable = observableValue(this, null); + override globalEditingSessionObs: IObservable = observableValue(this, null); override editingSessionsObs: IObservable = constObservable([]); }], [IChatAccessibilityService, new class extends mock() { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts index 81a5b3776ac4..f7647dda6a75 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts @@ -90,7 +90,7 @@ export class NotebookCellDiffDecorator extends DisposableStore { if (!editor) { return false; } - const value = this._chatEditingService.currentEditingSessionObs.read(r); + const value = this._chatEditingService.globalEditingSessionObs.read(r); if (!value || value.state.read(r) !== ChatEditingSessionState.StreamingEdits) { return false; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts index f58810283e32..8aa7a2c4c9d7 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts @@ -33,7 +33,7 @@ export class NotebookChatActionsOverlayController extends Disposable { const notebookModel = observableFromEvent(this.notebookEditor.onDidChangeModel, e => e); this._register(autorunWithStore((r, store) => { - const session = this._chatEditingService.currentEditingSessionObs.read(r); + const session = this._chatEditingService.globalEditingSessionObs.read(r); const model = notebookModel.read(r); if (!model || !session) { return; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts index 766e43a951c0..c6d9db8501f0 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts @@ -80,7 +80,7 @@ class NotebookChatEditorController extends Disposable { let notebookSynchronizer: IReference; const entryObs = derived((r) => { - const session = this._chatEditingService.currentEditingSessionObs.read(r); + const session = this._chatEditingService.globalEditingSessionObs.read(r); const model = notebookModel.read(r); if (!model || !session) { return; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts index 02ddf8d02ebc..2c376b3b0309 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts @@ -91,7 +91,7 @@ export class NotebookModelSynchronizer extends Disposable { super(); const entryObs = derived((r) => { - const session = _chatEditingService.currentEditingSessionObs.read(r); + const session = _chatEditingService.globalEditingSessionObs.read(r); if (!session) { return; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts index 9788828cf870..8f93143cccd0 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts @@ -28,7 +28,7 @@ class NotebookSynchronizerSaveParticipant extends NotebookSaveParticipant { } override async participate(workingCopy: IStoredFileWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress, token: CancellationToken): Promise { - const session = this._chatEditingService.currentEditingSessionObs.get(); + const session = this._chatEditingService.globalEditingSessionObs.get(); if (!session) { return; @@ -75,7 +75,7 @@ export class NotebookSynchronizerService extends Disposable implements INotebook // check if we have mirror document const resource = workingCopy.resource; - const session = this._chatEditingService.currentEditingSessionObs.get(); + const session = this._chatEditingService.globalEditingSessionObs.get(); if (session) { const entry = session.getEntry(resource); From 24851863ac83fc4d5ffe881517aa925ccfbf803f Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 3 Feb 2025 12:11:23 +0100 Subject: [PATCH 1146/3587] enable GC'ed-before-disposed warning (#239485) * fix leaking link provider * enable GC'ed-before-disposed warning --- .../promptSyntax/languageFeatures/promptLinkProvider.ts | 2 +- .../performance/browser/performance.contribution.ts | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts index 854272e736ed..bed3c7b3cf74 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts @@ -42,7 +42,7 @@ export class PromptLinkProvider extends Disposable implements LinkProvider { ) { super(); - this.languageService.linkProvider.register(languageSelector, this); + this._register(this.languageService.linkProvider.register(languageSelector, this)); this.parserProvider = this._register(new ObjectCache(this.createParser.bind(this))); } diff --git a/src/vs/workbench/contrib/performance/browser/performance.contribution.ts b/src/vs/workbench/contrib/performance/browser/performance.contribution.ts index bd69320c3584..cfe30f3c1936 100644 --- a/src/vs/workbench/contrib/performance/browser/performance.contribution.ts +++ b/src/vs/workbench/contrib/performance/browser/performance.contribution.ts @@ -142,17 +142,11 @@ Registry.as(Extensions.Workbench).registerWorkb // -- track leaking disposables, those that get GC'ed before having been disposed -// this is currently disabled because there is too many leaks and some false positives, e.g disposables from registers -// like MenuRegistry, CommandsRegistery etc should be marked as singleton - -const _enableLeakDetection = false - // || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this - ; class DisposableTracking { static readonly Id = 'perf.disposableTracking'; constructor(@IEnvironmentService envService: IEnvironmentService) { - if (!envService.isBuilt && _enableLeakDetection) { + if (!envService.isBuilt && !envService.extensionTestsLocationURI) { setDisposableTracker(new GCBasedDisposableTracker()); } } From b0472538f6d09a67da0e2dcffd02ca04e456cfac Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 3 Feb 2025 12:44:58 +0100 Subject: [PATCH 1147/3587] renaming hideWidgets to hideGlyphHover in GlyphHoverController (#236089) --- .../hover/browser/glyphHoverController.ts | 18 +++++++----------- .../contrib/chat/browser/codeBlockPart.ts | 6 +++--- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/vs/editor/contrib/hover/browser/glyphHoverController.ts b/src/vs/editor/contrib/hover/browser/glyphHoverController.ts index c0d37ae82e02..9048716322a2 100644 --- a/src/vs/editor/contrib/hover/browser/glyphHoverController.ts +++ b/src/vs/editor/contrib/hover/browser/glyphHoverController.ts @@ -93,7 +93,7 @@ export class GlyphHoverController extends Disposable implements IEditorContribut this._listenersStore.add(this._editor.onMouseLeave((e) => this._onEditorMouseLeave(e))); this._listenersStore.add(this._editor.onDidChangeModel(() => { this._cancelScheduler(); - this._hideWidgets(); + this.hideGlyphHover(); })); this._listenersStore.add(this._editor.onDidChangeModelContent(() => this._cancelScheduler())); this._listenersStore.add(this._editor.onDidScrollChange((e: IScrollEvent) => this._onEditorScrollChanged(e))); @@ -110,7 +110,7 @@ export class GlyphHoverController extends Disposable implements IEditorContribut private _onEditorScrollChanged(e: IScrollEvent): void { if (e.scrollTopChanged || e.scrollLeftChanged) { - this._hideWidgets(); + this.hideGlyphHover(); } } @@ -120,7 +120,7 @@ export class GlyphHoverController extends Disposable implements IEditorContribut if (shouldNotHideCurrentHoverWidget) { return; } - this._hideWidgets(); + this.hideGlyphHover(); } private _isMouseOnGlyphHoverWidget(mouseEvent: IPartialEditorMouseEvent): boolean { @@ -148,7 +148,7 @@ export class GlyphHoverController extends Disposable implements IEditorContribut if (_sticky) { return; } - this._hideWidgets(); + this.hideGlyphHover(); } private _shouldNotRecomputeCurrentHoverWidget(mouseEvent: IEditorMouseEvent): boolean { @@ -183,7 +183,7 @@ export class GlyphHoverController extends Disposable implements IEditorContribut if (_sticky) { return; } - this._hideWidgets(); + this.hideGlyphHover(); } private _tryShowHoverWidget(mouseEvent: IEditorMouseEvent): boolean { @@ -202,10 +202,10 @@ export class GlyphHoverController extends Disposable implements IEditorContribut // Do not hide hover when a modifier key is pressed return; } - this._hideWidgets(); + this.hideGlyphHover(); } - private _hideWidgets(): void { + public hideGlyphHover(): void { if (_sticky) { return; } @@ -219,10 +219,6 @@ export class GlyphHoverController extends Disposable implements IEditorContribut return this._glyphWidget; } - public hideContentHover(): void { - this._hideWidgets(); - } - public override dispose(): void { super.dispose(); this._unhookListeners(); diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index 29ed76b29c48..a4dd3ce27263 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -407,7 +407,7 @@ export class CodeBlockPart extends Disposable { private clearWidgets() { ContentHoverController.get(this.editor)?.hideContentHover(); - GlyphHoverController.get(this.editor)?.hideContentHover(); + GlyphHoverController.get(this.editor)?.hideGlyphHover(); } private async updateEditor(data: ICodeBlockData): Promise { @@ -742,8 +742,8 @@ export class CodeCompareBlockPart extends Disposable { private clearWidgets() { ContentHoverController.get(this.diffEditor.getOriginalEditor())?.hideContentHover(); ContentHoverController.get(this.diffEditor.getModifiedEditor())?.hideContentHover(); - GlyphHoverController.get(this.diffEditor.getOriginalEditor())?.hideContentHover(); - GlyphHoverController.get(this.diffEditor.getModifiedEditor())?.hideContentHover(); + GlyphHoverController.get(this.diffEditor.getOriginalEditor())?.hideGlyphHover(); + GlyphHoverController.get(this.diffEditor.getModifiedEditor())?.hideGlyphHover(); } private async updateEditor(data: ICodeCompareBlockData, token: CancellationToken): Promise { From b36ca1b25cf29bc060931320bf21fea7174a9d75 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 3 Feb 2025 12:46:33 +0100 Subject: [PATCH 1148/3587] Correctly placing folding icons in sticky scroll (#239487) adding code --- src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css | 4 ++-- .../editor/contrib/stickyScroll/browser/stickyScrollWidget.ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css index ffe968fdb575..12480664119e 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css @@ -37,8 +37,8 @@ .monaco-editor .sticky-line-number .codicon-folding-collapsed { float: right; transition: var(--vscode-editorStickyScroll-foldingOpacityTransition); - display: flex; - align-items: center; + position: absolute; + margin-left: 2px; } .monaco-editor .sticky-line-content { diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index f805b7879b1b..fada2e0c7cff 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -354,6 +354,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { const foldingIcon = this._renderFoldingIconForLine(foldingModel, line); if (foldingIcon) { lineNumberHTMLNode.appendChild(foldingIcon.domNode); + foldingIcon.domNode.style.left = `${layoutInfo.lineNumbersWidth + layoutInfo.lineNumbersLeft}px`; } this._editor.applyFontInfo(lineHTMLNode); @@ -525,8 +526,9 @@ class StickyFoldingIcon { public dimension: number ) { this.domNode = document.createElement('div'); - this.domNode.style.width = `${dimension}px`; + this.domNode.style.width = `26px`; this.domNode.style.height = `${dimension}px`; + this.domNode.style.lineHeight = `${dimension}px`; this.domNode.className = ThemeIcon.asClassName(isCollapsed ? foldingCollapsedIcon : foldingExpandedIcon); } From beb0595efe5aba2b3a35ef3930dd62c5355ddd84 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 3 Feb 2025 02:44:17 -0800 Subject: [PATCH 1149/3587] Add test for parsing out multiple CD PATHs and files --- .../browser/terminalCompletionService.test.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index 92d723eb8d9a..0c2ed66de936 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -423,5 +423,49 @@ suite('TerminalCompletionService', () => { { label: '../', detail: '/' }, ], { replacementIndex: 3, replacementLength: 0 }); }); + + test('cd | should support pulling from multiple paths in $CDPATH', async () => { + configurationService.setUserConfiguration('terminal.integrated.suggest.cdPath', 'relative'); + const pathPrefix = isWindows ? 'c:\\' : '/'; + const delimeter = isWindows ? ';' : ':'; + const separator = isWindows ? '\\' : '/'; + shellEnvDetection.setEnvironment({ CDPATH: `${pathPrefix}cdpath1_value${delimeter}${pathPrefix}cdpath2_value${separator}inner_dir` }, true); + + const uriPathPrefix = isWindows ? 'file:///c:/' : 'file:///'; + validResources = [ + URI.parse(`${uriPathPrefix}test`), + URI.parse(`${uriPathPrefix}cdpath1_value`), + URI.parse(`${uriPathPrefix}cdpath2_value`), + URI.parse(`${uriPathPrefix}cdpath2_value/inner_dir`) + ]; + childResources = [ + { resource: URI.parse(`${uriPathPrefix}cdpath1_value/folder1/`), isDirectory: true }, + { resource: URI.parse(`${uriPathPrefix}cdpath1_value/folder2/`), isDirectory: true }, + { resource: URI.parse(`${uriPathPrefix}cdpath1_value/file1.txt`), isFile: true }, + { resource: URI.parse(`${uriPathPrefix}cdpath2_value/inner_dir/folder1/`), isDirectory: true }, + { resource: URI.parse(`${uriPathPrefix}cdpath2_value/inner_dir/folder2/`), isDirectory: true }, + { resource: URI.parse(`${uriPathPrefix}cdpath2_value/inner_dir/file1.txt`), isFile: true }, + ]; + + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse(`${uriPathPrefix}test`), + foldersRequested: true, + filesRequested: true, + pathSeparator, + // TODO: This is a hack to make the test pass, clean up when https://github.com/microsoft/vscode/issues/239411 is done + shouldNormalizePrefix: !isWindows + }; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, capabilities); + + const finalPrefix = isWindows ? 'C:\\' : '/'; + assertCompletions(result, [ + { label: '.', detail: `${finalPrefix}test/` }, + { label: 'folder1', detail: `CDPATH ${finalPrefix}cdpath1_value/folder1/` }, + { label: 'folder2', detail: `CDPATH ${finalPrefix}cdpath1_value/folder2/` }, + { label: 'folder1', detail: `CDPATH ${finalPrefix}cdpath2_value/inner_dir/folder1/` }, + { label: 'folder2', detail: `CDPATH ${finalPrefix}cdpath2_value/inner_dir/folder2/` }, + { label: '../', detail: finalPrefix }, + ], { replacementIndex: 3, replacementLength: 0 }); + }); }); }); From ff39acb25de7a6c261e817f7c3da36bf9ebf7f79 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 3 Feb 2025 14:04:32 +0100 Subject: [PATCH 1150/3587] Edit Context: early return when edit context primary selection does not correspond to current selection (#239495) early return when edit context primary selection does not correspond to current selection --- .../browser/controller/editContext/native/nativeEditContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 537c23fec5fd..75acb7c2c8d7 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -304,7 +304,7 @@ export class NativeEditContext extends AbstractEditContext { return; } if (!this._editContextPrimarySelection.equalsSelection(this._primarySelection)) { - this._updateEditContext(); + return; } const model = this._context.viewModel.model; const startPositionOfEditContext = this._editContextStartPosition(); From 0e0b962c111745f19af8efed2a385310de2bb049 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 3 Feb 2025 06:01:57 -0800 Subject: [PATCH 1151/3587] Add tests for simple completion model and fix bad sorting issue --- .../suggest/browser/simpleCompletionModel.ts | 14 +- .../browser/simpleCompletionModel.test.ts | 199 ++++++++++++++++++ 2 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts index 4031bba7bae3..eff68db51be1 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts @@ -195,9 +195,14 @@ export class SimpleCompletionModel { return score; } - // Sort files with the same score against each other specially + // Sort by underscore penalty (eg. `__init__/` should be penalized) + if (a.underscorePenalty !== b.underscorePenalty) { + return a.underscorePenalty - b.underscorePenalty; + } + + // Sort files of the same name by extension const isArg = leadingLineContent.includes(' '); - if (!isArg && a.fileExtLow.length > 0 && b.fileExtLow.length > 0) { + if (!isArg && a.labelLowExcludeFileExt === b.labelLowExcludeFileExt) { // Then by label length ascending (excluding file extension if it's a file) score = a.labelLowExcludeFileExt.length - b.labelLowExcludeFileExt.length; if (score !== 0) { @@ -215,11 +220,6 @@ export class SimpleCompletionModel { } } - // Sort by underscore penalty (eg. `__init__/` should be penalized) - if (a.underscorePenalty !== b.underscorePenalty) { - return a.underscorePenalty - b.underscorePenalty; - } - // Sort by folder depth (eg. `vscode/` should come before `vscode-.../`) if (a.labelLowNormalizedPath && b.labelLowNormalizedPath) { // Directories diff --git a/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts b/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts new file mode 100644 index 000000000000..706c1fc1f30b --- /dev/null +++ b/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts @@ -0,0 +1,199 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { LineContext, SimpleCompletionModel } from '../../browser/simpleCompletionModel.js'; +import { SimpleCompletionItem, type ISimpleCompletion } from '../../browser/simpleCompletionItem.js'; + +function createItem(options: Partial): SimpleCompletionItem { + return new SimpleCompletionItem({ + ...options, + label: options.label || 'defaultLabel', + provider: options.provider || 'defaultProvider', + replacementIndex: options.replacementIndex || 0, + replacementLength: options.replacementLength || 1, + }); +} + +function createFileItems(...labels: string[]): SimpleCompletionItem[] { + return labels.map(label => createItem({ label, isFile: true })); +} + +function createFileItemsModel(...labels: string[]): SimpleCompletionModel { + return new SimpleCompletionModel( + createFileItems(...labels), + new LineContext('', 0) + ); +} + +function createFolderItems(...labels: string[]): SimpleCompletionItem[] { + return labels.map(label => createItem({ label, isDirectory: true })); +} + +function createFolderItemsModel(...labels: string[]): SimpleCompletionModel { + return new SimpleCompletionModel( + createFolderItems(...labels), + new LineContext('', 0) + ); +} + +function assertItems(model: SimpleCompletionModel, labels: string[]): void { + assert.deepStrictEqual(model.items.map(i => i.completion.label), labels); + assert.strictEqual(model.items.length, labels.length); // sanity check +} + +suite('SimpleCompletionModel', function () { + ensureNoDisposablesAreLeakedInTestSuite(); + + let model: SimpleCompletionModel; + + test('should handle an empty list', function () { + model = new SimpleCompletionModel([], new LineContext('', 0)); + + assert.strictEqual(model.items.length, 0); + }); + + test('should handle a list with one item', function () { + model = new SimpleCompletionModel([ + createItem({ label: 'a' }), + ], new LineContext('', 0)); + + assert.strictEqual(model.items.length, 1); + assert.strictEqual(model.items[0].completion.label, 'a'); + }); + + test('should sort alphabetically', function () { + model = new SimpleCompletionModel([ + createItem({ label: 'b' }), + createItem({ label: 'z' }), + createItem({ label: 'a' }), + ], new LineContext('', 0)); + + assert.strictEqual(model.items.length, 3); + assert.strictEqual(model.items[0].completion.label, 'a'); + assert.strictEqual(model.items[1].completion.label, 'b'); + assert.strictEqual(model.items[2].completion.label, 'z'); + }); + + suite('files and folders', () => { + test('should deprioritize files that start with underscore', function () { + const initial = ['_a', 'a', 'z']; + const expected = ['a', 'z', '_a']; + assertItems(createFileItemsModel(...initial), expected); + assertItems(createFolderItemsModel(...initial), expected); + }); + + test('should ignore the dot in dotfiles when sorting', function () { + const initial = ['b', '.a', 'a', '.b']; + const expected = ['.a', 'a', 'b', '.b']; + assertItems(createFileItemsModel(...initial), expected); + assertItems(createFolderItemsModel(...initial), expected); + }); + + test('should handle many files and folders correctly', function () { + // This is VS Code's root directory with some python items added that have special + // sorting + const items = [ + ...createFolderItems( + '__pycache', + '.build', + '.configurations', + '.devcontainer', + '.eslint-plugin-local', + '.github', + '.profile-oss', + '.vscode', + '.vscode-test', + 'build', + 'cli', + 'extensions', + 'node_modules', + 'out', + 'remote', + 'resources', + 'scripts', + 'src', + 'test', + ), + ...createFileItems( + '__init__.py', + '.editorconfig', + '.eslint-ignore', + '.git-blame-ignore-revs', + '.gitattributes', + '.gitignore', + '.lsifrc.json', + '.mailmap', + '.mention-bot', + '.npmrc', + '.nvmrc', + '.vscode-test.js', + 'cglicenses.json', + 'cgmanifest.json', + 'CodeQL.yml', + 'CONTRIBUTING.md', + 'eslint.config.js', + 'gulpfile.js', + 'LICENSE.txt', + 'package-lock.json', + 'package.json', + 'product.json', + 'README.md', + 'SECURITY.md', + 'ThirdPartyNotices.txt', + 'tsfmt.json', + ) + ]; + const model = new SimpleCompletionModel(items, new LineContext('', 0)); + assertItems(model, [ + '.build', + 'build', + 'cglicenses.json', + 'cgmanifest.json', + 'cli', + 'CodeQL.yml', + '.configurations', + 'CONTRIBUTING.md', + '.devcontainer', + '.npmrc', + '.gitignore', + '.editorconfig', + 'eslint.config.js', + '.eslint-ignore', + '.eslint-plugin-local', + 'extensions', + '.gitattributes', + '.git-blame-ignore-revs', + '.github', + 'gulpfile.js', + 'LICENSE.txt', + '.lsifrc.json', + '.nvmrc', + '.mailmap', + '.mention-bot', + 'node_modules', + 'out', + 'package.json', + 'package-lock.json', + 'product.json', + '.profile-oss', + 'README.md', + 'remote', + 'resources', + 'scripts', + 'SECURITY.md', + 'src', + 'test', + 'ThirdPartyNotices.txt', + 'tsfmt.json', + '.vscode', + '.vscode-test', + '.vscode-test.js', + '__init__.py', + '__pycache', + ]); + }); + }); +}); From bb4a59dbbc307681981bab3abcf72139320c4064 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 3 Feb 2025 15:40:35 +0100 Subject: [PATCH 1152/3587] debt - fix some disposabe leaks (#239497) --- src/vs/workbench/contrib/debug/browser/breakpointsView.ts | 4 ++-- src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts | 4 ++-- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index bc5b100d27b4..a4da5adbe0e3 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -160,7 +160,7 @@ export class BreakpointsView extends ViewPane { this._register(this.list.onContextMenu(this.onListContextMenu, this)); - this.list.onMouseMiddleClick(async ({ element }) => { + this._register(this.list.onMouseMiddleClick(async ({ element }) => { if (element instanceof Breakpoint) { await this.debugService.removeBreakpoints(element.getId()); } else if (element instanceof FunctionBreakpoint) { @@ -170,7 +170,7 @@ export class BreakpointsView extends ViewPane { } else if (element instanceof InstructionBreakpoint) { await this.debugService.removeInstructionBreakpoints(element.instructionReference, element.offset); } - }); + })); this._register(this.list.onDidOpen(async e => { if (!e.element) { diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 84c072d12f23..6e639a98c133 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -349,11 +349,11 @@ class MarkerWidget extends Disposable { return undefined; } })); - this.disposables.add(toDisposable(() => multilineActionbar.dispose())); + this.disposables.add(multilineActionbar); const viewModel = this.markersViewModel.getViewModel(marker); const multiline = viewModel && viewModel.multiline; - const action = new Action(toggleMultilineAction); + const action = this.disposables.add(new Action(toggleMultilineAction)); action.enabled = !!viewModel && marker.lines.length > 1; action.tooltip = multiline ? localize('single line', "Show message in single line") : localize('multi line', "Show message in multiple lines"); action.class = ThemeIcon.asClassName(multiline ? expandedIcon : collapsedIcon); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 84bec04b859e..c7c7b4c68ed6 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1457,7 +1457,7 @@ class SCMInputWidgetToolbar extends WorkbenchToolBar { this._disposables.value.add(input.repository.provider.onDidChangeResources(() => updateToolbar())); this._disposables.value.add(this.storageService.onDidChangeValue(StorageScope.PROFILE, SCMInputWidgetStorageKey.LastActionId, this._disposables.value)(() => updateToolbar())); - this.actionRunner = new SCMInputWidgetActionRunner(input, this.storageService); + this.actionRunner = this._disposables.value.add(new SCMInputWidgetActionRunner(input, this.storageService)); this._disposables.value.add(this.actionRunner.onWillRun(e => { if ((this.actionRunner as SCMInputWidgetActionRunner).runningActions.size === 0) { super.setActions([this._cancelAction], []); From d5a710b8802c7a73ccbe06e00664de7cd62eb975 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:42:35 +0100 Subject: [PATCH 1153/3587] Fix NES change between side by side and line replace after partial typing (#239498) fix NES change between side by side and line replace after typing partially --- .../contrib/inlineCompletions/browser/view/inlineEdits/view.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index b61c9947186b..4799e2d5d467 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -238,7 +238,7 @@ export class InlineEditsView extends Disposable { (this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible' && this._previousView?.view !== 'mixedLines') || (this._useInterleavedLinesDiff.read(reader) === 'afterJump' && this._previousView?.view !== 'interleavedLines') ); - const reconsiderViewEditorWidthChange = this._previousView?.editorWidth !== this._editor.getLayoutInfo().width && + const reconsiderViewEditorWidthChange = this._previousView?.editorWidth !== this._editorObs.layoutInfoWidth.read(reader) && ( this._previousView?.view === 'sideBySide' || this._previousView?.view === 'lineReplacement' From edfff01528b5ce3741a6abf7215d7fb74182e5ac Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 3 Feb 2025 16:39:19 +0100 Subject: [PATCH 1154/3587] Fixing incorrect width sizing in expandable hover (#239501) fixing incorrect width sizing --- src/vs/editor/contrib/hover/browser/hover.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/hover/browser/hover.css b/src/vs/editor/contrib/hover/browser/hover.css index 2f5c23527133..9a00f2d4d031 100644 --- a/src/vs/editor/contrib/hover/browser/hover.css +++ b/src/vs/editor/contrib/hover/browser/hover.css @@ -46,7 +46,7 @@ .monaco-editor .monaco-hover .hover-row .verbosity-actions { border-right: 1px solid var(--vscode-editorHoverWidget-border); width: 22px; - overflow: hidden; + overflow-y: clip; } .monaco-editor .monaco-hover .hover-row .verbosity-actions-inner { From d8587cf8e73c9ebfd7008b708de92fe6eff59b10 Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 3 Feb 2025 16:52:05 +0100 Subject: [PATCH 1155/3587] chore - rename and less use of `globalEditingSession` --- .../chat/browser/chatEditing/chatEditingActions.ts | 10 +++++----- .../chat/browser/chatEditing/chatEditingServiceImpl.ts | 2 +- .../chat/browser/contrib/chatInputCompletions.ts | 2 +- .../contrib/chat/common/chatEditingService.ts | 2 +- .../contrib/inlineChat/browser/inlineChatController.ts | 2 +- .../inlineChat/browser/inlineChatSessionServiceImpl.ts | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 17573774ae7a..22ac94c44ced 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -528,7 +528,7 @@ registerAction2(class RemoveAction extends Action2 { return; } - const session = chatEditingService.globalEditingSession; + const session = chatEditingService.getEditingSession(chatModel.sessionId); if (!session) { return; } @@ -620,17 +620,17 @@ registerAction2(class OpenWorkingSetHistoryAction extends Action2 { const editorService = accessor.get(IEditorService); const chatModel = chatService.getSession(context.sessionId); - const requests = chatModel?.getRequests(); - if (!requests) { + if (!chatModel) { return; } - const snapshotRequestIndex = requests?.findIndex((v, i) => i > 0 && requests[i - 1]?.id === context.requestId); + const requests = chatModel.getRequests(); + const snapshotRequestIndex = requests.findIndex((v, i) => i > 0 && requests[i - 1]?.id === context.requestId); if (snapshotRequestIndex < 1) { return; } const snapshotRequestId = requests[snapshotRequestIndex]?.id; if (snapshotRequestId) { - const snapshot = chatEditingService.globalEditingSession?.getSnapshotUri(snapshotRequestId, context.uri); + const snapshot = chatEditingService.getEditingSession(chatModel.sessionId)?.getSnapshotUri(snapshotRequestId, context.uri); if (snapshot) { const editor = await editorService.openEditor({ resource: snapshot, label: localize('chatEditing.snapshot', '{0} (Snapshot {1})', basename(context.uri), snapshotRequestIndex - 1), options: { transient: true, activation: EditorActivation.ACTIVATE } }); if (isCodeEditor(editor)) { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index 95d6d3330fa4..e49f8637673a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts @@ -213,7 +213,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic .find(candidate => candidate.chatSessionId === chatSessionId); } - async createAdhocEditingSession(chatSessionId: string): Promise { + async createEditingSession(chatSessionId: string): Promise { const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, false, this._editingSessionFileLimitPromise, this._lookupEntry.bind(this)); await session.init(); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 69c400a312c8..717ed7e796a4 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -572,7 +572,7 @@ class BuiltinDynamicCompletions extends Disposable { const len = result.suggestions.length; // RELATED FILES - if (widget.location === ChatAgentLocation.EditingSession && widget.viewModel && this._chatEditingService.globalEditingSessionObs.get()?.chatSessionId === widget.viewModel?.sessionId) { + 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)) ?? []; for (const relatedFileGroup of relatedFiles) { for (const relatedFile of relatedFileGroup.files) { diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 3a2b40178e2b..dc2a4a951209 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -40,7 +40,7 @@ export interface IChatEditingService { /** * Creates a new short lived editing session */ - createAdhocEditingSession(chatSessionId: string): Promise; + createEditingSession(chatSessionId: string): Promise; readonly editingSessionFileLimit: number; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 9c6b69b3e4bb..6ca0b87ae9d1 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -1139,7 +1139,7 @@ export class InlineChatController implements IEditorContribution { const uri = this._editor.getModel().uri; const chatModel = this._chatService.startSession(ChatAgentLocation.Editor, token); - const editSession = await this._chatEditingService.createAdhocEditingSession(chatModel.sessionId); + const editSession = await this._chatEditingService.createEditingSession(chatModel.sessionId); // const store = new DisposableStore(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index b502148e9a4a..fe4a356ad200 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -337,7 +337,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const chatModel = this._chatService.startSession(ChatAgentLocation.EditingSession, token); - const editingSession = await this._chatEditingService.createAdhocEditingSession(chatModel.sessionId); + const editingSession = await this._chatEditingService.createEditingSession(chatModel.sessionId); editingSession.addFileToWorkingSet(uri); const store = new DisposableStore(); From 25418c6a590442002093b95e8764e59c551e7712 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 3 Feb 2025 17:10:13 +0100 Subject: [PATCH 1156/3587] Adopt `getTitleBarStyle` to know if custom title is used (fix #238921) (#239496) --- src/vs/workbench/contrib/debug/browser/debugToolBar.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 2cbbdc5602aa..683bdf081209 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -32,7 +32,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { widgetBorder, widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js'; -import { TitleBarSetting } from '../../../../platform/window/common/window.js'; +import { getTitleBarStyle, TitlebarStyle } from '../../../../platform/window/common/window.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { EditorTabsMode, IWorkbenchLayoutService, LayoutSettings, Parts } from '../../../services/layout/browser/layoutService.js'; import { CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_IN_DEBUG_MODE, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugConfiguration, IDebugService, State, VIEWLET_ID } from '../common/debug.js'; @@ -80,7 +80,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { this.$el = dom.$('div.debug-toolbar'); // Note: changes to this setting require a restart, so no need to listen to it. - const customTitleBar = this.configurationService.getValue(TitleBarSetting.TITLE_BAR_STYLE) === 'custom'; + const customTitleBar = getTitleBarStyle(this.configurationService) === TitlebarStyle.CUSTOM; // Do not allow the widget to overflow or underflow window controls. // Use CSS calculations to avoid having to force layout with `.clientWidth` From b62486810ebb372fcb6a91da19543f210243894a Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 3 Feb 2025 17:50:42 +0100 Subject: [PATCH 1157/3587] fix grammar (#239505) --- .../contrib/extensions/browser/extensions.contribution.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index bfa6ded54f1d..ddbfb82a80d7 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -1873,7 +1873,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi this.registerExtensionAction({ id: 'workbench.extensions.action.manageTrustedPublishers', - title: localize2('workbench.extensions.action.manageTrustedPublishers', "Manage Trusted Extensions Publishers"), + title: localize2('workbench.extensions.action.manageTrustedPublishers', "Manage Trusted Extension Publishers"), category: EXTENSIONS_CATEGORY, f1: true, run: async (accessor: ServicesAccessor) => { @@ -1888,7 +1888,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi })).sort((a, b) => a.label.localeCompare(b.label)); const result = await quickInputService.pick(trustedPublisherItems, { canPickMany: true, - title: localize('trustedPublishers', "Manage Trusted Extensions Publishers"), + title: localize('trustedPublishers', "Manage Trusted Extension Publishers"), placeHolder: localize('trustedPublishersPlaceholder', "Choose which publishers to trust"), }); if (result) { From 7b7de00c8d23595cafb7a7bd1aeac12c5646478f Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 3 Feb 2025 09:53:30 -0800 Subject: [PATCH 1158/3587] fix typo in the documentation link title (#239509) [ui]: fix typo in the documentation link title --- .../contrib/chat/browser/actions/chatContextActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index f4590dbb70d5..007fca5f49d4 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -967,7 +967,7 @@ const selectPromptAttachment = async (options: ISelectPromptOptions): Promise Date: Mon, 3 Feb 2025 09:57:07 -0800 Subject: [PATCH 1159/3587] fix prompt file resolve logic for empty workspaces (#239508) [config]: fix prompt file resolve logic for empty workspaces --- .../chatInstructionsFileLocator.ts | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts index 99d0cf7dd3bc..2d09533d6725 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts @@ -7,8 +7,8 @@ import { URI } from '../../../../../base/common/uri.js'; import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; import { dirname, extUri } from '../../../../../base/common/resources.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js'; import { PROMPT_SNIPPET_FILE_EXTENSION } from '../../common/promptSyntax/contentProviders/promptContentsProviderBase.js'; /** @@ -50,26 +50,26 @@ export class ChatInstructionsFileLocator { * @returns List of possible prompt instructions file locations. */ private getSourceLocations(): readonly URI[] { - const state = this.workspaceService.getWorkbenchState(); - - // nothing to do if the workspace is empty - if (state === WorkbenchState.EMPTY) { - return []; - } - const sourceLocations = PromptFilesConfig.sourceLocations(this.configService); const result = []; // otherwise for each folder provided in the configuration, create // a URI per each folder in the current workspace - const { folders } = this.workspaceService.getWorkspace(); - const workspaceRootUri = dirname(folders[0].uri); - for (const folder of folders) { - for (const sourceFolderName of sourceLocations) { + for (const sourceFolderName of sourceLocations) { + // if source folder is an absolute path, add the path as is + // without trying to resolve it against the workspace folders + const sourceFolderUri = URI.file(sourceFolderName); + if (sourceFolderUri.path === sourceFolderName) { + result.push(sourceFolderUri); + continue; + } + + const { folders } = this.workspaceService.getWorkspace(); + for (const folder of folders) { // create the source path as a path relative to the workspace // folder, or as an absolute path if the absolute value is provided - const sourceFolderUri = extUri.resolvePath(folder.uri, sourceFolderName); - result.push(sourceFolderUri); + const relativeFolderUri = extUri.resolvePath(folder.uri, sourceFolderName); + result.push(relativeFolderUri); // if not inside a workspace, we are done if (folders.length <= 1) { @@ -79,6 +79,7 @@ export class ChatInstructionsFileLocator { // if inside a workspace, consider the specified source location inside // the workspace root, to allow users to use some (e.g., `.github/prompts`) // folder as a top-level folder in the workspace + const workspaceRootUri = dirname(folders[0].uri); const workspaceFolderUri = extUri.resolvePath(workspaceRootUri, sourceFolderName); if (workspaceFolderUri.fsPath.startsWith(folder.uri.fsPath)) { result.push(workspaceFolderUri); From 63d1401deeb796b3bf5e2ace0ed58fe0675a5de8 Mon Sep 17 00:00:00 2001 From: Devraj Mehta Date: Mon, 3 Feb 2025 13:54:18 -0500 Subject: [PATCH 1160/3587] fix: add electron as an external for webpack (#239134) * fix: add electron as an external for webpack * refactor: move electron external to shared webpack --- extensions/github-authentication/extension.webpack.config.js | 2 +- extensions/shared.webpack.config.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/github-authentication/extension.webpack.config.js b/extensions/github-authentication/extension.webpack.config.js index df3adb4ee7a3..d356151d68c5 100644 --- a/extensions/github-authentication/extension.webpack.config.js +++ b/extensions/github-authentication/extension.webpack.config.js @@ -13,5 +13,5 @@ module.exports = withDefaults({ context: __dirname, entry: { extension: './src/extension.ts', - } + }, }); diff --git a/extensions/shared.webpack.config.js b/extensions/shared.webpack.config.js index 279bc199bc4a..ad9d70c24908 100644 --- a/extensions/shared.webpack.config.js +++ b/extensions/shared.webpack.config.js @@ -56,6 +56,7 @@ function withNodeDefaults(/**@type WebpackConfig & { context: string }*/extConfi }] }, externals: { + 'electron': 'commonjs electron', // ignored to avoid bundling from node_modules 'vscode': 'commonjs vscode', // ignored because it doesn't exist, 'applicationinsights-native-metrics': 'commonjs applicationinsights-native-metrics', // ignored because we don't ship native module '@azure/functions-core': 'commonjs azure/functions-core', // optioinal dependency of appinsights that we don't use @@ -204,4 +205,3 @@ module.exports.node = withNodeDefaults; module.exports.browser = withBrowserDefaults; module.exports.nodePlugins = nodePlugins; module.exports.browserPlugins = browserPlugins; - From 2f57cf2b2fcc601b57c06a16652a4bcc583c71bb Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 3 Feb 2025 10:58:57 -0800 Subject: [PATCH 1161/3587] dedup prompt file locations defined in settings (#239512) * [config]: remove duplicated source location paths * [config]: simplify the implementation --- .../chatInstructionsFileLocator.ts | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts index 2d09533d6725..f8d81c94cebd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatInstructionsFileLocator.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from '../../../../../base/common/uri.js'; +import { ResourceSet } from '../../../../../base/common/map.js'; import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; import { dirname, extUri } from '../../../../../base/common/resources.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; @@ -50,8 +51,8 @@ export class ChatInstructionsFileLocator { * @returns List of possible prompt instructions file locations. */ private getSourceLocations(): readonly URI[] { + const paths = new ResourceSet(); const sourceLocations = PromptFilesConfig.sourceLocations(this.configService); - const result = []; // otherwise for each folder provided in the configuration, create // a URI per each folder in the current workspace @@ -60,7 +61,11 @@ export class ChatInstructionsFileLocator { // without trying to resolve it against the workspace folders const sourceFolderUri = URI.file(sourceFolderName); if (sourceFolderUri.path === sourceFolderName) { - result.push(sourceFolderUri); + if (paths.has(sourceFolderUri)) { + continue; + } + + paths.add(sourceFolderUri); continue; } @@ -69,7 +74,9 @@ export class ChatInstructionsFileLocator { // create the source path as a path relative to the workspace // folder, or as an absolute path if the absolute value is provided const relativeFolderUri = extUri.resolvePath(folder.uri, sourceFolderName); - result.push(relativeFolderUri); + if (!paths.has(relativeFolderUri)) { + paths.add(relativeFolderUri); + } // if not inside a workspace, we are done if (folders.length <= 1) { @@ -81,13 +88,21 @@ export class ChatInstructionsFileLocator { // folder as a top-level folder in the workspace const workspaceRootUri = dirname(folders[0].uri); const workspaceFolderUri = extUri.resolvePath(workspaceRootUri, sourceFolderName); + // if we already have this folder in the list, skip it + if (paths.has(workspaceFolderUri)) { + continue; + } + + // otherwise, if the source location is inside a top-level workspace folder, + // add it to the list of paths too; this helps to handle the case when a + // relative path must be resolved from `root` of the workspace if (workspaceFolderUri.fsPath.startsWith(folder.uri.fsPath)) { - result.push(workspaceFolderUri); + paths.add(workspaceFolderUri); } } } - return result; + return [...paths]; } /** @@ -139,6 +154,5 @@ export class ChatInstructionsFileLocator { } return files; - } } From 722c40ee76b501e3ca882c4031d0e598db47aad8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 3 Feb 2025 11:08:00 -0800 Subject: [PATCH 1162/3587] Update src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts --- .../suggest/browser/terminalCompletionService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 9d1865bc3d78..7883784d6f07 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -290,7 +290,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo // - `c:/foo/|` -> `c:/foo/` if (foldersRequested) { resourceCompletions.push({ - label: lastWordFolder.length === 0 ? '.' : lastWordFolder, + label: lastWordFolder, provider, kind: TerminalCompletionItemKind.Folder, isDirectory: true, From a90fa289d52f7531a63cce1dc561cb1bbf4f78ba Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Mon, 3 Feb 2025 11:24:43 -0800 Subject: [PATCH 1163/3587] Add context menus to notebook global toolbar + breadcrumbs (#239523) * context menu in notebook global toolbar * breadcrumb context menu also * lil shift around * nit cleaning --- src/vs/platform/actions/common/actions.ts | 2 ++ .../browser/parts/editor/breadcrumbsControl.ts | 15 +++++++++++++-- .../notebook/browser/controller/layoutActions.ts | 7 ++----- .../browser/viewParts/notebookEditorToolbar.ts | 9 +++++++++ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 0e10b0d232f2..3abdc9acf37f 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -79,6 +79,7 @@ export class MenuId { static readonly EditorTabsBarShowTabsSubmenu = new MenuId('EditorTabsBarShowTabsSubmenu'); static readonly EditorTabsBarShowTabsZenModeSubmenu = new MenuId('EditorTabsBarShowTabsZenModeSubmenu'); static readonly EditorActionsPositionSubmenu = new MenuId('EditorActionsPositionSubmenu'); + static readonly EditorBreadcrumbsContext = new MenuId('EditorBreadcrumbsContext'); static readonly ExplorerContext = new MenuId('ExplorerContext'); static readonly ExplorerContextShare = new MenuId('ExplorerContextShare'); static readonly ExtensionContext = new MenuId('ExtensionContext'); @@ -175,6 +176,7 @@ export class MenuId { static readonly ReplInputExecute = new MenuId('ReplInputExecute'); static readonly IssueReporter = new MenuId('IssueReporter'); static readonly NotebookToolbar = new MenuId('NotebookToolbar'); + static readonly NotebookToolbarContext = new MenuId('NotebookToolbarContext'); static readonly NotebookStickyScrollContext = new MenuId('NotebookStickyScrollContext'); static readonly NotebookCellTitle = new MenuId('NotebookCellTitle'); static readonly NotebookCellDelete = new MenuId('NotebookCellDelete'); diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 9a2581907c41..a0036fb3f028 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -24,7 +24,7 @@ import { Categories } from '../../../../platform/action/common/actionCommonCateg import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; +import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; import { fillInSymbolsDragData, LocalSelectionTransfer } from '../../../../platform/dnd/browser/dnd.js'; import { FileKind, IFileService, IFileStat } from '../../../../platform/files/common/files.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; @@ -271,6 +271,7 @@ export class BreadcrumbsControl { @IFileService private readonly _fileService: IFileService, @IEditorService private readonly _editorService: IEditorService, @ILabelService private readonly _labelService: ILabelService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, @IBreadcrumbsService breadcrumbsService: IBreadcrumbsService ) { @@ -299,6 +300,14 @@ export class BreadcrumbsControl { this._disposables.add(breadcrumbsService.register(this._editorGroup.id, this._widget)); this.hide(); + + this._disposables.add(dom.addDisposableListener(this.domNode, dom.EventType.CONTEXT_MENU, e => { + const event = new StandardMouseEvent(dom.getWindow(this.domNode), e); + this.contextMenuService.showContextMenu({ + menuId: MenuId.EditorBreadcrumbsContext, + getAnchor: () => event, + }); + })); } dispose(): void { @@ -687,7 +696,9 @@ registerAction2(class ToggleBreadcrumb extends Action2 { { id: MenuId.MenubarAppearanceMenu, group: '4_editor', order: 2 }, { id: MenuId.NotebookToolbar, group: 'notebookLayout', order: 2 }, { id: MenuId.StickyScrollContext }, - { id: MenuId.NotebookStickyScrollContext, group: 'notebookView', order: 2 } + { id: MenuId.EditorBreadcrumbsContext }, + { id: MenuId.NotebookStickyScrollContext, group: 'notebookView', order: 2 }, + { id: MenuId.NotebookToolbarContext, group: 'notebookView', order: 2 } ] }); } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts index df4c6f1fe559..a278485249fd 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts @@ -262,11 +262,8 @@ registerAction2(class ToggleNotebookStickyScroll extends Action2 { }, menu: [ { id: MenuId.CommandPalette }, - { - id: MenuId.NotebookStickyScrollContext, - group: 'notebookView', - order: 2 - } + { id: MenuId.NotebookStickyScrollContext, group: 'notebookView', order: 2 }, + { id: MenuId.NotebookToolbarContext, group: 'notebookView', order: 2 } ] }); } diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts index 13f928feb064..eda688c3999d 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from '../../../../../base/browser/dom.js'; +import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; import { DomScrollableElement } from '../../../../../base/browser/ui/scrollbar/scrollableElement.js'; import { ToolBar } from '../../../../../base/browser/ui/toolbar/toolbar.js'; import { IAction, Separator } from '../../../../../base/common/actions.js'; @@ -279,6 +280,14 @@ export class NotebookEditorWorkbenchToolbar extends Disposable { )(this._updatePerEditorChange, this)); this._registerNotebookActionsToolbar(); + + this._register(DOM.addDisposableListener(this.domNode, DOM.EventType.CONTEXT_MENU, e => { + const event = new StandardMouseEvent(DOM.getWindow(this.domNode), e); + this.contextMenuService.showContextMenu({ + menuId: MenuId.NotebookToolbarContext, + getAnchor: () => event, + }); + })); } private _buildBody() { From 3e01abb473a86a5d3da88e7d757127eebfd24b31 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Mon, 3 Feb 2025 13:01:13 -0800 Subject: [PATCH 1164/3587] more descriptions for chatReferenceBinary Data (#239528) * add more description to chatReferenceBinaryData * remove extra stuff --- src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts b/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts index ec10006fbe60..081c24ad8c3a 100644 --- a/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts +++ b/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts @@ -19,13 +19,13 @@ declare module 'vscode' { readonly mimeType: string; /** - * Retrieves the binary data of the reference. + * Retrieves the binary data of the reference. This is primarily used to receive image attachments from the chat. * @returns A promise that resolves to the binary data as a Uint8Array. */ data(): Thenable; /** - * + * Retrieves a URI reference to the binary data, if available. */ readonly reference?: Uri; From 1d7bbf4c89651c204ba4538631baf22b69f5a7c2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:15:48 -0800 Subject: [PATCH 1165/3587] Disable failing test temporarily Part of #239532 --- .../suggest/test/browser/simpleCompletionModel.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts b/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts index 706c1fc1f30b..92a62738e08f 100644 --- a/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts +++ b/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts @@ -92,7 +92,8 @@ suite('SimpleCompletionModel', function () { assertItems(createFolderItemsModel(...initial), expected); }); - test('should handle many files and folders correctly', function () { + // #239532 Failing on CI not locally? + test.skip('should handle many files and folders correctly', function () { // This is VS Code's root directory with some python items added that have special // sorting const items = [ From 81cbef2ccb371b0d67e7bf80261d9379153d9f9a Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 3 Feb 2025 16:33:54 -0600 Subject: [PATCH 1166/3587] return files/folder completions unless specific options / args are provided (#239384) Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> --- .../src/terminalSuggestMain.ts | 20 +++++---- .../src/test/terminalSuggestMain.test.ts | 16 ++++--- .../contrib/tasks/common/problemCollectors.ts | 42 ++++++++++--------- 3 files changed, 47 insertions(+), 31 deletions(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 34232ebda325..7c375d4ba706 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -241,6 +241,7 @@ export async function getCompletionItemsFromSpecs( const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1); + let specificItemsProvided = false; for (const spec of specs) { const specLabels = getLabel(spec); @@ -273,6 +274,7 @@ export async function getCompletionItemsFromSpecs( items.push(...argsCompletionResult.items); filesRequested ||= argsCompletionResult.filesRequested; foldersRequested ||= argsCompletionResult.foldersRequested; + specificItemsProvided ||= argsCompletionResult.items.length > 0; } if (!argsCompletionResult?.items.length) { // Arg completions are more specific, only get options if those are not provided. @@ -281,15 +283,13 @@ export async function getCompletionItemsFromSpecs( items.push(...optionsCompletionResult.items); filesRequested ||= optionsCompletionResult.filesRequested; foldersRequested ||= optionsCompletionResult.foldersRequested; + specificItemsProvided ||= optionsCompletionResult.items.length > 0; } } } } - const shouldShowResourceCompletions = - (!terminalContext.commandLine.trim() || !items.length) && - !filesRequested && - !foldersRequested; + if (tokenType === TokenType.Command) { // Include builitin/available commands in the results @@ -299,11 +299,17 @@ export async function getCompletionItemsFromSpecs( items.push(createCompletionItem(terminalContext.cursorPosition, prefix, command, command.detail)); } } - } - - if (shouldShowResourceCompletions) { filesRequested = true; foldersRequested = true; + } else { + const shouldShowResourceCompletions = + !specificItemsProvided && + !filesRequested && + !foldersRequested; + if (shouldShowResourceCompletions) { + filesRequested = true; + foldersRequested = true; + } } let cwd: vscode.Uri | undefined; diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index 030e9d0db995..68a2abbb2566 100644 --- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -46,7 +46,8 @@ function createCodeTestSpecs(executable: string): ITestSpec2[] { const typingTests: ITestSpec2[] = []; for (let i = 1; i < executable.length; i++) { - typingTests.push({ input: `${executable.slice(0, i)}|`, expectedCompletions: [executable] }); + const input = `${executable.slice(0, i)}|`; + typingTests.push({ input, expectedCompletions: [executable], expectedResourceRequests: input.endsWith(' ') ? undefined : { type: 'both', cwd: testCwd } }); } return [ @@ -85,6 +86,9 @@ const testSpecs2: ISuiteSpec[] = [ completionSpecs: [], availableCommands: [], testSpecs: [ + { input: '|', expectedCompletions: [], expectedResourceRequests: { type: 'both', cwd: testCwd } }, + { input: '|.', expectedCompletions: [], expectedResourceRequests: { type: 'both', cwd: testCwd } }, + { input: '|./', expectedCompletions: [], expectedResourceRequests: { type: 'both', cwd: testCwd } }, { input: 'fakecommand |', expectedCompletions: [], expectedResourceRequests: { type: 'both', cwd: testCwd } }, ] }, @@ -93,11 +97,13 @@ const testSpecs2: ISuiteSpec[] = [ completionSpecs: cdSpec, availableCommands: 'cd', testSpecs: [ + // Typing a path + { input: '.|', expectedCompletions: ['cd'], expectedResourceRequests: { type: 'both', cwd: testCwd } }, + { input: './|', expectedCompletions: ['cd'], expectedResourceRequests: { type: 'both', cwd: testCwd } }, + { input: './.|', expectedCompletions: ['cd'], expectedResourceRequests: { type: 'both', cwd: testCwd } }, // Typing the command - // TODO: Shouldn't this also request file resources that contain "c" as you can run a file as a command? - { input: 'c|', expectedCompletions: ['cd'] }, - // TODO: Shouldn't this also request file resources that contain "cd" as you can run a file as a command? - { input: 'cd|', expectedCompletions: ['cd'] }, + { input: 'c|', expectedCompletions: ['cd'], expectedResourceRequests: { type: 'both', cwd: testCwd } }, + { input: 'cd|', expectedCompletions: ['cd'], expectedResourceRequests: { type: 'both', cwd: testCwd } }, // Basic arguments { input: 'cd |', expectedCompletions: ['~', '-'], expectedResourceRequests: { type: 'folders', cwd: testCwd } }, diff --git a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts index f9aabbce56c5..3cb325d21a74 100644 --- a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts +++ b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts @@ -435,26 +435,30 @@ export class WatchingProblemCollector extends AbstractProblemCollector implement }); this.modelListeners.add(this.modelService.onModelRemoved(modelEvent => { - let markerChanged: IDisposable | undefined = - Event.debounce(this.markerService.onMarkerChanged, (last: readonly URI[] | undefined, e: readonly URI[]) => { - return (last ?? []).concat(e); - }, 500, false, true)(async (markerEvent) => { - markerChanged?.dispose(); + let markerChanged: IDisposable | undefined = Event.debounce( + this.markerService.onMarkerChanged, + (last: readonly URI[] | undefined, e: readonly URI[]) => (last ?? []).concat(e), + 500, + false, + true + )(async (markerEvent: readonly URI[]) => { + if (!markerEvent || !markerEvent.includes(modelEvent.uri) || (this.markerService.read({ resource: modelEvent.uri }).length !== 0)) { + return; + } + const oldLines = Array.from(this.lines); + for (const line of oldLines) { + await this.processLineInternal(line); + } + }); + + this._register(markerChanged); // Ensures markerChanged is tracked and disposed of properly + + setTimeout(() => { + if (markerChanged) { + const _markerChanged = markerChanged; markerChanged = undefined; - if (!markerEvent || !markerEvent.includes(modelEvent.uri) || (this.markerService.read({ resource: modelEvent.uri }).length !== 0)) { - return; - } - const oldLines = Array.from(this.lines); - for (const line of oldLines) { - await this.processLineInternal(line); - } - }); - setTimeout(async () => { - // Calling dispose below can trigger the debounce event (via flushOnListenerRemove), so we - // have to unset markerChanged first to make sure the handler above doesn't dispose it again. - const _markerChanged = markerChanged; - markerChanged = undefined; - _markerChanged?.dispose(); + _markerChanged.dispose(); + } }, 600); })); } From 557130816284cfae86601274065b9f89db93cedb Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Mon, 3 Feb 2025 14:56:45 -0800 Subject: [PATCH 1167/3587] Force an update after acquiring a token interactively (#239539) * Force an update after acquiring a token interactively This will make sure the account cache is up-to-date before the acquireTokenInteractive ends. A greater fix is maybe turning the accounts cache to be a promise... bit this is the candidate fix for now. Fixes #235327 * also delete event --- .../src/node/authProvider.ts | 1 - .../src/node/cachedPublicClientApplication.ts | 27 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/extensions/microsoft-authentication/src/node/authProvider.ts b/extensions/microsoft-authentication/src/node/authProvider.ts index 0a352c8eb868..40000e086200 100644 --- a/extensions/microsoft-authentication/src/node/authProvider.ts +++ b/extensions/microsoft-authentication/src/node/authProvider.ts @@ -233,7 +233,6 @@ export class MsalAuthProvider implements AuthenticationProvider { const session = this.sessionFromAuthenticationResult(result, scopeData.originalScopes); this._telemetryReporter.sendLoginEvent(session.scopes); this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'returned session'); - this._onDidChangeSessionsEmitter.fire({ added: [session], changed: [], removed: [] }); return session; } catch (e) { lastError = e; diff --git a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts index a986b217983e..8d081f1a825b 100644 --- a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts +++ b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts @@ -149,22 +149,29 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica async acquireTokenInteractive(request: InteractiveRequest): Promise { this._logger.debug(`[acquireTokenInteractive] [${this._clientId}] [${this._authority}] [${request.scopes?.join(' ')}] loopbackClientOverride: ${request.loopbackClient ? 'true' : 'false'}`); - const result = await window.withProgress( + return await window.withProgress( { location: ProgressLocation.Notification, cancellable: true, title: l10n.t('Signing in to Microsoft...') }, - (_process, token) => this._sequencer.queue(() => raceCancellationAndTimeoutError( - this._pca.acquireTokenInteractive(request), - token, - 1000 * 60 * 5 - )) + (_process, token) => this._sequencer.queue(async () => { + const result = await raceCancellationAndTimeoutError( + this._pca.acquireTokenInteractive(request), + token, + 1000 * 60 * 5 + ); + if (this._isBrokerAvailable) { + await this._accountAccess.setAllowedAccess(result.account!, true); + } + // Force an update so that the account cache is updated. + // TODO:@TylerLeonhardt The problem is, we use the sequencer for + // change events but we _don't_ use it for the accounts cache. + // We should probably use it for the accounts cache as well. + await this._update(); + return result; + }) ); - if (this._isBrokerAvailable) { - await this._accountAccess.setAllowedAccess(result.account!, true); - } - return result; } /** From f4244472c731c63eb075f263a1fdc845e9a80589 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 4 Feb 2025 10:00:49 +1100 Subject: [PATCH 1168/3587] Enable Serialize ipynb in worker (#239453) --- extensions/ipynb/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index d9a9dd7a5143..1cf1efd4e913 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -43,7 +43,7 @@ "type": "boolean", "scope": "resource", "markdownDescription": "%ipynb.experimental.serialization%", - "default": false, + "default": true, "tags": [ "experimental" ] From 7775bb6fc9d99a41560c20562addccf326b14a5f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:25:24 -0800 Subject: [PATCH 1169/3587] Re-enable skipped test Part of #239532 --- .../suggest/test/browser/simpleCompletionModel.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts b/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts index 92a62738e08f..706c1fc1f30b 100644 --- a/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts +++ b/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts @@ -92,8 +92,7 @@ suite('SimpleCompletionModel', function () { assertItems(createFolderItemsModel(...initial), expected); }); - // #239532 Failing on CI not locally? - test.skip('should handle many files and folders correctly', function () { + test('should handle many files and folders correctly', function () { // This is VS Code's root directory with some python items added that have special // sorting const items = [ From 541aae4082f1d48bb1295e65216270936f8bf439 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 3 Feb 2025 17:29:57 -0600 Subject: [PATCH 1170/3587] rm unnecessary check (#239530) fix #239407 --- .../suggest/browser/terminalCompletionService.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index cf4debc04f02..e91ff1ba9632 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -5,7 +5,6 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../../base/common/network.js'; import { basename } from '../../../../../base/common/path.js'; import { isWindows } from '../../../../../base/common/platform.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -373,8 +372,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo let kind: TerminalCompletionItemKind | undefined; if (foldersRequested && stat.isDirectory) { kind = TerminalCompletionItemKind.Folder; - } - if (filesRequested && !stat.isDirectory && (stat.isFile || stat.resource.scheme === Schemas.file)) { + } else if (filesRequested && stat.isFile) { kind = TerminalCompletionItemKind.File; } if (kind === undefined) { From 4b9eb5a7f0c469fc084e459017140b980b3d6bd6 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 3 Feb 2025 15:31:33 -0800 Subject: [PATCH 1171/3587] improve layout of the prompt attachment (#239514) * [ui]: improve layout of the prompt attachment * [ui]: remove redundant outline style for monaco button * [ui]: remove redundant CSS styles --- .../instructionsAttachment.ts | 10 +++++++++- .../contrib/chat/browser/media/chat.css | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts index f959d7eabf11..0cb9b04a7095 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/instructionsAttachment/instructionsAttachment.ts @@ -148,7 +148,15 @@ export class InstructionsAttachmentWidget extends Disposable { this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), hintElement, title)); // create the `remove` button - const removeButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: localize('remove', "Remove") })); + const removeButton = this.renderDisposables.add( + new Button( + this.domNode, + { + supportIcons: true, + title: localize('remove', "Remove"), + }, + ), + ); removeButton.icon = Codicon.x; this.renderDisposables.add(removeButton.onDidClick((e) => { e.stopPropagation(); diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 17ac0e584427..9a0c57c3bc56 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -968,6 +968,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .chat-attached-context .chat-prompt-instructions-attachment .chat-implicit-hint { opacity: 0.7; font-size: .9em; + margin-top: -0.5px; } .chat-attached-context .chat-prompt-instructions-attachment.warning { color: var(--vscode-notificationsWarningIcon-foreground); @@ -979,6 +980,20 @@ have to be updated for changes to the rules above, or to support more deeply nes border-style: dashed; opacity: 0.75; } +.chat-attached-context .chat-prompt-instructions-attachment .monaco-button { + border-left: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); + margin-left: 3px; +} +.chat-attached-context .chat-prompt-instructions-attachment.error .monaco-button, +.chat-attached-context .chat-prompt-instructions-attachment.warning .monaco-button { + border-left-color: currentColor; +} +.chat-attached-context .chat-prompt-instructions-attachment:focus .monaco-button { + border-color: var(--vscode-focusBorder); +} +.chat-attached-context .chat-prompt-instructions-attachment .monaco-icon-label-container { + margin-top: -0.1em; +} /* * This overly-specific CSS selector is needed to beat priority of some * styles applied on the the `.chat-attached-context-attachment` element. @@ -987,6 +1002,10 @@ have to be updated for changes to the rules above, or to support more deeply nes .chat-attached-context .chat-prompt-instructions-attachments .chat-prompt-instructions-attachment.warning.implicit { border: 1px solid currentColor; } +.chat-attached-context .chat-prompt-instructions-attachments .chat-prompt-instructions-attachment.implicit .monaco-button { + padding-left: 2px; + padding-right: 2px; +} /* * If in one of the non-normal states, make sure the `main icon` of * the component has the same color as the component itself From a9b6512fe76b0804eb7e8614e80d98ced57f1a49 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 3 Feb 2025 15:34:01 -0800 Subject: [PATCH 1172/3587] [debt]: remove unused config service injection (#239544) --- .../contrib/chatDynamicVariables/chatFileReference.ts | 4 +--- .../chat/common/promptSyntax/parsers/basePromptParser.ts | 7 ++----- .../chat/common/promptSyntax/parsers/filePromptParser.ts | 4 +--- .../common/promptSyntax/parsers/textModelPromptParser.ts | 4 +--- 4 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts index d4a4574aeace..cb6ce3b9d2d9 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts @@ -9,7 +9,6 @@ import { IDynamicVariable } from '../../../common/chatVariables.js'; import { IRange } from '../../../../../../editor/common/core/range.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { FilePromptParser } from '../../../common/promptSyntax/parsers/filePromptParser.js'; -import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; /** @@ -24,7 +23,6 @@ export class ChatFileReference extends FilePromptParser implements IDynamicVaria constructor( public readonly reference: IDynamicVariable, @IInstantiationService initService: IInstantiationService, - @IConfigurationService configService: IConfigurationService, @ILogService logService: ILogService, ) { const { data } = reference; @@ -34,7 +32,7 @@ export class ChatFileReference extends FilePromptParser implements IDynamicVaria `Variable data must be an URI, got '${data}'.`, ); - super(data, [], initService, configService, logService); + super(data, [], initService, logService); } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index 9ef8199579b6..aa277f4b7166 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -22,7 +22,6 @@ import { ObservableDisposable } from '../../../../../../base/common/observableDi import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; import { PROMPT_SNIPPET_FILE_EXTENSION } from '../contentProviders/promptContentsProviderBase.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; import { FileOpenFailed, NonPromptSnippetFile, RecursiveReference, ParseError, FailedToResolveContentsStream } from '../../promptFileReferenceErrors.js'; @@ -145,7 +144,6 @@ export abstract class BasePromptParser extend private readonly promptContentsProvider: T, seenReferences: string[] = [], @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IConfigurationService protected readonly configService: IConfigurationService, @ILogService protected readonly logService: ILogService, ) { super(); @@ -173,7 +171,7 @@ export abstract class BasePromptParser extend this._register( this.promptContentsProvider.onContentChanged((streamOrError) => { - // process the the received message + // process the received message this.onContentsChanged(streamOrError, seenReferences); // indicate that we've received at least one `onContentChanged` event @@ -559,13 +557,12 @@ export class PromptFileReference extends BasePromptParser Date: Tue, 4 Feb 2025 00:51:35 +0100 Subject: [PATCH 1173/3587] SCM - add quickDiffDecorationCount context key (#239547) --- .../contrib/scm/browser/quickDiffDecorator.ts | 48 ++++++++++++++++++- .../contrib/scm/browser/quickDiffWidget.ts | 5 +- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts index 8e701d3d92c3..01c9eb2173c2 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts @@ -21,6 +21,10 @@ import { QuickDiffModel, IQuickDiffModelService } from './quickDiffModel.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { ResourceMap } from '../../../../base/common/map.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { autorun, autorunWithStore, observableFromEvent } from '../../../../base/common/observable.js'; + +export const quickDiffDecorationCount = new RawContextKey('quickDiffDecorationCount', 0); class QuickDiffDecorator extends Disposable { @@ -166,7 +170,7 @@ class QuickDiffDecorator extends Disposable { override dispose(): void { if (this.decorationsCollection) { - this.decorationsCollection?.clear(); + this.decorationsCollection.clear(); } this.decorationsCollection = undefined; this.quickDiffModelRef.dispose(); @@ -182,6 +186,10 @@ interface QuickDiffWorkbenchControllerViewState { export class QuickDiffWorkbenchController extends Disposable implements IWorkbenchContribution { private enabled = false; + private readonly quickDiffDecorationCount: IContextKey; + + private readonly activeEditor = observableFromEvent(this, + this.editorService.onDidActiveEditorChange, () => this.editorService.activeEditor); // Resource URI -> Code Editor Id -> Decoration (Disposable) private readonly decorators = new ResourceMap>(); @@ -194,10 +202,13 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben @IConfigurationService private readonly configurationService: IConfigurationService, @IQuickDiffModelService private readonly quickDiffModelService: IQuickDiffModelService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IContextKeyService contextKeyService: IContextKeyService, ) { super(); this.stylesheet = domStylesheetsJs.createStyleSheet(undefined, undefined, this._store); + this.quickDiffDecorationCount = quickDiffDecorationCount.bindTo(contextKeyService); + const onDidChangeConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorations')); this._register(onDidChangeConfiguration(this.onDidChangeConfiguration, this)); this.onDidChangeConfiguration(); @@ -266,6 +277,9 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben this.transientDisposables.add(Event.any(this.editorService.onDidCloseEditor, this.editorService.onDidVisibleEditorsChange)(() => this.onEditorsChanged())); this.onEditorsChanged(); + + this.onDidActiveEditorChange(); + this.enabled = true; } @@ -275,6 +289,7 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben } this.transientDisposables.clear(); + this.quickDiffDecorationCount.set(0); for (const [uri, decoratorMap] of this.decorators.entries()) { decoratorMap.dispose(); @@ -284,6 +299,37 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben this.enabled = false; } + private onDidActiveEditorChange(): void { + this.transientDisposables.add(autorunWithStore((reader, store) => { + const activeEditor = this.activeEditor.read(reader); + const activeTextEditorControl = this.editorService.activeTextEditorControl; + + if (!isCodeEditor(activeTextEditorControl) || !activeEditor?.resource) { + this.quickDiffDecorationCount.set(0); + return; + } + + const quickDiffModelRef = this.quickDiffModelService.createQuickDiffModelReference(activeEditor.resource); + if (!quickDiffModelRef) { + this.quickDiffDecorationCount.set(0); + return; + } + + store.add(quickDiffModelRef); + + const visibleDecorationCount = observableFromEvent(this, + quickDiffModelRef.object.onDidChange, () => { + const visibleQuickDiffs = quickDiffModelRef.object.quickDiffs.filter(quickDiff => quickDiff.visible); + return quickDiffModelRef.object.changes.filter(labeledChange => visibleQuickDiffs.some(quickDiff => quickDiff.label === labeledChange.label)).length; + }); + + store.add(autorun(reader => { + const count = visibleDecorationCount.read(reader); + this.quickDiffDecorationCount.set(count); + })); + })); + } + private onEditorsChanged(): void { for (const editor of this.editorService.visibleTextEditorControls) { if (!isCodeEditor(editor)) { diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts index 37ed63fb396f..791a1eb7f317 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts @@ -49,6 +49,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { Color } from '../../../../base/common/color.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { getOuterEditor } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; +import { quickDiffDecorationCount } from './quickDiffDecorator.js'; export const isQuickDiffVisible = new RawContextKey('dirtyDiffVisible', false); @@ -804,7 +805,7 @@ export class GotoPreviousChangeAction extends EditorAction { super({ id: 'workbench.action.editor.previousChange', label: nls.localize2('move to previous change', "Go to Previous Change"), - precondition: TextCompareEditorActiveContext.toNegated(), + precondition: ContextKeyExpr.and(TextCompareEditorActiveContext.toNegated(), quickDiffDecorationCount.notEqualsTo(0)), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F5, weight: KeybindingWeight.EditorContrib } }); } @@ -844,7 +845,7 @@ export class GotoNextChangeAction extends EditorAction { super({ id: 'workbench.action.editor.nextChange', label: nls.localize2('move to next change', "Go to Next Change"), - precondition: TextCompareEditorActiveContext.toNegated(), + precondition: ContextKeyExpr.and(TextCompareEditorActiveContext.toNegated(), quickDiffDecorationCount.notEqualsTo(0)), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F5, weight: KeybindingWeight.EditorContrib } }); } From 6bc5734484b8a91569241a08376790aa568767dd Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 3 Feb 2025 21:41:17 -0800 Subject: [PATCH 1174/3587] [debt]: pull out common constants into a standalone module (#239560) --- .../browser/actions/chatContextActions.ts | 11 +++++----- .../instructionsAttachment.ts | 4 ++-- .../chatInstructionsFileLocator.ts | 4 ++-- .../chat/common/promptSyntax/config.ts | 9 ++------ .../chat/common/promptSyntax/constants.ts | 21 +++++++++++++++++++ .../promptContentsProviderBase.ts | 8 ++----- .../languageFeatures/promptLinkProvider.ts | 11 ++-------- .../promptSyntax/parsers/basePromptParser.ts | 4 ++-- 8 files changed, 38 insertions(+), 34 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 007fca5f49d4..cdd4135f32b5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -51,8 +51,7 @@ import { IChatRequestVariableEntry } from '../../common/chatModel.js'; import { ChatRequestAgentPart } from '../../common/chatParserTypes.js'; import { IChatVariableData, IChatVariablesService } from '../../common/chatVariables.js'; import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; -import { PromptFilesConfig } from '../../common/promptSyntax/config.js'; -import { PROMPT_SNIPPET_FILE_EXTENSION } from '../../common/promptSyntax/contentProviders/promptContentsProviderBase.js'; +import { DOCUMENTATION_URL, PROMPT_FILE_EXTENSION } from '../../common/promptSyntax/constants.js'; import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView, showEditsView } from '../chat.js'; import { imageToHash, isImage } from '../chatPasteProviders.js'; import { isQuickChat } from '../chatWidget.js'; @@ -949,7 +948,7 @@ const selectPromptAttachment = async (options: ISelectPromptOptions): Promise { return files.map((file) => { const fileBasename = basename(file); - const fileWithoutExtension = fileBasename.replace(PROMPT_SNIPPET_FILE_EXTENSION, ''); + const fileWithoutExtension = fileBasename.replace(PROMPT_FILE_EXTENSION, ''); const result: IQuickPickItem & { value: URI } = { type: 'item', label: fileWithoutExtension, @@ -968,9 +967,9 @@ const selectPromptAttachment = async (options: ISelectPromptOptions): Promise extend * Check if the provided URI points to a prompt snippet. */ public static isPromptSnippet(uri: URI): boolean { - return uri.path.endsWith(PROMPT_SNIPPET_FILE_EXTENSION); + return uri.path.endsWith(PROMPT_FILE_EXTENSION); } /** From 802eba821f216155e8164eb8469b4ca9c9e2da57 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Tue, 4 Feb 2025 14:55:33 +0800 Subject: [PATCH 1175/3587] refactor: clean up unused `detectedParticipant` API (#239566) --- .../workbench/api/common/extHost.api.impl.ts | 1 - .../api/common/extHostChatAgents2.ts | 10 ---------- .../api/common/extHostTypeConverters.ts | 19 ++----------------- src/vs/workbench/api/common/extHostTypes.ts | 10 ---------- .../contrib/chat/common/chatModel.ts | 6 ------ .../contrib/chat/common/chatService.ts | 7 ------- ...ode.proposed.chatParticipantAdditions.d.ts | 12 ++---------- 7 files changed, 4 insertions(+), 61 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index d058e61b0da9..7766c78231fc 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1777,7 +1777,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseTextEditPart: extHostTypes.ChatResponseTextEditPart, ChatResponseMarkdownWithVulnerabilitiesPart: extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart, ChatResponseCommandButtonPart: extHostTypes.ChatResponseCommandButtonPart, - ChatResponseDetectedParticipantPart: extHostTypes.ChatResponseDetectedParticipantPart, ChatResponseConfirmationPart: extHostTypes.ChatResponseConfirmationPart, ChatResponseMovePart: extHostTypes.ChatResponseMovePart, ChatResponseReferencePartStatusKind: extHostTypes.ChatResponseReferencePartStatusKind, diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 4bf84b803aab..0806fa58b72a 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -224,15 +224,6 @@ class ChatAgentResponseStream { _report(dto); return this; }, - detectedParticipant(participant, command) { - throwIfDone(this.detectedParticipant); - checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); - - const part = new extHostTypes.ChatResponseDetectedParticipantPart(participant, command); - const dto = typeConvert.ChatResponseDetectedParticipantPart.from(part); - _report(dto); - return this; - }, confirmation(title, message, data, buttons) { throwIfDone(this.confirmation); checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); @@ -248,7 +239,6 @@ class ChatAgentResponseStream { if ( part instanceof extHostTypes.ChatResponseTextEditPart || part instanceof extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart || - part instanceof extHostTypes.ChatResponseDetectedParticipantPart || part instanceof extHostTypes.ChatResponseWarningPart || part instanceof extHostTypes.ChatResponseConfirmationPart || part instanceof extHostTypes.ChatResponseCodeCitationPart || diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 7613e42da8d1..82cf5f714944 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -38,7 +38,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js'; import { IViewBadge } from '../../common/views.js'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js'; import { IChatRequestVariableEntry } from '../../contrib/chat/common/chatModel.js'; -import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; +import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; import { IToolData, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import * as chatProvider from '../../contrib/chat/common/languageModels.js'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from '../../contrib/debug/common/debug.js'; @@ -2430,19 +2430,6 @@ export namespace ChatResponseMarkdownWithVulnerabilitiesPart { } } -export namespace ChatResponseDetectedParticipantPart { - export function from(part: vscode.ChatResponseDetectedParticipantPart): Dto { - return { - kind: 'agentDetection', - agentId: part.participant, - command: part.command, - }; - } - export function to(part: Dto): vscode.ChatResponseDetectedParticipantPart { - return new types.ChatResponseDetectedParticipantPart(part.agentId, part.command); - } -} - export namespace ChatResponseConfirmationPart { export function from(part: vscode.ChatResponseConfirmationPart): Dto { return { @@ -2671,7 +2658,7 @@ export namespace ChatResponseCodeCitationPart { export namespace ChatResponsePart { - export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseDetectedParticipantPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseReferencePart2 | vscode.ChatResponseMovePart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { + export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseReferencePart2 | vscode.ChatResponseMovePart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { if (part instanceof types.ChatResponseMarkdownPart) { return ChatResponseMarkdownPart.from(part); } else if (part instanceof types.ChatResponseAnchorPart) { @@ -2690,8 +2677,6 @@ export namespace ChatResponsePart { return ChatResponseMarkdownWithVulnerabilitiesPart.from(part); } else if (part instanceof types.ChatResponseCodeblockUriPart) { return ChatResponseCodeblockUriPart.from(part); - } else if (part instanceof types.ChatResponseDetectedParticipantPart) { - return ChatResponseDetectedParticipantPart.from(part); } else if (part instanceof types.ChatResponseWarningPart) { return ChatResponseWarningPart.from(part); } else if (part instanceof types.ChatResponseConfirmationPart) { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 4130d79f0ab7..ec4a0874ad57 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4517,16 +4517,6 @@ export class ChatResponseMarkdownWithVulnerabilitiesPart { } } -export class ChatResponseDetectedParticipantPart { - participant: string; - // TODO@API validate this against statically-declared slash commands? - command?: vscode.ChatCommand; - constructor(participant: string, command?: vscode.ChatCommand) { - this.participant = participant; - this.command = command; - } -} - export class ChatResponseConfirmationPart { title: string; message: string; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 406896357199..9f99ef906786 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -1342,12 +1342,6 @@ export class ChatModel extends Disposable implements IChatModel { request.response.updateContent(progress, quiet); } else if (progress.kind === 'usedContext' || progress.kind === 'reference') { request.response.applyReference(progress); - } else if (progress.kind === 'agentDetection') { - const agent = this.chatAgentService.getAgent(progress.agentId); - if (agent) { - request.response.setAgent(agent, progress.command); - this._onDidChange.fire({ kind: 'setAgent', agent, command: progress.command }); - } } else if (progress.kind === 'codeCitation') { request.response.applyCodeCitation(progress); } else if (progress.kind === 'move') { diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 4251fe543566..6788a38ec4df 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -105,12 +105,6 @@ export interface IChatContentInlineReference { kind: 'inlineReference'; } -export interface IChatAgentDetection { - agentId: string; - command?: IChatAgentCommand; - kind: 'agentDetection'; -} - export interface IChatMarkdownContent { content: IMarkdownString; inlineReferences?: Record; @@ -231,7 +225,6 @@ export type IChatProgress = | IChatContentReference | IChatContentInlineReference | IChatCodeCitation - | IChatAgentDetection | IChatProgressMessage | IChatTask | IChatTaskResult diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 374ff087cc17..3fafc42e7a45 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -17,13 +17,6 @@ declare module 'vscode' { readonly description: string; } - export class ChatResponseDetectedParticipantPart { - participant: string; - // TODO@API validate this against statically-declared slash commands? - command?: ChatCommand; - constructor(participant: string, command?: ChatCommand); - } - export interface ChatVulnerability { title: string; description: string; @@ -77,7 +70,7 @@ declare module 'vscode' { constructor(value: Uri, license: string, snippet: string); } - export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart; + export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart; export class ChatResponseWarningPart { value: MarkdownString; @@ -175,8 +168,7 @@ declare module 'vscode' { markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): void; codeblockUri(uri: Uri): void; - detectedParticipant(participant: string, command?: ChatCommand): void; - push(part: ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseWarningPart | ChatResponseProgressPart2): void; + push(part: ChatResponsePart | ChatResponseTextEditPart | ChatResponseWarningPart | ChatResponseProgressPart2): void; /** * Show an inline message in the chat view asking the user to confirm an action. From 01e36597fe244ed478f0849efbad1a82243c894a Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 4 Feb 2025 09:42:34 +0100 Subject: [PATCH 1176/3587] fix a.setTimeout is not a function (#239572) --- .../extensionManagement/common/extensionGalleryService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 90b110c251ae..7975c97c5126 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -1569,7 +1569,6 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const context = await this.requestService.request({ type: 'GET', url: this.extensionsControlUrl, - timeout: 10000 /*10s*/ }, CancellationToken.None); if (context.res.statusCode !== 200) { From c9948b69ee93cebd049925aa9d6176589863644c Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 4 Feb 2025 10:29:01 +0100 Subject: [PATCH 1177/3587] Update grammars (#239483) Fixes #239353 --- extensions/csharp/cgmanifest.json | 2 +- .../csharp/syntaxes/csharp.tmLanguage.json | 4 +- extensions/latex/cgmanifest.json | 4 +- .../latex/syntaxes/LaTeX.tmLanguage.json | 47 +- .../cpp-grammar-bailout.tmLanguage.json | 1924 +++++++++++++---- .../markdown-latex-combined.tmLanguage.json | 32 +- extensions/log/cgmanifest.json | 4 +- extensions/log/syntaxes/log.tmLanguage.json | 10 +- extensions/perl/cgmanifest.json | 2 +- extensions/razor/cgmanifest.json | 2 +- .../razor/syntaxes/cshtml.tmLanguage.json | 5 +- extensions/swift/cgmanifest.json | 2 +- .../swift/syntaxes/swift.tmLanguage.json | 60 +- 13 files changed, 1631 insertions(+), 467 deletions(-) diff --git a/extensions/csharp/cgmanifest.json b/extensions/csharp/cgmanifest.json index 58a7408dbbe7..fefe63e47868 100644 --- a/extensions/csharp/cgmanifest.json +++ b/extensions/csharp/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "dotnet/csharp-tmLanguage", "repositoryUrl": "https://github.com/dotnet/csharp-tmLanguage", - "commitHash": "d63e2661d4e0c83b6c7810eb1d0eedc5da843b04" + "commitHash": "62026a70f9fcc42d9222eccfec34ed5ee0784f3d" } }, "license": "MIT", diff --git a/extensions/csharp/syntaxes/csharp.tmLanguage.json b/extensions/csharp/syntaxes/csharp.tmLanguage.json index 4a2497a064ab..c4d9a2519dc3 100644 --- a/extensions/csharp/syntaxes/csharp.tmLanguage.json +++ b/extensions/csharp/syntaxes/csharp.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/dotnet/csharp-tmLanguage/commit/d63e2661d4e0c83b6c7810eb1d0eedc5da843b04", + "version": "https://github.com/dotnet/csharp-tmLanguage/commit/62026a70f9fcc42d9222eccfec34ed5ee0784f3d", "name": "C#", "scopeName": "source.cs", "patterns": [ @@ -4206,7 +4206,7 @@ ] }, "invocation-expression": { - "begin": "(?x)\n(?:\n (?:(\\?)\\s*)? # preceding null-conditional operator?\n (\\.)\\s*| # preceding dot?\n (->)\\s* # preceding pointer arrow?\n)?\n(@?[_[:alpha:]][_[:alnum:]]*)\\s* # method name\n(\n <\n (?\n [^<>()]++|\n <\\g*+>|\n \\(\\g*+\\)\n )*+\n >\\s*\n)? # type arguments\n(?=\\() # open paren of argument list", + "begin": "(?x)\n(?:\n (?:(\\?)\\s*)? # preceding null-conditional operator?\n (\\.)\\s*| # preceding dot?\n (->)\\s* # preceding pointer arrow?\n)?\n(@?[_[:alpha:]][_[:alnum:]]*)\\s* # method name\n(\n <\n (?\n [^<>()]|\n \\((?:[^<>()]|<[^<>()]*>|\\([^<>()]*\\))*\\)|\n <\\g*>\n )*\n >\\s*\n)? # type arguments\n(?=\\() # open paren of argument list", "beginCaptures": { "1": { "name": "keyword.operator.null-conditional.cs" diff --git a/extensions/latex/cgmanifest.json b/extensions/latex/cgmanifest.json index 25b52bf3787e..c798b2aedd75 100644 --- a/extensions/latex/cgmanifest.json +++ b/extensions/latex/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "jlelong/vscode-latex-basics", "repositoryUrl": "https://github.com/jlelong/vscode-latex-basics", - "commitHash": "59971565a7065dbb617576c04add9d891b056319" + "commitHash": "dfa69a16a1154dbc820dc1111d72faa6954dd1e2" } }, "license": "MIT", - "version": "1.9.0", + "version": "1.10.0", "description": "The files in syntaxes/ were originally part of https://github.com/James-Yu/LaTeX-Workshop. They have been extracted in the hope that they can useful outside of the LaTeX-Workshop extension.", "licenseDetail": [ "Copyright (c) vscode-latex-basics authors", diff --git a/extensions/latex/syntaxes/LaTeX.tmLanguage.json b/extensions/latex/syntaxes/LaTeX.tmLanguage.json index e06d85538464..d7201e8c652f 100644 --- a/extensions/latex/syntaxes/LaTeX.tmLanguage.json +++ b/extensions/latex/syntaxes/LaTeX.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jlelong/vscode-latex-basics/commit/59971565a7065dbb617576c04add9d891b056319", + "version": "https://github.com/jlelong/vscode-latex-basics/commit/7a35f5e0f19b28f5f1366579e2a9ad34df4f40c9", "name": "LaTeX", "scopeName": "text.tex.latex", "patterns": [ @@ -761,6 +761,49 @@ } ] }, + { + "begin": "\\s*\\\\begin\\{(?:javacode|javaverbatim|javablock|javaconcode|javaconsole|javaconverbatim)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", + "end": "\\s*\\\\end\\{(?:javacode|javaverbatim|javablock|javaconcode|javaconsole|javaconverbatim)\\*?\\}", + "captures": { + "0": { + "patterns": [ + { + "include": "#begin-env-tokenizer" + } + ] + } + }, + "patterns": [ + { + "include": "#multiline-optional-arg-no-highlight" + }, + { + "begin": "(?:\\G|(?<=\\]))(\\{)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "end": "(\\})", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "contentName": "variable.parameter.function.latex" + }, + { + "begin": "^(?=\\s*)", + "end": "^\\s*(?=\\\\end\\{(?:javacode|javaverbatim|javablock|javaconcode|javaconsole|javaconverbatim)\\*?\\})", + "contentName": "source.java", + "patterns": [ + { + "include": "source.java" + } + ] + } + ] + }, { "begin": "\\s*\\\\begin\\{(?:jlcode|jlverbatim|jlblock|jlconcode|jlconsole|jlconverbatim)\\*?\\}(?:\\[[a-zA-Z0-9_-]*\\])?(?=\\[|\\{|\\s*$)", "end": "\\s*\\\\end\\{(?:jlcode|jlverbatim|jlblock|jlconcode|jlconsole|jlconverbatim)\\*?\\}", @@ -1106,7 +1149,7 @@ ] }, { - "begin": "\\s*\\\\begin\\{([a-zA-Z]*code|lstlisting|minted|pyglist)\\*?\\}(?:\\[.*\\])?(?:\\{.*\\})?", + "begin": "\\s*\\\\begin\\{((?:[a-zA-Z]*code|lstlisting|minted|pyglist)\\*?)\\}(?:\\[.*\\])?(?:\\{.*\\})?", "captures": { "0": { "patterns": [ diff --git a/extensions/latex/syntaxes/cpp-grammar-bailout.tmLanguage.json b/extensions/latex/syntaxes/cpp-grammar-bailout.tmLanguage.json index 1f62f492b76f..4f70702d0bcf 100644 --- a/extensions/latex/syntaxes/cpp-grammar-bailout.tmLanguage.json +++ b/extensions/latex/syntaxes/cpp-grammar-bailout.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jlelong/vscode-latex-basics/commit/f17f354528411340e22402230be1b72e5aa1126a", + "version": "https://github.com/jlelong/vscode-latex-basics/commit/dfa69a16a1154dbc820dc1111d72faa6954dd1e2", "name": "C++", "scopeName": "source.cpp.embedded.latex", "patterns": [ @@ -20,6 +20,9 @@ { "include": "#function_definition" }, + { + "include": "#simple_array_assignment" + }, { "include": "#operator_overload" }, @@ -98,7 +101,7 @@ ], "repository": { "access_control_keywords": { - "match": "((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(((?:(?:protected)|(?:private)|(?:public)))(?:\\s+)?(:))", + "match": "((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(((?:protected|private|public))(?:\\s+)?(:))", "captures": { "1": { "patterns": [ @@ -516,10 +519,16 @@ "name": "punctuation.definition.comment.end.cpp" } }, - "name": "comment.block.cpp" + "name": "comment.block.cpp", + "patterns": [ + { + "match": "[^\\*]*\\n" + } + ], + "applyEndPatternLast": 1 }, "builtin_storage_type_initilizer": { - "begin": "\\s*+(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:(?:(\\b[a-z0-9]+\\b)|(\\b[a-zA-Z0-9]+_[a-zA-Z0-9]*\\b))|(\\b[a-z]+[A-Z][a-zA-Z0-9]*\\b))|(\\b[A-Z][A-Z_0-9]*\\b))|((?|\\?\\?>)|(?=[;>\\[\\]=]))|(?=\\\\end\\{(?:minted|cppcode)\\})", "beginCaptures": { "0": { @@ -1936,7 +2162,7 @@ ] }, "control_flow_keywords": { - "match": "((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\{)", + "begin": "(\\s*+((?:(?:(?:\\[\\[.*?\\]\\]|__attribute(?:__)?\\s*\\(\\s*\\(.*?\\)\\s*\\))|__declspec\\(.*?\\))|alignas\\(.*?\\))(?!\\)))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:unsigned|signed|short|long)|(?:struct|class|union|enum))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:((?:::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\{)", "end": "\\}|(?=\\\\end\\{(?:minted|cppcode)\\})", "beginCaptures": { "1": { - "name": "meta.qualified_type.cpp", + "name": "meta.qualified-type.cpp", "patterns": [ { "match": "::", "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.cpp" }, { - "match": "(?|\\?\\?>)|(?=[;>\\[\\]=]))|(?=\\\\end\\{(?:minted|cppcode)\\})", "beginCaptures": { "0": { @@ -3442,6 +3668,15 @@ { "include": "#language_constants" }, + { + "include": "#constructor_bracket_call" + }, + { + "include": "#simple_constructor_call" + }, + { + "include": "#simple_array_assignment" + }, { "include": "#builtin_storage_type_initilizer" }, @@ -3477,6 +3712,9 @@ }, { "include": "#comma" + }, + { + "include": "#unknown_variable" } ] }, @@ -3503,9 +3741,6 @@ { "include": "#preprocessor_conditional_range" }, - { - "include": "#single_line_macro" - }, { "include": "#macro" }, @@ -3524,7 +3759,7 @@ ] }, "exception_keywords": { - "match": "((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*\\s*+)((?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)\\b(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?(\\()", - "end": "\\)|(?=\\\\end\\{(?:minted|cppcode)\\})", - "beginCaptures": { - "1": { - "patterns": [ - { - "include": "#scope_resolution_function_call_inner_generated" - } - ] - }, - "2": { - "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.function.call.cpp" - }, - "3": { - "patterns": [ - { - "include": "#template_call_range_helper" - } - ] - }, - "4": {}, - "5": { - "name": "entity.name.function.call.cpp" - }, - "6": { - "patterns": [ - { - "include": "#inline_comment" - } - ] - }, - "7": { - "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" - }, - "8": { - "name": "comment.block.cpp" - }, - "9": { - "name": "comment.block.cpp punctuation.definition.comment.end.cpp" - }, - "10": { - "name": "meta.template.call.cpp", + "patterns": [ + { + "begin": "((::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*\\s*+)([A-Z][A-Z_0-9]*)\\b(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?(\\()", + "end": "\\)|(?=\\\\end\\{(?:minted|cppcode)\\})", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#scope_resolution_function_call_inner_generated" + } + ] + }, + "2": { + "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.function.call.cpp" + }, + "3": { + "patterns": [ + { + "include": "#template_call_range_helper" + } + ] + }, + "4": {}, + "5": { + "name": "entity.name.function.call.upper-case.cpp entity.name.function.call.cpp" + }, + "6": { + "patterns": [ + { + "include": "#inline_comment" + } + ] + }, + "7": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "8": { + "name": "comment.block.cpp" + }, + "9": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + }, + "10": { + "name": "meta.template.call.cpp", + "patterns": [ + { + "include": "#template_call_range_helper" + } + ] + }, + "11": {}, + "12": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "13": { + "name": "comment.block.cpp" + }, + "14": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + }, + "15": { + "name": "punctuation.section.arguments.begin.bracket.round.function.call.cpp punctuation.section.arguments.begin.bracket.round.function.call.upper-case.cpp" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.section.arguments.end.bracket.round.function.call.cpp punctuation.section.arguments.begin.bracket.round.function.call.upper-case.cpp" + } + }, "patterns": [ { - "include": "#template_call_range_helper" + "include": "#evaluation_context" } ] }, - "11": {}, - "12": { - "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" - }, - "13": { - "name": "comment.block.cpp" - }, - "14": { - "name": "comment.block.cpp punctuation.definition.comment.end.cpp" - }, - "15": { - "name": "punctuation.section.arguments.begin.bracket.round.function.call.cpp" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.arguments.end.bracket.round.function.call.cpp" - } - }, - "patterns": [ { - "include": "#evaluation_context" + "begin": "((::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*\\s*+)((?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)\\b(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?(\\()", + "end": "\\)|(?=\\\\end\\{(?:minted|cppcode)\\})", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#scope_resolution_function_call_inner_generated" + } + ] + }, + "2": { + "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.function.call.cpp" + }, + "3": { + "patterns": [ + { + "include": "#template_call_range_helper" + } + ] + }, + "4": {}, + "5": { + "name": "entity.name.function.call.cpp" + }, + "6": { + "patterns": [ + { + "include": "#inline_comment" + } + ] + }, + "7": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "8": { + "name": "comment.block.cpp" + }, + "9": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + }, + "10": { + "name": "meta.template.call.cpp", + "patterns": [ + { + "include": "#template_call_range_helper" + } + ] + }, + "11": {}, + "12": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "13": { + "name": "comment.block.cpp" + }, + "14": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + }, + "15": { + "name": "punctuation.section.arguments.begin.bracket.round.function.call.cpp" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.section.arguments.end.bracket.round.function.call.cpp" + } + }, + "patterns": [ + { + "include": "#evaluation_context" + } + ] } ] }, "function_definition": { - "begin": "(?:(?:^|\\G|(?<=;|\\}))|(?<=>|\\*\\/))\\s*+(?:((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:__cdecl|__clrcall|__stdcall|__fastcall|__thiscall|__vectorcall)?)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*\\s*+)((?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)\\b(?|\\*\\/))\\s*+(?:((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:__cdecl|__clrcall|__stdcall|__fastcall|__thiscall|__vectorcall)?)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*\\s*+)((?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)\\b(?|\\?\\?>)|(?=[;>\\[\\]=]))|(?=\\\\end\\{(?:minted|cppcode)\\})", "beginCaptures": { "0": { @@ -3837,7 +4153,7 @@ "7": { "patterns": [ { - "match": "((?)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\s*+((?:(?:(?:\\[\\[.*?\\]\\]|__attribute(?:__)?\\s*\\(\\s*\\(.*?\\)\\s*\\))|__declspec\\(.*?\\))|alignas\\(.*?\\))(?!\\)))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:(?:unsigned)|(?:signed)|(?:short)|(?:long))|(?:(?:struct)|(?:class)|(?:union)|(?:enum)))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:((?:::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))", + "match": "(?<=^|\\))(?:\\s+)?(->)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\s*+((?:(?:(?:\\[\\[.*?\\]\\]|__attribute(?:__)?\\s*\\(\\s*\\(.*?\\)\\s*\\))|__declspec\\(.*?\\))|alignas\\(.*?\\))(?!\\)))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:unsigned|signed|short|long)|(?:struct|class|union|enum))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:((?:::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))", "captures": { "1": { "name": "punctuation.definition.function.return-type.cpp" @@ -4209,14 +4525,14 @@ "name": "comment.block.cpp punctuation.definition.comment.end.cpp" }, "6": { - "name": "meta.qualified_type.cpp", + "name": "meta.qualified-type.cpp", "patterns": [ { "match": "::", "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.cpp" }, { - "match": "(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\()(\\*)(?:\\s+)?((?:(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)?)(?:\\s+)?(?:(\\[)(\\w*)(\\])(?:\\s+)?)*(\\))(?:\\s+)?(\\()", - "end": "(\\))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?]|\\n)(?!\\()|(?=\\\\end\\{(?:minted|cppcode)\\})", + "begin": "(\\s*+((?:(?:(?:\\[\\[.*?\\]\\]|__attribute(?:__)?\\s*\\(\\s*\\(.*?\\)\\s*\\))|__declspec\\(.*?\\))|alignas\\(.*?\\))(?!\\)))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:unsigned|signed|short|long)|(?:struct|class|union|enum))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:((?:::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\()(\\*)(?:\\s+)?((?:(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)?)(?:\\s+)?(?:(\\[)(\\w*)(\\])(?:\\s+)?)*(\\))(?:\\s+)?(\\()", + "end": "(\\))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?]|\\n)(?!\\()|(?=\\\\end\\{(?:minted|cppcode)\\})", "beginCaptures": { "1": { - "name": "meta.qualified_type.cpp", + "name": "meta.qualified-type.cpp", "patterns": [ { "match": "::", "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.cpp" }, { - "match": "(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\()(\\*)(?:\\s+)?((?:(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)?)(?:\\s+)?(?:(\\[)(\\w*)(\\])(?:\\s+)?)*(\\))(?:\\s+)?(\\()", - "end": "(\\))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?]|\\n)(?!\\()|(?=\\\\end\\{(?:minted|cppcode)\\})", + "begin": "(\\s*+((?:(?:(?:\\[\\[.*?\\]\\]|__attribute(?:__)?\\s*\\(\\s*\\(.*?\\)\\s*\\))|__declspec\\(.*?\\))|alignas\\(.*?\\))(?!\\)))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:unsigned|signed|short|long)|(?:struct|class|union|enum))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:((?:::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\()(\\*)(?:\\s+)?((?:(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)?)(?:\\s+)?(?:(\\[)(\\w*)(\\])(?:\\s+)?)*(\\))(?:\\s+)?(\\()", + "end": "(\\))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?]|\\n)(?!\\()|(?=\\\\end\\{(?:minted|cppcode)\\})", "beginCaptures": { "1": { - "name": "meta.qualified_type.cpp", + "name": "meta.qualified-type.cpp", "patterns": [ { "match": "::", "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.cpp" }, { - "match": "(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))", + "match": "(?<=protected|virtual|private|public|,|:)(?:\\s+)?(?!(?:(?:protected|private|public)|virtual))(\\s*+((?:(?:(?:\\[\\[.*?\\]\\]|__attribute(?:__)?\\s*\\(\\s*\\(.*?\\)\\s*\\))|__declspec\\(.*?\\))|alignas\\(.*?\\))(?!\\)))?((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:unsigned|signed|short|long)|(?:struct|class|union|enum))((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:((?:::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))", "captures": { "1": { - "name": "meta.qualified_type.cpp", + "name": "meta.qualified-type.cpp", "patterns": [ { "match": "::", "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.cpp" }, { - "match": "(?\\*|->)))((?:(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*(?:\\s+)?(?:(?:\\.\\*|\\.)|(?:->\\*|->))(?:\\s+)?)*)(?:\\s+)?(\\b(?!uint_least32_t[^\\w]|uint_least16_t[^\\w]|uint_least64_t[^\\w]|int_least32_t[^\\w]|int_least64_t[^\\w]|uint_fast32_t[^\\w]|uint_fast64_t[^\\w]|uint_least8_t[^\\w]|uint_fast16_t[^\\w]|int_least16_t[^\\w]|int_fast16_t[^\\w]|int_least8_t[^\\w]|uint_fast8_t[^\\w]|int_fast64_t[^\\w]|int_fast32_t[^\\w]|int_fast8_t[^\\w]|suseconds_t[^\\w]|useconds_t[^\\w]|in_addr_t[^\\w]|uintmax_t[^\\w]|uintmax_t[^\\w]|uintmax_t[^\\w]|in_port_t[^\\w]|uintptr_t[^\\w]|blksize_t[^\\w]|uint32_t[^\\w]|uint64_t[^\\w]|u_quad_t[^\\w]|intmax_t[^\\w]|intmax_t[^\\w]|unsigned[^\\w]|blkcnt_t[^\\w]|uint16_t[^\\w]|intptr_t[^\\w]|swblk_t[^\\w]|wchar_t[^\\w]|u_short[^\\w]|qaddr_t[^\\w]|caddr_t[^\\w]|daddr_t[^\\w]|fixpt_t[^\\w]|nlink_t[^\\w]|segsz_t[^\\w]|clock_t[^\\w]|ssize_t[^\\w]|int16_t[^\\w]|int32_t[^\\w]|int64_t[^\\w]|uint8_t[^\\w]|int8_t[^\\w]|mode_t[^\\w]|quad_t[^\\w]|ushort[^\\w]|u_long[^\\w]|u_char[^\\w]|double[^\\w]|signed[^\\w]|time_t[^\\w]|size_t[^\\w]|key_t[^\\w]|div_t[^\\w]|ino_t[^\\w]|uid_t[^\\w]|gid_t[^\\w]|off_t[^\\w]|pid_t[^\\w]|float[^\\w]|dev_t[^\\w]|u_int[^\\w]|short[^\\w]|bool[^\\w]|id_t[^\\w]|uint[^\\w]|long[^\\w]|char[^\\w]|void[^\\w]|auto[^\\w]|id_t[^\\w]|int[^\\w])(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b(?!\\())", + "match": "(?:((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?\\*|->)))((?:(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*(?:\\s+)?(?:(?:\\.\\*|\\.)|(?:->\\*|->))(?:\\s+)?)*)(?:\\s+)?(\\b(?!uint_least32_t[^\\w]|uint_least16_t[^\\w]|uint_least64_t[^\\w]|int_least32_t[^\\w]|int_least64_t[^\\w]|uint_fast32_t[^\\w]|uint_fast64_t[^\\w]|uint_least8_t[^\\w]|uint_fast16_t[^\\w]|int_least16_t[^\\w]|int_fast16_t[^\\w]|int_least8_t[^\\w]|uint_fast8_t[^\\w]|int_fast64_t[^\\w]|int_fast32_t[^\\w]|int_fast8_t[^\\w]|suseconds_t[^\\w]|useconds_t[^\\w]|in_addr_t[^\\w]|uintmax_t[^\\w]|uintmax_t[^\\w]|uintmax_t[^\\w]|in_port_t[^\\w]|uintptr_t[^\\w]|blksize_t[^\\w]|uint32_t[^\\w]|uint64_t[^\\w]|u_quad_t[^\\w]|intmax_t[^\\w]|intmax_t[^\\w]|unsigned[^\\w]|blkcnt_t[^\\w]|uint16_t[^\\w]|intptr_t[^\\w]|swblk_t[^\\w]|wchar_t[^\\w]|u_short[^\\w]|qaddr_t[^\\w]|caddr_t[^\\w]|daddr_t[^\\w]|fixpt_t[^\\w]|nlink_t[^\\w]|segsz_t[^\\w]|clock_t[^\\w]|ssize_t[^\\w]|int16_t[^\\w]|int32_t[^\\w]|int64_t[^\\w]|uint8_t[^\\w]|int8_t[^\\w]|mode_t[^\\w]|quad_t[^\\w]|ushort[^\\w]|u_long[^\\w]|u_char[^\\w]|double[^\\w]|signed[^\\w]|time_t[^\\w]|size_t[^\\w]|key_t[^\\w]|div_t[^\\w]|ino_t[^\\w]|uid_t[^\\w]|gid_t[^\\w]|off_t[^\\w]|pid_t[^\\w]|float[^\\w]|dev_t[^\\w]|u_int[^\\w]|short[^\\w]|bool[^\\w]|id_t[^\\w]|uint[^\\w]|long[^\\w]|char[^\\w]|void[^\\w]|auto[^\\w]|id_t[^\\w]|int[^\\w])(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b(?!\\())", "captures": { "1": { "patterns": [ @@ -5881,18 +6197,30 @@ "name": "variable.language.this.cpp" }, "4": { - "name": "variable.other.object.access.cpp" + "name": "variable.lower-case.cpp variable.other.object.access.$4.cpp" }, "5": { - "name": "punctuation.separator.dot-access.cpp" + "name": "variable.snake-case.cpp variable.other.object.access.$5.cpp" }, "6": { - "name": "punctuation.separator.pointer-access.cpp" + "name": "variable.camel-case.cpp variable.other.object.access.$6.cpp" }, "7": { + "name": "variable.upper-case.cpp variable.other.object.access.$7.cpp" + }, + "8": { + "name": "variable.other.unknown.$8.cpp" + }, + "9": { + "name": "punctuation.separator.dot-access.cpp" + }, + "10": { + "name": "punctuation.separator.pointer-access.cpp" + }, + "11": { "patterns": [ { - "match": "(?<=(?:\\.\\*|\\.|->|->\\*))(?:\\s+)?(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?\\*|->)))", + "match": "(?<=(?:\\.\\*|\\.|->|->\\*))(?:\\s+)?(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?\\*|->)))", "captures": { "1": { "patterns": [ @@ -5914,18 +6242,30 @@ "name": "variable.language.this.cpp" }, "6": { - "name": "variable.other.object.property.cpp" + "name": "variable.lower-case.cpp variable.other.object.property.cpp" }, "7": { - "name": "punctuation.separator.dot-access.cpp" + "name": "variable.snake-case.cpp variable.other.object.property.cpp" }, "8": { + "name": "variable.camel-case.cpp variable.other.object.property.cpp" + }, + "9": { + "name": "variable.upper-case.cpp variable.other.object.property.cpp" + }, + "10": { + "name": "variable.other.unknown.$10.cpp" + }, + "11": { + "name": "punctuation.separator.dot-access.cpp" + }, + "12": { "name": "punctuation.separator.pointer-access.cpp" } } }, { - "match": "(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?\\*|->)))", + "match": "(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?\\*|->)))", "captures": { "1": { "patterns": [ @@ -5947,12 +6287,24 @@ "name": "variable.language.this.cpp" }, "6": { - "name": "variable.other.object.access.cpp" + "name": "variable.lower-case.cpp variable.other.object.access.$6.cpp" }, "7": { - "name": "punctuation.separator.dot-access.cpp" + "name": "variable.snake-case.cpp variable.other.object.access.$7.cpp" }, "8": { + "name": "variable.camel-case.cpp variable.other.object.access.$8.cpp" + }, + "9": { + "name": "variable.upper-case.cpp variable.other.object.access.$9.cpp" + }, + "10": { + "name": "variable.other.unknown.$10.cpp" + }, + "11": { + "name": "punctuation.separator.dot-access.cpp" + }, + "12": { "name": "punctuation.separator.pointer-access.cpp" } } @@ -5965,7 +6317,7 @@ } ] }, - "8": { + "12": { "name": "variable.other.property.cpp" } } @@ -6016,7 +6368,7 @@ } }, "method_access": { - "begin": "(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?\\*|->)))((?:(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*(?:\\s+)?(?:(?:\\.\\*|\\.)|(?:->\\*|->))(?:\\s+)?)*)(?:\\s+)?(~?(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)(?:\\s+)?(\\()", + "begin": "(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?\\*|->)))((?:(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*(?:\\s+)?(?:(?:\\.\\*|\\.)|(?:->\\*|->))(?:\\s+)?)*)(?:\\s+)?(~?(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)(?:\\s+)?(\\()", "end": "\\)|(?=\\\\end\\{(?:minted|cppcode)\\})", "beginCaptures": { "1": { @@ -6039,18 +6391,30 @@ "name": "variable.language.this.cpp" }, "6": { - "name": "variable.other.object.access.cpp" + "name": "variable.lower-case.cpp variable.other.object.access.$6.cpp" }, "7": { - "name": "punctuation.separator.dot-access.cpp" + "name": "variable.snake-case.cpp variable.other.object.access.$7.cpp" }, "8": { - "name": "punctuation.separator.pointer-access.cpp" + "name": "variable.camel-case.cpp variable.other.object.access.$8.cpp" }, "9": { + "name": "variable.upper-case.cpp variable.other.object.access.$9.cpp" + }, + "10": { + "name": "variable.other.unknown.$10.cpp" + }, + "11": { + "name": "punctuation.separator.dot-access.cpp" + }, + "12": { + "name": "punctuation.separator.pointer-access.cpp" + }, + "13": { "patterns": [ { - "match": "(?<=(?:\\.\\*|\\.|->|->\\*))(?:\\s+)?(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?\\*|->)))", + "match": "(?<=(?:\\.\\*|\\.|->|->\\*))(?:\\s+)?(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?\\*|->)))", "captures": { "1": { "patterns": [ @@ -6072,18 +6436,30 @@ "name": "variable.language.this.cpp" }, "6": { - "name": "variable.other.object.property.cpp" + "name": "variable.lower-case.cpp variable.other.object.property.cpp" }, "7": { - "name": "punctuation.separator.dot-access.cpp" + "name": "variable.snake-case.cpp variable.other.object.property.cpp" }, "8": { + "name": "variable.camel-case.cpp variable.other.object.property.cpp" + }, + "9": { + "name": "variable.upper-case.cpp variable.other.object.property.cpp" + }, + "10": { + "name": "variable.other.unknown.$10.cpp" + }, + "11": { + "name": "punctuation.separator.dot-access.cpp" + }, + "12": { "name": "punctuation.separator.pointer-access.cpp" } } }, { - "match": "(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?\\*|->)))", + "match": "(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?\\*|->)))", "captures": { "1": { "patterns": [ @@ -6105,12 +6481,24 @@ "name": "variable.language.this.cpp" }, "6": { - "name": "variable.other.object.access.cpp" + "name": "variable.lower-case.cpp variable.other.object.access.$6.cpp" }, "7": { - "name": "punctuation.separator.dot-access.cpp" + "name": "variable.snake-case.cpp variable.other.object.access.$7.cpp" }, "8": { + "name": "variable.camel-case.cpp variable.other.object.access.$8.cpp" + }, + "9": { + "name": "variable.upper-case.cpp variable.other.object.access.$9.cpp" + }, + "10": { + "name": "variable.other.unknown.$10.cpp" + }, + "11": { + "name": "punctuation.separator.dot-access.cpp" + }, + "12": { "name": "punctuation.separator.pointer-access.cpp" } } @@ -6123,10 +6511,10 @@ } ] }, - "10": { + "14": { "name": "entity.name.function.member.cpp" }, - "11": { + "15": { "name": "punctuation.section.arguments.begin.bracket.round.function.member.cpp" } }, @@ -6142,7 +6530,7 @@ ] }, "misc_keywords": { - "match": "((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(%=|\\+=|-=|\\*=|(?>=|\\|=)|(\\=))", + "begin": "^((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:((?:(?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:(?:(\\b[a-z0-9]+\\b)|(\\b[a-zA-Z0-9]+_[a-zA-Z0-9]*\\b))|(\\b[a-z]+[A-Z][a-zA-Z0-9]*\\b))|(\\b[A-Z][A-Z_0-9]*\\b))|((?>=|\\|=))|(\\=)))", "end": "(?=;)|(?=\\\\end\\{(?:minted|cppcode)\\})", "beginCaptures": { "1": { @@ -6446,46 +6834,49 @@ "name": "comment.block.cpp punctuation.definition.comment.end.cpp" }, "5": { + "name": "meta.assignment.cpp" + }, + "6": { "patterns": [ { "include": "#storage_specifiers" } ] }, - "6": { + "7": { "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" }, - "7": { + "8": { "name": "comment.block.cpp" }, - "8": { + "9": { "name": "comment.block.cpp punctuation.definition.comment.end.cpp" }, - "9": { + "10": { "patterns": [ { "include": "#inline_comment" } ] }, - "10": { + "11": { "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" }, - "11": { + "12": { "name": "comment.block.cpp" }, - "12": { + "13": { "name": "comment.block.cpp punctuation.definition.comment.end.cpp" }, - "13": { - "name": "meta.qualified_type.cpp", + "14": { + "name": "meta.qualified-type.cpp", "patterns": [ { "match": "::", "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.cpp" }, { - "match": "(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?=;|,)", + "begin": "^((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:((?:(?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:(?:(\\b[a-z0-9]+\\b)|(\\b[a-zA-Z0-9]+_[a-zA-Z0-9]*\\b))|(\\b[a-z]+[A-Z][a-zA-Z0-9]*\\b))|(\\b[A-Z][A-Z_0-9]*\\b))|((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?((?:__cdecl|__clrcall|__stdcall|__fastcall|__thiscall|__vectorcall)?)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)(operator)(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)(?:(?:((?:(?:delete\\[\\])|(?:delete)|(?:new\\[\\])|(?:<=>)|(?:<<=)|(?:new)|(?:>>=)|(?:\\->\\*)|(?:\\/=)|(?:%=)|(?:&=)|(?:>=)|(?:\\|=)|(?:\\+\\+)|(?:\\-\\-)|(?:\\(\\))|(?:\\[\\])|(?:\\->)|(?:\\+\\+)|(?:<<)|(?:>>)|(?:\\-\\-)|(?:<=)|(?:\\^=)|(?:==)|(?:!=)|(?:&&)|(?:\\|\\|)|(?:\\+=)|(?:\\-=)|(?:\\*=)|,|\\+|\\-|!|~|\\*|&|\\*|\\/|%|\\+|\\-|<|>|&|\\^|\\||=))|((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?((?:__cdecl|__clrcall|__stdcall|__fastcall|__thiscall|__vectorcall)?)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)(operator)(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)(?:(?:((?:delete\\[\\]|delete|new\\[\\]|<=>|<<=|new|>>=|\\->\\*|\\/=|%=|&=|>=|\\|=|\\+\\+|\\-\\-|\\(\\)|\\[\\]|\\->|\\+\\+|<<|>>|\\-\\-|<=|\\^=|==|!=|&&|\\|\\||\\+=|\\-=|\\*=|,|\\+|\\-|!|~|\\*|&|\\*|\\/|%|\\+|\\-|<|>|&|\\^|\\||=))|((?|\\?\\?>)|(?=[;>\\[\\]=]))|(?=\\\\end\\{(?:minted|cppcode)\\})", "beginCaptures": { "0": { @@ -7305,14 +7725,14 @@ "name": "comment.block.cpp punctuation.definition.comment.end.cpp" }, "5": { - "name": "meta.qualified_type.cpp", + "name": "meta.qualified-type.cpp", "patterns": [ { "match": "::", "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.cpp" }, { - "match": "(?>", + "match": "(?:<<|>>)", "name": "keyword.operator.bitwise.shift.cpp" }, { - "match": "!=|<=|>=|==|<|>", + "match": "(?:!=|<=|>=|==|<|>)", "name": "keyword.operator.comparison.cpp" }, { - "match": "&&|!|\\|\\|", + "match": "(?:&&|!|\\|\\|)", "name": "keyword.operator.logical.cpp" }, { - "match": "&|\\||\\^|~", + "match": "(?:&|\\||\\^|~)", "name": "keyword.operator.bitwise.cpp" }, { - "match": "(?:(%=|\\+=|-=|\\*=|(?>=|\\|=)|(\\=))", + "match": "(?:((?:%=|\\+=|-=|\\*=|(?>=|\\|=))|(\\=))", "captures": { "1": { "name": "keyword.operator.assignment.compound.cpp" @@ -8141,7 +8561,7 @@ } }, { - "match": "%|\\*|\\/|-|\\+", + "match": "(?:%|\\*|\\/|-|\\+)", "name": "keyword.operator.arithmetic.cpp" }, { @@ -9189,7 +9609,7 @@ "include": "#vararg_ellipses" }, { - "match": "((?:((?:(?:thread_local)|(?:volatile)|(?:register)|(?:restrict)|(?:static)|(?:extern)|(?:const)))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:\\s*+(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.])", + "match": "\\s*+((?:(?:(?:\\[\\[.*?\\]\\]|__attribute(?:__)?\\s*\\(\\s*\\(.*?\\)\\s*\\))|__declspec\\(.*?\\))|alignas\\(.*?\\))(?!\\)))?((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:unsigned|signed|short|long)|(?:struct|class|union|enum))((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:((?:::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.])", "captures": { "0": { "patterns": [ @@ -11311,7 +11728,7 @@ "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.cpp" }, { - "match": "(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(:)(?!:)", + "match": "((?:((?:(?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(:)(?!:)", "captures": { "1": { "name": "meta.type.cpp" @@ -11530,14 +12023,14 @@ "name": "comment.block.cpp punctuation.definition.comment.end.cpp" }, "10": { - "name": "meta.qualified_type.cpp", + "name": "meta.qualified-type.cpp", "patterns": [ { "match": "::", "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.cpp" }, { - "match": "(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\[)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(,)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*))*((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\])((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(:)(?!:)", + "match": "((?:((?:(?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\[)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(,)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*))*((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\])((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(:)(?!:)", "captures": { "1": { "name": "meta.type.cpp" @@ -11794,14 +12287,14 @@ "name": "comment.block.cpp punctuation.definition.comment.end.cpp" }, "10": { - "name": "meta.qualified_type.cpp", + "name": "meta.qualified-type.cpp", "patterns": [ { "match": "::", "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.cpp" }, { - "match": "(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*\\s*+", - "captures": { - "0": { - "patterns": [ - { - "include": "#scope_resolution_inner_generated" - } - ] - }, + "requires_keyword": { + "begin": "((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*\\s*+", - "captures": { - "0": { + }, + "3": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "4": { + "name": "comment.block.cpp" + }, + "5": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + }, + "6": { + "name": "punctuation.section.arguments.begin.bracket.round.requires.cpp" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.section.arguments.end.bracket.round.requires.cpp" + } + }, + "contentName": "meta.arguments.requires", + "patterns": [ + { + "include": "#evaluation_context" + } + ] + }, + "scope_resolution": { + "match": "(::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*\\s*+", + "captures": { + "0": { + "patterns": [ + { + "include": "#scope_resolution_inner_generated" + } + ] + }, + "1": { + "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.cpp" + }, + "2": { + "patterns": [ + { + "include": "#template_call_range_helper" + } + ] + } + } + }, + "scope_resolution_function_call": { + "match": "(::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*\\s*+", + "captures": { + "0": { "patterns": [ { "include": "#scope_resolution_function_call_inner_generated" @@ -12661,150 +13193,668 @@ } ] }, - "1": { - "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.template.call.cpp" - }, - "2": { + "1": { + "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.template.call.cpp" + }, + "2": { + "patterns": [ + { + "include": "#template_call_range_helper" + } + ] + } + } + }, + "scope_resolution_template_call_inner_generated": { + "match": "((::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*\\s*+)((?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?(::)", + "captures": { + "1": { + "patterns": [ + { + "include": "#scope_resolution_template_call_inner_generated" + } + ] + }, + "2": { + "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.template.call.cpp" + }, + "3": { + "patterns": [ + { + "include": "#template_call_range_helper" + } + ] + }, + "4": {}, + "5": { + "name": "entity.name.scope-resolution.template.call.cpp" + }, + "6": { + "name": "meta.template.call.cpp", + "patterns": [ + { + "include": "#template_call_range_helper" + } + ] + }, + "7": {}, + "8": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "9": { + "name": "comment.block.cpp" + }, + "10": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + }, + "11": { + "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.template.call.cpp" + } + } + }, + "scope_resolution_template_definition": { + "match": "(::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*\\s*+", + "captures": { + "0": { + "patterns": [ + { + "include": "#scope_resolution_template_definition_inner_generated" + } + ] + }, + "1": { + "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.template.definition.cpp" + }, + "2": { + "patterns": [ + { + "include": "#template_call_range_helper" + } + ] + } + } + }, + "scope_resolution_template_definition_inner_generated": { + "match": "((::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*\\s*+)((?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?(::)", + "captures": { + "1": { + "patterns": [ + { + "include": "#scope_resolution_template_definition_inner_generated" + } + ] + }, + "2": { + "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.template.definition.cpp" + }, + "3": { + "patterns": [ + { + "include": "#template_call_range_helper" + } + ] + }, + "4": {}, + "5": { + "name": "entity.name.scope-resolution.template.definition.cpp" + }, + "6": { + "name": "meta.template.call.cpp", + "patterns": [ + { + "include": "#template_call_range_helper" + } + ] + }, + "7": {}, + "8": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "9": { + "name": "comment.block.cpp" + }, + "10": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + }, + "11": { + "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.template.definition.cpp" + } + } + }, + "semicolon": { + "match": ";", + "name": "punctuation.terminator.statement.cpp" + }, + "simple_array_assignment": { + "match": "(\\s*+((?:(?:(?:\\[\\[.*?\\]\\]|__attribute(?:__)?\\s*\\(\\s*\\(.*?\\)\\s*\\))|__declspec\\(.*?\\))|alignas\\(.*?\\))(?!\\)))?((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:unsigned|signed|short|long)|(?:struct|class|union|enum))((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:((?:::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:(?:(\\b[a-z0-9]+\\b)|(\\b[a-zA-Z0-9]+_[a-zA-Z0-9]*\\b))|(\\b[a-z]+[A-Z][a-zA-Z0-9]*\\b))|(\\b[A-Z][A-Z_0-9]*\\b))|((?>=|\\|=))|(\\=))", + "captures": { + "1": { + "name": "meta.qualified-type.cpp", + "patterns": [ + { + "match": "::", + "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.cpp" + }, + { + "match": "(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(?=((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?=(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*\\s*+)((?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?(::)", - "captures": { - "1": { + }, + "6": { "patterns": [ { - "include": "#scope_resolution_template_call_inner_generated" + "match": "\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+", + "captures": { + "1": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "2": { + "name": "comment.block.cpp" + }, + "3": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + } + } } ] }, - "2": { - "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.template.call.cpp" - }, - "3": { + "7": { "patterns": [ + { + "match": "::", + "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.type.cpp" + }, + { + "match": "(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*\\s*+", - "captures": { - "0": { "patterns": [ { - "include": "#scope_resolution_template_definition_inner_generated" + "match": "\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+", + "captures": { + "1": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "2": { + "name": "comment.block.cpp" + }, + "3": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + } + } } ] }, - "1": { - "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.template.definition.cpp" - }, - "2": { + "12": {}, + "13": { "patterns": [ { - "include": "#template_call_range_helper" + "include": "#inline_comment" } ] - } - } - }, - "scope_resolution_template_definition_inner_generated": { - "match": "((::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*\\s*+)((?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?(::)", - "captures": { - "1": { + }, + "14": { "patterns": [ { - "include": "#scope_resolution_template_definition_inner_generated" + "match": "\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+", + "captures": { + "1": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "2": { + "name": "comment.block.cpp" + }, + "3": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + } + } } ] }, - "2": { - "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.template.definition.cpp" - }, - "3": { + "15": { "patterns": [ { - "include": "#template_call_range_helper" + "include": "#inline_comment" } ] }, - "4": {}, - "5": { - "name": "entity.name.scope-resolution.template.definition.cpp" - }, - "6": { - "name": "meta.template.call.cpp", + "16": { "patterns": [ { - "include": "#template_call_range_helper" + "match": "\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+", + "captures": { + "1": { + "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" + }, + "2": { + "name": "comment.block.cpp" + }, + "3": { + "name": "comment.block.cpp punctuation.definition.comment.end.cpp" + } + } } ] - }, - "7": {}, - "8": { - "name": "comment.block.cpp punctuation.definition.comment.begin.cpp" - }, - "9": { - "name": "comment.block.cpp" - }, - "10": { - "name": "comment.block.cpp punctuation.definition.comment.end.cpp" - }, - "11": { - "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.template.definition.cpp" } } }, - "semicolon": { - "match": ";", - "name": "punctuation.terminator.statement.cpp" - }, "simple_type": { - "match": "(\\s*+((?:(?:(?:\\[\\[.*?\\]\\]|__attribute(?:__)?\\s*\\(\\s*\\(.*?\\)\\s*\\))|__declspec\\(.*?\\))|alignas\\(.*?\\))(?!\\)))?((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:(?:unsigned)|(?:signed)|(?:short)|(?:long))|(?:(?:struct)|(?:class)|(?:union)|(?:enum)))((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:((?:::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?", + "match": "(\\s*+((?:(?:(?:\\[\\[.*?\\]\\]|__attribute(?:__)?\\s*\\(\\s*\\(.*?\\)\\s*\\))|__declspec\\(.*?\\))|alignas\\(.*?\\))(?!\\)))?((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:unsigned|signed|short|long)|(?:struct|class|union|enum))((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:((?:::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?", "captures": { "1": { - "name": "meta.qualified_type.cpp", + "name": "meta.qualified-type.cpp", "patterns": [ { "match": "::", "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.cpp" }, { - "match": "(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))|(.*(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))|(.*(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\()(\\*)(?:\\s+)?((?:(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)?)(?:\\s+)?(?:(\\[)(\\w*)(\\])(?:\\s+)?)*(\\))(?:\\s+)?(\\()", - "end": "(\\))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?]|\\n)(?!\\()|(?=\\\\end\\{(?:minted|cppcode)\\})", + "begin": "(\\s*+((?:(?:(?:\\[\\[.*?\\]\\]|__attribute(?:__)?\\s*\\(\\s*\\(.*?\\)\\s*\\))|__declspec\\(.*?\\))|alignas\\(.*?\\))(?!\\)))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:unsigned|signed|short|long)|(?:struct|class|union|enum))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:((?:::)?(?:(?!\\b(?:__has_cpp_attribute|reinterpret_cast|atomic_noexcept|atomic_commit|atomic_cancel|__has_include|thread_local|dynamic_cast|synchronized|static_cast|const_cast|consteval|co_return|protected|constinit|constexpr|co_return|consteval|namespace|constexpr|constexpr|co_await|explicit|volatile|noexcept|co_yield|noexcept|noexcept|requires|typename|decltype|operator|template|continue|co_await|co_yield|volatile|register|restrict|reflexpr|mutable|alignof|include|private|defined|typedef|_Pragma|__asm__|concept|mutable|warning|default|virtual|alignas|public|sizeof|delete|not_eq|bitand|and_eq|xor_eq|typeid|switch|return|struct|static|extern|inline|friend|ifndef|define|pragma|export|import|module|catch|throw|const|or_eq|compl|while|ifdef|const|bitor|union|class|undef|error|break|using|endif|goto|line|enum|this|case|else|elif|else|not|try|for|asm|and|xor|new|do|if|or|if)\\b)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(\\()(\\*)(?:\\s+)?((?:(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)?)(?:\\s+)?(?:(\\[)(\\w*)(\\])(?:\\s+)?)*(\\))(?:\\s+)?(\\()", + "end": "(\\))((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:((?:(?:(?:\\s*+(\\/\\*)((?:[^\\*]++|\\*+(?!\\/))*+(\\*\\/))\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?]|\\n)(?!\\()|(?=\\\\end\\{(?:minted|cppcode)\\})", "beginCaptures": { "1": { - "name": "meta.qualified_type.cpp", + "name": "meta.qualified-type.cpp", "patterns": [ { "match": "::", "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.cpp" }, { - "match": "(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))", + "match": "(((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))", "captures": { "1": { "name": "storage.modifier.cpp" @@ -17605,14 +18627,14 @@ ] }, "6": { - "name": "meta.qualified_type.cpp", + "name": "meta.qualified-type.cpp", "patterns": [ { "match": "::", "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.cpp" }, { - "match": "(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(%=|\\+=|-=|\\*=|(?>=|\\|=)|(\\=))", + "match": "(?:((?:(?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:(?:(\\b[a-z0-9]+\\b)|(\\b[a-zA-Z0-9]+_[a-zA-Z0-9]*\\b))|(\\b[a-z]+[A-Z][a-zA-Z0-9]*\\b))|(\\b[A-Z][A-Z_0-9]*\\b))|((?>=|\\|=))|(\\=))", "captures": { "1": { "patterns": [ @@ -18346,14 +19388,14 @@ ] }, "5": { - "name": "meta.qualified_type.cpp", + "name": "meta.qualified-type.cpp", "patterns": [ { "match": "::", "name": "punctuation.separator.namespace.access.cpp punctuation.separator.scope-resolution.cpp" }, { - "match": "(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:(?:transaction_safe_dynamic)|(?:__has_cpp_attribute)|(?:reinterpret_cast)|(?:transaction_safe)|(?:atomic_noexcept)|(?:atomic_commit)|(?:__has_include)|(?:atomic_cancel)|(?:synchronized)|(?:thread_local)|(?:dynamic_cast)|(?:static_cast)|(?:const_cast)|(?:constexpr)|(?:co_return)|(?:constinit)|(?:namespace)|(?:protected)|(?:consteval)|(?:constexpr)|(?:constexpr)|(?:co_return)|(?:consteval)|(?:co_await)|(?:continue)|(?:template)|(?:reflexpr)|(?:volatile)|(?:register)|(?:co_await)|(?:co_yield)|(?:restrict)|(?:noexcept)|(?:volatile)|(?:override)|(?:explicit)|(?:decltype)|(?:operator)|(?:noexcept)|(?:noexcept)|(?:typename)|(?:requires)|(?:co_yield)|(?:nullptr)|(?:alignof)|(?:alignas)|(?:default)|(?:mutable)|(?:virtual)|(?:mutable)|(?:private)|(?:include)|(?:warning)|(?:_Pragma)|(?:defined)|(?:typedef)|(?:__asm__)|(?:concept)|(?:define)|(?:module)|(?:sizeof)|(?:switch)|(?:delete)|(?:pragma)|(?:and_eq)|(?:inline)|(?:xor_eq)|(?:typeid)|(?:import)|(?:extern)|(?:public)|(?:bitand)|(?:static)|(?:export)|(?:return)|(?:friend)|(?:ifndef)|(?:not_eq)|(?:false)|(?:final)|(?:break)|(?:const)|(?:catch)|(?:endif)|(?:ifdef)|(?:undef)|(?:error)|(?:audit)|(?:while)|(?:using)|(?:axiom)|(?:or_eq)|(?:compl)|(?:throw)|(?:bitor)|(?:const)|(?:line)|(?:case)|(?:else)|(?:this)|(?:true)|(?:goto)|(?:else)|(?:NULL)|(?:elif)|(?:new)|(?:asm)|(?:xor)|(?:and)|(?:try)|(?:not)|(?:for)|(?:do)|(?:if)|(?:or)|(?:if))\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))((?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?=;|,)", + "match": "(?:((?:(?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)(?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)\\s*+)?::)*+)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))?(?!(?:transaction_safe_dynamic|__has_cpp_attribute|reinterpret_cast|transaction_safe|atomic_noexcept|atomic_commit|__has_include|atomic_cancel|synchronized|thread_local|dynamic_cast|static_cast|const_cast|constexpr|co_return|constinit|namespace|protected|consteval|constexpr|constexpr|co_return|consteval|co_await|continue|template|reflexpr|volatile|register|co_await|co_yield|restrict|noexcept|volatile|override|explicit|decltype|operator|noexcept|noexcept|typename|requires|co_yield|nullptr|alignof|alignas|default|mutable|virtual|mutable|private|include|warning|_Pragma|defined|typedef|__asm__|concept|define|module|sizeof|switch|delete|pragma|and_eq|inline|xor_eq|typeid|import|extern|public|bitand|static|export|return|friend|ifndef|not_eq|false|final|break|const|catch|endif|ifdef|undef|error|audit|while|using|axiom|or_eq|compl|throw|bitor|const|line|case|else|this|true|goto|else|NULL|elif|new|asm|xor|and|try|not|for|do|if|or|if)\\b)(?:[a-zA-Z_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))(?:[a-zA-Z0-9_]|(?:\\\\u[0-9a-fA-F]{4}|\\\\U[0-9a-fA-F]{8}))*\\b((?|(?:(?:[^'\"<>\\/]|\\/[^*])++))*>)?(?![\\w<:.]))(((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))?(?:(?:&|\\*)((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z)))*(?:&|\\*))?((?:((?:\\s*+\\/\\*(?:[^\\*]++|\\*+(?!\\/))*+\\*\\/\\s*+)+)|(?:\\s++)|(?<=\\W)|(?=\\W)|^|(?:\\n?$)|\\A|\\Z))(?:(?:(?:(?:(\\b[a-z0-9]+\\b)|(\\b[a-zA-Z0-9]+_[a-zA-Z0-9]*\\b))|(\\b[a-z]+[A-Z][a-zA-Z0-9]*\\b))|(\\b[A-Z][A-Z_0-9]*\\b))|((?\n (?> # no backtracking, avoids issues with negative lookbehind at end\n (?:\n \\\\Q\n (?:(?!\\\\E)(?!/\\2).)*+\n (?:\\\\E\n # A quoted sequence may not have a closing E, in which case it extends to the end of the regex\n | (?(3)|(?\\{)?+(?\\{)?+(?\\{)?+(?\\{)?+(?\\{)?+\n .+?\n \\}(?()\\})(?()\\})(?()\\})(?()\\})(?()\\})\n (?:\\[(?!\\d)\\w+\\])?\n [X<>]?\n \\)\n | (?\\[ (?:\\\\. | [^\\[\\]] | \\g)+ \\])\n | \\(\\g?+\\)\n | (?:(?!/\\2)[^()\\[\\\\])+ # any character (until end)\n )+\n )\n)?+\n# may end with a space only if it is an extended literal or contains only a single escaped space\n(?(3)|(?(5)(?\n (?> # no backtracking, avoids issues with negative lookbehind at end\n (?:\n \\\\Q\n (?:(?!\\\\E)(?!/\\2).)*+\n (?:\\\\E\n # A quoted sequence may not have a closing E, in which case it extends to the end of the regex\n | (?(3)|(?(\\{(?:\\g<-1>|(?!{).*?)\\}))\n (?:\\[(?!\\d)\\w+\\])?\n [X<>]?\n \\)\n | (?\\[ (?:\\\\. | [^\\[\\]] | \\g)+ \\])\n | \\(\\g?+\\)\n | (?:(?!/\\2)[^()\\[\\\\])+ # any character (until end)\n )+\n )\n)?+\n# may end with a space only if it is an extended literal or contains only a single escaped space\n(?(3)|(?(5)(?'\n \"\\k'\" NamedOrNumberRef \"'\"\n '\\g<' NamedOrNumberRef '>'\n \"\\g'\" NamedOrNumberRef \"'\"", - "match": "(?x)(\\\\[gk](<)|\\\\[gk]') (?: ((?!\\d)\\w+) (?:([+-])(\\d+))? | ([+-]?\\d+) (?:([+-])(\\d+))? ) ((?(2)>|'))", + "comment": "'\\k<' NamedOrNumberRef '>'\n '\\g<' NamedOrNumberRef '>'", + "match": "(?x)(\\\\[gk]<) (?: ((?!\\d)\\w+) (?:([+-])(\\d+))? | ([+-]?\\d+) (?:([+-])(\\d+))? ) (>)", "captures": { "1": { "name": "constant.character.escape.backslash.regexp" }, - "3": { + "2": { "name": "variable.other.group-name.regexp" }, - "4": { + "3": { "name": "keyword.operator.recursion-level.regexp" }, + "4": { + "name": "constant.numeric.integer.decimal.regexp" + }, "5": { "name": "constant.numeric.integer.decimal.regexp" }, "6": { - "name": "constant.numeric.integer.decimal.regexp" + "name": "keyword.operator.recursion-level.regexp" }, "7": { - "name": "keyword.operator.recursion-level.regexp" + "name": "constant.numeric.integer.decimal.regexp" }, "8": { + "name": "constant.character.escape.backslash.regexp" + } + } + }, + { + "comment": "\"\\k'\" NamedOrNumberRef \"'\"\n \"\\g'\" NamedOrNumberRef \"'\"", + "match": "(?x)(\\\\[gk]') (?: ((?!\\d)\\w+) (?:([+-])(\\d+))? | ([+-]?\\d+) (?:([+-])(\\d+))? ) (')", + "captures": { + "1": { + "name": "constant.character.escape.backslash.regexp" + }, + "2": { + "name": "variable.other.group-name.regexp" + }, + "3": { + "name": "keyword.operator.recursion-level.regexp" + }, + "4": { "name": "constant.numeric.integer.decimal.regexp" }, - "9": { + "5": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "6": { + "name": "keyword.operator.recursion-level.regexp" + }, + "7": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "8": { "name": "constant.character.escape.backslash.regexp" } } @@ -3291,7 +3321,7 @@ }, "literals-regular-expression-literal-callout": { "name": "meta.callout.regexp", - "match": "(?x)\n# PCRECallout\n(\\()(?\\?C)\n (?:\n (?\\d+)\n | `(?(?:[^`]|``)*)`\n | '(?(?:[^']|'')*)'\n | \"(?(?:[^\"]|\"\")*)\"\n | \\^(?(?:[^\\^]|\\^\\^)*)\\^\n | %(?(?:[^%]|%%)*)%\n | \\#(?(?:[^#]|\\#\\#)*)\\#\n | \\$(?(?:[^$]|\\$\\$)*)\\$\n | \\{(?(?:[^}]|\\}\\})*)\\}\n )?\n(\\))\n# NamedCallout\n| (\\()(?\\*)\n (?(?!\\d)\\w+)\n (?:\\[(?(?!\\d)\\w+)\\])?\n (?:\\{ [^,}]+ (?:,[^,}]+)* \\})?\n (\\))\n# InterpolatedCallout\n| (\\()(?\\?)\n # we only support a fixed maximum number of braces because otherwise we can't balance the number of open and close braces\n (\\{(?\\{)?+(?\\{)?+(?\\{)?+(?\\{)?+(?\\{)?+) .+? \\}(?()\\})(?()\\})(?()\\})(?()\\})(?()\\})\n (?:\\[(?(?!\\d)\\w+)\\])?\n (?[X<>]?)\n (\\))", + "match": "(?x)\n# PCRECallout\n(\\()(?\\?C)\n (?:\n (?\\d+)\n | `(?(?:[^`]|``)*)`\n | '(?(?:[^']|'')*)'\n | \"(?(?:[^\"]|\"\")*)\"\n | \\^(?(?:[^\\^]|\\^\\^)*)\\^\n | %(?(?:[^%]|%%)*)%\n | \\#(?(?:[^#]|\\#\\#)*)\\#\n | \\$(?(?:[^$]|\\$\\$)*)\\$\n | \\{(?(?:[^}]|\\}\\})*)\\}\n )?\n(\\))\n# NamedCallout\n| (\\()(?\\*)\n (?(?!\\d)\\w+)\n (?:\\[(?(?!\\d)\\w+)\\])?\n (?:\\{ [^,}]+ (?:,[^,}]+)* \\})?\n (\\))\n# InterpolatedCallout\n| (\\()(?\\?)\n (?>(\\{(?:\\g<-1>|(?!{).*?)\\}))\n (?:\\[(?(?!\\d)\\w+)\\])?\n (?[X<>]?)\n (\\))", "captures": { "1": { "name": "punctuation.definition.group.regexp" @@ -3350,13 +3380,13 @@ "19": { "name": "keyword.control.callout.regexp" }, - "26": { + "21": { "name": "variable.language.tag-name.regexp" }, - "27": { + "22": { "name": "keyword.control.callout.regexp" }, - "28": { + "23": { "name": "punctuation.definition.group.regexp" } } From 1430e1845cbf5ec29a2fc265f12c7fb5c3d685c3 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Tue, 4 Feb 2025 10:38:17 +0100 Subject: [PATCH 1178/3587] Ignoring URLs when adding line comment onEnter (#239576) removing urls from this regex --- extensions/javascript/javascript-language-configuration.json | 2 +- extensions/typescript-basics/language-configuration.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/javascript/javascript-language-configuration.json b/extensions/javascript/javascript-language-configuration.json index 46ee043c52cf..c2444c453b2b 100644 --- a/extensions/javascript/javascript-language-configuration.json +++ b/extensions/javascript/javascript-language-configuration.json @@ -231,7 +231,7 @@ // Add // when pressing enter from inside line comment { "beforeText": { - "pattern": "\/\/.*" + "pattern": "(? Date: Tue, 4 Feb 2025 11:13:07 +0100 Subject: [PATCH 1179/3587] fix #239579 (#239580) --- .../extensionManagement/node/extensionManagementService.ts | 3 +-- .../contrib/extensions/browser/extensions.contribution.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index e954abf073b7..05182a5c8b94 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -53,7 +53,6 @@ import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; -import { isLinux } from '../../../base/common/platform.js'; export const INativeServerExtensionManagementService = refineServiceDecorator(IExtensionManagementService); export interface INativeServerExtensionManagementService extends IExtensionManagementService { @@ -329,7 +328,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } const { location, verificationStatus } = await this.extensionsDownloader.download(extension, operation, verifySignature, clientTargetPlatform); - if (verificationStatus !== ExtensionSignatureVerificationCode.Success && verificationStatus !== ExtensionSignatureVerificationCode.NotSigned && verifySignature && this.environmentService.isBuilt && !isLinux) { + if (verificationStatus !== ExtensionSignatureVerificationCode.Success && verificationStatus !== ExtensionSignatureVerificationCode.NotSigned && verifySignature && this.environmentService.isBuilt) { try { await this.extensionsDownloader.delete(location); } catch (e) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index ddbfb82a80d7..0fb425ed79c8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -69,7 +69,7 @@ import { ExtensionsCompletionItemsProvider } from './extensionsCompletionItemsPr import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { Event } from '../../../../base/common/event.js'; import { UnsupportedExtensionsMigrationContrib } from './unsupportedExtensionsMigrationContribution.js'; -import { isLinux, isNative, isWeb } from '../../../../base/common/platform.js'; +import { isNative, isWeb } from '../../../../base/common/platform.js'; import { ExtensionStorageService } from '../../../../platform/extensionManagement/common/extensionStorage.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; @@ -263,7 +263,7 @@ Registry.as(ConfigurationExtensions.Configuration) description: localize('extensions.verifySignature', "When enabled, extensions are verified to be signed before getting installed."), default: true, scope: ConfigurationScope.APPLICATION, - included: isNative && !isLinux + included: isNative }, [UseUnpkgResourceApiConfigKey]: { type: 'boolean', From a54ca8f3bceecd3383334a298a250904fc83e03c Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:31:10 +0100 Subject: [PATCH 1180/3587] Remove single inline deletion representation for consistency (#239584) do not use strike through inline deletion anymore for consistency --- .../browser/view/inlineEdits/view.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 4799e2d5d467..ac19662118d8 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -261,7 +261,6 @@ export class InlineEditsView extends Disposable { this._useMixedLinesDiff.read(reader) === 'forStableInsertions' && this._useCodeShifting.read(reader) && isSingleLineInsertionAfterPosition(diff, edit.cursorPosition) - || isSingleLineDeletion(diff) ) ) { return 'insertionInline'; @@ -422,21 +421,6 @@ function isSingleMultiLineInsertion(diff: DetailedLineRangeMapping[]) { return true; } -function isSingleLineDeletion(diff: DetailedLineRangeMapping[]): boolean { - return diff.every(m => m.innerChanges!.every(r => isDeletion(r))); - - function isDeletion(r: RangeMapping) { - if (!r.modifiedRange.isEmpty()) { - return false; - } - const isDeletionWithinLine = r.originalRange.startLineNumber === r.originalRange.endLineNumber; - if (!isDeletionWithinLine) { - return false; - } - return true; - } -} - function growEditsToEntireWord(replacements: SingleTextEdit[], originalText: AbstractText): SingleTextEdit[] { return _growEdits(replacements, originalText, (char) => /^[a-zA-Z]$/.test(char)); } From a045859a7bd032b5e8540a5fd763847b497ab3de Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 4 Feb 2025 11:47:34 +0100 Subject: [PATCH 1181/3587] When typing a longer comment, the buttons scroll out of view (#239587) Part of #239578 --- src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts | 4 ++-- .../contrib/comments/browser/commentThreadZoneWidget.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts b/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts index a38f1639eea6..82c4eb90f567 100644 --- a/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts +++ b/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts @@ -497,9 +497,9 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { // implement in subclass } - protected _relayout(_newHeightInLines: number): void { + protected _relayout(_newHeightInLines: number, noMax?: boolean): void { const maxHeightInLines = this._getMaximumHeightInLines(); - const newHeightInLines = maxHeightInLines === undefined ? _newHeightInLines : Math.min(maxHeightInLines, _newHeightInLines); + const newHeightInLines = (!noMax && (maxHeightInLines !== undefined)) ? Math.min(maxHeightInLines, _newHeightInLines) : _newHeightInLines; if (this._viewZone && this._viewZone.heightInLines !== newHeightInLines) { this.editor.changeViewZones(accessor => { if (this._viewZone) { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 0039a4030eaa..d6c4d858d5c7 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -502,7 +502,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } const capture = StableEditorScrollState.capture(this.editor); - this._relayout(computedLinesNumber); + this._relayout(computedLinesNumber, true); capture.restore(this.editor); } } From 1ff1b71e4b5c86cbd0188ce85a7ec927706dc2a9 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:49:54 +0100 Subject: [PATCH 1182/3587] Remove collapsing inline completions functionality (#239588) remove collapsing inline completions --- .../controller/inlineCompletionsController.ts | 2 -- .../browser/model/inlineCompletionsModel.ts | 14 +------------- .../inlineCompletions/browser/model/inlineEdit.ts | 2 -- .../browser/view/inlineEdits/view.ts | 7 +------ .../view/inlineEdits/viewAndDiffProducer.ts | 4 +--- 5 files changed, 3 insertions(+), 26 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index 5b9811a2f036..fd94296cd562 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -191,8 +191,6 @@ export class InlineCompletionsController extends Disposable { if (!m) { return; } if (m.state.get()?.kind === 'ghostText') { this.model.get()?.stop(); - } else if (m.state.get()?.inlineCompletion) { - this.model.get()?.collapseInlineEdit(); } } })); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index d701eedd8120..e1582bea38c7 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -261,14 +261,6 @@ export class InlineCompletionsModel extends Disposable { }); } - private readonly _collapsedInlineEditId = observableValue(this, undefined); - - public collapseInlineEdit(): void { - const currentInlineEdit = this.inlineEditState.get()?.inlineCompletion; - if (!currentInlineEdit) { return; } - this._collapsedInlineEditId.set(currentInlineEdit.semanticId, undefined); - } - private readonly _inlineCompletionItems = derivedOpts({ owner: this }, reader => { const c = this._source.inlineCompletions.read(reader); if (!c) { return undefined; } @@ -379,13 +371,9 @@ export class InlineCompletionsModel extends Disposable { return undefined; } - const cursorDist = LineRange.fromRange(edit.range).distanceToLine(this.primaryPosition.read(reader).lineNumber); - const disableCollapsing = true; - const currentItemIsCollapsed = !disableCollapsing && (cursorDist > 1 && this._collapsedInlineEditId.read(reader) === inlineEditResult.semanticId); - const commands = inlineEditResult.inlineCompletion.source.inlineCompletions.commands; const renderExplicitly = this._jumpedTo.read(reader); - const inlineEdit = new InlineEdit(edit, currentItemIsCollapsed, renderExplicitly, commands ?? [], inlineEditResult.inlineCompletion); + const inlineEdit = new InlineEdit(edit, renderExplicitly, commands ?? [], inlineEditResult.inlineCompletion); return { kind: 'inlineEdit', inlineEdit, inlineCompletion: inlineEditResult, edits: [edit], cursorAtInlineEdit }; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts index d87d1c26d9b3..63e45b69ac7d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts @@ -10,7 +10,6 @@ import { InlineCompletionItem } from './provideInlineCompletions.js'; export class InlineEdit { constructor( public readonly edit: SingleTextEdit, - public readonly isCollapsed: boolean, public readonly renderExplicitly: boolean, public readonly commands: readonly Command[], public readonly inlineCompletion: InlineCompletionItem, @@ -26,7 +25,6 @@ export class InlineEdit { public equals(other: InlineEdit): boolean { return this.edit.equals(other.edit) - && this.isCollapsed === other.isCollapsed && this.renderExplicitly === other.renderExplicitly && this.inlineCompletion === other.inlineCompletion; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index ac19662118d8..d55b24fb5704 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -157,7 +157,7 @@ export class InlineEditsView extends Disposable { return { modifiedText: new StringText(e.newText), diff: e.diff, - mode: e.state.kind === 'collapsed' ? 'sideBySide' : e.state.kind, + mode: e.state.kind, modifiedCodeEditor: this._sideBySide.previewEditor, }; }); @@ -250,10 +250,6 @@ export class InlineEditsView extends Disposable { // Determine the view based on the edit / diff - if (edit.isCollapsed) { - return 'collapsed'; - } - const inner = diff.flatMap(d => d.innerChanges ?? []); const isSingleInnerEdit = inner.length === 1; if ( @@ -317,7 +313,6 @@ export class InlineEditsView extends Disposable { this._previousView = { id: edit.inlineCompletion.id, view, userJumpedToIt: edit.userJumpedToIt, editorWidth: this._editor.getLayoutInfo().width }; switch (view) { - case 'collapsed': return { kind: 'collapsed' as const }; case 'insertionInline': return { kind: 'insertionInline' as const }; case 'mixedLines': return { kind: 'mixedLines' as const }; case 'interleavedLines': return { kind: 'interleavedLines' as const }; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts index 11c86a03b3f0..81613dc0ad49 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts @@ -44,7 +44,7 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c const diffEdits = new TextEdit(edits); const text = new TextModelText(textModel); - return new InlineEditWithChanges(text, diffEdits, inlineEdit.isCollapsed, model.primaryPosition.get(), inlineEdit.renderExplicitly, inlineEdit.commands, inlineEdit.inlineCompletion); + return new InlineEditWithChanges(text, diffEdits, model.primaryPosition.get(), inlineEdit.renderExplicitly, inlineEdit.commands, inlineEdit.inlineCompletion); }); constructor( @@ -69,7 +69,6 @@ export class InlineEditWithChanges { constructor( public readonly originalText: AbstractText, public readonly edit: TextEdit, - public readonly isCollapsed: boolean, public readonly cursorPosition: Position, public readonly userJumpedToIt: boolean, public readonly commands: readonly Command[], @@ -80,7 +79,6 @@ export class InlineEditWithChanges { equals(other: InlineEditWithChanges) { return this.originalText.getValue() === other.originalText.getValue() && this.edit.equals(other.edit) && - this.isCollapsed === other.isCollapsed && this.cursorPosition.equals(other.cursorPosition) && this.userJumpedToIt === other.userJumpedToIt && this.commands === other.commands && From d0b83e6142c35e8635c86f86c117d15fd8748068 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 4 Feb 2025 13:01:11 +0100 Subject: [PATCH 1183/3587] support `InlineChatRunOptions` for inline chat (#239591) --- .../inlineChat/browser/inlineChatActions.ts | 14 ++++-- .../browser/inlineChatController.ts | 5 +- .../browser/inlineChatController2.ts | 46 +++++++++++++++---- 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 561bef81e76d..ba75016bb02e 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diff import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { InlineChatController, InlineChatRunOptions } from './inlineChatController.js'; -import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES, CTX_INLINE_CHAT_POSSIBLE, ACTION_START } from '../common/inlineChat.js'; +import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, CTX_INLINE_CHAT_HAS_AGENT2 } from '../common/inlineChat.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, IAction2Options, MenuId } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -20,7 +20,7 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../platform/accessibility/common/accessibility.js'; -import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; +import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -72,7 +72,10 @@ export class StartSessionAction extends Action2 { } }); } - override run(accessor: ServicesAccessor, ...args: any[]): void { + override run(accessor: ServicesAccessor, ...args: any[]): any { + + const commandService = accessor.get(ICommandService); + const contextKeyService = accessor.get(IContextKeyService); const codeEditorService = accessor.get(ICodeEditorService); const editor = codeEditorService.getActiveCodeEditor(); if (!editor || editor.isSimpleWidget) { @@ -80,6 +83,10 @@ export class StartSessionAction extends Action2 { return; } + if (contextKeyService.contextMatchesRules(CTX_INLINE_CHAT_HAS_AGENT2)) { + return commandService.executeCommand('inlineChat2.start', ...args); + } + // precondition does hold return editor.invokeWithinContext((editorAccessor) => { const kbService = editorAccessor.get(IContextKeyService); @@ -134,7 +141,6 @@ export class UnstashSessionAction extends EditorAction2 { if (session) { ctrl.run({ existingSession: session, - isUnstashed: true }); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 6ca0b87ae9d1..3ef89c1c8f11 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -78,10 +78,7 @@ export abstract class InlineChatRunOptions { message?: string; autoSend?: boolean; existingSession?: Session; - isUnstashed?: boolean; position?: IPosition; - withIntentDetection?: boolean; - headless?: boolean; static isInlineChatRunOptions(options: any): options is InlineChatRunOptions { const { initialSelection, initialRange, message, autoSend, position, existingSession } = options; @@ -314,7 +311,7 @@ export class InlineChatController implements IEditorContribution { delete options.position; } - const widgetPosition = this._showWidget(options.headless ?? session?.headless, true, initPosition); + const widgetPosition = this._showWidget(session?.headless, true, initPosition); // this._updatePlaceholder(); let errorMessage = localize('create.fail', "Failed to start editor chat"); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts index b735b3edcc0e..f07c82bc1548 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts @@ -33,7 +33,9 @@ import { ChatAgentLocation } from '../../chat/common/chatAgents.js'; import { WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; import { CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_POSSIBLE, CTX_INLINE_CHAT_VISIBLE } from '../common/inlineChat.js'; +import { InlineChatRunOptions } from './inlineChatController.js'; import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; +import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; @@ -49,11 +51,13 @@ export class InlineChatController2 implements IEditorContribution { } private readonly _store = new DisposableStore(); - - private readonly _showWidgetOverrideObs = observableValue(this, false); private readonly _isActiveController = observableValue(this, false); + private readonly _zone: Lazy; + get widget(): EditorBasedInlineChatWidget { + return this._zone.value.widget; + } constructor( private readonly _editor: ICodeEditor, @@ -66,7 +70,7 @@ export class InlineChatController2 implements IEditorContribution { const ctxHasSession = CTX_HAS_SESSION.bindTo(contextKeyService); const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); - const zone = new Lazy(() => { + this._zone = new Lazy(() => { const location: IChatWidgetLocationOptions = { @@ -176,17 +180,17 @@ export class InlineChatController2 implements IEditorContribution { const session = visibleSessionObs.read(r); if (!session) { - zone.rawValue?.hide(); + this._zone.rawValue?.hide(); _editor.focus(); ctxInlineChatVisible.reset(); } else { ctxInlineChatVisible.set(true); - zone.value.widget.setChatModel(session.chatModel); - if (!zone.value.position) { - zone.value.show(session.initialPosition); + this._zone.value.widget.setChatModel(session.chatModel); + if (!this._zone.value.position) { + this._zone.value.show(session.initialPosition); } - zone.value.reveal(zone.value.position!); - zone.value.widget.focus(); + this._zone.value.reveal(this._zone.value.position!); + this._zone.value.widget.focus(); session.editingSession.getEntry(session.uri)?.autoAcceptController.get()?.cancel(); } })); @@ -264,9 +268,31 @@ export class StartSessionAction2 extends EditorAction2 { if (!editor.hasModel()) { return; } + const ctrl = InlineChatController2.get(editor); + if (!ctrl) { + return; + } + const textModel = editor.getModel(); await inlineChatSessions.createSession2(editor, textModel.uri, CancellationToken.None); - InlineChatController2.get(editor)?.markActiveController(); + + ctrl.markActiveController(); + + const arg = args[0]; + if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) { + if (arg.initialRange) { + editor.revealRange(arg.initialRange); + } + if (arg.initialSelection) { + editor.setSelection(arg.initialSelection); + } + if (arg.message) { + ctrl.widget.chatWidget.setInput(arg.message); + if (arg.autoSend) { + await ctrl.widget.chatWidget.acceptInput(); + } + } + } } } From 4edc46abe1e05f0686eb8b0b4150a75c8eb1b9fb Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 4 Feb 2025 13:21:12 +0100 Subject: [PATCH 1184/3587] fix #239574 (#239594) --- src/vs/platform/request/node/requestService.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index 333705f23e22..7a381e4ec40f 100644 --- a/src/vs/platform/request/node/requestService.ts +++ b/src/vs/platform/request/node/requestService.ts @@ -201,8 +201,23 @@ export async function nodeRequest(options: NodeRequestOptions, token: Cancellati req.on('error', reject); + // Handle timeout if (options.timeout) { - req.setTimeout(options.timeout); + // Chromium network requests do not support the `timeout` option + if (options.isChromiumNetwork) { + // Use Node's setTimeout for Chromium network requests + const timeout = setTimeout(() => { + req.abort(); + reject(new Error(`Request timeout after ${options.timeout}ms`)); + }, options.timeout); + + // Clear timeout when request completes + req.on('response', () => clearTimeout(timeout)); + req.on('error', () => clearTimeout(timeout)); + req.on('abort', () => clearTimeout(timeout)); + } else { + req.setTimeout(options.timeout); + } } // Chromium will abort the request if forbidden headers are set. From 05582f9dd1ab81ca03be0863cd3e9af15cc00ad7 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 4 Feb 2025 13:22:33 +0100 Subject: [PATCH 1185/3587] fix #227446 (#239595) --- .../extensionManagement/common/extensionGalleryService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 7975c97c5126..90b110c251ae 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -1569,6 +1569,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const context = await this.requestService.request({ type: 'GET', url: this.extensionsControlUrl, + timeout: 10000 /*10s*/ }, CancellationToken.None); if (context.res.statusCode !== 200) { From 9cf5e81d4f5299c7c88dcf540c5e440ab5f52842 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:03:18 +0100 Subject: [PATCH 1186/3587] Git - switch to using diff information for the stage/unstage/revert selected ranges commands (#239597) --- extensions/git/src/commands.ts | 55 ++++++++++--------- extensions/git/src/staging.ts | 7 +++ ...de.proposed.textEditorDiffInformation.d.ts | 15 +++-- 3 files changed, 43 insertions(+), 34 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 47b1f087b94b..b7a1a85bb4f7 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -12,7 +12,7 @@ import { ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, Remo import { Git, Stash } from './git'; import { Model } from './model'; import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository'; -import { DiffEditorSelectionHunkToolbarContext, applyLineChanges, getModifiedRange, getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges } from './staging'; +import { DiffEditorSelectionHunkToolbarContext, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges } from './staging'; import { fromGitUri, toGitUri, isGitUri, toMergeUris, toMultiFileDiffEditorUris } from './uri'; import { dispose, getCommitShortHash, grep, isDefined, isDescendant, pathEquals, relativePath, truncate } from './util'; import { GitTimelineItem } from './timelineProvider'; @@ -1567,17 +1567,20 @@ export class CommandCenter { return; } - this.logger.trace(`[CommandCenter][stageSelectedChanges] changes: ${JSON.stringify(changes)}`); - const workingTreeDiffInformation = getWorkingTreeDiffInformation(textEditor); - if (workingTreeDiffInformation) { - this.logger.trace(`[CommandCenter][stageSelectedChanges] diffInformation: ${JSON.stringify(workingTreeDiffInformation)}`); - this.logger.trace(`[CommandCenter][stageSelectedChanges] diffInformation changes: ${JSON.stringify(toLineChanges(workingTreeDiffInformation))}`); + if (!workingTreeDiffInformation) { + return; } + const workingTreeLineChanges = toLineChanges(workingTreeDiffInformation); + + this.logger.trace(`[CommandCenter][stageSelectedChanges] changes: ${JSON.stringify(changes)}`); + this.logger.trace(`[CommandCenter][stageSelectedChanges] diffInformation: ${JSON.stringify(workingTreeDiffInformation)}`); + this.logger.trace(`[CommandCenter][stageSelectedChanges] diffInformation changes: ${JSON.stringify(workingTreeLineChanges)}`); + const modifiedDocument = textEditor.document; const selectedLines = toLineRanges(textEditor.selections, modifiedDocument); - const selectedChanges = changes + const selectedChanges = workingTreeLineChanges .map(change => selectedLines.reduce((result, range) => result || intersectDiffWithRange(modifiedDocument, change, range), null)) .filter(d => !!d) as LineChange[]; @@ -1759,22 +1762,25 @@ export class CommandCenter { return; } - this.logger.trace(`[CommandCenter][revertSelectedRanges] changes: ${JSON.stringify(changes)}`); - const workingTreeDiffInformation = getWorkingTreeDiffInformation(textEditor); - if (workingTreeDiffInformation) { - this.logger.trace(`[CommandCenter][revertSelectedRanges] diffInformation: ${JSON.stringify(workingTreeDiffInformation)}`); - this.logger.trace(`[CommandCenter][revertSelectedRanges] diffInformation changes: ${JSON.stringify(toLineChanges(workingTreeDiffInformation))}`); + if (!workingTreeDiffInformation) { + return; } + const workingTreeLineChanges = toLineChanges(workingTreeDiffInformation); + + this.logger.trace(`[CommandCenter][revertSelectedRanges] changes: ${JSON.stringify(changes)}`); + this.logger.trace(`[CommandCenter][revertSelectedRanges] diffInformation: ${JSON.stringify(workingTreeDiffInformation)}`); + this.logger.trace(`[CommandCenter][revertSelectedRanges] diffInformation changes: ${JSON.stringify(workingTreeLineChanges)}`); + const modifiedDocument = textEditor.document; const selections = textEditor.selections; - const selectedChanges = changes.filter(change => { + const selectedChanges = workingTreeLineChanges.filter(change => { const modifiedRange = getModifiedRange(modifiedDocument, change); return selections.every(selection => !selection.intersection(modifiedRange)); }); - if (selectedChanges.length === changes.length) { + if (selectedChanges.length === workingTreeLineChanges.length) { window.showInformationMessage(l10n.t('The selection range does not contain any changes.')); return; } @@ -1859,24 +1865,21 @@ export class CommandCenter { return; } - this.logger.trace(`[CommandCenter][unstageSelectedRanges] changes: ${JSON.stringify(changes)}`); - - const workingTreeDiffInformation = getWorkingTreeDiffInformation(textEditor); - if (workingTreeDiffInformation) { - this.logger.trace(`[CommandCenter][unstageSelectedRanges] diffInformation (working tree): ${JSON.stringify(workingTreeDiffInformation)}`); - this.logger.trace(`[CommandCenter][unstageSelectedRanges] diffInformation changes (working tree): ${JSON.stringify(toLineChanges(workingTreeDiffInformation))}`); + const indexDiffInformation = getIndexDiffInformation(textEditor); + if (!indexDiffInformation) { + return; } - const workingTreeAndIndexDiffInformation = getWorkingTreeAndIndexDiffInformation(textEditor); - if (workingTreeAndIndexDiffInformation) { - this.logger.trace(`[CommandCenter][unstageSelectedRanges] diffInformation (working tree + index): ${JSON.stringify(workingTreeAndIndexDiffInformation)}`); - this.logger.trace(`[CommandCenter][unstageSelectedRanges] diffInformation changes (working tree + index): ${JSON.stringify(toLineChanges(workingTreeAndIndexDiffInformation))}`); - } + const indexLineChanges = toLineChanges(indexDiffInformation); + + this.logger.trace(`[CommandCenter][unstageSelectedRanges] changes: ${JSON.stringify(changes)}`); + this.logger.trace(`[CommandCenter][unstageSelectedRanges] diffInformation: ${JSON.stringify(indexDiffInformation)}`); + this.logger.trace(`[CommandCenter][unstageSelectedRanges] diffInformation changes: ${JSON.stringify(indexLineChanges)}`); const originalUri = toGitUri(modifiedUri, 'HEAD'); const originalDocument = await workspace.openTextDocument(originalUri); const selectedLines = toLineRanges(textEditor.selections, modifiedDocument); - const selectedDiffs = changes + const selectedDiffs = indexLineChanges .map(change => selectedLines.reduce((result, range) => result || intersectDiffWithRange(modifiedDocument, change, range), null)) .filter(c => !!c) as LineChange[]; diff --git a/extensions/git/src/staging.ts b/extensions/git/src/staging.ts index 38a462aedf62..14e4e379e478 100644 --- a/extensions/git/src/staging.ts +++ b/extensions/git/src/staging.ts @@ -178,6 +178,13 @@ export function toLineChanges(diffInformation: TextEditorDiffInformation): LineC }); } +export function getIndexDiffInformation(textEditor: TextEditor): TextEditorDiffInformation | undefined { + // Diff Editor (Index) + return textEditor.diffInformation?.find(diff => + diff.original && isGitUri(diff.original) && fromGitUri(diff.original).ref === 'HEAD' && + diff.modified && isGitUri(diff.modified) && fromGitUri(diff.modified).ref === ''); +} + export function getWorkingTreeDiffInformation(textEditor: TextEditor): TextEditorDiffInformation | undefined { // Working tree diff information. Diff Editor (Working Tree) -> Text Editor return getDiffInformation(textEditor, '~') ?? getDiffInformation(textEditor, ''); diff --git a/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts b/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts index b86d377c7660..faa18c2932d6 100644 --- a/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts +++ b/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts @@ -12,15 +12,14 @@ declare module 'vscode' { Modification = 3 } + export interface TextEditorLineRange { + readonly startLineNumber: number; + readonly endLineNumberExclusive: number; + } + export interface TextEditorChange { - readonly original: { - readonly startLineNumber: number; - readonly endLineNumberExclusive: number; - }; - readonly modified: { - readonly startLineNumber: number; - readonly endLineNumberExclusive: number; - }; + readonly original: TextEditorLineRange; + readonly modified: TextEditorLineRange; readonly kind: TextEditorChangeKind; } From d35f9482c008bdd1cb34a42c2281cc166c8fc661 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 4 Feb 2025 06:23:06 -0800 Subject: [PATCH 1187/3587] Move tests fig spec tests into folder per spec Part of #239515 --- .../src/test/completions/cd.test.ts | 42 +++++ .../test/completions/code-insiders.test.ts | 15 ++ .../src/test/completions/code.test.ts | 58 +++++++ .../terminal-suggest/src/test/helpers.ts | 38 +++++ .../src/test/terminalSuggestMain.test.ts | 143 +++--------------- 5 files changed, 174 insertions(+), 122 deletions(-) create mode 100644 extensions/terminal-suggest/src/test/completions/cd.test.ts create mode 100644 extensions/terminal-suggest/src/test/completions/code-insiders.test.ts create mode 100644 extensions/terminal-suggest/src/test/completions/code.test.ts create mode 100644 extensions/terminal-suggest/src/test/helpers.ts diff --git a/extensions/terminal-suggest/src/test/completions/cd.test.ts b/extensions/terminal-suggest/src/test/completions/cd.test.ts new file mode 100644 index 000000000000..1c669559d592 --- /dev/null +++ b/extensions/terminal-suggest/src/test/completions/cd.test.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import cdSpec from '../../completions/cd'; +import { testPaths, type ISuiteSpec } from '../helpers'; + +export const cdTestSuiteSpec: ISuiteSpec = { + name: 'cd', + completionSpecs: cdSpec, + availableCommands: 'cd', + testSpecs: [ + // Typing a path + { input: '.|', expectedCompletions: ['cd'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: './|', expectedCompletions: ['cd'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: './.|', expectedCompletions: ['cd'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + + // Typing the command + { input: 'c|', expectedCompletions: ['cd'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'cd|', expectedCompletions: ['cd'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + + // Basic arguments + { input: 'cd |', expectedCompletions: ['~', '-'], expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, + { input: 'cd -|', expectedCompletions: ['-'], expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, + { input: 'cd ~|', expectedCompletions: ['~'], expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, + + // Relative paths + { input: 'cd c|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, + { input: 'cd child|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, + { input: 'cd .|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, + { input: 'cd ./|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, + { input: 'cd ./child|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, + { input: 'cd ..|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, + + // Relative directories (changes cwd due to /) + { input: 'cd child/|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwdChild } }, + { input: 'cd ../|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwdParent } }, + { input: 'cd ../sibling|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwdParent } }, + ] +}; diff --git a/extensions/terminal-suggest/src/test/completions/code-insiders.test.ts b/extensions/terminal-suggest/src/test/completions/code-insiders.test.ts new file mode 100644 index 000000000000..447772ad8b7f --- /dev/null +++ b/extensions/terminal-suggest/src/test/completions/code-insiders.test.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import codeInsidersCompletionSpec from '../../completions/code-insiders'; +import type { ISuiteSpec } from '../helpers'; +import { createCodeTestSpecs } from './code.test'; + +export const codeInsidersTestSuite: ISuiteSpec = { + name: 'code-insiders', + completionSpecs: codeInsidersCompletionSpec, + availableCommands: 'code-insiders', + testSpecs: createCodeTestSpecs('code-insiders') +}; diff --git a/extensions/terminal-suggest/src/test/completions/code.test.ts b/extensions/terminal-suggest/src/test/completions/code.test.ts new file mode 100644 index 000000000000..f65cd2e4d5d6 --- /dev/null +++ b/extensions/terminal-suggest/src/test/completions/code.test.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import codeCompletionSpec from '../../completions/code'; +import { testPaths, type ISuiteSpec, type ITestSpec } from '../helpers'; + +export function createCodeTestSpecs(executable: string): ITestSpec[] { + const codeOptions = ['-', '--add', '--category', '--diff', '--disable-extension', '--disable-extensions', '--disable-gpu', '--enable-proposed-api', '--extensions-dir', '--goto', '--help', '--inspect-brk-extensions', '--inspect-extensions', '--install-extension', '--list-extensions', '--locale', '--log', '--max-memory', '--merge', '--new-window', '--pre-release', '--prof-startup', '--profile', '--reuse-window', '--show-versions', '--status', '--sync', '--telemetry', '--uninstall-extension', '--user-data-dir', '--verbose', '--version', '--wait', '-a', '-d', '-g', '-h', '-m', '-n', '-r', '-s', '-v', '-w']; + const localeOptions = ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW']; + const categoryOptions = ['azure', 'data science', 'debuggers', 'extension packs', 'education', 'formatters', 'keymaps', 'language packs', 'linters', 'machine learning', 'notebooks', 'programming languages', 'scm providers', 'snippets', 'testing', 'themes', 'visualization', 'other']; + const logOptions = ['critical', 'error', 'warn', 'info', 'debug', 'trace', 'off']; + const syncOptions = ['on', 'off']; + + const typingTests: ITestSpec[] = []; + for (let i = 1; i < executable.length; i++) { + const input = `${executable.slice(0, i)}|`; + typingTests.push({ input, expectedCompletions: [executable], expectedResourceRequests: input.endsWith(' ') ? undefined : { type: 'both', cwd: testPaths.cwd } }); + } + + return [ + // Typing the command + ...typingTests, + + // Basic arguments + { input: `${executable} |`, expectedCompletions: codeOptions }, + { input: `${executable} --locale |`, expectedCompletions: localeOptions }, + { input: `${executable} --diff |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } }, + { input: `${executable} --diff ./file1 |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } }, + { input: `${executable} --merge |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } }, + { input: `${executable} --merge ./file1 ./file2 |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } }, + { input: `${executable} --merge ./file1 ./file2 ./base |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } }, + { input: `${executable} --goto |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } }, + { input: `${executable} --user-data-dir |`, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, + { input: `${executable} --profile |`, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: `${executable} --install-extension |`, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: `${executable} --uninstall-extension |`, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: `${executable} --log |`, expectedCompletions: logOptions }, + { input: `${executable} --sync |`, expectedCompletions: syncOptions }, + { input: `${executable} --extensions-dir |`, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, + { input: `${executable} --list-extensions |`, expectedCompletions: codeOptions }, + { input: `${executable} --show-versions |`, expectedCompletions: codeOptions }, + { input: `${executable} --category |`, expectedCompletions: categoryOptions }, + { input: `${executable} --category a|`, expectedCompletions: categoryOptions.filter(c => c.startsWith('a')) }, + + // Middle of command + { input: `${executable} | --locale`, expectedCompletions: codeOptions }, + ]; +} + +export const codeTestSuite: ISuiteSpec = { + name: 'code', + completionSpecs: codeCompletionSpec, + availableCommands: 'code', + testSpecs: createCodeTestSpecs('code') +}; diff --git a/extensions/terminal-suggest/src/test/helpers.ts b/extensions/terminal-suggest/src/test/helpers.ts new file mode 100644 index 000000000000..3075022d541f --- /dev/null +++ b/extensions/terminal-suggest/src/test/helpers.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import * as vscode from 'vscode'; +import type { Uri } from 'vscode'; + +export interface ISuiteSpec { + name: string; + completionSpecs: Fig.Spec | Fig.Spec[]; + // TODO: This seems unnecessary, ideally getCompletionItemsFromSpecs would only consider the + // spec's completions + availableCommands: string | string[]; + testSpecs: ITestSpec[]; +} + +export interface ITestSpec { + input: string; + expectedResourceRequests?: { + type: 'files' | 'folders' | 'both'; + cwd: Uri; + }; + expectedCompletions?: string[]; +} + +const fixtureDir = vscode.Uri.joinPath(vscode.Uri.file(__dirname), '../../testWorkspace'); + +/** + * A default set of paths shared across tests. + */ +export const testPaths = { + fixtureDir, + cwdParent: vscode.Uri.joinPath(fixtureDir, 'parent'), + cwd: vscode.Uri.joinPath(fixtureDir, 'parent/home'), + cwdChild: vscode.Uri.joinPath(fixtureDir, 'parent/home/child'), +}; diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index 68a2abbb2566..c84693144eee 100644 --- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -3,82 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; import { deepStrictEqual, strictEqual } from 'assert'; import 'mocha'; -import { asArray, getCompletionItemsFromSpecs } from '../terminalSuggestMain'; -import cdSpec from '../completions/cd'; -import codeCompletionSpec from '../completions/code'; -import codeInsidersCompletionSpec from '../completions/code-insiders'; -import type { Uri } from 'vscode'; import { basename } from 'path'; +import { asArray, getCompletionItemsFromSpecs } from '../terminalSuggestMain'; import { getTokenType } from '../tokens'; - -const fixtureDir = vscode.Uri.joinPath(vscode.Uri.file(__dirname), '../../testWorkspace'); -const testCwdParent = vscode.Uri.joinPath(fixtureDir, 'parent'); -const testCwd = vscode.Uri.joinPath(fixtureDir, 'parent/home'); -const testCwdChild = vscode.Uri.joinPath(fixtureDir, 'parent/home/child'); - -interface ISuiteSpec { - name: string; - completionSpecs: Fig.Spec | Fig.Spec[]; - // TODO: This seems unnecessary, ideally getCompletionItemsFromSpecs would only consider the - // spec's completions - availableCommands: string | string[]; - testSpecs: ITestSpec2[]; -} - -interface ITestSpec2 { - input: string; - expectedResourceRequests?: { - type: 'files' | 'folders' | 'both'; - cwd: Uri; - }; - expectedCompletions?: string[]; -} - -function createCodeTestSpecs(executable: string): ITestSpec2[] { - const codeOptions = ['-', '--add', '--category', '--diff', '--disable-extension', '--disable-extensions', '--disable-gpu', '--enable-proposed-api', '--extensions-dir', '--goto', '--help', '--inspect-brk-extensions', '--inspect-extensions', '--install-extension', '--list-extensions', '--locale', '--log', '--max-memory', '--merge', '--new-window', '--pre-release', '--prof-startup', '--profile', '--reuse-window', '--show-versions', '--status', '--sync', '--telemetry', '--uninstall-extension', '--user-data-dir', '--verbose', '--version', '--wait', '-a', '-d', '-g', '-h', '-m', '-n', '-r', '-s', '-v', '-w']; - const localeOptions = ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW']; - const categoryOptions = ['azure', 'data science', 'debuggers', 'extension packs', 'education', 'formatters', 'keymaps', 'language packs', 'linters', 'machine learning', 'notebooks', 'programming languages', 'scm providers', 'snippets', 'testing', 'themes', 'visualization', 'other']; - const logOptions = ['critical', 'error', 'warn', 'info', 'debug', 'trace', 'off']; - const syncOptions = ['on', 'off']; - - const typingTests: ITestSpec2[] = []; - for (let i = 1; i < executable.length; i++) { - const input = `${executable.slice(0, i)}|`; - typingTests.push({ input, expectedCompletions: [executable], expectedResourceRequests: input.endsWith(' ') ? undefined : { type: 'both', cwd: testCwd } }); - } - - return [ - // Typing the command - ...typingTests, - - // Basic arguments - { input: `${executable} |`, expectedCompletions: codeOptions }, - { input: `${executable} --locale |`, expectedCompletions: localeOptions }, - { input: `${executable} --diff |`, expectedResourceRequests: { type: 'files', cwd: testCwd } }, - { input: `${executable} --diff ./file1 |`, expectedResourceRequests: { type: 'files', cwd: testCwd } }, - { input: `${executable} --merge |`, expectedResourceRequests: { type: 'files', cwd: testCwd } }, - { input: `${executable} --merge ./file1 ./file2 |`, expectedResourceRequests: { type: 'files', cwd: testCwd } }, - { input: `${executable} --merge ./file1 ./file2 ./base |`, expectedResourceRequests: { type: 'files', cwd: testCwd } }, - { input: `${executable} --goto |`, expectedResourceRequests: { type: 'files', cwd: testCwd } }, - { input: `${executable} --user-data-dir |`, expectedResourceRequests: { type: 'folders', cwd: testCwd } }, - { input: `${executable} --profile |`, expectedResourceRequests: { type: 'both', cwd: testCwd } }, - { input: `${executable} --install-extension |`, expectedResourceRequests: { type: 'both', cwd: testCwd } }, - { input: `${executable} --uninstall-extension |`, expectedResourceRequests: { type: 'both', cwd: testCwd } }, - { input: `${executable} --log |`, expectedCompletions: logOptions }, - { input: `${executable} --sync |`, expectedCompletions: syncOptions }, - { input: `${executable} --extensions-dir |`, expectedResourceRequests: { type: 'folders', cwd: testCwd } }, - { input: `${executable} --list-extensions |`, expectedCompletions: codeOptions }, - { input: `${executable} --show-versions |`, expectedCompletions: codeOptions }, - { input: `${executable} --category |`, expectedCompletions: categoryOptions }, - { input: `${executable} --category a|`, expectedCompletions: categoryOptions.filter(c => c.startsWith('a')) }, - - // Middle of command - { input: `${executable} | --locale`, expectedCompletions: codeOptions }, - ]; -} +import { cdTestSuiteSpec as cdTestSuite } from './completions/cd.test'; +import { codeTestSuite } from './completions/code.test'; +import { testPaths, type ISuiteSpec } from './helpers'; +import { codeInsidersTestSuite } from './completions/code-insiders.test'; const testSpecs2: ISuiteSpec[] = [ { @@ -86,56 +19,15 @@ const testSpecs2: ISuiteSpec[] = [ completionSpecs: [], availableCommands: [], testSpecs: [ - { input: '|', expectedCompletions: [], expectedResourceRequests: { type: 'both', cwd: testCwd } }, - { input: '|.', expectedCompletions: [], expectedResourceRequests: { type: 'both', cwd: testCwd } }, - { input: '|./', expectedCompletions: [], expectedResourceRequests: { type: 'both', cwd: testCwd } }, - { input: 'fakecommand |', expectedCompletions: [], expectedResourceRequests: { type: 'both', cwd: testCwd } }, + { input: '|', expectedCompletions: [], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: '|.', expectedCompletions: [], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: '|./', expectedCompletions: [], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'fakecommand |', expectedCompletions: [], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, ] }, - { - name: 'cd', - completionSpecs: cdSpec, - availableCommands: 'cd', - testSpecs: [ - // Typing a path - { input: '.|', expectedCompletions: ['cd'], expectedResourceRequests: { type: 'both', cwd: testCwd } }, - { input: './|', expectedCompletions: ['cd'], expectedResourceRequests: { type: 'both', cwd: testCwd } }, - { input: './.|', expectedCompletions: ['cd'], expectedResourceRequests: { type: 'both', cwd: testCwd } }, - // Typing the command - { input: 'c|', expectedCompletions: ['cd'], expectedResourceRequests: { type: 'both', cwd: testCwd } }, - { input: 'cd|', expectedCompletions: ['cd'], expectedResourceRequests: { type: 'both', cwd: testCwd } }, - - // Basic arguments - { input: 'cd |', expectedCompletions: ['~', '-'], expectedResourceRequests: { type: 'folders', cwd: testCwd } }, - { input: 'cd -|', expectedCompletions: ['-'], expectedResourceRequests: { type: 'folders', cwd: testCwd } }, - { input: 'cd ~|', expectedCompletions: ['~'], expectedResourceRequests: { type: 'folders', cwd: testCwd } }, - - // Relative paths - { input: 'cd c|', expectedResourceRequests: { type: 'folders', cwd: testCwd } }, - { input: 'cd child|', expectedResourceRequests: { type: 'folders', cwd: testCwd } }, - { input: 'cd .|', expectedResourceRequests: { type: 'folders', cwd: testCwd } }, - { input: 'cd ./|', expectedResourceRequests: { type: 'folders', cwd: testCwd } }, - { input: 'cd ./child|', expectedResourceRequests: { type: 'folders', cwd: testCwd } }, - { input: 'cd ..|', expectedResourceRequests: { type: 'folders', cwd: testCwd } }, - - // Relative directories (changes cwd due to /) - { input: 'cd child/|', expectedResourceRequests: { type: 'folders', cwd: testCwdChild } }, - { input: 'cd ../|', expectedResourceRequests: { type: 'folders', cwd: testCwdParent } }, - { input: 'cd ../sibling|', expectedResourceRequests: { type: 'folders', cwd: testCwdParent } }, - ] - }, - { - name: 'code', - completionSpecs: codeCompletionSpec, - availableCommands: 'code', - testSpecs: createCodeTestSpecs('code') - }, - { - name: 'code-insiders', - completionSpecs: codeInsidersCompletionSpec, - availableCommands: 'code-insiders', - testSpecs: createCodeTestSpecs('code-insiders') - } + cdTestSuite, + codeTestSuite, + codeInsidersTestSuite, ]; suite('Terminal Suggest', () => { @@ -147,7 +39,7 @@ suite('Terminal Suggest', () => { let expectedString = testSpec.expectedCompletions ? `[${testSpec.expectedCompletions.map(e => `'${e}'`).join(', ')}]` : '[]'; if (testSpec.expectedResourceRequests) { expectedString += ` + ${testSpec.expectedResourceRequests.type}`; - if (testSpec.expectedResourceRequests.cwd.fsPath !== testCwd.fsPath) { + if (testSpec.expectedResourceRequests.cwd.fsPath !== testPaths.cwd.fsPath) { expectedString += ` @ ${basename(testSpec.expectedResourceRequests.cwd.fsPath)}/`; } } @@ -158,7 +50,14 @@ suite('Terminal Suggest', () => { const filesRequested = testSpec.expectedResourceRequests?.type === 'files' || testSpec.expectedResourceRequests?.type === 'both'; const foldersRequested = testSpec.expectedResourceRequests?.type === 'folders' || testSpec.expectedResourceRequests?.type === 'both'; const terminalContext = { commandLine, cursorPosition }; - const result = await getCompletionItemsFromSpecs(completionSpecs, terminalContext, availableCommands.map(c => { return { label: c }; }), prefix, getTokenType(terminalContext, undefined), testCwd); + const result = await getCompletionItemsFromSpecs( + completionSpecs, + terminalContext, + availableCommands.map(c => { return { label: c }; }), + prefix, + getTokenType(terminalContext, undefined), + testPaths.cwd + ); deepStrictEqual(result.items.map(i => i.label).sort(), (testSpec.expectedCompletions ?? []).sort()); strictEqual(result.filesRequested, filesRequested); strictEqual(result.foldersRequested, foldersRequested); From 6d43829c8d0698d92ed383065ddf47f65256f571 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 4 Feb 2025 06:39:02 -0800 Subject: [PATCH 1188/3587] ls spec unit tests --- .../src/test/completions/upstream/ls.test.ts | 101 ++++++++++++++++++ .../src/test/terminalSuggestMain.test.ts | 6 ++ 2 files changed, 107 insertions(+) create mode 100644 extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts diff --git a/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts b/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts new file mode 100644 index 000000000000..33bb80d88220 --- /dev/null +++ b/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import { testPaths, type ISuiteSpec } from '../../helpers'; +import lsSpec from '../../../completions/upstream/ls'; + +const allOptions = [ + '-%', + '-,', + '--color', + '-1', + '-@', + '-A', + '-B', + '-C', + '-F', + '-G', + '-H', + '-L', + '-O', + '-P', + '-R', + '-S', + '-T', + '-U', + '-W', + '-a', + '-b', + '-c', + '-d', + '-e', + '-f', + '-g', + '-h', + '-i', + '-k', + '-l', + '-m', + '-n', + '-o', + '-p', + '-q', + '-r', + '-s', + '-t', + '-u', + '-v', + '-w', + '-x', +]; + +export function removeEntry(array: T[], element: T): T[] { + const index = array.indexOf(element); + if (index > -1) { + array.splice(index, 1); + } + return array; +} + +export const lsTestSuiteSpec: ISuiteSpec = { + name: 'ls', + completionSpecs: lsSpec, + availableCommands: 'ls', + testSpecs: [ + // Empty input + { input: '|', expectedCompletions: ['ls'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + + // Typing the command + { input: 'l|', expectedCompletions: ['ls'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'ls|', expectedCompletions: ['ls'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + + // Basic options + // TODO: The spec wants file paths and folders (which seems like it should only be folders), + // but neither are requested + { input: 'ls |', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } + { input: 'ls -|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } + + // Filtering options should request all options so client side can filter + { input: 'ls -a|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } + + // Duplicate option + // TODO: Duplicate options should not be presented + // { input: 'ls -a -|', expectedCompletions: removeEntry(allOptions, '-a'), expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } + + // Relative paths + { input: 'ls c|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } + { input: 'ls child|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } + { input: 'ls .|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } + { input: 'ls ./|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } + { input: 'ls ./child|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } + { input: 'ls ..|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } + + // Relative directories (changes cwd due to /) + { input: 'ls child/|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwdChild } + { input: 'ls ../|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwdParent } + { input: 'ls ../sibling|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwdParent } + ] +}; diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index c84693144eee..a704196df1de 100644 --- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -12,6 +12,7 @@ import { cdTestSuiteSpec as cdTestSuite } from './completions/cd.test'; import { codeTestSuite } from './completions/code.test'; import { testPaths, type ISuiteSpec } from './helpers'; import { codeInsidersTestSuite } from './completions/code-insiders.test'; +import { lsTestSuiteSpec } from './completions/upstream/ls.test'; const testSpecs2: ISuiteSpec[] = [ { @@ -25,9 +26,14 @@ const testSpecs2: ISuiteSpec[] = [ { input: 'fakecommand |', expectedCompletions: [], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, ] }, + + // completions/ cdTestSuite, codeTestSuite, codeInsidersTestSuite, + + // completions/upstream/ + lsTestSuiteSpec, ]; suite('Terminal Suggest', () => { From 4e364a5c7b01a173af2f7b8c094840080bed4ace Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 4 Feb 2025 06:50:57 -0800 Subject: [PATCH 1189/3587] Add issue references --- .../src/test/completions/upstream/ls.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts b/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts index 33bb80d88220..e8f0b7de7b5b 100644 --- a/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts +++ b/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts @@ -52,13 +52,13 @@ const allOptions = [ '-x', ]; -export function removeEntry(array: T[], element: T): T[] { - const index = array.indexOf(element); - if (index > -1) { - array.splice(index, 1); - } - return array; -} +// function removeEntry(array: T[], element: T): T[] { +// const index = array.indexOf(element); +// if (index > -1) { +// array.splice(index, 1); +// } +// return array; +// } export const lsTestSuiteSpec: ISuiteSpec = { name: 'ls', @@ -74,7 +74,7 @@ export const lsTestSuiteSpec: ISuiteSpec = { // Basic options // TODO: The spec wants file paths and folders (which seems like it should only be folders), - // but neither are requested + // but neither are requested https://github.com/microsoft/vscode/issues/239606 { input: 'ls |', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } { input: 'ls -|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } @@ -82,7 +82,7 @@ export const lsTestSuiteSpec: ISuiteSpec = { { input: 'ls -a|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } // Duplicate option - // TODO: Duplicate options should not be presented + // TODO: Duplicate options should not be presented https://github.com/microsoft/vscode/issues/239607 // { input: 'ls -a -|', expectedCompletions: removeEntry(allOptions, '-a'), expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } // Relative paths From 79c194bfda67d2906e0059dde79ce565f4fb09d9 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 4 Feb 2025 15:56:22 +0100 Subject: [PATCH 1190/3587] fix https://github.com/microsoft/vscode-copilot/issues/12823 (#239603) --- src/vs/workbench/contrib/chat/browser/chatListRenderer.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index e61430a76851..84b7734190ad 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -532,6 +532,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { const context: IChatContentPartRenderContext = { element, @@ -543,6 +546,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this._onDidClickRerunWithAgentOrCommandDetection.fire({ sessionId: element.sessionId, requestId: element.id })); templateData.value.appendChild(cmdPart.domNode); parts.push(cmdPart); + inlineSlashCommandRendered = true; } templateData.value.appendChild(newPart.domNode); From ad0173f59aacf537b06b861b0ee87421bae92619 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 4 Feb 2025 07:00:38 -0800 Subject: [PATCH 1191/3587] echo fig spec tests --- .../test/completions/upstream/echo.test.ts | 38 +++++++++++++++++++ .../src/test/completions/upstream/ls.test.ts | 10 +---- .../terminal-suggest/src/test/helpers.ts | 10 +++++ .../src/test/terminalSuggestMain.test.ts | 2 + 4 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 extensions/terminal-suggest/src/test/completions/upstream/echo.test.ts diff --git a/extensions/terminal-suggest/src/test/completions/upstream/echo.test.ts b/extensions/terminal-suggest/src/test/completions/upstream/echo.test.ts new file mode 100644 index 000000000000..7334c8d219b0 --- /dev/null +++ b/extensions/terminal-suggest/src/test/completions/upstream/echo.test.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import { testPaths, type ISuiteSpec } from '../../helpers'; +import echoSpec from '../../../completions/upstream/echo'; + +const allOptions = [ + '-E', + '-e', + '-n', +]; + +export const echoTestSuiteSpec: ISuiteSpec = { + name: 'echo', + completionSpecs: echoSpec, + availableCommands: 'echo', + testSpecs: [ + // Empty input + { input: '|', expectedCompletions: ['echo'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + + // Typing the command + { input: 'e|', expectedCompletions: ['echo'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'ec|', expectedCompletions: ['echo'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'ech|', expectedCompletions: ['echo'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'echo|', expectedCompletions: ['echo'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + + // Basic options + { input: 'echo |', expectedCompletions: allOptions }, + + // Duplicate option + // TODO: Duplicate options should not be presented https://github.com/microsoft/vscode/issues/239607 + // { input: 'echo -e -|', expectedCompletions: removeArrayEntries(allOptions, '-e') }, + // { input: 'echo -e -E -|', expectedCompletions: removeArrayEntries(allOptions, '-e', '-E') }, + ] +}; diff --git a/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts b/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts index e8f0b7de7b5b..dfd7da8a93e9 100644 --- a/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts +++ b/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts @@ -52,14 +52,6 @@ const allOptions = [ '-x', ]; -// function removeEntry(array: T[], element: T): T[] { -// const index = array.indexOf(element); -// if (index > -1) { -// array.splice(index, 1); -// } -// return array; -// } - export const lsTestSuiteSpec: ISuiteSpec = { name: 'ls', completionSpecs: lsSpec, @@ -83,7 +75,7 @@ export const lsTestSuiteSpec: ISuiteSpec = { // Duplicate option // TODO: Duplicate options should not be presented https://github.com/microsoft/vscode/issues/239607 - // { input: 'ls -a -|', expectedCompletions: removeEntry(allOptions, '-a'), expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } + // { input: 'ls -a -|', expectedCompletions: removeArrayEntry(allOptions, '-a'), expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } // Relative paths { input: 'ls c|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } diff --git a/extensions/terminal-suggest/src/test/helpers.ts b/extensions/terminal-suggest/src/test/helpers.ts index 3075022d541f..48ad02c06787 100644 --- a/extensions/terminal-suggest/src/test/helpers.ts +++ b/extensions/terminal-suggest/src/test/helpers.ts @@ -36,3 +36,13 @@ export const testPaths = { cwd: vscode.Uri.joinPath(fixtureDir, 'parent/home'), cwdChild: vscode.Uri.joinPath(fixtureDir, 'parent/home/child'), }; + +export function removeArrayEntries(array: T[], ...elements: T[]): T[] { + for (const element of elements) { + const index = array.indexOf(element); + if (index > -1) { + array.splice(index, 1); + } + } + return array; +} diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index a704196df1de..f097db24d8ab 100644 --- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -13,6 +13,7 @@ import { codeTestSuite } from './completions/code.test'; import { testPaths, type ISuiteSpec } from './helpers'; import { codeInsidersTestSuite } from './completions/code-insiders.test'; import { lsTestSuiteSpec } from './completions/upstream/ls.test'; +import { echoTestSuiteSpec } from './completions/upstream/echo.test'; const testSpecs2: ISuiteSpec[] = [ { @@ -33,6 +34,7 @@ const testSpecs2: ISuiteSpec[] = [ codeInsidersTestSuite, // completions/upstream/ + echoTestSuiteSpec, lsTestSuiteSpec, ]; From 77f6745f4279eb2bd24693cb6ecc1b824785b349 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 4 Feb 2025 07:04:49 -0800 Subject: [PATCH 1192/3587] mkdir fig spec tests --- .../test/completions/upstream/mkdir.test.ts | 43 +++++++++++++++++++ .../src/test/terminalSuggestMain.test.ts | 2 + 2 files changed, 45 insertions(+) create mode 100644 extensions/terminal-suggest/src/test/completions/upstream/mkdir.test.ts diff --git a/extensions/terminal-suggest/src/test/completions/upstream/mkdir.test.ts b/extensions/terminal-suggest/src/test/completions/upstream/mkdir.test.ts new file mode 100644 index 000000000000..f471a6650461 --- /dev/null +++ b/extensions/terminal-suggest/src/test/completions/upstream/mkdir.test.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import { testPaths, type ISuiteSpec } from '../../helpers'; +import mkdirSpec from '../../../completions/upstream/mkdir'; + +const allOptions = [ + '--context', + '--help', + '--mode', + '--parents', + '--verbose', + '--version', + '-Z', + '-m', + '-p', + '-v', +]; + +export const mkdirTestSuiteSpec: ISuiteSpec = { + name: 'mkdir', + completionSpecs: mkdirSpec, + availableCommands: 'mkdir', + testSpecs: [ + // Empty input + { input: '|', expectedCompletions: ['mkdir'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + + // Typing the command + { input: 'm|', expectedCompletions: ['mkdir'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'mkdir|', expectedCompletions: ['mkdir'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + + // Basic options + { input: 'mkdir |', expectedCompletions: allOptions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, + + // Duplicate option + // TODO: Duplicate options should not be presented https://github.com/microsoft/vscode/issues/239607 + // { input: 'mkdir -Z -|', expectedCompletions: removeArrayEntries(allOptions, '-z') }, + // { input: 'mkdir -Z -m -|', expectedCompletions: removeArrayEntries(allOptions, '-z', '-m') }, + ] +}; diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index f097db24d8ab..a79f1c7432af 100644 --- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -14,6 +14,7 @@ import { testPaths, type ISuiteSpec } from './helpers'; import { codeInsidersTestSuite } from './completions/code-insiders.test'; import { lsTestSuiteSpec } from './completions/upstream/ls.test'; import { echoTestSuiteSpec } from './completions/upstream/echo.test'; +import { mkdirTestSuiteSpec } from './completions/upstream/mkdir.test'; const testSpecs2: ISuiteSpec[] = [ { @@ -36,6 +37,7 @@ const testSpecs2: ISuiteSpec[] = [ // completions/upstream/ echoTestSuiteSpec, lsTestSuiteSpec, + mkdirTestSuiteSpec, ]; suite('Terminal Suggest', () => { From c54d20f63c596826d9e3c96fcc1e2ab09b49c341 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:08:34 +0100 Subject: [PATCH 1193/3587] Fix scrolling issue when pressing Enter on multi-line suggestions (#239605) fixes https://github.com/microsoft/vscode/issues/239599 --- .../browser/view/ghostText/ghostTextView.ts | 11 +++++++++-- .../browser/view/inlineCompletionsView.ts | 16 ++++++++++------ .../browser/view/inlineEdits/insertionView.ts | 1 + 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts index 47644496fea2..a7ed17ddad61 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts @@ -43,6 +43,7 @@ export class GhostTextView extends Disposable { extraClasses?: string[]; syntaxHighlightingEnabled: boolean; }>, + private readonly _shouldKeepCursorStable: boolean, @ILanguageService private readonly _languageService: ILanguageService, ) { super(); @@ -151,7 +152,8 @@ export class GhostTextView extends Disposable { minReservedLineCount: uiState.additionalReservedLineCount, targetTextModel: uiState.targetTextModel, } : undefined; - }) + }), + this._shouldKeepCursorStable ) ); @@ -257,7 +259,8 @@ export class AdditionalLinesWidget extends Disposable { lineNumber: number; additionalLines: LineData[]; minReservedLineCount: number; - } | undefined> + } | undefined>, + private readonly shouldKeepCursorStable: boolean ) { super(); @@ -334,6 +337,10 @@ export class AdditionalLinesWidget extends Disposable { } private keepCursorStable(lineNumber: number, heightInLines: number): void { + if (!this.shouldKeepCursorStable) { + return; + } + const cursorLineNumber = this.editor.getSelection()?.getStartPosition()?.lineNumber; if (cursorLineNumber !== undefined && lineNumber < cursorLineNumber) { this.editor.setScrollTop(this.editor.getScrollTop() + heightInLines * this.editor.getOption(EditorOption.lineHeight)); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts index cbbbb35734e2..7cd21d3b088e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts @@ -25,12 +25,16 @@ export class InlineCompletionsView extends Disposable { private readonly _stablizedGhostTexts = convertItemsToStableObservables(this._ghostTexts, this._store); private readonly _editorObs = observableCodeEditor(this._editor); - private readonly _ghostTextWidgets = mapObservableArrayCached(this, this._stablizedGhostTexts, (ghostText, store) => derivedDisposable((reader) => this._instantiationService.createInstance(readHotReloadableExport(GhostTextView, reader), this._editor, { - ghostText: ghostText, - minReservedLineCount: constObservable(0), - targetTextModel: this._model.map(v => v?.textModel), - }, - this._editorObs.getOption(EditorOption.inlineSuggest).map(v => ({ syntaxHighlightingEnabled: v.syntaxHighlightingEnabled }))) + private readonly _ghostTextWidgets = mapObservableArrayCached(this, this._stablizedGhostTexts, (ghostText, store) => derivedDisposable((reader) => this._instantiationService.createInstance(readHotReloadableExport(GhostTextView, reader), + this._editor, + { + ghostText: ghostText, + minReservedLineCount: constObservable(0), + targetTextModel: this._model.map(v => v?.textModel), + }, + this._editorObs.getOption(EditorOption.inlineSuggest).map(v => ({ syntaxHighlightingEnabled: v.syntaxHighlightingEnabled })), + false, + ) ).recomputeInitiallyAndOnChange(store) ).recomputeInitiallyAndOnChange(this._store); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts index 0f545a59f56a..47ee5171571c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts @@ -54,6 +54,7 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits targetTextModel: this._editorObs.model.map(model => model ?? undefined), }, observableValue(this, { syntaxHighlightingEnabled: true, extraClasses: ['inline-edit'] }), + true, )); constructor( From f614d9d870e308477a5d452b0e3abaa28bec4b21 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 4 Feb 2025 16:30:29 +0100 Subject: [PATCH 1194/3587] leak - fix leaking file widgets (#239612) --- .../contrib/chat/browser/chatMarkdownDecorationsRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index 922e47f563a6..46a068ee3dce 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -250,7 +250,7 @@ export class ChatMarkdownDecorationsRenderer { } const inlineAnchor = store.add(this.instantiationService.createInstance(InlineAnchorWidget, a, data)); - this.chatMarkdownAnchorService.register(inlineAnchor); + store.add(this.chatMarkdownAnchorService.register(inlineAnchor)); } private renderResourceWidget(name: string, args: IDecorationWidgetArgs | undefined, store: DisposableStore): HTMLElement { From a2a0272687a193cc50dd3eecc9cc03e0a8b8821a Mon Sep 17 00:00:00 2001 From: Vitaly Date: Tue, 4 Feb 2025 18:35:47 +0300 Subject: [PATCH 1195/3587] [Git] Migrate to git autostash when pulling for better performance (#187850) * git: migrate to git autostash when pulling (better performance) * should be implemented correctly! * refactor other op * Pull request feedback --------- Co-authored-by: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> --- extensions/git/src/git.ts | 10 ++++++++-- extensions/git/src/repository.ts | 7 +++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 431d50b88afe..b6d5582b8fb6 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1127,8 +1127,9 @@ function parseGitBlame(data: string): BlameInformation[] { } export interface PullOptions { - unshallow?: boolean; - tags?: boolean; + readonly unshallow?: boolean; + readonly tags?: boolean; + readonly autoStash?: boolean; readonly cancellationToken?: CancellationToken; } @@ -2087,6 +2088,11 @@ export class Repository { args.push('--unshallow'); } + // --auto-stash option is only available `git pull --merge` starting with git 2.27.0 + if (options.autoStash && this._git.compareGitVersionTo('2.27.0') !== -1) { + args.push('--autostash'); + } + if (rebase) { args.push('-r'); } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index e791e3d500d5..560477c586d5 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1798,6 +1798,7 @@ export class Repository implements Disposable { await this.run(Operation.Pull, async () => { await this.maybeAutoStash(async () => { const config = workspace.getConfiguration('git', Uri.file(this.root)); + const autoStash = config.get('autoStash'); const fetchOnPull = config.get('fetchOnPull'); const tags = config.get('pullTags'); @@ -1807,7 +1808,7 @@ export class Repository implements Disposable { } if (await this.checkIfMaybeRebased(this.HEAD?.name)) { - await this._pullAndHandleTagConflict(rebase, remote, branch, { unshallow, tags }); + await this._pullAndHandleTagConflict(rebase, remote, branch, { unshallow, tags, autoStash }); } }); }); @@ -1881,6 +1882,7 @@ export class Repository implements Disposable { await this.run(Operation.Sync, async () => { await this.maybeAutoStash(async () => { const config = workspace.getConfiguration('git', Uri.file(this.root)); + const autoStash = config.get('autoStash'); const fetchOnPull = config.get('fetchOnPull'); const tags = config.get('pullTags'); const followTags = config.get('followTagsWhenSync'); @@ -1893,7 +1895,7 @@ export class Repository implements Disposable { } if (await this.checkIfMaybeRebased(this.HEAD?.name)) { - await this._pullAndHandleTagConflict(rebase, remoteName, pullBranch, { tags, cancellationToken }); + await this._pullAndHandleTagConflict(rebase, remoteName, pullBranch, { tags, cancellationToken, autoStash }); } }; @@ -2530,6 +2532,7 @@ export class Repository implements Disposable { private async maybeAutoStash(runOperation: () => Promise): Promise { const config = workspace.getConfiguration('git', Uri.file(this.root)); const shouldAutoStash = config.get('autoStash') + && this.repository.git.compareGitVersionTo('2.27.0') < 0 && (this.indexGroup.resourceStates.length > 0 || this.workingTreeGroup.resourceStates.some( r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED)); From 43469df88d9b6a4bbcd49043b959389941ac2248 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 4 Feb 2025 16:36:58 +0100 Subject: [PATCH 1196/3587] chore - make `reviewEdits` a dedicated function (#239611) --- .../browser/actions/chatCodeblockActions.ts | 5 +- .../browser/actions/codeBlockOperations.ts | 10 +-- .../browser/inlineChatController.ts | 80 +++++++++---------- 3 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 247fc3082701..5448cdeed929 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -27,7 +27,7 @@ import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { IUntitledTextResourceEditorInput } from '../../../../common/editor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { accessibleViewInCodeBlock } from '../../../accessibility/browser/accessibilityConfiguration.js'; -import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js'; +import { InlineChatController, reviewEdits } from '../../../inlineChat/browser/inlineChatController.js'; import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from '../../../terminal/browser/terminal.js'; import { ChatAgentLocation } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; @@ -575,6 +575,7 @@ export function registerChatCodeCompareBlockActions() { async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise { + const instaService = accessor.get(IInstantiationService); const editorService = accessor.get(ICodeEditorService); const item = context.edit; @@ -601,7 +602,7 @@ export function registerChatCodeCompareBlockActions() { const inlineChatController = InlineChatController.get(editorToApply); if (inlineChatController) { editorToApply.revealLineInCenterIfOutsideViewport(firstEdit.range.startLineNumber); - inlineChatController.reviewEdits(textEdits, CancellationToken.None); + instaService.invokeFunction(reviewEdits, editorToApply, textEdits, CancellationToken.None); response.setEditApplied(item, 1); return true; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index 03de808a6428..8831b4880040 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -24,7 +24,7 @@ import { ILogService } from '../../../../../platform/log/common/log.js'; import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; -import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js'; +import { reviewEdits } from '../../../inlineChat/browser/inlineChatController.js'; import { insertCell } from '../../../notebook/browser/controller/cellOperations.js'; import { IActiveNotebookEditor, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; import { CellKind, NOTEBOOK_EDITOR_ID } from '../../../notebook/common/notebookCommon.js'; @@ -34,6 +34,7 @@ import { isResponseVM } from '../../common/chatViewModel.js'; import { ICodeBlockActionContext } from '../codeBlockPart.js'; import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; export class InsertCodeBlockOperation { constructor( @@ -115,6 +116,7 @@ export class ApplyCodeBlockOperation { @IProgressService private readonly progressService: IProgressService, @IQuickInputService private readonly quickInputService: IQuickInputService, @ILabelService private readonly labelService: ILabelService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { } @@ -303,11 +305,7 @@ export class ApplyCodeBlockOperation { } private async applyWithInlinePreview(edits: AsyncIterable, codeEditor: IActiveCodeEditor, tokenSource: CancellationTokenSource): Promise { - const inlineChatController = InlineChatController.get(codeEditor); - if (inlineChatController) { - return inlineChatController.reviewEdits(edits, tokenSource.token); - } - return false; + return this.instantiationService.invokeFunction(reviewEdits, codeEditor, edits, tokenSource.token); } private tryToRevealCodeBlock(codeEditor: IActiveCodeEditor, codeBlock: string): void { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 3ef89c1c8f11..4154f8239f90 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -141,7 +141,6 @@ export class InlineChatController implements IEditorContribution { @IDialogService private readonly _dialogService: IDialogService, @IContextKeyService contextKeyService: IContextKeyService, @IChatService private readonly _chatService: IChatService, - @IChatEditingService private readonly _chatEditingService: IChatEditingService, @IEditorService private readonly _editorService: IEditorService, @INotebookEditorService notebookEditorService: INotebookEditorService, ) { @@ -985,9 +984,6 @@ export class InlineChatController implements IEditorContribution { } } - cancelCurrentRequest(): void { - this._messages.fire(Message.CANCEL_INPUT | Message.CANCEL_REQUEST); - } arrowOut(up: boolean): void { if (this._ui.value.position && this._editor.hasModel()) { @@ -1127,57 +1123,61 @@ export class InlineChatController implements IEditorContribution { joinCurrentRun(): Promise | undefined { return this._currentRun; } +} - async reviewEdits(stream: AsyncIterable, token: CancellationToken) { - if (!this._editor.hasModel()) { - return false; - } - const uri = this._editor.getModel().uri; - const chatModel = this._chatService.startSession(ChatAgentLocation.Editor, token); +export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEditor, stream: AsyncIterable, token: CancellationToken): Promise { + if (!editor.hasModel()) { + return false; + } - const editSession = await this._chatEditingService.createEditingSession(chatModel.sessionId); + const chatService = accessor.get(IChatService); + const chatEditingService = accessor.get(IChatEditingService); - // - const store = new DisposableStore(); - store.add(chatModel); - store.add(editSession); + const uri = editor.getModel().uri; + const chatModel = chatService.startSession(ChatAgentLocation.Editor, token); - // STREAM - const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); - assertType(chatRequest.response); - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); - for await (const chunk of stream) { + const editSession = await chatEditingService.createEditingSession(chatModel.sessionId); - if (token.isCancellationRequested) { - chatRequest.response.cancel(); - break; - } + const store = new DisposableStore(); + store.add(chatModel); + store.add(editSession); - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: chunk, done: false }); - } - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); + // STREAM + const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); + assertType(chatRequest.response); + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); + for await (const chunk of stream) { - if (!token.isCancellationRequested) { - chatRequest.response.complete(); + if (token.isCancellationRequested) { + chatRequest.response.cancel(); + break; } - const whenDecided = new Promise(resolve => { - store.add(autorun(r => { - if (!editSession.entries.read(r).some(e => e.state.read(r) === WorkingSetEntryState.Modified)) { - resolve(undefined); - } - })); - }); + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: chunk, done: false }); + } + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); - await raceCancellation(whenDecided, token); + if (!token.isCancellationRequested) { + chatRequest.response.complete(); + } - store.dispose(); + const whenDecided = new Promise(resolve => { + store.add(autorun(r => { + if (!editSession.entries.read(r).some(e => e.state.read(r) === WorkingSetEntryState.Modified)) { + resolve(undefined); + } + })); + }); - return true; - } + await raceCancellation(whenDecided, token); + + store.dispose(); + + return true; } + async function moveToPanelChat(accessor: ServicesAccessor, model: ChatModel | undefined) { const viewsService = accessor.get(IViewsService); From d04d44610f9dc5dc1c9eadb1e177fcb1c17693a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 08:05:29 -0800 Subject: [PATCH 1197/3587] Bump openssl from 0.10.66 to 0.10.70 in /cli (#239517) Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.66 to 0.10.70. - [Release notes](https://github.com/sfackler/rust-openssl/releases) - [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.66...openssl-v0.10.70) --- updated-dependencies: - dependency-name: openssl dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- cli/Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 5da9906e1acd..ff45765a0c1d 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -1717,9 +1717,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" dependencies = [ "bitflags 2.5.0", "cfg-if", @@ -1749,9 +1749,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" dependencies = [ "cc", "libc", From 776bf43309b3a6d5ebbffcf302b092fa16e678d1 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 4 Feb 2025 17:34:53 +0100 Subject: [PATCH 1198/3587] chore - remove dead code and align controllers a little (#239619) --- .../browser/actions/chatQuickInputActions.ts | 33 ------------------- .../browser/inlineChatController.ts | 25 +++----------- .../browser/inlineChatController2.ts | 4 +++ .../contrib/inlineChat/common/inlineChat.ts | 3 -- .../electron-sandbox/inlineChatActions.ts | 2 +- .../test/browser/inlineChatController.test.ts | 10 +++--- 6 files changed, 13 insertions(+), 64 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts index 3c75e0c503c0..2266ef267ee5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts @@ -5,7 +5,6 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; import { Selection } from '../../../../../editor/common/core/selection.js'; import { localize, localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; @@ -14,7 +13,6 @@ import { KeybindingWeight } from '../../../../../platform/keybinding/common/keyb import { CHAT_CATEGORY } from './chatActions.js'; import { IQuickChatOpenOptions, IQuickChatService } from '../chat.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js'; export const ASK_QUICK_QUESTION_ACTION_ID = 'workbench.action.quickchat.toggle'; export function registerQuickChatActions() { @@ -65,37 +63,6 @@ export function registerQuickChatActions() { } }); - registerAction2(class LaunchInlineChatFromQuickChatAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.quickchat.launchInlineChat', - title: localize2('chat.launchInlineChat.label', "Launch Inline Chat"), - f1: false, - category: CHAT_CATEGORY - }); - } - - async run(accessor: ServicesAccessor) { - const quickChatService = accessor.get(IQuickChatService); - const codeEditorService = accessor.get(ICodeEditorService); - if (quickChatService.focused) { - quickChatService.close(); - } - const codeEditor = codeEditorService.getActiveCodeEditor(); - if (!codeEditor) { - return; - } - - const controller = InlineChatController.get(codeEditor); - if (!controller) { - return; - } - - await controller.run(); - controller.focus(); - } - }); - } class QuickChatGlobalAction extends Action2 { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 4154f8239f90..f3c7605e6e52 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -45,7 +45,7 @@ import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/cha import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from '../../chat/common/chatModel.js'; import { IChatService } from '../../chat/common/chatService.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; -import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_VISIBLE, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js'; import { HunkInformation, Session, StashedSession } from './inlineChatSession.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { InlineChatError } from './inlineChatSessionServiceImpl.js'; @@ -110,7 +110,6 @@ export class InlineChatController implements IEditorContribution { private readonly _ctxVisible: IContextKey; private readonly _ctxEditing: IContextKey; private readonly _ctxResponseType: IContextKey; - private readonly _ctxUserDidEdit: IContextKey; private readonly _ctxRequestInProgress: IContextKey; private readonly _ctxResponse: IContextKey; @@ -146,7 +145,6 @@ export class InlineChatController implements IEditorContribution { ) { this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); this._ctxEditing = CTX_INLINE_CHAT_EDITING.bindTo(contextKeyService); - this._ctxUserDidEdit = CTX_INLINE_CHAT_USER_DID_EDIT.bindTo(contextKeyService); this._ctxResponseType = CTX_INLINE_CHAT_RESPONSE_TYPE.bindTo(contextKeyService); this._ctxRequestInProgress = CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.bindTo(contextKeyService); @@ -409,13 +407,9 @@ export class InlineChatController implements IEditorContribution { this._messages.fire(msg); })); - const altVersionNow = this._editor.getModel()?.getAlternativeVersionId(); this._sessionStore.add(this._editor.onDidChangeModelContent(e => { - if (!this._session?.hunkData.ignoreTextModelNChanges) { - this._ctxUserDidEdit.set(altVersionNow !== this._editor.getModel()?.getAlternativeVersionId()); - } if (this._session?.hunkData.ignoreTextModelNChanges || this._ui.value.widget.hasFocus()) { return; @@ -491,7 +485,7 @@ export class InlineChatController implements IEditorContribution { this._updatePlaceholder(); if (options.message) { - this.updateInput(options.message); + this._updateInput(options.message); aria.alert(options.message); delete options.message; this._showWidget(this._session.headless, false); @@ -888,7 +882,6 @@ export class InlineChatController implements IEditorContribution { this._sessionStore.clear(); this._ctxVisible.reset(); - this._ctxUserDidEdit.reset(); this._ui.rawValue?.hide(); @@ -969,13 +962,7 @@ export class InlineChatController implements IEditorContribution { this._ui.value.widget.placeholder = this._session?.agent.description ?? ''; } - // ---- controller API - - acceptInput() { - return this.chatWidget.acceptInput(); - } - - updateInput(text: string, selectAll = true): void { + private _updateInput(text: string, selectAll = true): void { this._ui.value.widget.chatWidget.setInput(text); if (selectAll) { @@ -984,6 +971,7 @@ export class InlineChatController implements IEditorContribution { } } + // ---- controller API arrowOut(up: boolean): void { if (this._ui.value.position && this._editor.hasModel()) { @@ -999,11 +987,6 @@ export class InlineChatController implements IEditorContribution { this._ui.value.widget.focus(); } - hasFocus(): boolean { - return this._ui.value.widget.hasFocus(); - } - - async viewInChat() { if (!this._strategy || !this._session) { return; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts index f07c82bc1548..78b47cf777de 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts @@ -232,6 +232,10 @@ export class InlineChatController2 implements IEditorContribution { markActiveController() { this._isActiveController.set(true, undefined); } + + focus() { + this._zone.rawValue?.widget.focus(); + } } export class StartSessionAction2 extends EditorAction2 { diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 21dcf39b8d66..7215c80e416f 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -82,11 +82,8 @@ export const CTX_INLINE_CHAT_RESPONSE_FOCUSED = new RawContextKey('inli export const CTX_INLINE_CHAT_EMPTY = new RawContextKey('inlineChatEmpty', false, localize('inlineChatEmpty', "Whether the interactive editor input is empty")); export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey('inlineChatInnerCursorFirst', false, localize('inlineChatInnerCursorFirst', "Whether the cursor of the iteractive editor input is on the first line")); export const CTX_INLINE_CHAT_INNER_CURSOR_LAST = new RawContextKey('inlineChatInnerCursorLast', false, localize('inlineChatInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line")); -export const CTX_INLINE_CHAT_INNER_CURSOR_START = new RawContextKey('inlineChatInnerCursorStart', false, localize('inlineChatInnerCursorStart', "Whether the cursor of the iteractive editor input is on the start of the input")); -export const CTX_INLINE_CHAT_INNER_CURSOR_END = new RawContextKey('inlineChatInnerCursorEnd', false, localize('inlineChatInnerCursorEnd', "Whether the cursor of the iteractive editor input is on the end of the input")); export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input")); export const CTX_INLINE_CHAT_HAS_STASHED_SESSION = new RawContextKey('inlineChatHasStashedSession', false, localize('inlineChatHasStashedSession', "Whether interactive editor has kept a session for quick restore")); -export const CTX_INLINE_CHAT_USER_DID_EDIT = new RawContextKey('inlineChatUserDidEdit', undefined, localize('inlineChatUserDidEdit', "Whether the user did changes ontop of the inline chat")); export const CTX_INLINE_CHAT_CHANGE_HAS_DIFF = new RawContextKey('inlineChatChangeHasDiff', false, localize('inlineChatChangeHasDiff', "Whether the current change supports showing a diff")); export const CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF = new RawContextKey('inlineChatChangeShowsDiff', false, localize('inlineChatChangeShowsDiff', "Whether the current change showing a diff")); export const CTX_INLINE_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('inlineChatRequestInProgress', false, localize('inlineChatRequestInProgress', "Whether an inline chat request is currently in progress")); diff --git a/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatActions.ts index ddf6acf9f638..cc0bb05c1dc2 100644 --- a/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatActions.ts @@ -67,7 +67,7 @@ function holdForSpeech(accessor: ServicesAccessor, ctrl: InlineChatController, a holdMode.finally(() => { if (listening) { commandService.executeCommand(StopListeningAction.ID).finally(() => { - ctrl.acceptInput(); + ctrl.chatWidget.acceptInput(); }); } handle.dispose(); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 1e12c2c3544c..44fe68ae3509 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -34,7 +34,7 @@ import { IChatAccessibilityService, IChatWidget, IChatWidgetService } from '../. import { ChatAgentLocation, ChatAgentService, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../../../chat/common/chatAgents.js'; import { IChatResponseViewModel } from '../../../chat/common/chatViewModel.js'; import { InlineChatController, State } from '../../browser/inlineChatController.js'; -import { CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, InlineChatConfigKeys, InlineChatResponseType } from '../../common/inlineChat.js'; +import { CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatConfigKeys, InlineChatResponseType } from '../../common/inlineChat.js'; import { TestViewsService, workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; import { IChatProgress, IChatService } from '../../../chat/common/chatService.js'; @@ -331,7 +331,7 @@ suite('InlineChatController', function () { assert.deepStrictEqual(session.wholeRange.value, new Range(3, 1, 3, 3)); // initial ctrl.chatWidget.setInput('GENGEN'); - ctrl.acceptInput(); + ctrl.chatWidget.acceptInput(); assert.strictEqual(await ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]), undefined); assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 4, 3)); @@ -452,7 +452,6 @@ suite('InlineChatController', function () { assert.strictEqual(await p, undefined); assert.ok(model.getValue().includes('GENERATED')); - assert.strictEqual(contextKeyService.getContextKeyValue(CTX_INLINE_CHAT_USER_DID_EDIT.key), undefined); ctrl.cancelSession(); await r; assert.ok(!model.getValue().includes('GENERATED')); @@ -470,7 +469,6 @@ suite('InlineChatController', function () { assert.ok(model.getValue().includes('GENERATED')); editor.executeEdits('test', [EditOperation.insert(model.getFullModelRange().getEndPosition(), 'MANUAL')]); - assert.strictEqual(contextKeyService.getContextKeyValue(CTX_INLINE_CHAT_USER_DID_EDIT.key), true); ctrl.finishExistingSession(); await r; @@ -548,7 +546,7 @@ suite('InlineChatController', function () { // REQUEST 2 const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); ctrl.chatWidget.setInput('1'); - await ctrl.acceptInput(); + await ctrl.chatWidget.acceptInput(); assert.strictEqual(await p2, undefined); assert.strictEqual(model.getValue(), 'zwei-eins-'); @@ -632,7 +630,7 @@ suite('InlineChatController', function () { // REQUEST 2 const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); ctrl.chatWidget.setInput('1'); - await ctrl.acceptInput(); + await ctrl.chatWidget.acceptInput(); assert.strictEqual(await p2, undefined); assert.strictEqual(model.getValue(), 'zwei\neins\nHello\nWorld\nHello Again\nHello World\n'); From 9624242dd541b620b8eccb9a3e2f44973ce63b8d Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 4 Feb 2025 17:39:12 +0100 Subject: [PATCH 1199/3587] chore - renames and moves (#239616) * chore - renames and moves * update CSS paths too --- .../browser/editorAccessibilityHelp.ts | 2 +- .../chat/browser/actions/chatClearActions.ts | 2 +- .../contrib/chat/browser/chat.contribution.ts | 6 +- .../chatEditingEditorActions.ts} | 36 +++++----- .../chatEditingEditorController.ts} | 70 +++++++++---------- .../chatEditingEditorOverlay.ts} | 52 +++++++------- .../browser/inlineChatController2.ts | 4 +- .../chatEdit/notebookChatActionsOverlay.ts | 2 +- 8 files changed, 87 insertions(+), 87 deletions(-) rename src/vs/workbench/contrib/chat/browser/{chatEditorActions.ts => chatEditing/chatEditingEditorActions.ts} (86%) rename src/vs/workbench/contrib/chat/browser/{chatEditorController.ts => chatEditing/chatEditingEditorController.ts} (91%) rename src/vs/workbench/contrib/chat/browser/{chatEditorOverlay.ts => chatEditing/chatEditingEditorOverlay.ts} (86%) diff --git a/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts b/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts index 9a0043120880..b4c25bbba1e0 100644 --- a/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts @@ -19,7 +19,7 @@ import { CommentContextKeys } from '../../comments/common/commentContextKeys.js' import { NEW_UNTITLED_FILE_COMMAND_ID } from '../../files/browser/fileConstants.js'; import { IAccessibleViewService, IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType } from '../../../../platform/accessibility/browser/accessibleView.js'; import { AccessibilityVerbositySettingId } from './accessibilityConfiguration.js'; -import { ctxHasEditorModification, ctxHasRequestInProgress } from '../../chat/browser/chatEditorController.js'; +import { ctxHasEditorModification, ctxHasRequestInProgress } from '../../chat/browser/chatEditing/chatEditingEditorController.js'; export class EditorAccessibilityHelpContribution extends Disposable { static ID: 'editorAccessibilityHelpContribution'; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index 04ed62ec3f73..57a57061aed5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -19,7 +19,7 @@ import { ChatAgentLocation } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { ChatViewId, EditsViewId, IChatWidgetService } from '../chat.js'; -import { ctxIsGlobalEditingSession } from '../chatEditorController.js'; +import { ctxIsGlobalEditingSession } from '../chatEditing/chatEditingEditorController.js'; import { ChatEditorInput } from '../chatEditorInput.js'; import { ChatViewPane } from '../chatViewPane.js'; import { CHAT_CATEGORY } from './chatActions.js'; diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index f570cd3641de..e09865516b6a 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -56,8 +56,8 @@ import './chatAttachmentModel.js'; import { ChatMarkdownAnchorService, IChatMarkdownAnchorService } from './chatContentParts/chatMarkdownAnchorService.js'; import { ChatEditingService } from './chatEditing/chatEditingServiceImpl.js'; import { ChatEditor, IChatEditorOptions } from './chatEditor.js'; -import { registerChatEditorActions } from './chatEditorActions.js'; -import { ChatEditorController } from './chatEditorController.js'; +import { registerChatEditorActions } from './chatEditing/chatEditingEditorActions.js'; +import { ChatEditorController } from './chatEditing/chatEditingEditorController.js'; import { ChatEditorInput, ChatEditorInputSerializer } from './chatEditorInput.js'; import { ChatInputBoxContentProvider } from './chatEdinputInputContentProvider.js'; import { agentSlashCommandToMarkdown, agentToMarkdown } from './chatMarkdownDecorationsRenderer.js'; @@ -80,7 +80,7 @@ import { Extensions, IConfigurationMigrationRegistry } from '../../../common/con import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesContrib.js'; import { ChatQuotasService, ChatQuotasStatusBarEntry, IChatQuotasService } from './chatQuotasService.js'; import { ChatSetupContribution } from './chatSetup.js'; -import { ChatEditorOverlayController } from './chatEditorOverlay.js'; +import { ChatEditorOverlayController } from './chatEditing/chatEditingEditorOverlay.js'; import '../common/promptSyntax/languageFeatures/promptLinkProvider.js'; import { PromptFilesConfig } from '../common/promptSyntax/config.js'; import { BuiltinToolsContribution } from '../common/tools/tools.js'; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts similarity index 86% rename from src/vs/workbench/contrib/chat/browser/chatEditorActions.ts rename to src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts index 681e61fb730c..072855972ec1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts @@ -2,24 +2,24 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; -import { localize, localize2 } from '../../../../nls.js'; -import { EditorAction2, ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { CHAT_CATEGORY } from './actions/chatActions.js'; -import { ChatEditorController, ctxHasEditorModification, ctxReviewModeEnabled } from './chatEditorController.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; -import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js'; -import { IChatEditingService } from '../common/chatEditingService.js'; -import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { isEqual } from '../../../../base/common/resources.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { getNotebookEditorFromEditorPane } from '../../notebook/browser/notebookBrowser.js'; -import { ctxNotebookHasEditorModification } from '../../notebook/browser/contrib/chatEdit/notebookChatEditContext.js'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; +import { localize, localize2 } from '../../../../../nls.js'; +import { EditorAction2, ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { CHAT_CATEGORY } from '../actions/chatActions.js'; +import { ChatEditorController, ctxHasEditorModification, ctxReviewModeEnabled } from './chatEditingEditorController.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; +import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; +import { ACTIVE_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js'; +import { IChatEditingService } from '../../common/chatEditingService.js'; +import { ChatContextKeys } from '../../common/chatContextKeys.js'; +import { isEqual } from '../../../../../base/common/resources.js'; +import { Range } from '../../../../../editor/common/core/range.js'; +import { getNotebookEditorFromEditorPane } from '../../../notebook/browser/notebookBrowser.js'; +import { ctxNotebookHasEditorModification } from '../../../notebook/browser/contrib/chatEdit/notebookChatEditContext.js'; abstract class NavigateAction extends Action2 { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorController.ts similarity index 91% rename from src/vs/workbench/contrib/chat/browser/chatEditorController.ts rename to src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorController.ts index 13e98f95ec93..3f36f502b92a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorController.ts @@ -3,41 +3,41 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/chatEditorController.css'; -import { addStandardDisposableListener, getTotalWidth } from '../../../../base/browser/dom.js'; -import { Disposable, DisposableStore, dispose, toDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, autorunWithStore, derived, IObservable, observableFromEvent, observableFromEventOpts, observableValue } from '../../../../base/common/observable.js'; -import { themeColorFromId } from '../../../../base/common/themables.js'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, IViewZone, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; -import { LineSource, renderLines, RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; -import { diffAddDecoration, diffDeleteDecoration, diffWholeLineAddDecoration } from '../../../../editor/browser/widget/diffEditor/registrations.contribution.js'; -import { EditorOption, IEditorOptions } from '../../../../editor/common/config/editorOptions.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { IDocumentDiff } from '../../../../editor/common/diff/documentDiffProvider.js'; -import { IEditorContribution, ScrollType } from '../../../../editor/common/editorCommon.js'; -import { IModelDeltaDecoration, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from '../../../../editor/common/model.js'; -import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; -import { InlineDecoration, InlineDecorationType } from '../../../../editor/common/viewModel.js'; -import { localize } from '../../../../nls.js'; -import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { ChatEditingSessionState, IChatEditingService, IModifiedFileEntry, WorkingSetEntryState } from '../common/chatEditingService.js'; -import { Event } from '../../../../base/common/event.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { Position } from '../../../../editor/common/core/position.js'; -import { Selection } from '../../../../editor/common/core/selection.js'; -import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; -import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; -import { minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../../scm/common/quickDiff.js'; -import { DetailedLineRangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; -import { isDiffEditorForEntry } from './chatEditing/chatEditing.js'; -import { basename, isEqual } from '../../../../base/common/resources.js'; -import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; -import { EditorsOrder, IEditorIdentifier, isDiffEditorInput } from '../../../common/editor.js'; -import { ChatEditorOverlayController } from './chatEditorOverlay.js'; -import { IChatService } from '../common/chatService.js'; -import { StableEditorScrollState } from '../../../../editor/browser/stableEditorScroll.js'; +import '../media/chatEditorController.css'; +import { addStandardDisposableListener, getTotalWidth } from '../../../../../base/browser/dom.js'; +import { Disposable, DisposableStore, dispose, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { autorun, autorunWithStore, derived, IObservable, observableFromEvent, observableFromEventOpts, observableValue } from '../../../../../base/common/observable.js'; +import { themeColorFromId } from '../../../../../base/common/themables.js'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, IViewZone, MouseTargetType } from '../../../../../editor/browser/editorBrowser.js'; +import { LineSource, renderLines, RenderOptions } from '../../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; +import { diffAddDecoration, diffDeleteDecoration, diffWholeLineAddDecoration } from '../../../../../editor/browser/widget/diffEditor/registrations.contribution.js'; +import { EditorOption, IEditorOptions } from '../../../../../editor/common/config/editorOptions.js'; +import { Range } from '../../../../../editor/common/core/range.js'; +import { IDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js'; +import { IEditorContribution, ScrollType } from '../../../../../editor/common/editorCommon.js'; +import { IModelDeltaDecoration, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from '../../../../../editor/common/model.js'; +import { ModelDecorationOptions } from '../../../../../editor/common/model/textModel.js'; +import { InlineDecoration, InlineDecorationType } from '../../../../../editor/common/viewModel.js'; +import { localize } from '../../../../../nls.js'; +import { IContextKey, IContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ChatEditingSessionState, IChatEditingService, IModifiedFileEntry, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { Event } from '../../../../../base/common/event.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { MenuId } from '../../../../../platform/actions/common/actions.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { Position } from '../../../../../editor/common/core/position.js'; +import { Selection } from '../../../../../editor/common/core/selection.js'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; +import { observableCodeEditor } from '../../../../../editor/browser/observableCodeEditor.js'; +import { minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../../../scm/common/quickDiff.js'; +import { DetailedLineRangeMapping } from '../../../../../editor/common/diff/rangeMapping.js'; +import { isDiffEditorForEntry } from './chatEditing.js'; +import { basename, isEqual } from '../../../../../base/common/resources.js'; +import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; +import { EditorsOrder, IEditorIdentifier, isDiffEditorInput } from '../../../../common/editor.js'; +import { ChatEditorOverlayController } from './chatEditingEditorOverlay.js'; +import { IChatService } from '../../common/chatService.js'; +import { StableEditorScrollState } from '../../../../../editor/browser/stableEditorScroll.js'; export const ctxIsGlobalEditingSession = new RawContextKey('chat.isGlobalEditingSession', undefined, localize('chat.ctxEditSessionIsGlobal', "The current editor is part of the global edit session")); export const ctxHasEditorModification = new RawContextKey('chat.hasEditorModifications', undefined, localize('chat.hasEditorModifications', "The current editor contains chat modifications")); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts similarity index 86% rename from src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts rename to src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts index acf876c17c4c..0db38aaf8bb9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts @@ -3,32 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, IObservable, observableFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js'; -import { HiddenItemStrategy, MenuWorkbenchToolBar, WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IChatEditingSession, IModifiedFileEntry } from '../common/chatEditingService.js'; -import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { IActionRunner } from '../../../../base/common/actions.js'; -import { $, addDisposableGenericMouseMoveListener, append, EventLike, reset } from '../../../../base/browser/dom.js'; -import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { assertType } from '../../../../base/common/types.js'; -import { localize } from '../../../../nls.js'; -import { AcceptAction, navigationBearingFakeActionId, RejectAction } from './chatEditorActions.js'; -import { ChatEditorController } from './chatEditorController.js'; -import './media/chatEditorOverlay.css'; -import { findDiffEditorContainingCodeEditor } from '../../../../editor/browser/widget/diffEditor/commands.js'; -import { IChatService } from '../common/chatService.js'; -import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; -import { rcut } from '../../../../base/common/strings.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { Lazy } from '../../../../base/common/lazy.js'; +import { DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { autorun, IObservable, observableFromEvent, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from '../../../../../editor/browser/editorBrowser.js'; +import { HiddenItemStrategy, MenuWorkbenchToolBar, WorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IChatEditingSession, IModifiedFileEntry } from '../../common/chatEditingService.js'; +import { MenuId } from '../../../../../platform/actions/common/actions.js'; +import { ActionViewItem } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { ACTIVE_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js'; +import { Range } from '../../../../../editor/common/core/range.js'; +import { IActionRunner } from '../../../../../base/common/actions.js'; +import { $, addDisposableGenericMouseMoveListener, append, EventLike, reset } from '../../../../../base/browser/dom.js'; +import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { assertType } from '../../../../../base/common/types.js'; +import { localize } from '../../../../../nls.js'; +import { AcceptAction, navigationBearingFakeActionId, RejectAction } from './chatEditingEditorActions.js'; +import { ChatEditorController } from './chatEditingEditorController.js'; +import '../media/chatEditorOverlay.css'; +import { findDiffEditorContainingCodeEditor } from '../../../../../editor/browser/widget/diffEditor/commands.js'; +import { IChatService } from '../../common/chatService.js'; +import { IEditorContribution } from '../../../../../editor/common/editorCommon.js'; +import { rcut } from '../../../../../base/common/strings.js'; +import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; +import { Lazy } from '../../../../../base/common/lazy.js'; class ChatEditorOverlayWidget implements IOverlayWidget { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts index 78b47cf777de..23bb36062f48 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts @@ -26,8 +26,8 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { ctxIsGlobalEditingSession } from '../../chat/browser/chatEditorController.js'; -import { ChatEditorOverlayController } from '../../chat/browser/chatEditorOverlay.js'; +import { ctxIsGlobalEditingSession } from '../../chat/browser/chatEditing/chatEditingEditorController.js'; +import { ChatEditorOverlayController } from '../../chat/browser/chatEditing/chatEditingEditorOverlay.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; import { ChatAgentLocation } from '../../chat/common/chatAgents.js'; import { WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts index 8aa7a2c4c9d7..f9c9ed3a545f 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts @@ -18,7 +18,7 @@ import { autorun, autorunWithStore, IObservable, ISettableObservable, observable import { isEqual } from '../../../../../../base/common/resources.js'; import { CellDiffInfo } from '../../diff/notebookDiffViewModel.js'; import { INotebookDeletedCellDecorator } from './notebookCellDecorators.js'; -import { AcceptAction, navigationBearingFakeActionId, RejectAction } from '../../../../chat/browser/chatEditorActions.js'; +import { AcceptAction, navigationBearingFakeActionId, RejectAction } from '../../../../chat/browser/chatEditing/chatEditingEditorActions.js'; export class NotebookChatActionsOverlayController extends Disposable { constructor( From 2b0bc7595ba9190e3a9428b1d821f03f251f5b6e Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 4 Feb 2025 17:51:18 +0100 Subject: [PATCH 1200/3587] Side By Side NES does not update when model is set (#239621) fixes https://github.com/microsoft/vscode-copilot/issues/12749 --- .../inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index afd6599ebb9e..cd78afcce01b 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -314,6 +314,7 @@ export class InlineEditsSideBySideDiff extends Disposable implements IInlineEdit private _activeViewZones: string[] = []; private readonly _updatePreviewEditor = derived(reader => { this._editorContainer.readEffect(reader); + this._previewEditorObs.model.read(reader); // update when the model is set // Setting this here explicitly to make sure that the preview editor is // visible when needed, we're also checking that these fields are defined From b1abd2b2737d6084d8010d9e765a3895357324ee Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 4 Feb 2025 09:09:41 -0800 Subject: [PATCH 1201/3587] create prompt syntax service (#239554) * [service]: II of prompt syntax service * [service]: refactor * [service]: add unit tests * [service]: fix unit tests on Windows machines --- .../contrib/chat/browser/chat.contribution.ts | 4 + .../languageFeatures/promptLinkProvider.ts | 38 +- .../parsers/textModelPromptParser.ts | 4 +- .../service/promptSyntaxService.ts | 76 +++ .../chat/common/promptSyntax/service/types.ts | 31 ++ .../service/promptSyntaxService.test.ts | 527 ++++++++++++++++++ 6 files changed, 645 insertions(+), 35 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/service/promptSyntaxService.ts create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts create mode 100644 src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptSyntaxService.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index e09865516b6a..e6463f990d0f 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -88,6 +88,8 @@ import { IWorkbenchAssignmentService } from '../../../services/assignment/common import { IProductService } from '../../../../platform/product/common/productService.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; +import { IPromptSyntaxService } from '../common/promptSyntax/service/types.js'; +import { PromptSyntaxService } from '../common/promptSyntax/service/promptSyntaxService.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -428,3 +430,5 @@ registerSingleton(IChatEditingService, ChatEditingService, InstantiationType.Del registerSingleton(IChatMarkdownAnchorService, ChatMarkdownAnchorService, InstantiationType.Delayed); registerSingleton(ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService, InstantiationType.Delayed); registerSingleton(IChatQuotasService, ChatQuotasService, InstantiationType.Delayed); + +registerSingleton(IPromptSyntaxService, PromptSyntaxService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts index 169e320390b4..34bb7f530304 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts @@ -4,61 +4,31 @@ *--------------------------------------------------------------------------------------------*/ import { LANGUAGE_SELECTOR } from '../constants.js'; +import { IPromptSyntaxService } from '../service/types.js'; import { assert } from '../../../../../../base/common/assert.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; import { assertDefined } from '../../../../../../base/common/types.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { NonPromptSnippetFile } from '../../promptFileReferenceErrors.js'; -import { ObjectCache } from '../../../../../../base/common/objectCache.js'; import { CancellationError } from '../../../../../../base/common/errors.js'; -import { TextModelPromptParser } from '../parsers/textModelPromptParser.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 { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; -import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../../common/contributions.js'; /** * Provides link references for prompt files. */ export class PromptLinkProvider extends Disposable implements LinkProvider { - /** - * Cache of text model content prompt parsers. - */ - private readonly parserProvider: ObjectCache; - constructor( - @IInstantiationService private readonly initService: IInstantiationService, + @IPromptSyntaxService private readonly promptSyntaxService: IPromptSyntaxService, @ILanguageFeaturesService private readonly languageService: ILanguageFeaturesService, ) { super(); this._register(this.languageService.linkProvider.register(LANGUAGE_SELECTOR, this)); - this.parserProvider = this._register(new ObjectCache(this.createParser.bind(this))); - } - - /** - * Create new prompt parser instance for the provided text model. - * - * @param model - text model to create the parser for - * @param initService - the instantiation service - */ - private createParser( - model: ITextModel, - ): TextModelPromptParser & { disposed: false } { - const parser: TextModelPromptParser = this.initService.createInstance( - TextModelPromptParser, - model, - [], - ); - - parser.assertNotDisposed( - 'Created prompt parser must not be disposed.', - ); - - return parser; } /** @@ -73,7 +43,7 @@ export class PromptLinkProvider extends Disposable implements LinkProvider { new CancellationError(), ); - const parser = this.parserProvider.get(model); + const parser = this.promptSyntaxService.getParserFor(model); assert( !parser.disposed, 'Prompt parser must not be disposed.', @@ -123,6 +93,6 @@ export class PromptLinkProvider extends Disposable implements LinkProvider { } } -// register the text model prompt decorators provider as a workbench contribution +// register this provider as a workbench contribution Registry.as(WorkbenchExtensions.Workbench) .registerWorkbenchContribution(PromptLinkProvider, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts index 08c3ceeb9bf9..6a0e32b50804 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts @@ -20,7 +20,9 @@ export class TextModelPromptParser extends BasePromptParser this.dispose()); + super(contentsProvider, seenReferences, initService, logService); } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptSyntaxService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptSyntaxService.ts new file mode 100644 index 000000000000..b0bc31a93d2f --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptSyntaxService.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPromptSyntaxService } from './types.js'; +import { assert } from '../../../../../../base/common/assert.js'; +import { ITextModel } from '../../../../../../editor/common/model.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { ObjectCache } from '../../../../../../base/common/objectCache.js'; +import { TextModelPromptParser } from '../parsers/textModelPromptParser.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; + +/** + * Provides prompt syntax services. + */ +export class PromptSyntaxService extends Disposable implements IPromptSyntaxService { + declare readonly _serviceBrand: undefined; + + /** + * Cache of text model content prompt parsers. + */ + private readonly cache: ObjectCache; + + constructor( + @IInstantiationService initService: IInstantiationService, + ) { + super(); + + // the factory function below creates a new prompt parser object + // for the provided model, if no active non-disposed parser exists + this.cache = this._register( + new ObjectCache((model) => { + /** + * Note! When/if shared with "file" prompts, the `seenReferences` array below must be taken into account. + * Otherwise consumers will either see incorrect failing or incorrect successful results, based on their + * use case, timing of their calls to the {@link getParserFor} function, and state of this service. + */ + const parser: TextModelPromptParser = initService.createInstance( + TextModelPromptParser, + model, + [], + ); + + parser.start(); + + // this is a sanity check and the contract of the object cache, + // we must return a non-disposed object from this factory function + parser.assertNotDisposed( + 'Created prompt parser must not be disposed.', + ); + + return parser; + }) + ); + } + + /** + * Gets a prompt syntax parser for the provided text model. + * + * @throws {Error} if: + * - the provided model is disposed + * - newly created parser is disposed immediately on initialization. + * See factory function in the {@link constructor} for more info. + */ + public getParserFor( + model: ITextModel, + ): TextModelPromptParser & { disposed: false } { + assert( + !model.isDisposed(), + 'Cannot create a prompt parser for a disposed model.', + ); + + return this.cache.get(model); + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts new file mode 100644 index 000000000000..211f909e5f97 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITextModel } from '../../../../../../editor/common/model.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { TextModelPromptParser } from '../parsers/textModelPromptParser.js'; +import { createDecorator } from '../../../../../../platform/instantiation/common/instantiation.js'; + +/** + * Provides prompt syntax services. + */ +export const IPromptSyntaxService = createDecorator('IPromptSyntaxService'); + +/** + * Provides prompt syntax services. + */ +export interface IPromptSyntaxService extends IDisposable { + readonly _serviceBrand: undefined; + + /** + * Get a prompt syntax parser for the provided text model. + * See {@link TextModelPromptParser} for more info on the parse API. + * + * @throws {Error} If a newly created parser gets immediately disposed. + */ + getParserFor( + model: ITextModel, + ): TextModelPromptParser & { disposed: false }; +} diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptSyntaxService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptSyntaxService.test.ts new file mode 100644 index 000000000000..65c876889897 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptSyntaxService.test.ts @@ -0,0 +1,527 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { isWindows } from '../../../../../../../base/common/platform.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; +import { assertDefined } from '../../../../../../../base/common/types.js'; +import { waitRandom } from '../../../../../../../base/test/common/testUtils.js'; +import { IFileService } from '../../../../../../../platform/files/common/files.js'; +import { IPromptFileReference } from '../../../../common/promptSyntax/parsers/types.js'; +import { IPromptSyntaxService } from '../../../../common/promptSyntax/service/types.js'; +import { FileService } from '../../../../../../../platform/files/common/fileService.js'; +import { createTextModel } from '../../../../../../../editor/test/common/testTextModel.js'; +import { ILogService, NullLogService } from '../../../../../../../platform/log/common/log.js'; +import { PromptSyntaxService } from '../../../../common/promptSyntax/service/promptSyntaxService.js'; +import { TextModelPromptParser } from '../../../../common/promptSyntax/parsers/textModelPromptParser.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; +import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; + +/** + * Helper class to assert the properties of a link. + */ +class ExpectedLink { + constructor( + public readonly uri: URI, + public readonly fullRange: Range, + public readonly linkRange: Range, + ) { } + + /** + * Assert a provided link has the same properties as this object. + */ + public assertEqual(link: IPromptFileReference) { + assert.strictEqual( + link.type, + 'file', + 'Link must have correct type.', + ); + + assert.strictEqual( + link.uri.toString(), + this.uri.toString(), + 'Link must have correct URI.', + ); + + assert( + this.fullRange.equalsRange(link.range), + `Full range must be '${this.fullRange}', got '${link.range}'.`, + ); + + assertDefined( + link.linkRange, + 'Link must have a link range.', + ); + + assert( + this.linkRange.equalsRange(link.linkRange), + `Link range must be '${this.linkRange}', got '${link.linkRange}'.`, + ); + } +} + +/** + * Asserts that provided links are equal to the expected links. + * @param links Links to assert. + * @param expectedLinks Expected links to compare against. + */ +const assertLinks = ( + links: readonly IPromptFileReference[], + expectedLinks: readonly ExpectedLink[], +) => { + for (let i = 0; i < links.length; i++) { + try { + expectedLinks[i].assertEqual(links[i]); + } catch (error) { + throw new Error(`link#${i}: ${error}`); + } + } + + assert.strictEqual( + links.length, + expectedLinks.length, + `Links count must be correct.`, + ); +}; + +/** + * Creates cross-platform URI. On Windows, absolute paths + * are prefixed with the disk name. + */ +const createURI = (linkPath: string): URI => { + if (isWindows && linkPath.startsWith('/')) { + return URI.file('/d:' + linkPath); + } + + return URI.file(linkPath); +}; + +suite('PromptSyntaxService', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let service: IPromptSyntaxService; + let instantiationService: TestInstantiationService; + + setup(async () => { + instantiationService = disposables.add(new TestInstantiationService()); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IFileService, new TestConfigurationService()); + instantiationService.stub(IFileService, disposables.add(instantiationService.createInstance(FileService))); + + service = disposables.add(instantiationService.createInstance(PromptSyntaxService)); + }); + + suite('getParserFor', () => { + test('provides cached parser instance', async () => { + const langId = 'fooLang'; + + /** + * Create a text model, get a parser for it, and perform basic assertions. + */ + + const model1 = disposables.add(createTextModel( + 'test1\n\t#file:./file.md\n\n\n [bin file](/root/tmp.bin)\t\n', + langId, + undefined, + createURI('/Users/vscode/repos/test/file1.txt'), + )); + + const parser1 = service.getParserFor(model1); + assert.strictEqual( + parser1.uri.toString(), + model1.uri.toString(), + 'Must create parser1 with the correct URI.', + ); + + assert( + !parser1.disposed, + 'Parser1 must not be disposed.', + ); + + assert( + parser1 instanceof TextModelPromptParser, + 'Parser1 must be an instance of TextModelPromptParser.', + ); + + /** + * Validate that all links of the model are correctly parsed. + */ + + await parser1.settled(); + assertLinks( + parser1.allReferences, + [ + new ExpectedLink( + createURI('/Users/vscode/repos/test/file.md'), + new Range(2, 2, 2, 2 + 15), + new Range(2, 8, 2, 8 + 9), + ), + new ExpectedLink( + createURI('/root/tmp.bin'), + new Range(5, 4, 5, 4 + 25), + new Range(5, 15, 5, 15 + 13), + ), + ], + ); + + // wait for some random amount of time + await waitRandom(5); + + /** + * Next, get parser for the same exact model and + * validate that the same cached object is returned. + */ + + // get the same parser again, the call must return the same object + const parser1_1 = service.getParserFor(model1); + assert.strictEqual( + parser1, + parser1_1, + 'Must return the same parser object.', + ); + + assert.strictEqual( + parser1_1.uri.toString(), + model1.uri.toString(), + 'Must create parser1_1 with the correct URI.', + ); + + /** + * Get parser for a different model and perform basic assertions. + */ + + const model2 = disposables.add(createTextModel( + 'some text #file:/absolute/path.txt \t\ntest-text2', + langId, + undefined, + createURI('/Users/vscode/repos/test/some-folder/file.md'), + )); + + // wait for some random amount of time + await waitRandom(5); + + const parser2 = service.getParserFor(model2); + + assert.strictEqual( + parser2.uri.toString(), + model2.uri.toString(), + 'Must create parser2 with the correct URI.', + ); + + assert( + !parser2.disposed, + 'Parser2 must not be disposed.', + ); + + assert( + parser2 instanceof TextModelPromptParser, + 'Parser2 must be an instance of TextModelPromptParser.', + ); + + assert( + !parser2.disposed, + 'Parser2 must not be disposed.', + ); + + assert( + !parser1.disposed, + 'Parser1 must not be disposed.', + ); + + assert( + !parser1_1.disposed, + 'Parser1_1 must not be disposed.', + ); + + /** + * Validate that all links of the model 2 are correctly parsed. + */ + + await parser2.settled(); + + assert.notStrictEqual( + parser1.uri.toString(), + parser2.uri.toString(), + 'Parser2 must have its own URI.', + ); + + assertLinks( + parser2.allReferences, + [ + new ExpectedLink( + createURI('/absolute/path.txt'), + new Range(1, 11, 1, 11 + 24), + new Range(1, 17, 1, 17 + 18), + ), + ], + ); + + /** + * Validate the first parser was not affected by the presence + * of the second parser. + */ + + await parser1_1.settled(); + + // parser1_1 has the same exact links as before + assertLinks( + parser1_1.allReferences, + [ + new ExpectedLink( + createURI('/Users/vscode/repos/test/file.md'), + new Range(2, 2, 2, 2 + 15), + new Range(2, 8, 2, 8 + 9), + ), + new ExpectedLink( + createURI('/root/tmp.bin'), + new Range(5, 4, 5, 4 + 25), + new Range(5, 15, 5, 15 + 13), + ), + ], + ); + + // wait for some random amount of time + await waitRandom(5); + + /** + * Dispose the first parser, perform basic validations, and confirm + * that the second parser is not affected by the disposal of the first one. + */ + parser1.dispose(); + + assert( + parser1.disposed, + 'Parser1 must be disposed.', + ); + + assert( + parser1_1.disposed, + 'Parser1_1 must be disposed.', + ); + + assert( + !parser2.disposed, + 'Parser2 must not be disposed.', + ); + + + /** + * Get parser for the first model again. Confirm that we get + * a new non-disposed parser object back with correct properties. + */ + + const parser1_2 = service.getParserFor(model1); + + assert( + !parser1_2.disposed, + 'Parser1_2 must not be disposed.', + ); + + assert.notStrictEqual( + parser1_2, + parser1, + 'Must create a new parser object for the model1.', + ); + + assert.strictEqual( + parser1_2.uri.toString(), + model1.uri.toString(), + 'Must create parser1_2 with the correct URI.', + ); + + /** + * Validate that the contents of the second parser did not change. + */ + + await parser1_2.settled(); + + // parser1_2 must have the same exact links as before + assertLinks( + parser1_2.allReferences, + [ + new ExpectedLink( + createURI('/Users/vscode/repos/test/file.md'), + new Range(2, 2, 2, 2 + 15), + new Range(2, 8, 2, 8 + 9), + ), + new ExpectedLink( + createURI('/root/tmp.bin'), + new Range(5, 4, 5, 4 + 25), + new Range(5, 15, 5, 15 + 13), + ), + ], + ); + + // wait for some random amount of time + await waitRandom(5); + + /** + * This time dispose model of the second parser instead of + * the parser itself. Validate that the parser is disposed too, but + * the newly created first parser is not affected. + */ + + // dispose the `model` of the second parser now + model2.dispose(); + + // assert that the parser is also disposed + assert( + parser2.disposed, + 'Parser2 must be disposed.', + ); + + // sanity check that the other parser is not affected + assert( + !parser1_2.disposed, + 'Parser1_2 must not be disposed.', + ); + + /** + * Create a new second parser with new model - we cannot use + * the old one because it was disposed. This new model also has + * a different second link. + */ + + // we cannot use the same model since it was already disposed + const model2_1 = disposables.add(createTextModel( + 'some text #file:/absolute/path.txt \n [caption](.copilot/prompts/test.prompt.md)\t\n\t\n more text', + langId, + undefined, + createURI('/Users/vscode/repos/test/some-folder/file.md'), + )); + const parser2_1 = service.getParserFor(model2_1); + + assert( + !parser2_1.disposed, + 'Parser2_1 must not be disposed.', + ); + + assert.notStrictEqual( + parser2_1, + parser2, + 'Parser2_1 must be a new object.', + ); + + assert.strictEqual( + parser2_1.uri.toString(), + model2.uri.toString(), + 'Must create parser2_1 with the correct URI.', + ); + + /** + * Validate that new model2 contents are parsed correctly. + */ + + await parser2_1.settled(); + + // parser2_1 must have 2 links now + assertLinks( + parser2_1.allReferences, + [ + // the first link didn't change + new ExpectedLink( + createURI('/absolute/path.txt'), + new Range(1, 11, 1, 11 + 24), + new Range(1, 17, 1, 17 + 18), + ), + // the second link is new + new ExpectedLink( + createURI('/Users/vscode/repos/test/some-folder/.copilot/prompts/test.prompt.md'), + new Range(2, 2, 2, 2 + 42), + new Range(2, 12, 2, 12 + 31), + ), + ], + ); + }); + + test('auto-updated on model changes', async () => { + const langId = 'bazLang'; + + const model = disposables.add(createTextModel( + ' \t #file:../file.md\ntest1\n\t\n [another file](/Users/root/tmp/file2.txt)\t\n', + langId, + undefined, + createURI('/repos/test/file1.txt'), + )); + + const parser = service.getParserFor(model); + + // sanity checks + assert( + !parser.disposed, + 'Parser must not be disposed.', + ); + assert( + parser instanceof TextModelPromptParser, + 'Parser must be an instance of TextModelPromptParser.', + ); + + await parser.settled(); + + assertLinks( + parser.allReferences, + [ + new ExpectedLink( + createURI('/repos/file.md'), + new Range(1, 4, 1, 4 + 16), + new Range(1, 10, 1, 10 + 10), + ), + new ExpectedLink( + createURI('/Users/root/tmp/file2.txt'), + new Range(4, 3, 4, 3 + 41), + new Range(4, 18, 4, 18 + 25), + ), + ], + ); + + model.applyEdits([ + { + range: new Range(4, 18, 4, 18 + 25), + text: '/Users/root/tmp/file3.txt', + }, + ]); + + await parser.settled(); + + assertLinks( + parser.allReferences, + [ + // link1 didn't change + new ExpectedLink( + createURI('/repos/file.md'), + new Range(1, 4, 1, 4 + 16), + new Range(1, 10, 1, 10 + 10), + ), + // link2 changed in the file name only + new ExpectedLink( + createURI('/Users/root/tmp/file3.txt'), + new Range(4, 3, 4, 3 + 41), + new Range(4, 18, 4, 18 + 25), + ), + ], + ); + }); + + test('throws if disposed model provided', async function () { + const model = disposables.add(createTextModel( + 'test1\ntest2\n\ntest3\t\n', + 'barLang', + undefined, + URI.parse('./github/prompts/file.prompt.md'), + )); + + // dispose the model before using it + model.dispose(); + + assert.throws(() => { + service.getParserFor(model); + }, 'Cannot create a prompt parser for a disposed model.'); + }); + }); +}); From a58d48f1812d61033bc7fe8a0e5d4eb79705405d Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 4 Feb 2025 09:16:08 -0800 Subject: [PATCH 1202/3587] Ensure agent experiment defaults to not disabled (#239625) * Safer fix for 239274 (#239511) * Revert "fix config-context-key handling when config isn't known yet (#239294)" This reverts commit d231e6ca13225ff4cbd715566263dc5f2e5792d6. * Ensure agentModeDisallowed key always changes when setting is registered * Ensure defaults to not disabled --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 7 ++++--- src/vs/workbench/contrib/chat/common/chatContextKeys.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index e6463f990d0f..878728cea293 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -234,10 +234,11 @@ class ChatAgentSettingContribution implements IWorkbenchContribution { } const expDisabledKey = ChatContextKeys.Editing.agentModeDisallowed.bindTo(contextKeyService); - experimentService.getTreatment('chatAgentEnabled').then(value => { - if (value) { + experimentService.getTreatment('chatAgentEnabled').then(enabled => { + if (enabled) { this.registerSetting(); - } else if (value === false) { + expDisabledKey.set(false); + } else if (enabled === false) { this.deregisterSetting(); expDisabledKey.set(true); } diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 4a2798b9f0eb..591bbdcad92c 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -83,6 +83,6 @@ export namespace ChatContextKeys { export const Editing = { hasToolsAgent: new RawContextKey('chatHasToolsAgent', false, { type: 'boolean', description: localize('chatEditingHasToolsAgent', "True when a tools agent is registered.") }), agentMode: new RawContextKey('chatAgentMode', false, { type: 'boolean', description: localize('chatEditingAgentMode', "True when edits is in agent mode.") }), - agentModeDisallowed: new RawContextKey('chatAgentModeDisallowed', false, { type: 'boolean', description: localize('chatAgentModeDisallowed', "True when agent mode is not allowed.") }), // experiment-driven disablement + agentModeDisallowed: new RawContextKey('chatAgentModeDisallowed', undefined, { type: 'boolean', description: localize('chatAgentModeDisallowed', "True when agent mode is not allowed.") }), // experiment-driven disablement }; } From 30dbfbb6838d4460fb43ffbc4abaaa57647cd77c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 4 Feb 2025 19:31:22 +0100 Subject: [PATCH 1203/3587] Provide a way to reveal the entire URI in an 'Allow extension to open URI" dialog (fix #239272) (#239527) --- .../extensions/browser/extensionUrlHandler.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index d1a25424a4e0..ee1e81a8f15c 100644 --- a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -26,6 +26,7 @@ import { mainWindow } from '../../../../base/browser/window.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; const FIVE_MINUTES = 5 * 60 * 1000; const THIRTY_SECONDS = 30 * 1000; @@ -197,10 +198,11 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { || this.didUserTrustExtension(ExtensionIdentifier.toKey(extensionId)); if (!trusted) { - let uriString = uri.toString(false); + const uriString = uri.toString(false); + let uriLabel = uriString; - if (uriString.length > 40) { - uriString = `${uriString.substring(0, 30)}...${uriString.substring(uriString.length - 5)}`; + if (uriLabel.length > 40) { + uriLabel = `${uriLabel.substring(0, 30)}...${uriLabel.substring(uriLabel.length - 5)}`; } const result = await this.dialogService.confirm({ @@ -208,8 +210,12 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { checkbox: { label: localize('rememberConfirmUrl', "Do not ask me again for this extension"), }, - detail: uriString, - primaryButton: localize({ key: 'open', comment: ['&& denotes a mnemonic'] }, "&&Open") + primaryButton: localize({ key: 'open', comment: ['&& denotes a mnemonic'] }, "&&Open"), + custom: { + markdownDetails: [{ + markdown: new MarkdownString(`

`, { supportHtml: true }), + }] + } }); if (!result.confirmed) { From 12acdeaa357f2b1d821d76673b44f897a2a3ef58 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 4 Feb 2025 20:19:29 +0100 Subject: [PATCH 1204/3587] dnd for references widget (#239637) fixes https://github.com/microsoft/vscode/issues/239636 --- .../browser/peek/referencesWidget.ts | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts index fce167fb08fa..9581b5cf6ff6 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts @@ -34,6 +34,10 @@ import { ILabelService } from '../../../../../platform/label/common/label.js'; import { IWorkbenchAsyncDataTreeOptions, WorkbenchAsyncDataTree } from '../../../../../platform/list/browser/listService.js'; import { IColorTheme, IThemeService } from '../../../../../platform/theme/common/themeService.js'; import { FileReferences, OneReference, ReferencesModel } from '../referencesModel.js'; +import { ITreeDragAndDrop, ITreeDragOverReaction } from '../../../../../base/browser/ui/tree/tree.js'; +import { DataTransfers, IDragAndDropData } from '../../../../../base/browser/dnd.js'; +import { ElementsDragAndDropData } from '../../../../../base/browser/ui/list/listView.js'; +import { withSelection } from '../../../../../platform/opener/common/opener.js'; class DecorationsManager implements IDisposable { @@ -188,6 +192,51 @@ export interface SelectionEvent { class ReferencesTree extends WorkbenchAsyncDataTree { } +class ReferencesDragAndDrop implements ITreeDragAndDrop { + + private readonly disposables = new DisposableStore(); + + constructor(@ILabelService private readonly labelService: ILabelService) { } + + getDragURI(element: TreeElement): string | null { + if (element instanceof FileReferences) { + return element.uri.toString(); + } else if (element instanceof OneReference) { + return withSelection(element.uri, element.range).toString(); + } + return null; + } + + getDragLabel(elements: TreeElement[]): string | undefined { + if (elements.length === 0) { + return undefined; + } + const labels = elements.map(e => this.labelService.getUriBasenameLabel(e.uri)); + return labels.join(', '); + } + + onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void { + if (!originalEvent.dataTransfer) { + return; + } + + const elements = (data as ElementsDragAndDropData).elements; + const resources = elements.map(e => this.getDragURI(e)).filter(Boolean); + + if (resources.length) { + // Apply resources as resource-list + originalEvent.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify(resources)); + + // Also add as plain text for outside consumers + originalEvent.dataTransfer.setData(DataTransfers.TEXT, resources.join('\n')); + } + } + + onDragOver(): boolean | ITreeDragOverReaction { return false; } + drop(): void { } + dispose(): void { this.disposables.dispose(); } +} + /** * ZoneWidget that is shown inside the editor */ @@ -328,7 +377,8 @@ export class ReferenceWidget extends peekView.PeekViewWidget { selectionNavigation: true, overrideStyles: { listBackground: peekView.peekViewResultsBackground - } + }, + dnd: this._instantiationService.createInstance(ReferencesDragAndDrop) }; if (this._defaultTreeKeyboardSupport) { // the tree will consume `Escape` and prevent the widget from closing From fe6c1b1d12a414093b690e69e6c15d143e992f91 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 4 Feb 2025 11:54:41 -0800 Subject: [PATCH 1205/3587] Add builtin #selection variable (#239638) * Add builtin #selection variable Acts as a helper to add the current selection as a reference * Simplify variable completions registration --- .../chat/browser/actions/chatActions.ts | 2 +- .../browser/actions/chatContextActions.ts | 2 +- .../contrib/chat/browser/chat.contribution.ts | 2 +- .../contrib/chat/browser/chatVariables.ts | 7 +- .../browser/contrib/chatInputCompletions.ts | 174 ++++++++++++------ .../contrib/chat/common/chatVariables.ts | 2 +- 6 files changed, 124 insertions(+), 65 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index d7532a3514b8..b7177da3eb9b 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -139,7 +139,7 @@ class OpenChatGlobalAction extends Action2 { } } if (opts?.variableIds && opts.variableIds.length > 0) { - const actualVariables = chatVariablesService.getVariables(ChatAgentLocation.Panel); + const actualVariables = chatVariablesService.getVariables(); for (const actualVariable of actualVariables) { if (opts.variableIds.includes(actualVariable.id)) { chatWidget.attachmentModel.addContext({ diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index cdd4135f32b5..f091b1600a3c 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -677,7 +677,7 @@ export class AttachContextAction extends Action2 { const slowSupported = usedAgent ? usedAgent.agent.metadata.supportsSlowVariables : true; const quickPickItems: IAttachmentQuickPickItem[] = []; if (!context || !context.showFilesOnly) { - for (const variable of chatVariablesService.getVariables(widget.location)) { + for (const variable of chatVariablesService.getVariables()) { if (variable.fullName && (!variable.isSlow || slowSupported)) { quickPickItems.push({ kind: 'variable', diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 878728cea293..824ba88a2f45 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -349,7 +349,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { } const variables = [ - ...chatVariablesService.getVariables(ChatAgentLocation.Panel), + ...chatVariablesService.getVariables(), { name: 'file', description: nls.localize('file', "Choose a file in the workspace") } ]; const variableText = variables diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 4ab91fa76d76..7d92c1e31d99 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -123,12 +123,9 @@ export class ChatVariablesService implements IChatVariablesService { return this._resolver.get(name.toLowerCase())?.data; } - getVariables(location: ChatAgentLocation): Iterable> { + getVariables(): Iterable> { const all = Iterable.map(this._resolver.values(), data => data.data); - return Iterable.filter(all, data => { - // TODO@jrieken this is improper and should be know from the variable registeration data - return location !== ChatAgentLocation.Editor || !new Set(['selection', 'editor']).has(data.name); - }); + return all; } getDynamicVariables(sessionId: string): ReadonlyArray { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 717ed7e796a4..9ab466c60a44 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -11,10 +11,11 @@ import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; import { URI } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; +import { isCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; import { Position } from '../../../../../editor/common/core/position.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { IWordAtPosition, getWordAtText } from '../../../../../editor/common/core/wordHelper.js'; -import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, DocumentSymbol, Location, SymbolKind, SymbolKinds } from '../../../../../editor/common/languages.js'; +import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, DocumentSymbol, Location, ProviderResult, SymbolKind, SymbolKinds } from '../../../../../editor/common/languages.js'; import { ITextModel } from '../../../../../editor/common/model.js'; import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; import { IOutlineModelService } from '../../../../../editor/contrib/documentSymbols/browser/outlineModel.js'; @@ -27,6 +28,7 @@ import { ILabelService } from '../../../../../platform/label/common/label.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../common/contributions.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IHistoryService } from '../../../../services/history/common/history.js'; import { LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js'; import { QueryBuilder } from '../../../../services/search/common/queryBuilder.js'; @@ -436,6 +438,14 @@ class ReferenceArgument { ) { } } +interface IVariableCompletionsDetails { + model: ITextModel; + position: Position; + context: CompletionContext; + widget: IChatWidget; + range: IChatCompletionRangeResult; +} + class BuiltinDynamicCompletions extends Disposable { private static readonly addReferenceCommand = '_addReferenceCmd'; private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag @@ -452,82 +462,134 @@ class BuiltinDynamicCompletions extends Disposable { @IChatEditingService private readonly _chatEditingService: IChatEditingService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IOutlineModelService private readonly outlineService: IOutlineModelService, + @IEditorService private readonly editorService: IEditorService, ) { super(); // File completions - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatDynamicFileCompletions', - triggerCharacters: [chatVariableLeader], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.supportsFileReferences) { - return null; - } + this.registerVariableCompletions('file', async ({ widget, range, position, model }, token) => { + if (!widget.supportsFileReferences) { + return null; + } - const result: CompletionList = { suggestions: [] }; - const range = computeCompletionRanges(model, position, BuiltinDynamicCompletions.VariableNameDef, true); + const result: CompletionList = { suggestions: [] }; + + const afterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#file:'.length); + result.suggestions.push({ + label: `${chatVariableLeader}file`, + insertText: `${chatVariableLeader}file:`, + documentation: localize('pickFileLabel', "Pick a file"), + range, + kind: CompletionItemKind.Text, + command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: afterRange }] }, + sortText: 'z' + }); - if (range) { - const afterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#file:'.length); - result.suggestions.push({ - label: `${chatVariableLeader}file`, - insertText: `${chatVariableLeader}file:`, - documentation: localize('pickFileLabel', "Pick a file"), - range, - kind: CompletionItemKind.Text, - command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: afterRange }] }, - sortText: 'z' - }); - } + const range2 = computeCompletionRanges(model, position, new RegExp(`${chatVariableLeader}[^\\s]*`, 'g'), true); + if (range2) { + await this.addFileEntries(widget, result, range2, token); + } - const range2 = computeCompletionRanges(model, position, new RegExp(`${chatVariableLeader}[^\\s]*`, 'g'), true); - if (range2) { - await this.addFileEntries(widget, result, range2, token); - } + return result; + }); - return result; + // Selection completion + this.registerVariableCompletions('selection', ({ widget, range }, token) => { + if (!widget.supportsFileReferences) { + return; } - })); + + if (widget.location === ChatAgentLocation.Editor) { + return; + } + + const active = this.editorService.activeTextEditorControl; + if (!isCodeEditor(active)) { + return; + } + + const currentResource = active.getModel()?.uri; + const currentSelection = active.getSelection(); + if (!currentSelection || !currentResource || currentSelection.isEmpty()) { + return; + } + + const basename = this.labelService.getUriBasenameLabel(currentResource); + const text = `${chatVariableLeader}file:${basename}:${currentSelection.startLineNumber}-${currentSelection.endLineNumber}`; + const fullRangeText = `:${currentSelection.startLineNumber}:${currentSelection.startColumn}-${currentSelection.endLineNumber}:${currentSelection.endColumn}`; + const description = this.labelService.getUriLabel(currentResource, { relative: true }) + fullRangeText; + + const result: CompletionList = { suggestions: [] }; + result.suggestions.push({ + label: { label: `${chatVariableLeader}selection`, description }, + filterText: `${chatVariableLeader}selection`, + insertText: range.varWord?.endColumn === range.replace.endColumn ? `${text} ` : text, + range, + kind: CompletionItemKind.Text, + sortText: 'z', + command: { + id: BuiltinDynamicCompletions.addReferenceCommand, title: '', arguments: [new ReferenceArgument(widget, { + id: 'vscode.file', + prefix: 'file', + isFile: true, + range: { startLineNumber: range.replace.startLineNumber, startColumn: range.replace.startColumn, endLineNumber: range.replace.endLineNumber, endColumn: range.replace.startColumn + text.length }, + data: { range: currentSelection, uri: currentResource } satisfies Location + })] + } + }); + return result; + }); // Symbol completions + this.registerVariableCompletions('symbol', ({ widget, range, position, model }, token) => { + if (!widget.supportsFileReferences) { + return null; + } + + const result: CompletionList = { suggestions: [] }; + + const afterRangeSym = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#sym:'.length); + result.suggestions.push({ + label: `${chatVariableLeader}sym`, + insertText: `${chatVariableLeader}sym:`, + documentation: localize('pickSymbolLabel', "Pick a symbol"), + range, + kind: CompletionItemKind.Text, + command: { id: SelectAndInsertSymAction.ID, title: SelectAndInsertSymAction.ID, arguments: [{ widget, range: afterRangeSym }] }, + sortText: 'z' + }); + + const range2 = computeCompletionRanges(model, position, new RegExp(`${chatVariableLeader}[^\\s]*`, 'g'), true); + if (range2) { + this.addSymbolEntries(widget, result, range2, token); + } + + return result; + }); + + this._register(CommandsRegistry.registerCommand(BuiltinDynamicCompletions.addReferenceCommand, (_services, arg) => this.cmdAddReference(arg))); + + this.queryBuilder = this.instantiationService.createInstance(QueryBuilder); + } + + private registerVariableCompletions(debugName: string, provider: (details: IVariableCompletionsDetails, token: CancellationToken) => ProviderResult) { this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatDynamicSymbolCompletions', + _debugDisplayName: `chatVarCompletions-${debugName}`, triggerCharacters: [chatVariableLeader], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { + provideCompletionItems: async (model: ITextModel, position: Position, context: CompletionContext, token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.supportsFileReferences) { - return null; + if (!widget) { + return; } - const result: CompletionList = { suggestions: [] }; const range = computeCompletionRanges(model, position, BuiltinDynamicCompletions.VariableNameDef, true); - if (range) { - const afterRangeSym = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#sym:'.length); - result.suggestions.push({ - label: `${chatVariableLeader}sym`, - insertText: `${chatVariableLeader}sym:`, - documentation: localize('pickSymbolLabel', "Pick a symbol"), - range, - kind: CompletionItemKind.Text, - command: { id: SelectAndInsertSymAction.ID, title: SelectAndInsertSymAction.ID, arguments: [{ widget, range: afterRangeSym }] }, - sortText: 'z' - }); - } - - const range2 = computeCompletionRanges(model, position, new RegExp(`${chatVariableLeader}[^\\s]*`, 'g'), true); - if (range2) { - this.addSymbolEntries(widget, result, range2, token); + return provider({ model, position, widget, range, context }, token); } - return result; + return; } })); - - this._register(CommandsRegistry.registerCommand(BuiltinDynamicCompletions.addReferenceCommand, (_services, arg) => this.cmdAddReference(arg))); - - this.queryBuilder = this.instantiationService.createInstance(QueryBuilder); } private cacheKey?: { key: string; time: number }; @@ -797,7 +859,7 @@ class VariableCompletions extends Disposable { const usedVariables = widget.parsedInput.parts.filter((p): p is ChatRequestVariablePart => p instanceof ChatRequestVariablePart); const usedVariableNames = new Set(usedVariables.map(v => v.variableName)); - const variableItems = Array.from(this.chatVariablesService.getVariables(widget.location)) + const variableItems = Array.from(this.chatVariablesService.getVariables()) // This doesn't look at dynamic variables like `file`, where multiple makes sense. .filter(v => !usedVariableNames.has(v.name)) .filter(v => !v.isSlow || slowSupported) diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index 1bd1db9c3964..170a2e133671 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -43,7 +43,7 @@ export interface IChatVariablesService { registerVariable(data: IChatVariableData, resolver: IChatVariableResolver): IDisposable; hasVariable(name: string): boolean; getVariable(name: string): IChatVariableData | undefined; - getVariables(location: ChatAgentLocation): Iterable>; + getVariables(): Iterable>; getDynamicVariables(sessionId: string): ReadonlyArray; // should be its own service? attachContext(name: string, value: string | URI | Location | unknown, location: ChatAgentLocation): void; From a7a2fff97af332e1a73c4f036d450c4761ede207 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 4 Feb 2025 12:39:10 -0800 Subject: [PATCH 1206/3587] Fix video layout in walkthroughs (#239639) --- .../browser/gettingStartedDetailsRenderer.ts | 39 ++----------------- .../browser/media/gettingStarted.css | 15 +++++-- 2 files changed, 15 insertions(+), 39 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts index 8fc561417acb..d2be9cd03095 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts @@ -213,46 +213,15 @@ export class GettingStartedDetailsRenderer { video { max-width: 100%; max-height: 100%; - object-fit: cover; - } - vertically-centered { - display: flex; - justify-content: center; /* Centers horizontally */ - align-items: center; /* Centers vertically */ - height: 100vh; /* Added missing semicolon */ + object-fit: cover; } - - - + - - `; } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css index 3edd78f86400..cd312dfbe331 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css @@ -530,7 +530,7 @@ .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent { height: 100%; - max-width: 1200px; + max-width: 80%; margin: 0 auto; padding: 0 32px; display: grid; @@ -580,10 +580,12 @@ } .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent.video > .getting-started-media { - grid-area: title-start / media-start / steps-end / media-end; - align-self: unset; + grid-area: steps-start / media-start / footer-start / media-end; + align-self: self-start; display: flex; - justify-content: center; + justify-content:center ; + height: 100%; + width: 100%; } .monaco-workbench .part.editor > .content .gettingStartedContainer.width-semi-constrained .gettingStartedSlideDetails .gettingStartedDetailsContent.video > .getting-started-media { @@ -613,6 +615,11 @@ justify-content: center; } +.monaco-workbench .part.editor > .content .gettingStartedContainer.width-constrained .gettingStartedSlideDetails .gettingStartedDetailsContent.image > .getting-started-media, +.monaco-workbench .part.editor > .content .gettingStartedContainer.width-constrained .gettingStartedSlideDetails .gettingStartedDetailsContent.video > .getting-started-media { + display: none; +} + .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent > .getting-started-media > video { max-width: 100%; max-height: 100%; From 1c931b181d6922ddc1eac2469117fba2c500da07 Mon Sep 17 00:00:00 2001 From: John Murray Date: Tue, 4 Feb 2025 21:14:23 +0000 Subject: [PATCH 1207/3587] Supply multiselects to `scm/resourceGroup/context` menu commands (fix #92337) (#192172) * Supply multiselects to `scm/resourceGroup/context` menu commands (fixes #92337) * Fix faulty merge * Pull request feedback --------- Co-authored-by: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> --- .../contrib/scm/browser/scmViewPane.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index c7c7b4c68ed6..cec11c8fff97 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -432,6 +432,7 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer (ISCMResource | IResourceNode)[]) { + constructor(private getSelectedResources: () => (ISCMResourceGroup | ISCMResource | IResourceNode)[]) { super(); } - protected override async runAction(action: IAction, context: ISCMResource | IResourceNode): Promise { + protected override async runAction(action: IAction, context: ISCMResourceGroup | ISCMResource | IResourceNode): Promise { if (!(action instanceof MenuItemAction)) { return super.runAction(action, context); } - const selection = this.getSelectedResources(); + const isContextResourceGroup = isSCMResourceGroup(context); + const selection = this.getSelectedResources().filter(r => isSCMResourceGroup(r) === isContextResourceGroup); + const contextIsSelected = selection.some(s => s === context); const actualContext = contextIsSelected ? selection : [context]; const args = actualContext.map(e => ResourceTree.isResourceNode(e) ? ResourceTree.collect(e) : [e]).flat(); @@ -2326,7 +2332,7 @@ export class SCMViewPane extends ViewPane { this.inputRenderer, this.actionButtonRenderer, this.instantiationService.createInstance(RepositoryRenderer, MenuId.SCMTitle, getActionViewItemProvider(this.instantiationService)), - this.instantiationService.createInstance(ResourceGroupRenderer, getActionViewItemProvider(this.instantiationService)), + this.instantiationService.createInstance(ResourceGroupRenderer, getActionViewItemProvider(this.instantiationService), resourceActionRunner), this.instantiationService.createInstance(ResourceRenderer, () => this.viewMode, this.listLabels, getActionViewItemProvider(this.instantiationService), resourceActionRunner) ], treeDataSource, @@ -2611,9 +2617,8 @@ export class SCMViewPane extends ViewPane { return Array.from(new Set([...focusedRepositories, ...selectedRepositories])); } - private getSelectedResources(): (ISCMResource | IResourceNode)[] { - return this.tree.getSelection() - .filter(r => !!r && !isSCMResourceGroup(r))! as any; + private getSelectedResources(): (ISCMResourceGroup | ISCMResource | IResourceNode)[] { + return this.tree.getSelection().filter(r => isSCMResourceGroup(r) || isSCMResource(r) || isSCMResourceNode(r)); } private getViewMode(): ViewMode { From 5c4c37a9204b2b1ac111f5c0d550e23a9e0e0bb9 Mon Sep 17 00:00:00 2001 From: Christian Klaussner Date: Tue, 4 Feb 2025 23:20:03 +0100 Subject: [PATCH 1208/3587] Fix traffic light centering on macOS (#212471) Co-authored-by: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> --- src/vs/platform/windows/electron-main/windowImpl.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 3c8efe30dd68..f5bf1b62cb9c 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -342,11 +342,16 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { // macOS: traffic lights else if (isMacintosh && options.height !== undefined) { - const verticalOffset = (options.height - 15) / 2; // 15px is the height of the traffic lights - if (!verticalOffset) { + // The traffic lights have a height of 12px. There's an invisible margin + // of 2px at the top and bottom, and 1px on the left and right. Therefore, + // the height for centering is 12px + 2 * 2px = 16px. When the position + // is set, the horizontal margin is offset to ensure the distance between + // the traffic lights and the window frame is equal in both directions. + const offset = Math.floor((options.height - 16) / 2); + if (!offset) { win.setWindowButtonPosition(null); } else { - win.setWindowButtonPosition({ x: verticalOffset, y: verticalOffset }); + win.setWindowButtonPosition({ x: offset + 1, y: offset }); } } } From c18871a3c0c7f4053198d51399829e9ff216b41b Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Tue, 4 Feb 2025 14:34:49 -0800 Subject: [PATCH 1209/3587] Re #239460. Empty notebook defaults cell language to last used kernel (#239647) --- .../notebook/browser/controller/cellOperations.ts | 14 +++++++++++++- .../browser/controller/insertCellActions.ts | 14 ++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts b/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts index 3432d899d1bf..0795f4536e70 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts @@ -18,6 +18,7 @@ import { CellEditType, CellKind, ICellEditOperation, ICellReplaceEdit, IOutputDt import { cellRangeContains, cellRangesToIndexes, ICellRange } from '../../common/notebookRange.js'; import { localize } from '../../../../../nls.js'; import { INotificationService } from '../../../../../platform/notification/common/notification.js'; +import { INotebookKernelHistoryService } from '../../common/notebookKernelService.js'; export async function changeCellToKind(kind: CellKind, context: INotebookActionContext, language?: string, mime?: string): Promise { const { notebookEditor } = context; @@ -662,7 +663,8 @@ export function insertCell( type: CellKind, direction: 'above' | 'below' = 'above', initialText: string = '', - ui: boolean = false + ui: boolean = false, + kernelHistoryService?: INotebookKernelHistoryService ) { const viewModel = editor.getViewModel() as NotebookViewModel; const activeKernel = editor.activeKernel; @@ -676,6 +678,7 @@ export function insertCell( if (type === CellKind.Code) { const supportedLanguages = activeKernel?.supportedLanguages ?? languageService.getRegisteredLanguageIds(); const defaultLanguage = supportedLanguages[0] || PLAINTEXT_LANGUAGE_ID; + if (cell?.cellKind === CellKind.Code) { language = cell.language; } else if (cell?.cellKind === CellKind.Markup) { @@ -685,6 +688,15 @@ export function insertCell( } else { language = defaultLanguage; } + } else if (!cell && viewModel.length === 0) { + // No cells in notebook - check kernel history + const lastKernels = kernelHistoryService?.getKernels(viewModel.notebookDocument); + if (lastKernels?.all.length) { + const lastKernel = lastKernels.all[0]; + language = lastKernel.supportedLanguages[0] || defaultLanguage; + } else { + language = defaultLanguage; + } } else { if (cell === undefined && direction === 'above') { // insert cell at the very top diff --git a/src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts index dc3fbcc179db..b7bdf36f3478 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts @@ -18,6 +18,7 @@ import { NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_EDITOR_EDITABLE } from '../../comm import { CellViewModel } from '../viewModel/notebookViewModelImpl.js'; import { CellKind, NotebookSetting } from '../../common/notebookCommon.js'; import { CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION } from './chat/notebookChatContext.js'; +import { INotebookKernelHistoryService } from '../../common/notebookKernelService.js'; const INSERT_CODE_CELL_ABOVE_COMMAND_ID = 'notebook.cell.insertCodeCellAbove'; const INSERT_CODE_CELL_BELOW_COMMAND_ID = 'notebook.cell.insertCodeCellBelow'; @@ -35,13 +36,15 @@ export function insertNewCell(accessor: ServicesAccessor, context: INotebookActi } const languageService = accessor.get(ILanguageService); + const kernelHistoryService = accessor.get(INotebookKernelHistoryService); + if (context.cell) { const idx = context.notebookEditor.getCellIndex(context.cell); - newCell = insertCell(languageService, context.notebookEditor, idx, kind, direction, undefined, true); + newCell = insertCell(languageService, context.notebookEditor, idx, kind, direction, undefined, true, kernelHistoryService); } else { const focusRange = context.notebookEditor.getFocus(); const next = Math.max(focusRange.end - 1, 0); - newCell = insertCell(languageService, context.notebookEditor, next, kind, direction, undefined, true); + newCell = insertCell(languageService, context.notebookEditor, next, kind, direction, undefined, true, kernelHistoryService); } return newCell; @@ -193,7 +196,8 @@ registerAction2(class InsertCodeCellAtTopAction extends NotebookAction { async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { const languageService = accessor.get(ILanguageService); - const newCell = insertCell(languageService, context.notebookEditor, 0, CellKind.Code, 'above', undefined, true); + const kernelHistoryService = accessor.get(INotebookKernelHistoryService); + const newCell = insertCell(languageService, context.notebookEditor, 0, CellKind.Code, 'above', undefined, true, kernelHistoryService); if (newCell) { await context.notebookEditor.focusNotebookCell(newCell, 'editor'); @@ -220,7 +224,9 @@ registerAction2(class InsertMarkdownCellAtTopAction extends NotebookAction { async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { const languageService = accessor.get(ILanguageService); - const newCell = insertCell(languageService, context.notebookEditor, 0, CellKind.Markup, 'above', undefined, true); + const kernelHistoryService = accessor.get(INotebookKernelHistoryService); + + const newCell = insertCell(languageService, context.notebookEditor, 0, CellKind.Markup, 'above', undefined, true, kernelHistoryService); if (newCell) { await context.notebookEditor.focusNotebookCell(newCell, 'editor'); From 363d424f91f0e47e871c9b8e21ec6bb2afb7a2db Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:37:31 -0800 Subject: [PATCH 1210/3587] adopt `ensureNoDisposablesAreLeakedInTestSuite` (#239536) * adopt * fix import change * remove disposablestore and organize imports * fix disposable leak error * more disposables fix * rename to code actions disposable --------- Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> --- eslint.config.js | 1 - .../contrib/codeAction/browser/codeAction.ts | 19 +++++++----- .../codeAction/browser/codeActionModel.ts | 28 +++++++++++------ .../test/browser/codeActionModel.test.ts | 31 +++++++++---------- 4 files changed, 44 insertions(+), 35 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index f9f120acd41b..b3167b40a5c7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -219,7 +219,6 @@ export default tseslint.config( { // Files should (only) be removed from the list they adopt the leak detector 'exclude': [ - 'src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts', 'src/vs/platform/configuration/test/common/configuration.test.ts', 'src/vs/platform/opener/test/common/opener.test.ts', 'src/vs/platform/registry/test/common/platform.test.ts', diff --git a/src/vs/editor/contrib/codeAction/browser/codeAction.ts b/src/vs/editor/contrib/codeAction/browser/codeAction.ts index 6120bae3584a..c78ecc080d4c 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeAction.ts @@ -6,8 +6,16 @@ import { coalesce, equals, isNonEmptyArray } from '../../../../base/common/arrays.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { illegalArgument, isCancellationError, onUnexpectedExternalError } from '../../../../base/common/errors.js'; +import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; +import * as nls from '../../../../nls.js'; +import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; +import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { IProgress, Progress } from '../../../../platform/progress/common/progress.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { IBulkEditService } from '../../../browser/services/bulkEditService.js'; import { Range } from '../../../common/core/range.js'; @@ -18,15 +26,7 @@ import { ITextModel } from '../../../common/model.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; import { IModelService } from '../../../common/services/model.js'; import { TextModelCancellationTokenSource } from '../../editorState/browser/editorState.js'; -import * as nls from '../../../../nls.js'; -import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { IProgress, Progress } from '../../../../platform/progress/common/progress.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { CodeActionFilter, CodeActionItem, CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource, filtersAction, mayIncludeActionsOfKind } from '../common/types.js'; -import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; -import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; export const codeActionCommandId = 'editor.action.codeAction'; export const quickFixCommandId = 'editor.action.quickFix'; @@ -165,6 +165,9 @@ export async function getCodeActions( ...getAdditionalDocumentationForShowingActions(registry, model, trigger, allActions) ]; return new ManagedCodeActionSet(allActions, allDocumentation, disposables); + } catch (err) { + disposables.dispose(); + throw err; } finally { listener.dispose(); cts.dispose(); diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts b/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts index 10381ff4ffce..d5289c2cb6bc 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts @@ -6,24 +6,24 @@ import { CancelablePromise, createCancelablePromise, TimeoutTimer } from '../../../../base/common/async.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { Emitter } from '../../../../base/common/event.js'; -import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; +import { Disposable, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { isEqual } from '../../../../base/common/resources.js'; +import { StopWatch } from '../../../../base/common/stopwatch.js'; import { URI } from '../../../../base/common/uri.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IMarkerService } from '../../../../platform/markers/common/markers.js'; +import { IEditorProgressService, Progress } from '../../../../platform/progress/common/progress.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { EditorOption, ShowLightbulbIconMode } from '../../../common/config/editorOptions.js'; import { Position } from '../../../common/core/position.js'; import { Selection } from '../../../common/core/selection.js'; import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js'; import { CodeActionProvider, CodeActionTriggerType } from '../../../common/languages.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { IMarkerService } from '../../../../platform/markers/common/markers.js'; -import { IEditorProgressService, Progress } from '../../../../platform/progress/common/progress.js'; import { CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from '../common/types.js'; import { getCodeActions } from './codeAction.js'; -import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; -import { StopWatch } from '../../../../base/common/stopwatch.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; export const SUPPORTED_CODE_ACTIONS = new RawContextKey('supportedCodeAction', ''); @@ -165,6 +165,8 @@ export class CodeActionModel extends Disposable { private readonly _onDidChangeState = this._register(new Emitter()); public readonly onDidChangeState = this._onDidChangeState.event; + private readonly codeActionsDisposable: MutableDisposable = this._register(new MutableDisposable()); + private _disposed = false; constructor( @@ -233,6 +235,7 @@ export class CodeActionModel extends Disposable { const actions = createCancelablePromise(async token => { if (this._settingEnabledNearbyQuickfixes() && trigger.trigger.type === CodeActionTriggerType.Invoke && (trigger.trigger.triggerAction === CodeActionTriggerSource.QuickFix || trigger.trigger.filter?.include?.contains(CodeActionKind.QuickFix))) { const codeActionSet = await getCodeActions(this._registry, model, trigger.selection, trigger.trigger, Progress.None, token); + this.codeActionsDisposable.value = codeActionSet; const allCodeActions = [...codeActionSet.allActions]; if (token.isCancellationRequested) { codeActionSet.dispose(); @@ -250,7 +253,7 @@ export class CodeActionModel extends Disposable { } return { validActions: codeActionSet.validActions, allActions: allCodeActions, documentation: codeActionSet.documentation, hasAutoFix: codeActionSet.hasAutoFix, hasAIFix: codeActionSet.hasAIFix, allAIFixes: codeActionSet.allAIFixes, dispose: () => { codeActionSet.dispose(); } }; } else if (!foundQuickfix) { - // If markers exists, and there are no quickfixes found or length is zero, check for quickfixes on that line. + // If markers exist, and there are no quickfixes found or length is zero, check for quickfixes on that line. if (allMarkers.length > 0) { const currPosition = trigger.selection.getPosition(); let trackedPosition = currPosition; @@ -275,6 +278,7 @@ export class CodeActionModel extends Disposable { const selectionAsPosition = new Selection(trackedPosition.lineNumber, trackedPosition.column, trackedPosition.lineNumber, trackedPosition.column); const actionsAtMarker = await getCodeActions(this._registry, model, selectionAsPosition, newCodeActionTrigger, Progress.None, token); + this.codeActionsDisposable.value = actionsAtMarker; if (actionsAtMarker.validActions.length !== 0) { for (const action of actionsAtMarker.validActions) { @@ -348,8 +352,11 @@ export class CodeActionModel extends Disposable { return codeActions; } - return getCodeActions(this._registry, model, trigger.selection, trigger.trigger, Progress.None, token); + const codeActionSet = await getCodeActions(this._registry, model, trigger.selection, trigger.trigger, Progress.None, token); + this.codeActionsDisposable.value = codeActionSet; + return codeActionSet; }); + if (trigger.trigger.type === CodeActionTriggerType.Invoke) { this._progressService?.showWhile(actions, 250); } @@ -381,6 +388,7 @@ export class CodeActionModel extends Disposable { public trigger(trigger: CodeActionTrigger) { this._codeActionOracle.value?.trigger(trigger); + this.codeActionsDisposable.clear(); } private setState(newState: CodeActionsState.State, skipNotify?: boolean) { diff --git a/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts b/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts index 666b185bda57..ab6c174582a9 100644 --- a/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts +++ b/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts @@ -5,19 +5,19 @@ import assert from 'assert'; import { promiseWithResolvers } from '../../../../../base/common/async.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { assertType } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; +import { MarkerService } from '../../../../../platform/markers/common/markerService.js'; import { ICodeEditor } from '../../../../browser/editorBrowser.js'; import { LanguageFeatureRegistry } from '../../../../common/languageFeatureRegistry.js'; import * as languages from '../../../../common/languages.js'; import { TextModel } from '../../../../common/model/textModel.js'; -import { CodeActionModel, CodeActionsState } from '../../browser/codeActionModel.js'; import { createTestCodeEditor } from '../../../../test/browser/testCodeEditor.js'; import { createTextModel } from '../../../../test/common/testTextModel.js'; -import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; -import { MarkerService } from '../../../../../platform/markers/common/markerService.js'; +import { CodeActionModel, CodeActionsState } from '../../browser/codeActionModel.js'; const testProvider = { provideCodeActions(): languages.CodeActionList { @@ -38,10 +38,8 @@ suite('CodeActionModel', () => { let markerService: MarkerService; let editor: ICodeEditor; let registry: LanguageFeatureRegistry; - const disposables = new DisposableStore(); setup(() => { - disposables.clear(); markerService = new MarkerService(); model = createTextModel('foobar foo bar\nfarboo far boo', languageId, undefined, uri); editor = createTestCodeEditor(model); @@ -49,8 +47,9 @@ suite('CodeActionModel', () => { registry = new LanguageFeatureRegistry(); }); + const store = ensureNoDisposablesAreLeakedInTestSuite(); + teardown(() => { - disposables.clear(); editor.dispose(); model.dispose(); markerService.dispose(); @@ -61,11 +60,11 @@ suite('CodeActionModel', () => { await runWithFakedTimers({ useFakeTimers: true }, () => { const reg = registry.register(languageId, testProvider); - disposables.add(reg); + store.add(reg); const contextKeys = new MockContextKeyService(); - const model = disposables.add(new CodeActionModel(editor, registry, markerService, contextKeys, undefined)); - disposables.add(model.onDidChangeState((e: CodeActionsState.State) => { + const model = store.add(new CodeActionModel(editor, registry, markerService, contextKeys, undefined)); + store.add(model.onDidChangeState((e: CodeActionsState.State) => { assertType(e.type === CodeActionsState.Type.Triggered); assert.strictEqual(e.trigger.type, languages.CodeActionTriggerType.Auto); @@ -93,7 +92,7 @@ suite('CodeActionModel', () => { test('Oracle -> position changed', async () => { await runWithFakedTimers({ useFakeTimers: true }, () => { const reg = registry.register(languageId, testProvider); - disposables.add(reg); + store.add(reg); markerService.changeOne('fake', uri, [{ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 6, @@ -107,8 +106,8 @@ suite('CodeActionModel', () => { return new Promise((resolve, reject) => { const contextKeys = new MockContextKeyService(); - const model = disposables.add(new CodeActionModel(editor, registry, markerService, contextKeys, undefined)); - disposables.add(model.onDidChangeState((e: CodeActionsState.State) => { + const model = store.add(new CodeActionModel(editor, registry, markerService, contextKeys, undefined)); + store.add(model.onDidChangeState((e: CodeActionsState.State) => { assertType(e.type === CodeActionsState.Type.Triggered); assert.strictEqual(e.trigger.type, languages.CodeActionTriggerType.Auto); @@ -129,12 +128,12 @@ suite('CodeActionModel', () => { const { promise: donePromise, resolve: done } = promiseWithResolvers(); await runWithFakedTimers({ useFakeTimers: true }, () => { const reg = registry.register(languageId, testProvider); - disposables.add(reg); + store.add(reg); let triggerCount = 0; const contextKeys = new MockContextKeyService(); - const model = disposables.add(new CodeActionModel(editor, registry, markerService, contextKeys, undefined)); - disposables.add(model.onDidChangeState((e: CodeActionsState.State) => { + const model = store.add(new CodeActionModel(editor, registry, markerService, contextKeys, undefined)); + store.add(model.onDidChangeState((e: CodeActionsState.State) => { assertType(e.type === CodeActionsState.Type.Triggered); assert.strictEqual(e.trigger.type, languages.CodeActionTriggerType.Auto); From 1195e2cf772e4f9b3bfeab7ba20345bb68deff61 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 4 Feb 2025 15:35:23 -0800 Subject: [PATCH 1211/3587] fix the `the the` typos (#239646) fix the `the the` -> `the` typos --- .devcontainer/README.md | 2 +- extensions/ipynb/src/notebookModelStoreSync.ts | 2 +- .../test/colorize-fixtures/test-treeView.ts | 4 ++-- src/vs/base/browser/ui/hover/hover.ts | 2 +- src/vs/editor/browser/editorDom.ts | 2 +- src/vs/editor/browser/gpu/atlas/atlas.ts | 4 ++-- src/vs/editor/browser/gpu/raster/raster.ts | 4 ++-- src/vs/editor/browser/view/viewLayer.ts | 2 +- src/vs/editor/common/textModelGuides.ts | 2 +- .../extensionManagement/common/extensionGalleryService.ts | 4 ++-- .../common/capabilities/commandDetectionCapability.ts | 2 +- src/vs/workbench/browser/parts/views/treeView.ts | 4 ++-- src/vs/workbench/contrib/chat/browser/media/chat.css | 2 +- src/vs/workbench/contrib/comments/browser/commentService.ts | 2 +- src/vs/workbench/contrib/comments/browser/commentsModel.ts | 2 +- .../externalUriOpener/common/externalUriOpenerService.ts | 2 +- .../contrib/markdown/browser/markdownSettingRenderer.ts | 2 +- src/vs/workbench/contrib/terminal/browser/terminal.ts | 2 +- src/vs/workbench/contrib/terminal/browser/terminalActions.ts | 2 +- src/vs/workbench/contrib/terminal/browser/terminalInstance.ts | 2 +- .../contrib/workspace/browser/workspaceTrustEditor.ts | 2 +- src/vscode-dts/vscode.d.ts | 2 +- src/vscode-dts/vscode.proposed.resolvers.d.ts | 2 +- src/vscode-dts/vscode.proposed.tunnelFactory.d.ts | 2 +- test/automation/src/terminal.ts | 2 +- 25 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 809b0f6aa557..3d1e15e5d550 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -56,7 +56,7 @@ Next: **[Try it out!](#try-it)** You may see improved VNC responsiveness when accessing a codespace from VS Code client since you can use a [VNC Viewer][def]. Here's how to do it. -1. Install [Visual Studio Code Stable](https://code.visualstudio.com/) or [Insiders](https://code.visualstudio.com/insiders/) and the the [GitHub Codespaces extension](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces). +1. Install [Visual Studio Code Stable](https://code.visualstudio.com/) or [Insiders](https://code.visualstudio.com/insiders/) and the [GitHub Codespaces extension](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces). > **Note:** The GitHub Codespaces extension requires the Visual Studio Code distribution of Code - OSS. diff --git a/extensions/ipynb/src/notebookModelStoreSync.ts b/extensions/ipynb/src/notebookModelStoreSync.ts index 836e1c8afc5d..1d83d980b2da 100644 --- a/extensions/ipynb/src/notebookModelStoreSync.ts +++ b/extensions/ipynb/src/notebookModelStoreSync.ts @@ -14,7 +14,7 @@ const noop = () => { }; /** - * Code here is used to ensure the Notebook Model is in sync the the ipynb JSON file. + * Code here is used to ensure the Notebook Model is in sync the ipynb JSON file. * E.g. assume you add a new cell, this new cell will not have any metadata at all. * However when we save the ipynb, the metadata will be an empty object `{}`. * Now thats completely different from the metadata os being `empty/undefined` in the model. diff --git a/extensions/vscode-colorize-perf-tests/test/colorize-fixtures/test-treeView.ts b/extensions/vscode-colorize-perf-tests/test/colorize-fixtures/test-treeView.ts index 08927639e762..612ee41bc2af 100644 --- a/extensions/vscode-colorize-perf-tests/test/colorize-fixtures/test-treeView.ts +++ b/extensions/vscode-colorize-perf-tests/test/colorize-fixtures/test-treeView.ts @@ -300,7 +300,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { } this._isInitialized = true; - // Remember when adding to this method that it isn't called until the the view is visible, meaning that + // Remember when adding to this method that it isn't called until the view is visible, meaning that // properties could be set and events could be fired before we're initialized and that this needs to be handled. this.contextKeyService.bufferChangeEvents(() => { @@ -534,7 +534,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { private initializeShowCollapseAllAction(startingValue: boolean = false) { if (!this.collapseAllContext) { - this.collapseAllContextKey = new RawContextKey(`treeView.${this.id}.enableCollapseAll`, startingValue, localize('treeView.enableCollapseAll', "Whether the the tree view with id {0} enables collapse all.", this.id)); + this.collapseAllContextKey = new RawContextKey(`treeView.${this.id}.enableCollapseAll`, startingValue, localize('treeView.enableCollapseAll', "Whether the tree view with id {0} enables collapse all.", this.id)); this.collapseAllContext = this.collapseAllContextKey.bindTo(this.contextKeyService); } return true; diff --git a/src/vs/base/browser/ui/hover/hover.ts b/src/vs/base/browser/ui/hover/hover.ts index 7f30cd1854e6..ec7408607080 100644 --- a/src/vs/base/browser/ui/hover/hover.ts +++ b/src/vs/base/browser/ui/hover/hover.ts @@ -101,7 +101,7 @@ export interface IHoverDelegate2 { ): IDisposable; /** - * Hides the hover if it was visible. This call will be ignored if the the hover is currently + * Hides the hover if it was visible. This call will be ignored if the hover is currently * "locked" via the alt/option key. */ hideHover(): void; diff --git a/src/vs/editor/browser/editorDom.ts b/src/vs/editor/browser/editorDom.ts index e42959d81b6f..5a3627d87913 100644 --- a/src/vs/editor/browser/editorDom.ts +++ b/src/vs/editor/browser/editorDom.ts @@ -64,7 +64,7 @@ export class EditorPagePosition { } /** - * Coordinates relative to the the (top;left) of the editor that can be used safely with other internal editor metrics. + * Coordinates relative to the (top;left) of the editor that can be used safely with other internal editor metrics. * **NOTE**: This position is obtained by taking page coordinates and transforming them relative to the * editor's (top;left) position in a way in which scale transformations are taken into account. * **NOTE**: These coordinates could be negative if the mouse position is outside the editor. diff --git a/src/vs/editor/browser/gpu/atlas/atlas.ts b/src/vs/editor/browser/gpu/atlas/atlas.ts index 17900d5273c8..7ef9da7d5f55 100644 --- a/src/vs/editor/browser/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/gpu/atlas/atlas.ts @@ -32,14 +32,14 @@ export interface ITextureAtlasPageGlyph { /** The y offset from {@link y} of the glyph's origin. */ originOffsetY: number; /** - * The distance from the the glyph baseline to the top of the highest bounding rectangle of all + * The distance from the glyph baseline to the top of the highest bounding rectangle of all * fonts used to render the text. * * @see {@link TextMetrics.fontBoundingBoxAscent} */ fontBoundingBoxAscent: number; /** - * The distance from the the glyph baseline to the bottom of the bounding rectangle of all fonts + * The distance from the glyph baseline to the bottom of the bounding rectangle of all fonts * used to render the text. * * @see {@link TextMetrics.fontBoundingBoxDescent} diff --git a/src/vs/editor/browser/gpu/raster/raster.ts b/src/vs/editor/browser/gpu/raster/raster.ts index eb6c56f1ffd3..32bee48f24d8 100644 --- a/src/vs/editor/browser/gpu/raster/raster.ts +++ b/src/vs/editor/browser/gpu/raster/raster.ts @@ -67,14 +67,14 @@ export interface IRasterizedGlyph { */ originOffset: { x: number; y: number }; /** - * The distance from the the glyph baseline to the top of the highest bounding rectangle of all + * The distance from the glyph baseline to the top of the highest bounding rectangle of all * fonts used to render the text. * * @see {@link TextMetrics.fontBoundingBoxAscent} */ fontBoundingBoxAscent: number; /** - * The distance from the the glyph baseline to the bottom of the bounding rectangle of all fonts + * The distance from the glyph baseline to the bottom of the bounding rectangle of all fonts * used to render the text. * * @see {@link TextMetrics.fontBoundingBoxDescent} diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index ec5d8bce6f3d..4058205be2fe 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -279,7 +279,7 @@ export class VisibleLinesCollection { public onFlushed(e: viewEvents.ViewFlushedEvent, flushDom?: boolean): boolean { // No need to clear the dom node because a full .innerHTML will occur in - // ViewLayerRenderer._render, however the the fallbakc mechanism in the + // ViewLayerRenderer._render, however the fallback mechanism in the // GPU renderer may cause this to be necessary as the .innerHTML call // may not happen depending on the new state, leaving stale DOM nodes // around. diff --git a/src/vs/editor/common/textModelGuides.ts b/src/vs/editor/common/textModelGuides.ts index a109109e430e..f1cd766ec3f8 100644 --- a/src/vs/editor/common/textModelGuides.ts +++ b/src/vs/editor/common/textModelGuides.ts @@ -17,7 +17,7 @@ export interface IGuidesTextModelPart { getLinesIndentGuides(startLineNumber: number, endLineNumber: number): number[]; /** - * Requests the the indent guides for the given range of lines. + * Requests the indent guides for the given range of lines. * `result[i]` will contain the indent guides of the `startLineNumber + i`th line. * @internal */ diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 90b110c251ae..83b0491704bf 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -688,7 +688,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi { count: number }, { owner: 'sandy081'; - comment: 'Report the query to the the Marketplace for fetching extensions by name'; + comment: 'Report the query to the Marketplace for fetching extensions by name'; readonly count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of extensions to fetch' }; }>('galleryService:additionalQueryByName', { count: extensionInfosByName.length @@ -1296,7 +1296,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi finally { type GalleryServiceGetLatestEventClassification = { owner: 'sandy081'; - comment: 'Report the query to the the Marketplace for fetching latest version of an extension'; + comment: 'Report the query to the Marketplace for fetching latest version of an extension'; extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the extension' }; duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Duration in ms for the query' }; errorCode?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The error code in case of error' }; diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index 8f9d90cf49b7..d2413b5897ea 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -358,7 +358,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe this._logService.debug('CommandDetectionCapability#handleCommandFinished', this._terminal.buffer.active.cursorX, options?.marker?.line, this._currentCommand.command, this._currentCommand); // HACK: Handle a special case on some versions of bash where identical commands get merged - // in the output of `history`, this detects that case and sets the exit code to the the last + // in the output of `history`, this detects that case and sets the exit code to the last // command's exit code. This covered the majority of cases but will fail if the same command // runs with a different exit code, that will need a more robust fix where we send the // command ID and exit code over to the capability to adjust there. diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 3384ee711fa1..84027d9d013c 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -300,7 +300,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { } this._isInitialized = true; - // Remember when adding to this method that it isn't called until the the view is visible, meaning that + // Remember when adding to this method that it isn't called until the view is visible, meaning that // properties could be set and events could be fired before we're initialized and that this needs to be handled. this.contextKeyService.bufferChangeEvents(() => { @@ -534,7 +534,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { private initializeShowCollapseAllAction(startingValue: boolean = false) { if (!this.collapseAllContext) { - this.collapseAllContextKey = new RawContextKey(`treeView.${this.id}.enableCollapseAll`, startingValue, localize('treeView.enableCollapseAll', "Whether the the tree view with id {0} enables collapse all.", this.id)); + this.collapseAllContextKey = new RawContextKey(`treeView.${this.id}.enableCollapseAll`, startingValue, localize('treeView.enableCollapseAll', "Whether the tree view with id {0} enables collapse all.", this.id)); this.collapseAllContext = this.collapseAllContextKey.bindTo(this.contextKeyService); } return true; diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 9a0c57c3bc56..138dd32b1507 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -996,7 +996,7 @@ have to be updated for changes to the rules above, or to support more deeply nes } /* * This overly-specific CSS selector is needed to beat priority of some - * styles applied on the the `.chat-attached-context-attachment` element. + * styles applied on the `.chat-attached-context-attachment` element. */ .chat-attached-context .chat-prompt-instructions-attachments .chat-prompt-instructions-attachment.error.implicit, .chat-attached-context .chat-prompt-instructions-attachments .chat-prompt-instructions-attachment.warning.implicit { diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index b7d1cde9aead..309745ca99d2 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -293,7 +293,7 @@ export class CommentService extends Disposable implements ICommentService { } /** - * The active comment thread is the the thread that is currently being edited. + * The active comment thread is the thread that is currently being edited. * @param commentThread */ setActiveEditingCommentThread(commentThread: CommentThread | null) { diff --git a/src/vs/workbench/contrib/comments/browser/commentsModel.ts b/src/vs/workbench/contrib/comments/browser/commentsModel.ts index 909955ccd20d..a2191238f6e8 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsModel.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsModel.ts @@ -121,7 +121,7 @@ export class CommentsModel extends Disposable implements ICommentsModel { public hasCommentThreads(): boolean { // There's a resource with at least one thread return !!this._resourceCommentThreads.length && this._resourceCommentThreads.some(resource => { - // At least one of the threads in the the resource has comments + // At least one of the threads in the resource has comments return (resource.commentThreads.length > 0) && resource.commentThreads.some(thread => { // At least one of the comments in the thread is not empty return threadHasMeaningfulComments(thread.thread); diff --git a/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts b/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts index fe02f19396d1..68495c2158db 100644 --- a/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts +++ b/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts @@ -45,7 +45,7 @@ export interface IExternalUriOpenerService { registerExternalOpenerProvider(provider: IExternalOpenerProvider): IDisposable; /** - * Get the configured IExternalUriOpener for the the uri. + * Get the configured IExternalUriOpener for the uri. * If there is no opener configured, then returns the first opener that can handle the uri. */ getOpener(uri: URI, ctx: { sourceUri: URI; preferredOpenerId?: string }, token: CancellationToken): Promise; diff --git a/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts index 14dfabbd451c..5cf78c4d4759 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts @@ -299,7 +299,7 @@ export class SimpleSettingRenderer { if (uri.scheme === Schemas.codeSetting) { type ReleaseNotesSettingUsedClassification = { owner: 'alexr00'; - comment: 'Used to understand if the the action to update settings from the release notes is used.'; + comment: 'Used to understand if the action to update settings from the release notes is used.'; settingId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the setting that was clicked on in the release notes' }; }; type ReleaseNotesSettingUsed = { diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 7e65338102bc..d5d2b44a3a8c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -973,7 +973,7 @@ export interface ITerminalInstance extends IBaseTerminalInstance { /** * Sets the terminal instance's dimensions to the values provided via the onDidOverrideDimensions event, - * which allows overriding the the regular dimensions (fit to the size of the panel). + * which allows overriding the regular dimensions (fit to the size of the panel). */ setOverrideDimensions(dimensions: ITerminalDimensions): void; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 00927f236f3c..664eea289081 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -204,7 +204,7 @@ export function registerContextualInstanceAction( activeInstanceType?: 'view' | 'editor'; run: (instance: ITerminalInstance, c: ITerminalServicesCollection, accessor: ServicesAccessor, args?: unknown) => void | Promise; /** - * A callback to run after the the `run` callbacks have completed. + * A callback to run after the `run` callbacks have completed. * @param instances The selected instance(s) that the command was run on. */ runAfter?: (instances: ITerminalInstance[], c: ITerminalServicesCollection, accessor: ServicesAccessor, args?: unknown) => void | Promise; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 35f316a4bd0c..bffdd6596871 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -951,7 +951,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } /** - * Opens the the terminal instance inside the parent DOM element previously set with + * Opens the terminal instance inside the parent DOM element previously set with * `attachToElement`, you must ensure the parent DOM element is explicitly visible before * invoking this function as it performs some DOM calculations internally */ diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 8d37002068d8..fb904f26b045 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -1091,7 +1091,7 @@ export class WorkspaceTrustEditor extends EditorPane { const textElement = append(parent, $('.workspace-trust-untrusted-description')); if (!this.workspaceTrustManagementService.isWorkspaceTrustForced()) { - textElement.innerText = this.workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE ? localize('untrustedWorkspaceReason', "This workspace is trusted via the bolded entries in the trusted folders below.") : localize('untrustedFolderReason', "This folder is trusted via the bolded entries in the the trusted folders below."); + textElement.innerText = this.workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE ? localize('untrustedWorkspaceReason', "This workspace is trusted via the bolded entries in the trusted folders below.") : localize('untrustedFolderReason', "This folder is trusted via the bolded entries in the trusted folders below."); } else { textElement.innerText = localize('trustedForcedReason', "This window is trusted by nature of the workspace that is opened."); } diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 67d44daf01f1..55ea480be702 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -11906,7 +11906,7 @@ declare module 'vscode' { * A map containing a mapping of the mime type of the corresponding transferred data. * * Drag and drop controllers that implement {@link TreeDragAndDropController.handleDrag `handleDrag`} can add additional mime types to the - * data transfer. These additional mime types will only be included in the `handleDrop` when the the drag was initiated from + * data transfer. These additional mime types will only be included in the `handleDrop` when the drag was initiated from * an element in the same drag and drop controller. */ export class DataTransfer implements Iterable<[mimeType: string, item: DataTransferItem]> { diff --git a/src/vscode-dts/vscode.proposed.resolvers.d.ts b/src/vscode-dts/vscode.proposed.resolvers.d.ts index 68a2c0639279..5df4f6fc1c52 100644 --- a/src/vscode-dts/vscode.proposed.resolvers.d.ts +++ b/src/vscode-dts/vscode.proposed.resolvers.d.ts @@ -121,7 +121,7 @@ declare module 'vscode' { tunnelFeatures?: { elevation: boolean; /** - * One of the the options must have the ID "private". + * One of the options must have the ID "private". */ privacyOptions: TunnelPrivacy[]; /** diff --git a/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts b/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts index 828a948b80f6..eab3ffa5d2e4 100644 --- a/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts +++ b/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts @@ -19,7 +19,7 @@ declare module 'vscode' { tunnelFeatures?: { elevation: boolean; /** - * One of the the options must have the ID "private". + * One of the options must have the ID "private". */ privacyOptions: TunnelPrivacy[]; /** diff --git a/test/automation/src/terminal.ts b/test/automation/src/terminal.ts index 8ebcbd686361..1b3ecc7cdfaa 100644 --- a/test/automation/src/terminal.ts +++ b/test/automation/src/terminal.ts @@ -210,7 +210,7 @@ export class Terminal { name: title.textContent.replace(/^[├┌└]\s*/, ''), description: description?.textContent }; - // It's a new group if the the tab does not start with ├ or └ + // It's a new group if the tab does not start with ├ or └ if (title.textContent.match(/^[├└]/)) { groups[groups.length - 1].push(label); } else { From 194a08f5f2572012863f15a2c9e7c71e990cb92e Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 4 Feb 2025 17:32:27 -0800 Subject: [PATCH 1212/3587] [prompts]: implement basic paths autocompletion for `Unix` machines (#239643) * [autocompletions]: II for `#file:` paths * [autocompletions]: refactor and add more doc comments * [autocompletions]: refactor the provider to use the new syntax service * [autocompletions]: refactor, add unit tests --- src/vs/base/common/types.ts | 38 ++ src/vs/base/test/common/types.test.ts | 552 +++++++++++++++++- .../contrib/chat/browser/chat.contribution.ts | 1 + .../languageFeatures/promptLinkProvider.ts | 2 +- .../promptPathAutocompletion.ts | 362 ++++++++++++ .../common/promptSyntax/parsers/types.d.ts | 10 + 6 files changed, 963 insertions(+), 2 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptPathAutocompletion.ts diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index 54edfafe71fe..b1e0b54f779d 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -163,6 +163,44 @@ export function assertAllDefined(...args: (unknown | null | undefined)[]): unkno return result; } +/** + * Asserts that the provided `item` is one of the items in the `list`. + * Helps to narrow down broader `TType` of the `item` to the more + * specific `TSubtype` type. + * + * ## Examples + * + * ```typescript + * // note! item type is a `subset of string` + * type TItem = ':' | '.' | '/'; + * + * // note! item is type of `string` here + * const item: string = ':'; + * // list of the items to check against + * const list: TItem[] = [':', '.']; + * + * // ok + * assertOneOf( + * item, + * list, + * 'Must succeed', + * ); + * + * // `item` is of `TItem` type now + * ``` + */ +export function assertOneOf( + item: TType, + list: readonly TSubtype[], + errorPrefix: string, +): asserts item is TSubtype { + // note! it's ok to type cast here because `TSubtype` is a subtype of `TType` + assert( + list.includes(item as TSubtype), + `${errorPrefix}: Expected '${item}' to be one of [${list.join(', ')}].`, + ); +} + const hasOwnProperty = Object.prototype.hasOwnProperty; /** diff --git a/src/vs/base/test/common/types.test.ts b/src/vs/base/test/common/types.test.ts index da0055392010..163b8d576855 100644 --- a/src/vs/base/test/common/types.test.ts +++ b/src/vs/base/test/common/types.test.ts @@ -5,8 +5,8 @@ import assert from 'assert'; import * as types from '../../common/types.js'; +import { assertDefined, assertOneOf } from '../../common/types.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; -import { assertDefined } from '../../common/types.js'; suite('Types', () => { @@ -304,6 +304,556 @@ suite('Types', () => { }); }); + suite('assertOneOf', () => { + suite('success', () => { + /** + * Simple type check function to compile-time validate + * type of passed argument. + */ + function typeCheck(thing: T): T { + return thing; + } + + suite('string', () => { + test('type', () => { + assert.doesNotThrow(() => { + assertOneOf( + 'foo', + ['foo', 'bar'], + 'Foo must be one of: foo, bar', + ); + }); + }); + + test('subtype', () => { + assert.doesNotThrow(() => { + const item: string = 'hi'; + const list: ('hi' | 'ciao' | 'hola')[] = ['hi', 'ciao']; + + assertOneOf( + item, + list, + 'Hi must be one of: hi, ciao', + ); + + typeCheck<'hi' | 'ciao' | 'hola'>(item); + }); + }); + }); + + suite('number', () => { + test('type', () => { + assert.doesNotThrow(() => { + assertOneOf( + 10, + [10, 100], + '10 must be one of: 10, 100', + ); + }); + }); + + test('subtype', () => { + assert.doesNotThrow(() => { + const item: number = 20; + const list: (20 | 2000)[] = [20, 2000]; + + assertOneOf( + item, + list, + '20 must be one of: 20, 2000', + ); + + typeCheck<20 | 2000>(item); + }); + }); + + }); + + suite('boolean', () => { + test('type', () => { + assert.doesNotThrow(() => { + assertOneOf( + true, + [true, false], + 'true must be one of: true, false', + ); + }); + + assert.doesNotThrow(() => { + assertOneOf( + false, + [true, false], + 'false must be one of: true, false', + ); + }); + }); + + test('subtype (true)', () => { + assert.doesNotThrow(() => { + const item: boolean = true; + const list: (true)[] = [true, true]; + + assertOneOf( + item, + list, + 'true must be one of: true, true', + ); + + typeCheck(item); + }); + }); + + test('subtype (false)', () => { + assert.doesNotThrow(() => { + const item: boolean = false; + const list: (false | true)[] = [false, true]; + + assertOneOf( + item, + list, + 'false must be one of: false, true', + ); + + typeCheck(item); + }); + }); + }); + + suite('undefined', () => { + test('type', () => { + assert.doesNotThrow(() => { + assertOneOf( + undefined, + [undefined], + 'undefined must be one of: undefined', + ); + }); + + assert.doesNotThrow(() => { + assertOneOf( + undefined, + [void 0], + 'undefined must be one of: void 0', + ); + }); + }); + + test('subtype', () => { + assert.doesNotThrow(() => { + let item: undefined | null; + const list: (undefined)[] = [undefined]; + + assertOneOf( + item, + list, + 'undefined | null must be one of: undefined', + ); + + typeCheck(item); + }); + }); + }); + + suite('null', () => { + test('type', () => { + assert.doesNotThrow(() => { + assertOneOf( + null, + [null], + 'null must be one of: null', + ); + }); + }); + + test('subtype', () => { + assert.doesNotThrow(() => { + const item: undefined | null | string = null; + const list: (null)[] = [null]; + + assertOneOf( + item, + list, + 'null must be one of: null', + ); + + typeCheck(item); + }); + }); + }); + + suite('any', () => { + test('item', () => { + assert.doesNotThrow(() => { + const item: any = '1'; + const list: ('1' | '2')[] = ['2', '1']; + + assertOneOf( + item, + list, + '1 must be one of: 2, 1', + ); + + typeCheck<'1' | '2'>(item); + }); + }); + + test('list', () => { + assert.doesNotThrow(() => { + const item: '5' = '5'; + const list: any[] = ['3', '5', '2.5']; + + assertOneOf( + item, + list, + '5 must be one of: 3, 5, 2.5', + ); + + typeCheck<'5'>(item); + }); + }); + + test('both', () => { + assert.doesNotThrow(() => { + const item: any = '12'; + const list: any[] = ['14.25', '7', '12']; + + assertOneOf( + item, + list, + '12 must be one of: 14.25, 7, 12', + ); + + typeCheck(item); + }); + }); + }); + + suite('unknown', () => { + test('item', () => { + assert.doesNotThrow(() => { + const item: unknown = '1'; + const list: ('1' | '2')[] = ['2', '1']; + + assertOneOf( + item, + list, + '1 must be one of: 2, 1', + ); + + typeCheck<'1' | '2'>(item); + }); + }); + + test('both', () => { + assert.doesNotThrow(() => { + const item: unknown = '12'; + const list: unknown[] = ['14.25', '7', '12']; + + assertOneOf( + item, + list, + '12 must be one of: 14.25, 7, 12', + ); + + typeCheck(item); + }); + }); + }); + }); + + suite('failure', () => { + suite('string', () => { + test('type', () => { + assert.throws(() => { + assertOneOf( + 'baz', + ['foo', 'bar'], + 'Baz must not be one of: foo, bar', + ); + }); + }); + + test('subtype', () => { + assert.throws(() => { + const item: string = 'vitannia'; + const list: ('hi' | 'ciao' | 'hola')[] = ['hi', 'ciao']; + + assertOneOf( + item, + list, + 'vitannia must be one of: hi, ciao', + ); + }); + }); + + test('empty', () => { + assert.throws(() => { + const item: string = 'vitannia'; + const list: ('hi' | 'ciao' | 'hola')[] = []; + + assertOneOf( + item, + list, + 'vitannia must be one of: empty', + ); + }); + }); + }); + + suite('number', () => { + test('type', () => { + assert.throws(() => { + assertOneOf( + 19, + [10, 100], + '19 must not be one of: 10, 100', + ); + }); + }); + + test('subtype', () => { + assert.throws(() => { + const item: number = 24; + const list: (20 | 2000)[] = [20, 2000]; + + assertOneOf( + item, + list, + '24 must not be one of: 20, 2000', + ); + }); + }); + + test('empty', () => { + assert.throws(() => { + const item: number = 20; + const list: (20 | 2000)[] = []; + + assertOneOf( + item, + list, + '20 must not be one of: empty', + ); + }); + }); + }); + + suite('boolean', () => { + test('type', () => { + assert.throws(() => { + assertOneOf( + true, + [false], + 'true must not be one of: false', + ); + }); + + assert.throws(() => { + assertOneOf( + false, + [true], + 'false must not be one of: true', + ); + }); + }); + + test('subtype (true)', () => { + assert.throws(() => { + const item: boolean = true; + const list: (true | false)[] = [false]; + + assertOneOf( + item, + list, + 'true must not be one of: false', + ); + }); + }); + + test('subtype (false)', () => { + assert.throws(() => { + const item: boolean = false; + const list: (false | true)[] = [true, true, true]; + + assertOneOf( + item, + list, + 'false must be one of: true, true, true', + ); + }); + }); + + test('empty', () => { + assert.throws(() => { + const item: boolean = true; + const list: (false | true)[] = []; + + assertOneOf( + item, + list, + 'true must be one of: empty', + ); + }); + }); + }); + + suite('undefined', () => { + test('type', () => { + assert.throws(() => { + assertOneOf( + undefined, + [], + 'undefined must not be one of: empty', + ); + }); + + assert.throws(() => { + assertOneOf( + void 0, + [], + 'void 0 must not be one of: empty', + ); + }); + }); + + test('subtype', () => { + assert.throws(() => { + let item: undefined | null; + const list: (undefined | null)[] = [null]; + + assertOneOf( + item, + list, + 'undefined must be one of: null', + ); + }); + }); + + test('empty', () => { + assert.throws(() => { + let item: undefined | null; + const list: (undefined | null)[] = []; + + assertOneOf( + item, + list, + 'undefined must be one of: empty', + ); + }); + }); + }); + + suite('null', () => { + test('type', () => { + assert.throws(() => { + assertOneOf( + null, + [], + 'null must be one of: empty', + ); + }); + }); + + test('subtype', () => { + assert.throws(() => { + const item: undefined | null | string = null; + const list: null[] = []; + + assertOneOf( + item, + list, + 'null must be one of: empty', + ); + }); + }); + }); + + suite('any', () => { + test('item', () => { + assert.throws(() => { + const item: any = '1'; + const list: ('1' | '2' | '3' | '4')[] = ['3', '4']; + + assertOneOf( + item, + list, + '1 must not be one of: 3, 4', + ); + }); + }); + + test('list', () => { + assert.throws(() => { + const item: '5' = '5'; + const list: any[] = ['3', '6', '2.5']; + + assertOneOf( + item, + list, + '5 must not be one of: 3, 6, 2.5', + ); + }); + }); + + test('both', () => { + assert.throws(() => { + const item: any = '12'; + const list: any[] = ['14.25', '7', '15']; + + assertOneOf( + item, + list, + '12 must not be one of: 14.25, 7, 15', + ); + }); + }); + + test('empty', () => { + assert.throws(() => { + const item: any = '25'; + const list: any[] = []; + + assertOneOf( + item, + list, + '25 must not be one of: empty', + ); + }); + }); + }); + + suite('unknown', () => { + test('item', () => { + assert.throws(() => { + const item: unknown = '100'; + const list: ('11' | '12')[] = ['12', '11']; + + assertOneOf( + item, + list, + '100 must not be one of: 12, 11', + ); + + }); + + test('both', () => { + assert.throws(() => { + const item: unknown = '21'; + const list: unknown[] = ['14.25', '7', '12']; + + assertOneOf( + item, + list, + '21 must not be one of: 14.25, 7, 12', + ); + + }); + }); + }); + }); + }); + }); + test('validateConstraints', () => { types.validateConstraints([1, 'test', true], [Number, String, Boolean]); types.validateConstraints([1, 'test', true], ['number', 'string', 'boolean']); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 824ba88a2f45..47c41d56362b 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -82,6 +82,7 @@ import { ChatQuotasService, ChatQuotasStatusBarEntry, IChatQuotasService } from import { ChatSetupContribution } from './chatSetup.js'; import { ChatEditorOverlayController } from './chatEditing/chatEditingEditorOverlay.js'; import '../common/promptSyntax/languageFeatures/promptLinkProvider.js'; +import '../common/promptSyntax/languageFeatures/promptPathAutocompletion.js'; import { PromptFilesConfig } from '../common/promptSyntax/config.js'; import { BuiltinToolsContribution } from '../common/tools/tools.js'; import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js'; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts index 34bb7f530304..9030c7c00e68 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts @@ -93,6 +93,6 @@ export class PromptLinkProvider extends Disposable implements LinkProvider { } } -// register this provider as a workbench contribution +// register the provider as a workbench contribution Registry.as(WorkbenchExtensions.Workbench) .registerWorkbenchContribution(PromptLinkProvider, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptPathAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptPathAutocompletion.ts new file mode 100644 index 000000000000..1dac14fd1e06 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptPathAutocompletion.ts @@ -0,0 +1,362 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Notes on what to implement next: + * - re-trigger suggestions dialog on `folder` selection because the `#file:` references take + * `file` paths, therefore a "folder" completion is never final + * - provide the same suggestions that the `#file:` variables in the chat input have, e.g., + * recently used files, related files, etc. + * - support markdown links; markdown extension does sometimes provide the paths completions, but + * the prompt completions give more options (e.g., recently used files, related files, etc.) + * - add `Windows` support + */ + +import { LANGUAGE_SELECTOR } from '../constants.js'; +import { IPromptSyntaxService } from '../service/types.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { IPromptFileReference } from '../parsers/types.js'; +import { FileReference } from '../codecs/tokens/fileReference.js'; +import { assertOneOf } from '../../../../../../base/common/types.js'; +import { isWindows } from '../../../../../../base/common/platform.js'; +import { ITextModel } from '../../../../../../editor/common/model.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { CancellationError } from '../../../../../../base/common/errors.js'; +import { Position } from '../../../../../../editor/common/core/position.js'; +import { dirname, extUri } from '../../../../../../base/common/resources.js'; +import { assert, assertNever } from '../../../../../../base/common/assert.js'; +import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Registry } from '../../../../../../platform/registry/common/platform.js'; +import { LifecyclePhase } from '../../../../../services/lifecycle/common/lifecycle.js'; +import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../../common/contributions.js'; +import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList } from '../../../../../../editor/common/languages.js'; + +/** + * Type for a filesystem completion item - the one that has its {@link CompletionItem.kind kind} set + * to either {@link CompletionItemKind.File} or {@link CompletionItemKind.Folder}. + */ +type TFilesystemCompletionItem = CompletionItem & { kind: CompletionItemKind.File | CompletionItemKind.Folder }; + +/** + * Type for a "raw" folder suggestion. Unlike the full completion item, + * this one does not have `insertText` and `range` properties which are + * meant to be added later. + */ +type TFolderSuggestion = Omit & { label: string }; + +/** + * Type for trigger characters handled by this autocompletion provider. + */ +type TTriggerCharacter = ':' | '.' | '/'; + +/** + * Finds a file reference that suites the provided `position`. + */ +const findFileReference = ( + references: readonly IPromptFileReference[], + position: Position, +): IPromptFileReference | undefined => { + for (const reference of references) { + const { range } = reference; + + // ignore any other types of references + if (reference.type !== 'file') { + return undefined; + } + + // this ensures that we handle only the `#file:` references for now + if (!reference.text.startsWith(FileReference.TOKEN_START)) { + return undefined; + } + + // reference must match the provided position + const { startLineNumber, endColumn } = range; + if ((startLineNumber !== position.lineNumber) || (endColumn !== position.column)) { + continue; + } + + return reference; + } + + return undefined; +}; + +/** + * Provides reference paths autocompletion for the `#file:` variables inside prompts. + */ +export class PromptPathAutocompletion extends Disposable implements CompletionItemProvider { + /** + * Debug display name for this provider. + */ + public readonly _debugDisplayName: string = 'PromptPathAutocompletion'; + + /** + * List of trigger characters handled by this provider. + */ + public readonly triggerCharacters: TTriggerCharacter[] = [':', '.', '/']; + + constructor( + @IFileService private readonly fileService: IFileService, + @IPromptSyntaxService private readonly promptSyntaxService: IPromptSyntaxService, + @ILanguageFeaturesService private readonly languageService: ILanguageFeaturesService, + ) { + super(); + + this.languageService.completionProvider.register(LANGUAGE_SELECTOR, this); + } + + /** + * The main function of this provider that calculates + * completion items based on the provided arguments. + */ + public async provideCompletionItems( + model: ITextModel, + position: Position, + context: CompletionContext, + token: CancellationToken, + ): Promise { + assert( + !token.isCancellationRequested, + new CancellationError(), + ); + + const { triggerCharacter } = context; + + // it must always have been triggered by a character + if (!triggerCharacter) { + return undefined; + } + + assertOneOf( + triggerCharacter, + this.triggerCharacters, + `Prompt path autocompletion provider`, + ); + + const parser = this.promptSyntaxService.getParserFor(model); + assert( + !parser.disposed, + 'Prompt parser must not be disposed.', + ); + + // start the parser in case it was not started yet, + // and wait for it to settle to a final result + const { references } = await parser + .start() + .settled(); + + // validate that the cancellation was not yet requested + assert( + !token.isCancellationRequested, + new CancellationError(), + ); + + const fileReference = findFileReference(references, position); + if (!fileReference) { + return undefined; + } + + const modelDirname = dirname(model.uri); + + // in the case of the '.' trigger character, we must check if this is the first + // dot in the link path, otherwise the dot could be a part of a folder name + if (triggerCharacter === ':' || (triggerCharacter === '.' && fileReference.path === '.')) { + return { + suggestions: await this.getFirstFolderSuggestions( + triggerCharacter, + modelDirname, + fileReference, + ), + }; + } + + if (triggerCharacter === '/' || triggerCharacter === '.') { + return { + suggestions: await this.getNonFirstFolderSuggestions( + triggerCharacter, + modelDirname, + fileReference, + ), + }; + } + + assertNever( + triggerCharacter, + `Unexpected trigger character '${triggerCharacter}'.`, + ); + } + + /** + * Gets "raw" folder suggestions. Unlike the full completion items, + * these ones do not have `insertText` and `range` properties which + * are meant to be added by the caller later on. + */ + private async getFolderSuggestions( + uri: URI, + ): Promise { + const { children } = await this.fileService.resolve(uri); + const suggestions: TFolderSuggestion[] = []; + + // no `children` - no suggestions + if (!children) { + return suggestions; + } + + for (const child of children) { + const kind = child.isDirectory + ? CompletionItemKind.Folder + : CompletionItemKind.File; + + const sortText = child.isDirectory + ? '1' + : '2'; + + suggestions.push({ + label: child.name, + kind, + sortText, + }); + } + + return suggestions; + } + + /** + * Gets suggestions for a first folder/file name in the path. E.g., the one + * that follows immediately after the `:` character of the `#file:` variable. + * + * The main difference between this and "subsequent" folder cases is that in + * the beginning of the path the suggestions also contain the `..` item and + * the `./` normalization prefix for relative paths. + * + * See also {@link getNonFirstFolderSuggestions}. + */ + private async getFirstFolderSuggestions( + character: ':' | '.', + fileFolderUri: URI, + fileReference: IPromptFileReference, + ): Promise { + const { linkRange } = fileReference; + + // when character is `:`, there must be no link present yet + // otherwise the `:` was used in the middle of the link hence + // we don't want to provide suggestions for that + if (character === ':' && linkRange !== undefined) { + return []; + } + + // otherwise when the `.` character is present, it is inside the link part + // of the reference, hence we always expect the link range to be present + if (character === '.' && linkRange === undefined) { + return []; + } + + const suggestions = await this.getFolderSuggestions(fileFolderUri); + + // replacement range of the suggestions + // when character is `.` we want to also replace it, because we add + // the `./` at the beginning of all the relative paths + const startColumnOffset = (character === '.') ? 1 : 0; + const range = { + ...fileReference.range, + endColumn: fileReference.range.endColumn, + startColumn: fileReference.range.endColumn - startColumnOffset, + }; + + return [ + { + label: '..', + kind: CompletionItemKind.Folder, + insertText: '..', + range, + sortText: '0', + }, + ...suggestions + .map((suggestion) => { + // add space at the end of file names since no completions + // that follow the file name are expected anymore + const suffix = (suggestion.kind === CompletionItemKind.File) + ? ' ' + : ''; + + return { + ...suggestion, + range, + label: `./${suggestion.label}${suffix}`, + // we use the `./` prefix for consistency + insertText: `./${suggestion.label}${suffix}`, + }; + }), + ]; + } + + /** + * Gets suggestions for a folder/file name that follows after the first one. + * See also {@link getFirstFolderSuggestions}. + */ + private async getNonFirstFolderSuggestions( + character: '/' | '.', + fileFolderUri: URI, + fileReference: IPromptFileReference, + ): Promise { + const { linkRange, path } = fileReference; + + if (linkRange === undefined) { + return []; + } + + const currenFolder = extUri.resolvePath(fileFolderUri, path); + let suggestions = await this.getFolderSuggestions(currenFolder); + + // when trigger character was a `.`, which is we know is inside + // the folder/file name in the path, filter out to only items + // that start with the dot instead of showing all of them + if (character === '.') { + suggestions = suggestions.filter((suggestion) => { + return suggestion.label.startsWith('.'); + }); + } + + // replacement range of the suggestions + // when character is `.` we want to also replace it too + const startColumnOffset = (character === '.') ? 1 : 0; + const range = { + ...fileReference.range, + endColumn: fileReference.range.endColumn, + startColumn: fileReference.range.endColumn - startColumnOffset, + }; + + return suggestions + .map((suggestion) => { + // add space at the end of file names since no completions + // that follow the file name are expected anymore + const suffix = (suggestion.kind === CompletionItemKind.File) + ? ' ' + : ''; + + return { + ...suggestion, + insertText: `${suggestion.label}${suffix}`, + range, + }; + }); + } +} + +/** + * We restrict this provider to `Unix` machines for now because of + * the filesystem paths differences on `Windows` operating system. + * + * Notes on `Windows` support: + * - we add the `./` for the first path component, which may not work on `Windows` + * - the first path component of the absolute paths must be a drive letter + */ +if (!isWindows) { + // register the provider as a workbench contribution + Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(PromptPathAutocompletion, LifecyclePhase.Eventually); +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts index 91aef1479f12..586b134d8e1f 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts @@ -55,6 +55,16 @@ export interface IPromptReference extends IDisposable { */ readonly linkRange: IRange | undefined; + /** + * Text of the reference as it appears in the source. + */ + readonly text: string; + + /** + * Original link path as it appears in the source. + */ + readonly path: string; + /** * Whether the current reference points to a prompt snippet file. */ From 77d6d7111815d41b049fd553ceacab1e274be1e9 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Tue, 4 Feb 2025 18:30:12 -0800 Subject: [PATCH 1213/3587] Default keybinding for change cell language. (#239652) --- .../contrib/notebook/browser/controller/editActions.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts index f613e034b6aa..3fe9771b282d 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { KeyChord, KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { Mimes } from '../../../../../base/common/mime.js'; import { URI } from '../../../../../base/common/uri.js'; import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; @@ -361,6 +361,11 @@ registerAction2(class ChangeCellLanguageAction extends NotebookCellAction Date: Tue, 4 Feb 2025 18:27:55 -0800 Subject: [PATCH 1214/3587] add unit tests for the `TextModelPromptParser` class --- .../common/promptSyntax/parsers/types.d.ts | 11 +- .../parsers/textModelPromptParser.test.ts | 261 ++++++++++++++++++ .../promptSyntax/testUtils/createUri.ts | 19 ++ .../testUtils/expectedReference.ts | 155 +++++++++++ 4 files changed, 443 insertions(+), 3 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts create mode 100644 src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/createUri.ts create mode 100644 src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedReference.ts diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts index 586b134d8e1f..325e5c3ec451 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts @@ -99,17 +99,22 @@ export interface IPromptReference extends IDisposable { */ readonly topError: IResolveError | undefined; + /** + * Direct references of the current reference. + */ + references: readonly IPromptReference[]; + /** * All references that the current reference may have, - * including the all possible nested child references. + * including all possible nested child references. */ allReferences: readonly IPromptReference[]; /** * All *valid* references that the current reference may have, - * including the all possible nested child references. + * including all possible nested child references. * - * A valid reference is the one that points to an existing resource, + * A valid reference is one that points to an existing resource, * without creating a circular reference loop or having any other * issues that would make the reference resolve logic to fail. */ diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts new file mode 100644 index 000000000000..430824a9abe8 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts @@ -0,0 +1,261 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { createURI } from '../testUtils/createUri.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +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 { IFileService } from '../../../../../../../platform/files/common/files.js'; +import { randomBoolean } from '../../../../../../../base/test/common/testUtils.js'; +import { FileService } from '../../../../../../../platform/files/common/fileService.js'; +import { createTextModel } from '../../../../../../../editor/test/common/testTextModel.js'; +import { ILogService, NullLogService } from '../../../../../../../platform/log/common/log.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { TextModelPromptParser } from '../../../../common/promptSyntax/parsers/textModelPromptParser.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { InMemoryFileSystemProvider } from '../../../../../../../platform/files/common/inMemoryFilesystemProvider.js'; +import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; + +/** + * Test helper to run unit tests for the {@link TextModelPromptParser} + * class using different test input parameters + */ +class TextModelPromptParserTest extends Disposable { + public readonly model: ITextModel; + public readonly parser: TextModelPromptParser; + + constructor( + uri: URI, + initialContents: string[], + @IFileService fileService: IFileService, + @IInstantiationService initService: IInstantiationService, + ) { + super(); + + // create in-memory file system for this test instance + const fileSystemProvider = this._register(new InMemoryFileSystemProvider()); + this._register(fileService.registerProvider(Schemas.file, fileSystemProvider)); + + // both line endings should yield the same results + const lineEnding = (randomBoolean()) ? '\r\n' : '\n'; + + // create the underlying model + this.model = this._register( + createTextModel( + initialContents.join(lineEnding), + 'fooLang', + undefined, + uri, + ), + ); + + // create the parser instance + this.parser = this._register( + initService.createInstance(TextModelPromptParser, this.model, []), + ).start(); + } + + /** + * Validate the current state of the parser. + */ + public async validateReferences( + expectedReferences: readonly ExpectedReference[], + ) { + await this.parser.allSettled(); + + const { references } = this.parser; + for (let i = 0; i < expectedReferences.length; i++) { + expectedReferences[i].validateEqual(references[i]); + } + + assert.strictEqual( + expectedReferences.length, + references.length, + `[${this.model.uri}] Unexpected number of references.`, + ); + } +} + +suite('TextModelPromptParser', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + + setup(async () => { + instantiationService = disposables.add(new TestInstantiationService()); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IFileService, disposables.add(instantiationService.createInstance(FileService))); + }); + + /** + * Create a new test instance with provided input parameters. + */ + const createTest = ( + uri: URI, + initialContents: string[], + ): TextModelPromptParserTest => { + return disposables.add( + instantiationService.createInstance( + TextModelPromptParserTest, + uri, + initialContents, + ), + ); + }; + + test('core logic #1', async () => { + const test = createTest( + createURI('/foo/bar.md'), + [ + /* 01 */"The quick brown fox tries #file:/abs/path/to/file.md online yoga for the first time.", + /* 02 */"Maria discovered a stray turtle roaming in her kitchen.", + /* 03 */"Why did the robot write a poem about existential dread?", + /* 04 */"Sundays are made for two things: pancakes and procrastination.", + /* 05 */"Sometimes, the best code is the one you never have to write.", + /* 06 */"A lone kangaroo once hopped into the local cafe, seeking free Wi-Fi.", + /* 07 */"Critical #file:./folder/binary.file thinking is like coffee; best served strong [md link](/etc/hosts/random-file.txt) and without sugar.", + /* 08 */"Music is the mind’s way of doodling in the air.", + /* 09 */"Stargazing is just turning your eyes into cosmic explorers.", + /* 10 */"Never trust a balloon salesman who hates birthdays.", + /* 11 */"Running backward can be surprisingly enlightening.", + /* 12 */"There’s an art to whispering loudly.", + ], + ); + + await test.validateReferences([ + new ExpectedReference({ + uri: createURI('/abs/path/to/file.md'), + text: '#file:/abs/path/to/file.md', + path: '/abs/path/to/file.md', + startLine: 1, + startColumn: 27, + pathStartColumn: 33, + childrenOrError: new FileOpenFailed(createURI('/abs/path/to/file.md'), 'File not found.'), + }), + new ExpectedReference({ + uri: createURI('/foo/folder/binary.file'), + text: '#file:./folder/binary.file', + path: './folder/binary.file', + startLine: 7, + startColumn: 10, + pathStartColumn: 16, + childrenOrError: new FileOpenFailed(createURI('/foo/folder/binary.file'), 'File not found.'), + }), + new ExpectedReference({ + uri: createURI('/etc/hosts/random-file.txt'), + text: '[md link](/etc/hosts/random-file.txt)', + path: '/etc/hosts/random-file.txt', + startLine: 7, + startColumn: 81, + pathStartColumn: 91, + childrenOrError: new FileOpenFailed(createURI('/etc/hosts/random-file.txt'), 'File not found.'), + }), + ]); + }); + + test('core logic #2', async () => { + const test = createTest( + createURI('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"The penguin wore sunglasses but never left the iceberg.", + /* 02 */"I once saw a cloud that looked like an antique teapot.", + /* 03 */"Midnight snacks are the secret to eternal [link text](./foo-bar-baz/another-file.ts) happiness.", + /* 04 */"A stray sock in the hallway is a sign of chaotic creativity.", + /* 05 */"Dogs dream in colorful squeaks and belly rubs.", + /* 06 */"Never [caption](../../../c/file_name.prompt.md)\t underestimate the power of a well-timed nap.", + /* 07 */"The cactus on my desk has a thriving Instagram account.", + /* 08 */"In an alternate universe, pigeons deliver sushi by drone.", + /* 09 */"Lunar rainbows only appear when you sing in falsetto.", + /* 10 */"Carrots have secret telepathic abilities, but only on Tuesdays.", + /* 11 */"Sometimes, the best advice comes \t\t#file:../../main.rs\t#file:./somefolder/../samefile.jpeg\tfrom a talking dishwasher.", + /* 12 */"Paper airplanes believe they can fly until proven otherwise.", + /* 13 */"A library without stories is just a room full of silent trees.", + /* 14 */"The invisible cat meows only when it sees a postman.", + /* 15 */"Code reviews are like detective novels without the plot twists." + ], + ); + + await test.validateReferences([ + new ExpectedReference({ + uri: createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), + text: '[link text](./foo-bar-baz/another-file.ts)', + path: './foo-bar-baz/another-file.ts', + startLine: 3, + startColumn: 43, + pathStartColumn: 55, + childrenOrError: new FileOpenFailed(createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), + }), + new ExpectedReference({ + uri: createURI('/absolute/c/file_name.prompt.md'), + text: '[caption](../../../c/file_name.prompt.md)', + path: '../../../c/file_name.prompt.md', + startLine: 6, + startColumn: 7, + pathStartColumn: 17, + childrenOrError: new FileOpenFailed(createURI('/absolute/c/file_name.prompt.md'), 'File not found.'), + }), + new ExpectedReference({ + uri: createURI('/absolute/folder/main.rs'), + text: '#file:../../main.rs', + path: '../../main.rs', + startLine: 11, + startColumn: 36, + pathStartColumn: 42, + childrenOrError: new FileOpenFailed(createURI('/absolute/folder/main.rs'), 'File not found.'), + }), + new ExpectedReference({ + uri: createURI('/absolute/folder/and/a/samefile.jpeg'), + text: '#file:./somefolder/../samefile.jpeg', + path: './somefolder/../samefile.jpeg', + startLine: 11, + startColumn: 56, + pathStartColumn: 62, + childrenOrError: new FileOpenFailed(createURI('/absolute/folder/and/a/samefile.jpeg'), 'File not found.'), + }), + ]); + }); + + test('gets disposed with the model', async () => { + const test = createTest( + createURI('/some/path/file.prompt.md'), + [ + 'line1', + 'line2', + 'line3', + ], + ); + + // no references in the model contents + await test.validateReferences([]); + + test.model.dispose(); + + assert( + test.parser.disposed, + 'The parser should be disposed with its model.', + ); + }); + + test('toString() implementation', async () => { + const test = createTest( + createURI('/Users/legomushroom/repos/prompt-snippets/README.md'), + [ + 'line1', + 'line2', + 'line3', + ], + ); + + assert.strictEqual( + test.parser.toString(), + 'text-model-prompt:/Users/legomushroom/repos/prompt-snippets/README.md', + 'The parser should provide correct `toString()` implementation.', + ); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/createUri.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/createUri.ts new file mode 100644 index 000000000000..a5bf124484aa --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/createUri.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../../../../base/common/uri.js'; +import { isWindows } from '../../../../../../../base/common/platform.js'; + +/** + * Creates cross-platform URI for testing purposes. + * On `Windows`, absolute paths are prefixed with the disk name. + */ +export const createURI = (linkPath: string): URI => { + if (isWindows && linkPath.startsWith('/')) { + return URI.file('/d:' + linkPath); + } + + return URI.file(linkPath); +}; diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedReference.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedReference.ts new file mode 100644 index 000000000000..6fd8283b34ed --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedReference.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Range } from '../../../../../../../editor/common/core/range.js'; +import { assertDefined } from '../../../../../../../base/common/types.js'; +import { ParseError } from '../../../../common/promptFileReferenceErrors.js'; +import { IPromptFileReference } from '../../../../common/promptSyntax/parsers/types.js'; +import { TErrorCondition } from '../../../../common/promptSyntax/parsers/basePromptParser.js'; + +/** + * Options for the {@link ExpectedReference} class. + */ +interface IExpectedReferenceOptions { + readonly uri: URI; + readonly text: string; + readonly path: string; + readonly startLine: number; + readonly startColumn: number; + readonly pathStartColumn: number; + readonly childrenOrError?: TErrorCondition | (ExpectedReference[]); +} + +/** + * An expected child reference to use in tests. + */ +export class ExpectedReference { + constructor(private readonly options: IExpectedReferenceOptions) { } + + /** + * Validate that the provided reference is equal to this object. + */ + public validateEqual(other: IPromptFileReference) { + const { uri, text, path, childrenOrError = [] } = this.options; + const errorPrefix = `[${uri}] `; + + /** + * Validate the base properties of the reference first. + */ + + assert.strictEqual( + other.uri.toString(), + uri.toString(), + `${errorPrefix} Incorrect 'uri'.`, + ); + + assert.strictEqual( + other.text, + text, + `${errorPrefix} Incorrect 'text'.`, + ); + + assert.strictEqual( + other.path, + path, + `${errorPrefix} Incorrect 'path'.`, + ); + + const range = new Range( + this.options.startLine, + this.options.startColumn, + this.options.startLine, + this.options.startColumn + text.length, + ); + + assert( + range.equalsRange(other.range), + `${errorPrefix} Incorrect 'range': expected '${range}', got '${other.range}'.`, + ); + + if (path.length) { + assertDefined( + other.linkRange, + `${errorPrefix} Link range must be defined.`, + ); + + const linkRange = new Range( + this.options.startLine, + this.options.pathStartColumn, + this.options.startLine, + this.options.pathStartColumn + path.length, + ); + + assert( + linkRange.equalsRange(other.linkRange), + `${errorPrefix} Incorrect 'linkRange': expected '${linkRange}', got '${other.linkRange}'.`, + ); + } else { + assert.strictEqual( + other.linkRange, + undefined, + `${errorPrefix} Link range must be 'undefined'.`, + ); + } + + /** + * Next validate children or error condition. + */ + + if (childrenOrError instanceof ParseError) { + const error = childrenOrError; + const { errorCondition } = other; + assertDefined( + errorCondition, + `${errorPrefix} Expected 'errorCondition' to be defined.`, + ); + + assert( + errorCondition instanceof ParseError, + `${errorPrefix} Expected 'errorCondition' to be a 'ParseError'.`, + ); + + assert( + error.sameTypeAs(errorCondition), + `${errorPrefix} Incorrect 'errorCondition' type.`, + ); + + return; + } + + const children = childrenOrError; + const { references } = other; + + for (let i = 0; i < children.length; i++) { + children[i].validateEqual(references[i]); + } + + if (references.length > children.length) { + const extraReference = references[children.length]; + + // sanity check + assertDefined( + extraReference, + `${errorPrefix} Extra reference must be defined.`, + ); + + throw new Error(`${errorPrefix} Expected no more references, got '${extraReference.text}'.`); + } + + if (children.length > references.length) { + const expectedReference = children[references.length]; + + // sanity check + assertDefined( + expectedReference, + `${errorPrefix} Expected reference must be defined.`, + ); + + throw new Error(`${errorPrefix} Expected another reference '${expectedReference.options.text}', got 'undefined'.`); + } + } +} From 19262a9bdcf5fca76b29064b7b9709792ed34b4f Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 4 Feb 2025 18:55:11 -0800 Subject: [PATCH 1215/3587] fix unit tests on `Windows` add more doc comments --- .../parsers/textModelPromptParser.test.ts | 12 +++++-- .../testUtils/expectedReference.ts | 32 ++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts index 430824a9abe8..e6c088257d65 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts @@ -27,7 +27,14 @@ import { TestInstantiationService } from '../../../../../../../platform/instanti * class using different test input parameters */ class TextModelPromptParserTest extends Disposable { + /** + * Underlying text model of the parser. + */ public readonly model: ITextModel; + + /** + * The parser instance. + */ public readonly parser: TextModelPromptParser; constructor( @@ -243,8 +250,9 @@ suite('TextModelPromptParser', () => { }); test('toString() implementation', async () => { + const modelUri = createURI('/Users/legomushroom/repos/prompt-snippets/README.md'); const test = createTest( - createURI('/Users/legomushroom/repos/prompt-snippets/README.md'), + modelUri, [ 'line1', 'line2', @@ -254,7 +262,7 @@ suite('TextModelPromptParser', () => { assert.strictEqual( test.parser.toString(), - 'text-model-prompt:/Users/legomushroom/repos/prompt-snippets/README.md', + `text-model-prompt:${modelUri}`, 'The parser should provide correct `toString()` implementation.', ); }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedReference.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedReference.ts index 6fd8283b34ed..45bd8552489d 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedReference.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedReference.ts @@ -15,13 +15,43 @@ import { TErrorCondition } from '../../../../common/promptSyntax/parsers/basePro * Options for the {@link ExpectedReference} class. */ interface IExpectedReferenceOptions { + /** + * Final `URI` of the reference. + */ readonly uri: URI; + + /** + * Full text of the reference as it appears in the source text. + */ readonly text: string; + + /** + * The `path` part of the reference (e.g., the `/abs/path/to/file.md` + * part of the `[](/abs/path/to/file.md)` reference). + */ readonly path: string; + + /** + * Start line of the reference in the source text. Because links cannot + * contain line breaks, the end line number is also equal to this value. + */ readonly startLine: number; + + /** + * Start column of the full reference text as it appears in the source text. + */ readonly startColumn: number; + + /** + * Start column number of the `path` part of the reference. + */ readonly pathStartColumn: number; - readonly childrenOrError?: TErrorCondition | (ExpectedReference[]); + + /** + * Either an `error` that was generated during attempt to resolve this reference, + * or a list of expected child references if the attempt was successful. + */ + readonly childrenOrError?: TErrorCondition | ExpectedReference[]; } /** From 95bafc078d71735d4c1e2ef6e490cd43df59315c Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 4 Feb 2025 19:09:30 -0800 Subject: [PATCH 1216/3587] fix `toString()` unit test of the `TextModelpromptParser` --- .../common/promptSyntax/parsers/textModelPromptParser.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts index e6c088257d65..95ec4f3bd17a 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts @@ -262,7 +262,7 @@ suite('TextModelPromptParser', () => { assert.strictEqual( test.parser.toString(), - `text-model-prompt:${modelUri}`, + `text-model-prompt:${modelUri.path}`, 'The parser should provide correct `toString()` implementation.', ); }); From c363306526528d2b39bc71cc5df95760b11c057f Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Wed, 5 Feb 2025 14:18:05 +0800 Subject: [PATCH 1217/3587] refactor: reduce references to global editing session (#239660) * refactor: reduce references to global editing session * refactor: rework related files contrib to support multiple editing sessions * refactor: handle multiple editing sessions in `chatContextActions.ts` --- .../chat/browser/actions/chatClearActions.ts | 19 +++--- .../browser/actions/chatContextActions.ts | 19 ++++-- .../browser/actions/chatExecuteActions.ts | 17 ++--- .../browser/chatEditing/chatEditingActions.ts | 8 ++- .../contrib/chatInputRelatedFilesContrib.ts | 64 +++++++++---------- 5 files changed, 63 insertions(+), 64 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index 57a57061aed5..52c81f2fbf39 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -17,8 +17,9 @@ import { IViewsService } from '../../../../services/views/common/viewsService.js import { isChatViewTitleActionContext } from '../../common/chatActions.js'; import { ChatAgentLocation } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, WorkingSetEntryState } from '../../common/chatEditingService.js'; -import { ChatViewId, EditsViewId, IChatWidgetService } from '../chat.js'; +import { hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { ChatViewId, EditsViewId, IChatWidget, IChatWidgetService } from '../chat.js'; +import { EditingSessionAction } from '../chatEditing/chatEditingActions.js'; import { ctxIsGlobalEditingSession } from '../chatEditing/chatEditingEditorController.js'; import { ChatEditorInput } from '../chatEditorInput.js'; import { ChatViewPane } from '../chatViewPane.js'; @@ -102,7 +103,7 @@ export function registerNewChatActions() { } }); - registerAction2(class NewEditSessionAction extends Action2 { + registerAction2(class NewEditSessionAction extends EditingSessionAction { constructor() { super({ id: ACTION_ID_NEW_EDIT_SESSION, @@ -136,8 +137,7 @@ export function registerNewChatActions() { * * @returns false if the user had edits and did not action the dialog to take action on them, true otherwise */ - private async _handleCurrentEditingSession(chatEditingService: IChatEditingService, dialogService: IDialogService): Promise { - const currentEditingSession = chatEditingService.globalEditingSessionObs.get(); + private async _handleCurrentEditingSession(currentEditingSession: IChatEditingSession, dialogService: IDialogService): Promise { const currentEdits = currentEditingSession?.entries.get(); const currentEditCount = currentEdits?.length; @@ -174,14 +174,13 @@ export function registerNewChatActions() { return true; } - async run(accessor: ServicesAccessor, ...args: any[]) { + async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]) { const context = args[0]; const accessibilitySignalService = accessor.get(IAccessibilitySignalService); const widgetService = accessor.get(IChatWidgetService); - const chatEditingService = accessor.get(IChatEditingService); const dialogService = accessor.get(IDialogService); const viewsService = accessor.get(IViewsService); - if (!(await this._handleCurrentEditingSession(chatEditingService, dialogService))) { + if (!(await this._handleCurrentEditingSession(editingSession, dialogService))) { return; } if (isChatViewTitleActionContext(context)) { @@ -189,7 +188,7 @@ export function registerNewChatActions() { announceChatCleared(accessibilitySignalService); const widget = widgetService.getWidgetBySessionId(context.sessionId); if (widget) { - await chatEditingService.globalEditingSessionObs.get()?.stop(true); + await editingSession.stop(true); widget.clear(); widget.attachmentModel.clear(); widget.focusInput(); @@ -200,7 +199,7 @@ export function registerNewChatActions() { const widget = chatView.widget; announceChatCleared(accessibilitySignalService); - await chatEditingService.globalEditingSessionObs.get()?.stop(true); + await editingSession.stop(true); widget.clear(); widget.attachmentModel.clear(); widget.focusInput(); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index f091b1600a3c..e070c3bbfd37 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -495,7 +495,7 @@ export class AttachContextAction extends Action2 { } else { // file attachment if (chatEditingService) { - chatEditingService.globalEditingSessionObs.get()?.addFileToWorkingSet(pick.resource); + getEditingSession(chatEditingService, widget)?.addFileToWorkingSet(pick.resource); } else { toAttach.push({ id: this._getFileContextId({ resource: pick.resource }), @@ -520,7 +520,7 @@ export class AttachContextAction extends Action2 { const uri = editor instanceof DiffEditorInput ? editor.modified.resource : editor.resource; if (uri) { if (chatEditingService) { - chatEditingService.globalEditingSessionObs.get()?.addFileToWorkingSet(uri); + getEditingSession(chatEditingService, widget)?.addFileToWorkingSet(uri); } else { toAttach.push({ id: this._getFileContextId({ resource: uri }), @@ -536,7 +536,7 @@ export class AttachContextAction extends Action2 { const searchView = viewsService.getViewWithId(SEARCH_VIEW_ID) as SearchView; for (const result of searchView.model.searchResult.matches()) { if (chatEditingService) { - chatEditingService.globalEditingSessionObs.get()?.addFileToWorkingSet(result.resource); + getEditingSession(chatEditingService, widget)?.addFileToWorkingSet(result.resource); } else { toAttach.push({ id: this._getFileContextId({ resource: result.resource }), @@ -575,7 +575,7 @@ export class AttachContextAction extends Action2 { }, [])); const selectedFiles = await quickInputService.pick(itemsPromise, { placeHolder: localize('relatedFiles', 'Add related files to your working set'), canPickMany: true }); for (const file of selectedFiles ?? []) { - chatEditingService?.globalEditingSessionObs.get()?.addFileToWorkingSet(file.value); + chatEditingService.getEditingSession(chatSessionId)?.addFileToWorkingSet(file.value); } } else if (isScreenshotQuickPickItem(pick)) { const blob = await hostService.getScreenshot(); @@ -778,7 +778,7 @@ export class AttachContextAction extends Action2 { }); } } else if (context.showFilesOnly) { - if (chatEditingService?.hasRelatedFilesProviders() && (widget.getInput() || chatEditingService.globalEditingSessionObs.get()?.workingSet.size)) { + if (chatEditingService?.hasRelatedFilesProviders() && (widget.getInput() || (getEditingSession(chatEditingService, widget)?.workingSet.size))) { quickPickItems.push({ kind: 'related-files', id: 'related-files', @@ -852,7 +852,7 @@ export class AttachContextAction extends Action2 { // Avoid attaching the same context twice const attachedContext = widget.attachmentModel.getAttachmentIDs(); if (chatEditingService) { - for (const [file, state] of chatEditingService.globalEditingSessionObs.get()?.workingSet.entries() ?? []) { + for (const [file, state] of getEditingSession(chatEditingService, widget)?.workingSet.entries() ?? []) { if (state.state !== WorkingSetEntryState.Suggested) { attachedContext.add(this._getFileContextId({ resource: file })); } @@ -1002,3 +1002,10 @@ const selectPromptAttachment = async (options: ISelectPromptOptions): Promise) { @@ -45,8 +49,10 @@ export abstract class EditingSessionAction extends Action2 { } run(accessor: ServicesAccessor, ...args: any[]) { + const context: IEditingSessionActionContext | undefined = args[0]; + const chatEditingService = accessor.get(IChatEditingService); - const chatWidget = accessor.get(IChatWidgetService).lastFocusedWidget; + const chatWidget = context?.widget ?? accessor.get(IChatWidgetService).lastFocusedWidget; if (chatWidget?.location !== ChatAgentLocation.EditingSession || !chatWidget.viewModel) { return; diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts index 2389cf9a3d54..6451b341dcdc 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts @@ -7,17 +7,17 @@ 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 { 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 { IChatWidgetService } from '../chat.js'; +import { IChatWidget, IChatWidgetService } from '../chat.js'; export class ChatRelatedFilesContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'chat.relatedFilesWorkingSet'; - private readonly chatEditingSessionDisposables = new DisposableStore(); + private readonly chatEditingSessionDisposables = new Map(); private _currentRelatedFilesRetrievalOperation: Promise | undefined; constructor( @@ -26,35 +26,29 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben ) { super(); - this._register(autorun(r => { - this.chatEditingSessionDisposables.clear(); - const session = this.chatEditingService.globalEditingSessionObs.read(r); - if (session) { - this._handleNewEditingSession(session); - } - })); + 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); + } + } + }), + ); } - private _updateRelatedFileSuggestions() { + private _updateRelatedFileSuggestions(currentEditingSession: IChatEditingSession, widget: IChatWidget) { if (this._currentRelatedFilesRetrievalOperation) { return; } - const currentEditingSession = this.chatEditingService.globalEditingSessionObs.get(); - if (!currentEditingSession) { - return; - } const workingSetEntries = currentEditingSession.entries.get(); if (workingSetEntries.length > 0) { // Do this only for the initial working set state return; } - const widget = this.chatWidgetService.getWidgetBySessionId(currentEditingSession.chatSessionId); - if (!widget) { - return; - } - this._currentRelatedFilesRetrievalOperation = this.chatEditingService.getRelatedFiles(currentEditingSession.chatSessionId, widget.getInput(), CancellationToken.None) .then((files) => { if (!files?.length) { @@ -98,29 +92,31 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben } - private _handleNewEditingSession(currentEditingSession: IChatEditingSession) { - - const widget = this.chatWidgetService.getWidgetBySessionId(currentEditingSession.chatSessionId); - if (!widget || widget.viewModel?.sessionId !== currentEditingSession.chatSessionId) { - return; - } - this.chatEditingSessionDisposables.add(currentEditingSession.onDidDispose(() => { - this.chatEditingSessionDisposables.clear(); + private _handleNewEditingSession(currentEditingSession: IChatEditingSession, widget: IChatWidget) { + const disposableStore = new DisposableStore(); + disposableStore.add(currentEditingSession.onDidDispose(() => { + disposableStore.clear(); })); - this._updateRelatedFileSuggestions(); + this._updateRelatedFileSuggestions(currentEditingSession, widget); const onDebouncedType = Event.debounce(widget.inputEditor.onDidChangeModelContent, () => null, 3000); - this.chatEditingSessionDisposables.add(onDebouncedType(() => { - this._updateRelatedFileSuggestions(); + disposableStore.add(onDebouncedType(() => { + this._updateRelatedFileSuggestions(currentEditingSession, widget); })); - this.chatEditingSessionDisposables.add(currentEditingSession.onDidChange((e) => { + disposableStore.add(currentEditingSession.onDidChange((e) => { if (e === ChatEditingSessionChangeType.WorkingSet) { - this._updateRelatedFileSuggestions(); + this._updateRelatedFileSuggestions(currentEditingSession, widget); } })); + disposableStore.add(currentEditingSession.onDidDispose(() => { + disposableStore.dispose(); + })); + this.chatEditingSessionDisposables.set(currentEditingSession.chatSessionId, disposableStore); } override dispose() { - this.chatEditingSessionDisposables.dispose(); + for (const store of this.chatEditingSessionDisposables.values()) { + store.dispose(); + } super.dispose(); } } From 0185e3b037e035edb89f976babc6ec40a2d337ac Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 5 Feb 2025 11:45:24 +0100 Subject: [PATCH 1218/3587] Flip the zone widget noMax to be useMax (opt in) (#239672) Per the candidate fix in #239589 Fixes #239578 --- src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts | 4 ++-- .../contrib/comments/browser/commentThreadZoneWidget.ts | 2 +- src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts b/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts index 82c4eb90f567..611fcb7e21c1 100644 --- a/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts +++ b/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts @@ -497,9 +497,9 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { // implement in subclass } - protected _relayout(_newHeightInLines: number, noMax?: boolean): void { + protected _relayout(_newHeightInLines: number, useMax?: boolean): void { const maxHeightInLines = this._getMaximumHeightInLines(); - const newHeightInLines = (!noMax && (maxHeightInLines !== undefined)) ? Math.min(maxHeightInLines, _newHeightInLines) : _newHeightInLines; + const newHeightInLines = (useMax && (maxHeightInLines !== undefined)) ? Math.min(maxHeightInLines, _newHeightInLines) : _newHeightInLines; if (this._viewZone && this._viewZone.heightInLines !== newHeightInLines) { this.editor.changeViewZones(accessor => { if (this._viewZone) { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index d6c4d858d5c7..0039a4030eaa 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -502,7 +502,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } const capture = StableEditorScrollState.capture(this.editor); - this._relayout(computedLinesNumber, true); + this._relayout(computedLinesNumber); capture.restore(this.editor); } } diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 21417a0ea2e0..08611d53fc3e 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -794,7 +794,7 @@ class TestResultsPeek extends PeekViewWidget { const displayed = this._getMaximumHeightInLines(); if (displayed) { - this._relayout(Math.min(displayed, this.getVisibleEditorLines() / 2)); + this._relayout(Math.min(displayed, this.getVisibleEditorLines() / 2), true); if (!contentHeightSettleTimer.isScheduled()) { contentHeightSettleTimer.schedule(); } From 057edbab16852247e3df1b176ed2d20f66f754fb Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 5 Feb 2025 12:26:25 +0100 Subject: [PATCH 1219/3587] add sync vs async test case for error'ing `provideLanguageModelResponse` calls (#239676) https://github.com/microsoft/vscode/issues/235322 --- .../src/singlefolder-tests/lm.test.ts | 36 ++++++++++++--- .../api/browser/mainThreadLanguageModels.ts | 8 +++- .../api/common/extHostLanguageModels.ts | 44 ++++++++++--------- 3 files changed, 61 insertions(+), 27 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts index cc995c08497b..97875753f88c 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts @@ -139,11 +139,34 @@ suite('lm', function () { } }); - test('LanguageModelError instance is not thrown to extensions#235322', async function () { + test('LanguageModelError instance is not thrown to extensions#235322 (SYNC)', async function () { + + disposables.push(vscode.lm.registerChatModelProvider('test-lm', { + provideLanguageModelResponse(_messages, _options, _extensionId, _progress, _token) { + throw vscode.LanguageModelError.Blocked('You have been blocked SYNC'); + }, + async provideTokenCount(_text, _token) { + return 1; + } + }, testProviderOptions)); + + const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); + assert.strictEqual(models.length, 1); + + try { + await models[0].sendRequest([vscode.LanguageModelChatMessage.User('Hello')]); + assert.ok(false, 'EXPECTED error'); + } catch (error) { + assert.ok(error instanceof vscode.LanguageModelError); + assert.strictEqual(error.message, 'You have been blocked SYNC'); + } + }); + + test('LanguageModelError instance is not thrown to extensions#235322 (ASYNC)', async function () { disposables.push(vscode.lm.registerChatModelProvider('test-lm', { async provideLanguageModelResponse(_messages, _options, _extensionId, _progress, _token) { - throw vscode.LanguageModelError.Blocked('You have been blocked'); + throw vscode.LanguageModelError.Blocked('You have been blocked ASYNC'); }, async provideTokenCount(_text, _token) { return 1; @@ -153,17 +176,18 @@ suite('lm', function () { const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); assert.strictEqual(models.length, 1); - let output = ''; + const response = await models[0].sendRequest([vscode.LanguageModelChatMessage.User('Hello')]); + assert.ok(response); + + let output = ''; try { - const response = await models[0].sendRequest([vscode.LanguageModelChatMessage.User('Hello')]); - assert.ok(response); for await (const thing of response.text) { output += thing; } } catch (error) { assert.ok(error instanceof vscode.LanguageModelError); - assert.strictEqual(error.message, 'You have been blocked'); + assert.strictEqual(error.message, 'You have been blocked ASYNC'); } assert.strictEqual(output, ''); }); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index 1b1cb20d72b6..693eab881f2f 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -123,7 +123,13 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { async $tryStartChatRequest(extension: ExtensionIdentifier, providerId: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise { this._logService.trace('[CHAT] request STARTED', extension.value, requestId); - const response = await this._chatProviderService.sendChatRequest(providerId, extension, messages, options, token); + let response: ILanguageModelChatResponse; + try { + response = await this._chatProviderService.sendChatRequest(providerId, extension, messages, options, token); + } catch (err) { + this._logService.error('[CHAT] request FAILED', extension.value, requestId, err); + throw err; + } // !!! IMPORTANT !!! // This method must return before the response is done (has streamed all parts) diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index 7ae740ff9fa5..ff74aa62a75f 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -220,30 +220,34 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { this._proxy.$reportResponsePart(requestId, { index: fragment.index, part }); }); - let p: Promise; + let value: any; - if (data.provider.provideLanguageModelResponse2) { - - p = Promise.resolve(data.provider.provideLanguageModelResponse2( - messages.map(typeConvert.LanguageModelChatMessage.to), - options, - ExtensionIdentifier.toKey(from), - progress, - token - )); + try { + if (data.provider.provideLanguageModelResponse2) { + value = data.provider.provideLanguageModelResponse2( + messages.map(typeConvert.LanguageModelChatMessage.to), + options, + ExtensionIdentifier.toKey(from), + progress, + token + ); - } else { + } else { + value = data.provider.provideLanguageModelResponse( + messages.map(typeConvert.LanguageModelChatMessage.to), + options, + ExtensionIdentifier.toKey(from), + progress, + token + ); + } - p = Promise.resolve(data.provider.provideLanguageModelResponse( - messages.map(typeConvert.LanguageModelChatMessage.to), - options, - ExtensionIdentifier.toKey(from), - progress, - token - )); + } catch (err) { + // synchronously failed + throw err; } - p.then(() => { + Promise.resolve(value).then(() => { this._proxy.$reportResponseDone(requestId, undefined); }, err => { this._proxy.$reportResponseDone(requestId, transformErrorForSerialization(err)); @@ -391,7 +395,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { // error'ing here means that the request could NOT be started/made, e.g. wrong model, no access, etc, but // later the response can fail as well. Those failures are communicated via the stream-object this._pendingRequest.delete(requestId); - throw error; + throw extHostTypes.LanguageModelError.tryDeserialize(error) ?? error; } return res.apiObject; From 7e63e808c98af6078a7f3f78e38503174f71c076 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 5 Feb 2025 13:02:09 +0100 Subject: [PATCH 1220/3587] Fix some disposable leak warnings --- src/vs/workbench/browser/parts/titlebar/menubarControl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 50e683186508..48de66e71071 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -646,7 +646,7 @@ export class CustomMenubarControl extends MenubarControl { title = menuItem.item.toggled.mnemonicTitle ?? menuItem.item.toggled.title ?? title; } - const newAction = new Action(menuItem.id, mnemonicMenuLabel(title), menuItem.class, menuItem.enabled, () => this.commandService.executeCommand(menuItem.id)); + const newAction = this._register(new Action(menuItem.id, mnemonicMenuLabel(title), menuItem.class, menuItem.enabled, () => this.commandService.executeCommand(menuItem.id))); newAction.tooltip = menuItem.tooltip; newAction.checked = menuItem.checked; target.push(newAction); From f7db3121e476b873840c44f0a10c4245a22d9cbb Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 5 Feb 2025 14:08:18 +0100 Subject: [PATCH 1221/3587] debt: do not pass window loggers (#239680) --- src/vs/platform/log/electron-main/loggerService.ts | 6 +++--- .../sharedProcess/electron-main/sharedProcess.ts | 2 +- src/vs/platform/window/common/window.ts | 5 +---- src/vs/platform/windows/electron-main/windowImpl.ts | 5 +---- .../windows/electron-main/windowsMainService.ts | 10 ++-------- src/vs/workbench/electron-sandbox/desktop.main.ts | 5 +---- .../electron-sandbox/workingCopyBackupService.test.ts | 2 +- 7 files changed, 10 insertions(+), 25 deletions(-) diff --git a/src/vs/platform/log/electron-main/loggerService.ts b/src/vs/platform/log/electron-main/loggerService.ts index 5d569519f79a..0291c12e3e1a 100644 --- a/src/vs/platform/log/electron-main/loggerService.ts +++ b/src/vs/platform/log/electron-main/loggerService.ts @@ -26,7 +26,7 @@ export interface ILoggerMainService extends ILoggerService { registerLogger(resource: ILoggerResource, windowId?: number): void; - getRegisteredLoggers(windowId?: number): ILoggerResource[]; + getGlobalLoggers(): ILoggerResource[]; deregisterLoggers(windowId: number): void; @@ -60,10 +60,10 @@ export class LoggerMainService extends LoggerService implements ILoggerMainServi super.deregisterLogger(resource); } - override getRegisteredLoggers(windowId?: number): ILoggerResource[] { + getGlobalLoggers(): ILoggerResource[] { const resources: ILoggerResource[] = []; for (const resource of super.getRegisteredLoggers()) { - if (windowId === this.loggerResourcesByWindow.get(resource.resource)) { + if (!this.loggerResourcesByWindow.has(resource.resource)) { resources.push(resource); } } diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 054f3b7419dc..9e7389c9826b 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -190,7 +190,7 @@ export class SharedProcess extends Disposable { }, args: this.environmentMainService.args, logLevel: this.loggerMainService.getLogLevel(), - loggers: this.loggerMainService.getRegisteredLoggers(), + loggers: this.loggerMainService.getGlobalLoggers(), policiesData: this.policyService.serialize() }; } diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index cec025d66324..105fc2886b3a 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -392,10 +392,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native isInitialStartup?: boolean; logLevel: LogLevel; - loggers: { - global: UriDto[]; - window: UriDto[]; - }; + loggers: UriDto[]; fullscreen?: boolean; maximized?: boolean; diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index f5bf1b62cb9c..a3f0ad85d78a 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -1121,10 +1121,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { home: this.userDataProfilesService.profilesHome }; configuration.logLevel = this.loggerMainService.getLogLevel(); - configuration.loggers = { - window: this.loggerMainService.getRegisteredLoggers(this.id), - global: this.loggerMainService.getRegisteredLoggers() - }; + configuration.loggers = this.loggerMainService.getGlobalLoggers(); // Load config this.load(configuration, { isReload: true, disableExtensions: cli?.['disable-extensions'] }); diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index d569ef654779..ec20394532be 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -1496,10 +1496,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic filesToWait: options.filesToOpen?.filesToWait, logLevel: this.loggerService.getLogLevel(), - loggers: { - window: [], - global: this.loggerService.getRegisteredLoggers() - }, + loggers: this.loggerService.getGlobalLoggers(), logsPath: this.environmentMainService.logsHome.with({ scheme: Schemas.file }).fsPath, product, @@ -1583,10 +1580,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic configuration['extensions-dir'] = currentWindowConfig['extensions-dir']; configuration['disable-extensions'] = currentWindowConfig['disable-extensions']; } - configuration.loggers = { - global: configuration.loggers.global, - window: currentWindowConfig?.loggers.window ?? configuration.loggers.window - }; + configuration.loggers = configuration.loggers; } // Update window identifier and session now diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index 04b054017cf0..a728f602f530 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -205,10 +205,7 @@ export class DesktopMain extends Disposable { serviceCollection.set(INativeWorkbenchEnvironmentService, environmentService); // Logger - const loggers = [ - ...this.configuration.loggers.global.map(loggerResource => ({ ...loggerResource, resource: URI.revive(loggerResource.resource) })), - ...this.configuration.loggers.window.map(loggerResource => ({ ...loggerResource, resource: URI.revive(loggerResource.resource), hidden: true })), - ]; + const loggers = this.configuration.loggers.map(loggerResource => ({ ...loggerResource, resource: URI.revive(loggerResource.resource) })); const loggerService = new LoggerChannelClient(this.configuration.windowId, this.configuration.logLevel, environmentService.windowLogsPath, loggers, mainProcessService.getChannel('logger')); serviceCollection.set(ILoggerService, loggerService); diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts index a6013924b1ba..ca4531327275 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts @@ -58,7 +58,7 @@ const TestNativeWindowConfiguration: INativeWindowConfiguration = { sqmId: 'testSqmId', devDeviceId: 'testdevDeviceId', logLevel: LogLevel.Error, - loggers: { global: [], window: [] }, + loggers: [], mainPid: 0, appRoot: '', userEnv: {}, From bf3a0f0e89f1658cd63afadf56dc3b419ac810ab Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 5 Feb 2025 05:18:03 -0800 Subject: [PATCH 1222/3587] rm fig spec tests, allow multiple template requests Fixes #239606 --- .../src/terminalSuggestMain.ts | 50 ++++++++++++------- .../src/test/completions/code.test.ts | 8 +-- .../src/test/completions/upstream/ls.test.ts | 26 +++++----- .../src/test/completions/upstream/rm.test.ts | 40 +++++++++++++++ .../src/test/terminalSuggestMain.test.ts | 2 + 5 files changed, 90 insertions(+), 36 deletions(-) create mode 100644 extensions/terminal-suggest/src/test/completions/upstream/rm.test.ts diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 7c375d4ba706..fa79d5c378aa 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -240,6 +240,8 @@ export async function getCompletionItemsFromSpecs( let foldersRequested = false; const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1); + // TODO: Normalize precedingText to ignore file extensions on Windows + // precedingText = precedingText.replace('.cmd', ''); let specificItemsProvided = false; for (const spec of specs) { @@ -251,6 +253,8 @@ export async function getCompletionItemsFromSpecs( for (const specLabel of specLabels) { const availableCommand = availableCommands.find(command => specLabel === command.label); + // TODO: Normalize commands to ignore file extensions on Windows https://github.com/microsoft/vscode/issues/237598 + // const availableCommand = availableCommands.find(command => command.label.startsWith(specLabel)); if (!availableCommand || (token && token.isCancellationRequested)) { continue; } @@ -263,27 +267,29 @@ export async function getCompletionItemsFromSpecs( continue; } + // TODO: Normalize commands to ignore file extensions on Windows https://github.com/microsoft/vscode/issues/237598 + // const commandAndAliases = availableCommands.filter(command => specLabel === (command.definitionCommand ?? command.label).replace('.cmd', '')); + // if (!commandAndAliases.some(e => terminalContext.commandLine.startsWith(`${e.label} `) || terminalContext.commandLine.startsWith(`${e.label}.cmd `))) { const commandAndAliases = availableCommands.filter(command => specLabel === (command.definitionCommand ?? command.label)); if (!commandAndAliases.some(e => terminalContext.commandLine.startsWith(`${e.label} `))) { // the spec label is not the first word in the command line, so do not provide options or args continue; } - const argsCompletionResult = handleArguments(specLabel, spec, terminalContext, precedingText); - if (argsCompletionResult) { - items.push(...argsCompletionResult.items); - filesRequested ||= argsCompletionResult.filesRequested; - foldersRequested ||= argsCompletionResult.foldersRequested; - specificItemsProvided ||= argsCompletionResult.items.length > 0; + const optionsCompletionResult = handleOptions(specLabel, spec, terminalContext, precedingText, prefix); + if (optionsCompletionResult) { + items.push(...optionsCompletionResult.items); + filesRequested ||= optionsCompletionResult.filesRequested; + foldersRequested ||= optionsCompletionResult.foldersRequested; + specificItemsProvided ||= optionsCompletionResult.items.length > 0; } - if (!argsCompletionResult?.items.length) { - // Arg completions are more specific, only get options if those are not provided. - const optionsCompletionResult = handleOptions(specLabel, spec, terminalContext, precedingText, prefix); - if (optionsCompletionResult) { - items.push(...optionsCompletionResult.items); - filesRequested ||= optionsCompletionResult.filesRequested; - foldersRequested ||= optionsCompletionResult.foldersRequested; - specificItemsProvided ||= optionsCompletionResult.items.length > 0; + if (!optionsCompletionResult?.isOptionArg) { + const argsCompletionResult = handleArguments(specLabel, spec, terminalContext, precedingText); + if (argsCompletionResult) { + items.push(...argsCompletionResult.items); + filesRequested ||= argsCompletionResult.filesRequested; + foldersRequested ||= argsCompletionResult.foldersRequested; + specificItemsProvided ||= argsCompletionResult.items.length > 0; } } } @@ -341,7 +347,7 @@ function handleArguments(specLabel: string, spec: Fig.Spec, terminalContext: { c return argsCompletions; } -function handleOptions(specLabel: string, spec: Fig.Spec, terminalContext: { commandLine: string; cursorPosition: number }, precedingText: string, prefix: string): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean } | undefined { +function handleOptions(specLabel: string, spec: Fig.Spec, terminalContext: { commandLine: string; cursorPosition: number }, precedingText: string, prefix: string): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; isOptionArg: boolean } | undefined { let options; if ('options' in spec && spec.options) { options = spec.options; @@ -387,12 +393,12 @@ function handleOptions(specLabel: string, spec: Fig.Spec, terminalContext: { com const argsCompletions = getCompletionItemsFromArgs(option.args, currentPrefix, terminalContext); if (argsCompletions) { - return { items: argsCompletions.items, filesRequested: argsCompletions.filesRequested, foldersRequested: argsCompletions.foldersRequested }; + return { items: argsCompletions.items, filesRequested: argsCompletions.filesRequested, foldersRequested: argsCompletions.foldersRequested, isOptionArg: true }; } } } - return { items: optionItems, filesRequested: false, foldersRequested: false }; + return { items: optionItems, filesRequested: false, foldersRequested: false, isOptionArg: false }; } @@ -409,11 +415,17 @@ function getCompletionItemsFromArgs(args: Fig.SingleOrArray | undefined continue; } if (arg.template) { - if (arg.template === 'filepaths') { + if (Array.isArray(arg.template) ? arg.template.includes('filepaths') : arg.template === 'filepaths') { filesRequested = true; - } else if (arg.template === 'folders') { + } + if (Array.isArray(arg.template) ? arg.template.includes('folders') : arg.template === 'folders') { foldersRequested = true; } + // if (arg.template === 'filepaths') { + // filesRequested = true; + // } else if (arg.template === 'folders') { + // foldersRequested = true; + // } } if (arg.suggestions?.length) { // there are specific suggestions to show diff --git a/extensions/terminal-suggest/src/test/completions/code.test.ts b/extensions/terminal-suggest/src/test/completions/code.test.ts index f65cd2e4d5d6..50ad0458a224 100644 --- a/extensions/terminal-suggest/src/test/completions/code.test.ts +++ b/extensions/terminal-suggest/src/test/completions/code.test.ts @@ -25,7 +25,7 @@ export function createCodeTestSpecs(executable: string): ITestSpec[] { ...typingTests, // Basic arguments - { input: `${executable} |`, expectedCompletions: codeOptions }, + { input: `${executable} |`, expectedCompletions: codeOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, { input: `${executable} --locale |`, expectedCompletions: localeOptions }, { input: `${executable} --diff |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } }, { input: `${executable} --diff ./file1 |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } }, @@ -40,13 +40,13 @@ export function createCodeTestSpecs(executable: string): ITestSpec[] { { input: `${executable} --log |`, expectedCompletions: logOptions }, { input: `${executable} --sync |`, expectedCompletions: syncOptions }, { input: `${executable} --extensions-dir |`, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, - { input: `${executable} --list-extensions |`, expectedCompletions: codeOptions }, - { input: `${executable} --show-versions |`, expectedCompletions: codeOptions }, + { input: `${executable} --list-extensions |`, expectedCompletions: codeOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: `${executable} --show-versions |`, expectedCompletions: codeOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, { input: `${executable} --category |`, expectedCompletions: categoryOptions }, { input: `${executable} --category a|`, expectedCompletions: categoryOptions.filter(c => c.startsWith('a')) }, // Middle of command - { input: `${executable} | --locale`, expectedCompletions: codeOptions }, + { input: `${executable} | --locale`, expectedCompletions: codeOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, ]; } diff --git a/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts b/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts index dfd7da8a93e9..36ea72e40835 100644 --- a/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts +++ b/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts @@ -67,27 +67,27 @@ export const lsTestSuiteSpec: ISuiteSpec = { // Basic options // TODO: The spec wants file paths and folders (which seems like it should only be folders), // but neither are requested https://github.com/microsoft/vscode/issues/239606 - { input: 'ls |', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } - { input: 'ls -|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } + { input: 'ls |', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'ls -|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, // Filtering options should request all options so client side can filter - { input: 'ls -a|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } + { input: 'ls -a|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, // Duplicate option // TODO: Duplicate options should not be presented https://github.com/microsoft/vscode/issues/239607 - // { input: 'ls -a -|', expectedCompletions: removeArrayEntry(allOptions, '-a'), expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } + // { input: 'ls -a -|', expectedCompletions: removeArrayEntry(allOptions, '-a'), expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, // Relative paths - { input: 'ls c|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } - { input: 'ls child|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } - { input: 'ls .|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } - { input: 'ls ./|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } - { input: 'ls ./child|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } - { input: 'ls ..|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwd } + { input: 'ls c|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'ls child|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'ls .|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'ls ./|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'ls ./child|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'ls ..|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, // Relative directories (changes cwd due to /) - { input: 'ls child/|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwdChild } - { input: 'ls ../|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwdParent } - { input: 'ls ../sibling|', expectedCompletions: allOptions, expectedResourceRequests: undefined }, // { type: 'folders', cwd: testPaths.cwdParent } + { input: 'ls child/|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwdChild } }, + { input: 'ls ../|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwdParent } }, + { input: 'ls ../sibling|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwdParent } }, ] }; diff --git a/extensions/terminal-suggest/src/test/completions/upstream/rm.test.ts b/extensions/terminal-suggest/src/test/completions/upstream/rm.test.ts new file mode 100644 index 000000000000..ad025c27a7b2 --- /dev/null +++ b/extensions/terminal-suggest/src/test/completions/upstream/rm.test.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import { testPaths, type ISuiteSpec } from '../../helpers'; +import rmSpec from '../../../completions/upstream/rm'; + +const allOptions = [ + '-P', + '-R', + '-d', + '-f', + '-i', + '-r', + '-v', +]; + +export const rmTestSuiteSpec: ISuiteSpec = { + name: 'rm', + completionSpecs: rmSpec, + availableCommands: 'rm', + testSpecs: [ + // Empty input + { input: '|', expectedCompletions: ['rm'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + + // Typing the command + { input: 'r|', expectedCompletions: ['rm'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'rm|', expectedCompletions: ['rm'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + + // Basic options + { input: 'rm |', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + + // Duplicate option + // TODO: Duplicate options should not be presented https://github.com/microsoft/vscode/issues/239607 + // { input: `rm -${allOptions[0]} -|`, expectedCompletions: removeArrayEntries(allOptions, allOptions[0]) }, + // { input: `rm -${allOptions[0]} -${allOptions[1]} -|`, expectedCompletions: removeArrayEntries(allOptions, allOptions[0], allOptions[1]) }, + ] +}; diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index a79f1c7432af..0cdf23b70d9a 100644 --- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -15,6 +15,7 @@ import { codeInsidersTestSuite } from './completions/code-insiders.test'; import { lsTestSuiteSpec } from './completions/upstream/ls.test'; import { echoTestSuiteSpec } from './completions/upstream/echo.test'; import { mkdirTestSuiteSpec } from './completions/upstream/mkdir.test'; +import { rmTestSuiteSpec } from './completions/upstream/rm.test'; const testSpecs2: ISuiteSpec[] = [ { @@ -38,6 +39,7 @@ const testSpecs2: ISuiteSpec[] = [ echoTestSuiteSpec, lsTestSuiteSpec, mkdirTestSuiteSpec, + rmTestSuiteSpec, ]; suite('Terminal Suggest', () => { From b1cf1fd15101d61d795decfca87b3e9cf527cfdf Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 5 Feb 2025 05:23:14 -0800 Subject: [PATCH 1223/3587] rmdir and touch fig spec tests --- .../src/terminalSuggestMain.ts | 5 --- .../test/completions/upstream/rmdir.test.ts | 29 +++++++++++++++ .../test/completions/upstream/touch.test.ts | 36 +++++++++++++++++++ .../src/test/terminalSuggestMain.test.ts | 4 +++ 4 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 extensions/terminal-suggest/src/test/completions/upstream/rmdir.test.ts create mode 100644 extensions/terminal-suggest/src/test/completions/upstream/touch.test.ts diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index fa79d5c378aa..685ed8410056 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -421,11 +421,6 @@ function getCompletionItemsFromArgs(args: Fig.SingleOrArray | undefined if (Array.isArray(arg.template) ? arg.template.includes('folders') : arg.template === 'folders') { foldersRequested = true; } - // if (arg.template === 'filepaths') { - // filesRequested = true; - // } else if (arg.template === 'folders') { - // foldersRequested = true; - // } } if (arg.suggestions?.length) { // there are specific suggestions to show diff --git a/extensions/terminal-suggest/src/test/completions/upstream/rmdir.test.ts b/extensions/terminal-suggest/src/test/completions/upstream/rmdir.test.ts new file mode 100644 index 000000000000..e77092edb68b --- /dev/null +++ b/extensions/terminal-suggest/src/test/completions/upstream/rmdir.test.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import { testPaths, type ISuiteSpec } from '../../helpers'; +import rmdirSpec from '../../../completions/upstream/rmdir'; + +const allOptions = [ + '-p', +]; + +export const rmdirTestSuiteSpec: ISuiteSpec = { + name: 'rmdir', + completionSpecs: rmdirSpec, + availableCommands: 'rmdir', + testSpecs: [ + // Empty input + { input: '|', expectedCompletions: ['rmdir'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + + // Typing the command + { input: 'r|', expectedCompletions: ['rmdir'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'rmdir|', expectedCompletions: ['rmdir'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + + // Basic options + { input: 'rmdir |', expectedCompletions: allOptions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, + ] +}; diff --git a/extensions/terminal-suggest/src/test/completions/upstream/touch.test.ts b/extensions/terminal-suggest/src/test/completions/upstream/touch.test.ts new file mode 100644 index 000000000000..da70835a5932 --- /dev/null +++ b/extensions/terminal-suggest/src/test/completions/upstream/touch.test.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import { testPaths, type ISuiteSpec } from '../../helpers'; +import touchSpec from '../../../completions/upstream/touch'; + +const allOptions = [ + '-A', + '-a', + '-c', + '-f', + '-h', + '-m', + '-r', + '-t', +]; + +export const touchTestSuiteSpec: ISuiteSpec = { + name: 'touch', + completionSpecs: touchSpec, + availableCommands: 'touch', + testSpecs: [ + // Empty input + { input: '|', expectedCompletions: ['touch'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + + // Typing the command + { input: 't|', expectedCompletions: ['touch'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'touch|', expectedCompletions: ['touch'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + + // Basic options + { input: 'touch |', expectedCompletions: allOptions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, + ] +}; diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index 0cdf23b70d9a..eca864c9727c 100644 --- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -16,6 +16,8 @@ import { lsTestSuiteSpec } from './completions/upstream/ls.test'; import { echoTestSuiteSpec } from './completions/upstream/echo.test'; import { mkdirTestSuiteSpec } from './completions/upstream/mkdir.test'; import { rmTestSuiteSpec } from './completions/upstream/rm.test'; +import { rmdirTestSuiteSpec } from './completions/upstream/rmdir.test'; +import { touchTestSuiteSpec } from './completions/upstream/touch.test'; const testSpecs2: ISuiteSpec[] = [ { @@ -40,6 +42,8 @@ const testSpecs2: ISuiteSpec[] = [ lsTestSuiteSpec, mkdirTestSuiteSpec, rmTestSuiteSpec, + rmdirTestSuiteSpec, + touchTestSuiteSpec, ]; suite('Terminal Suggest', () => { From fb31b5b05f7f980fd1e36dc8adc35947c065ced7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 5 Feb 2025 05:41:22 -0800 Subject: [PATCH 1224/3587] Don't treat dotfiles with a single period as having an extension --- .../services/suggest/browser/simpleCompletionItem.ts | 3 ++- .../suggest/test/browser/simpleCompletionModel.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts index b1301167f1a0..d3590fbf873c 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts @@ -101,8 +101,9 @@ export class SimpleCompletionItem { if (isWindows) { this.labelLow = this.labelLow.replaceAll('/', '\\'); } + // Don't include dotfiles as extensions when sorting const extIndex = this.labelLow.lastIndexOf('.'); - if (extIndex !== -1) { + if (extIndex > 0) { this.labelLowExcludeFileExt = this.labelLow.substring(0, extIndex); this.fileExtLow = this.labelLow.substring(extIndex + 1); } diff --git a/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts b/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts index 706c1fc1f30b..216f8c91b78a 100644 --- a/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts +++ b/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts @@ -157,8 +157,6 @@ suite('SimpleCompletionModel', function () { '.configurations', 'CONTRIBUTING.md', '.devcontainer', - '.npmrc', - '.gitignore', '.editorconfig', 'eslint.config.js', '.eslint-ignore', @@ -167,13 +165,15 @@ suite('SimpleCompletionModel', function () { '.gitattributes', '.git-blame-ignore-revs', '.github', + '.gitignore', 'gulpfile.js', 'LICENSE.txt', '.lsifrc.json', - '.nvmrc', '.mailmap', '.mention-bot', 'node_modules', + '.npmrc', + '.nvmrc', 'out', 'package.json', 'package-lock.json', From 18946eca523fd295e8971b37bc11cc479c087427 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 5 Feb 2025 14:45:23 +0100 Subject: [PATCH 1225/3587] skip integration test that needs latest insiders (#239681) --- extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts index 97875753f88c..ed00dc790426 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts @@ -139,7 +139,8 @@ suite('lm', function () { } }); - test('LanguageModelError instance is not thrown to extensions#235322 (SYNC)', async function () { + // SKIPPED until Feb 6 2025 + test.skip('LanguageModelError instance is not thrown to extensions#235322 (SYNC)', async function () { disposables.push(vscode.lm.registerChatModelProvider('test-lm', { provideLanguageModelResponse(_messages, _options, _extensionId, _progress, _token) { From feb20a3698337c90f3017deee4e924754cade8ea Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 5 Feb 2025 15:11:51 +0100 Subject: [PATCH 1226/3587] migrate undo/redo actions (#239682) https://github.com/microsoft/vscode-copilot/issues/12820 --- .../chat/browser/actions/chatClearActions.ts | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index 52c81f2fbf39..2efaaef704b9 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -17,7 +17,7 @@ import { IViewsService } from '../../../../services/views/common/viewsService.js import { isChatViewTitleActionContext } from '../../common/chatActions.js'; import { ChatAgentLocation } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingSession, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { ChatViewId, EditsViewId, IChatWidget, IChatWidgetService } from '../chat.js'; import { EditingSessionAction } from '../chatEditing/chatEditingActions.js'; import { ctxIsGlobalEditingSession } from '../chatEditing/chatEditingEditorController.js'; @@ -252,7 +252,7 @@ export function registerNewChatActions() { } }); - registerAction2(class UndoChatEditInteractionAction extends Action2 { + registerAction2(class UndoChatEditInteractionAction extends EditingSessionAction { constructor() { super({ id: 'workbench.action.chat.undoEdit', @@ -270,17 +270,12 @@ export function registerNewChatActions() { }); } - async run(accessor: ServicesAccessor, ...args: any[]) { - const chatEditingService = accessor.get(IChatEditingService); - const currentEditingSession = chatEditingService.globalEditingSession; - if (!currentEditingSession) { - return; - } - await currentEditingSession.undoInteraction(); + async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession) { + await editingSession.undoInteraction(); } }); - registerAction2(class RedoChatEditInteractionAction extends Action2 { + registerAction2(class RedoChatEditInteractionAction extends EditingSessionAction { constructor() { super({ id: 'workbench.action.chat.redoEdit', @@ -298,13 +293,8 @@ export function registerNewChatActions() { }); } - async run(accessor: ServicesAccessor, ...args: any[]) { - const chatEditingService = accessor.get(IChatEditingService); - const currentEditingSession = chatEditingService.globalEditingSession; - if (!currentEditingSession) { - return; - } - await currentEditingSession.redoInteraction(); + async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession) { + await editingSession.redoInteraction(); } }); From ea87a7360231b204142eebd946dcc8a995673afe Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:38:43 +0100 Subject: [PATCH 1227/3587] Better approximation of `fitsInsideViewport` for side by side (#239685) fixes https://github.com/microsoft/vscode-copilot/issues/12831 --- .../view/inlineEdits/sideBySideDiff.ts | 8 ++-- .../browser/view/inlineEdits/view.ts | 44 +++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts index cd78afcce01b..c94d6c4eaf35 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts @@ -110,19 +110,19 @@ const ENABLE_OVERFLOW = false; export class InlineEditsSideBySideDiff extends Disposable implements IInlineEditsView { // This is an approximation and should be improved by using the real parameters used bellow - static fitsInsideViewport(editor: ICodeEditor, edit: InlineEditWithChanges, reader: IReader): boolean { + static fitsInsideViewport(editor: ICodeEditor, edit: InlineEditWithChanges, originalDisplayRange: LineRange, reader: IReader): boolean { const editorObs = observableCodeEditor(editor); const editorWidth = editorObs.layoutInfoWidth.read(reader); const editorContentLeft = editorObs.layoutInfoContentLeft.read(reader); - const editorVerticalScrollBar = editor.getLayoutInfo().verticalScrollbarWidth; + const editorVerticalScrollbar = editor.getLayoutInfo().verticalScrollbarWidth; const w = editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; - const maxOriginalContent = maxContentWidthInRange(editorObs, edit.originalLineRange, undefined/* do not reconsider on each layout info change */); + const maxOriginalContent = maxContentWidthInRange(editorObs, originalDisplayRange, undefined/* do not reconsider on each layout info change */); const maxModifiedContent = edit.lineEdit.newLines.reduce((max, line) => Math.max(max, line.length * w), 0); const endOfEditorPadding = 20; // padding after last line of editor const editorsPadding = edit.modifiedLineRange.length <= edit.originalLineRange.length ? PADDING * 3 + endOfEditorPadding : 60 + endOfEditorPadding * 2; - return maxOriginalContent + maxModifiedContent + editorsPadding < editorWidth - editorContentLeft - editorVerticalScrollBar; + return maxOriginalContent + maxModifiedContent + editorsPadding < editorWidth - editorContentLeft - editorVerticalScrollbar; } private readonly _editorObs = observableCodeEditor(this._editor); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index d55b24fb5704..1ec946c6dec1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -73,7 +73,13 @@ export class InlineEditsView extends Disposable { let newText = edit.edit.apply(edit.originalText); let diff = lineRangeMappingFromRangeMappings(mappings, edit.originalText, new StringText(newText)); - const state = this.determineRenderState(edit, reader, diff, new StringText(newText)); + const originalDisplayRange = edit.originalText.lineRange.intersect( + edit.originalLineRange.join( + LineRange.ofLength(edit.originalLineRange.startLineNumber, edit.lineEdit.newLines.length) + ) + )!; + + const state = this.determineRenderState(edit, reader, diff, new StringText(newText), originalDisplayRange); if (!state) { this._model.get()?.stop(); return undefined; @@ -87,12 +93,6 @@ export class InlineEditsView extends Disposable { diff = lineRangeMappingFromRangeMappings(mappings, edit.originalText, new StringText(newText)); } - const originalDisplayRange = edit.originalText.lineRange.intersect( - edit.originalLineRange.join( - LineRange.ofLength(edit.originalLineRange.startLineNumber, edit.lineEdit.newLines.length) - ) - )!; - this._previewTextModel.setLanguage(this._editor.getModel()!.getLanguageId()); const previousNewText = this._previewTextModel.getValue(); @@ -196,20 +196,21 @@ export class InlineEditsView extends Disposable { return 0; }); - private readonly _originalDisplayRange = derived(this, reader => { - const state = this._uiState.read(reader); - if (state?.state?.kind === 'insertionMultiLine') { - return this._insertion.originalLines.read(reader); - } - return state?.originalDisplayRange; - }); - protected readonly _indicator = this._register(autorunWithStore((reader, store) => { + + const indicatorDisplayRange = derived(this, reader => { + const state = this._uiState.read(reader); + if (state?.state?.kind === 'insertionMultiLine') { + return this._insertion.originalLines.read(reader); + } + return state?.originalDisplayRange; + }); + if (this._useGutterIndicator.read(reader)) { store.add(this._instantiationService.createInstance( InlineEditsGutterIndicator, this._editorObs, - this._originalDisplayRange, + indicatorDisplayRange, this._gutterIndicatorOffset, this._model, this._inlineEditsIsHovered, @@ -220,7 +221,7 @@ export class InlineEditsView extends Disposable { this._editorObs, derived(reader => { const state = this._uiState.read(reader); - const range = this._originalDisplayRange.read(reader); + const range = indicatorDisplayRange.read(reader); if (!state || !state.state || !range) { return undefined; } const top = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader) + this._gutterIndicatorOffset.read(reader); return { editTop: top, showAlways: state.state.kind !== 'sideBySide' }; @@ -230,7 +231,7 @@ export class InlineEditsView extends Disposable { } })); - private determineView(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText): string { + private determineView(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText, originalDisplayRange: LineRange): string { // Check if we can use the previous view if it is the same InlineCompletion as previously shown const canUseCache = this._previousView?.id === edit.inlineCompletion.id; const reconsiderViewAfterJump = edit.userJumpedToIt !== this._previousView?.userJumpedToIt && @@ -283,9 +284,8 @@ export class InlineEditsView extends Disposable { return 'wordReplacements'; } } - if (numOriginalLines > 0 && numModifiedLines > 0) { - if (this._renderSideBySide.read(reader) !== 'never' && InlineEditsSideBySideDiff.fitsInsideViewport(this._editor, edit, reader)) { + if (this._renderSideBySide.read(reader) !== 'never' && InlineEditsSideBySideDiff.fitsInsideViewport(this._editor, edit, originalDisplayRange, reader)) { return 'sideBySide'; } @@ -306,9 +306,9 @@ export class InlineEditsView extends Disposable { return 'sideBySide'; } - private determineRenderState(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText) { + private determineRenderState(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText, originalDisplayRange: LineRange) { - const view = this.determineView(edit, reader, diff, newText); + const view = this.determineView(edit, reader, diff, newText, originalDisplayRange); this._previousView = { id: edit.inlineCompletion.id, view, userJumpedToIt: edit.userJumpedToIt, editorWidth: this._editor.getLayoutInfo().width }; From 297a228fb8e551e004f7b2d18c188293cb5a93f4 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:41:30 +0100 Subject: [PATCH 1228/3587] Git - refactor delete remote tag (#239684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Git - refactor delete remote tag * 💄 fix parameter name * 💄 add missing parameter name --- extensions/git/src/commands.ts | 2 +- extensions/git/src/git.ts | 10 ++++++++-- extensions/git/src/operation.ts | 8 ++++---- extensions/git/src/repository.ts | 4 ++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index b7a1a85bb4f7..497edcb9e8e4 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -178,7 +178,7 @@ class RemoteTagDeleteItem extends RefItem { async run(repository: Repository, remote: string): Promise { if (this.ref.name) { - await repository.deleteRemoteTag(remote, this.ref.name); + await repository.deleteRemoteRef(remote, this.ref.name); } } } diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index b6d5582b8fb6..2b32add17ce1 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1927,8 +1927,14 @@ export class Repository { await this.exec(args); } - async deleteRemoteTag(remoteName: string, tagName: string): Promise { - const args = ['push', '--delete', remoteName, tagName]; + async deleteRemoteRef(remoteName: string, refName: string, options?: { force?: boolean }): Promise { + const args = ['push', remoteName, '--delete']; + + if (options?.force) { + args.push('--force'); + } + + args.push(refName); await this.exec(args); } diff --git a/extensions/git/src/operation.ts b/extensions/git/src/operation.ts index 65a932f77020..4946c70d9665 100644 --- a/extensions/git/src/operation.ts +++ b/extensions/git/src/operation.ts @@ -20,7 +20,7 @@ export const enum OperationKind { Config = 'Config', DeleteBranch = 'DeleteBranch', DeleteRef = 'DeleteRef', - DeleteRemoteTag = 'DeleteRemoteTag', + DeleteRemoteRef = 'DeleteRemoteRef', DeleteTag = 'DeleteTag', Diff = 'Diff', Fetch = 'Fetch', @@ -67,7 +67,7 @@ export const enum OperationKind { export type Operation = AddOperation | ApplyOperation | BlameOperation | BranchOperation | CheckIgnoreOperation | CherryPickOperation | CheckoutOperation | CheckoutTrackingOperation | CleanOperation | CommitOperation | ConfigOperation | DeleteBranchOperation | - DeleteRefOperation | DeleteRemoteTagOperation | DeleteTagOperation | DiffOperation | FetchOperation | FindTrackingBranchesOperation | + DeleteRefOperation | DeleteRemoteRefOperation | DeleteTagOperation | DiffOperation | FetchOperation | FindTrackingBranchesOperation | GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetObjectFilesOperation | GetRefsOperation | GetRemoteRefsOperation | HashObjectOperation | IgnoreOperation | LogOperation | LogFileOperation | MergeOperation | MergeAbortOperation | MergeBaseOperation | MergeContinueOperation | MoveOperation | PostCommitCommandOperation | PullOperation | PushOperation | RemoteOperation | @@ -89,7 +89,7 @@ export type CommitOperation = BaseOperation & { kind: OperationKind.Commit }; export type ConfigOperation = BaseOperation & { kind: OperationKind.Config }; export type DeleteBranchOperation = BaseOperation & { kind: OperationKind.DeleteBranch }; export type DeleteRefOperation = BaseOperation & { kind: OperationKind.DeleteRef }; -export type DeleteRemoteTagOperation = BaseOperation & { kind: OperationKind.DeleteRemoteTag }; +export type DeleteRemoteRefOperation = BaseOperation & { kind: OperationKind.DeleteRemoteRef }; export type DeleteTagOperation = BaseOperation & { kind: OperationKind.DeleteTag }; export type DiffOperation = BaseOperation & { kind: OperationKind.Diff }; export type FetchOperation = BaseOperation & { kind: OperationKind.Fetch }; @@ -147,7 +147,7 @@ export const Operation = { Config: (readOnly: boolean) => ({ kind: OperationKind.Config, blocking: false, readOnly, remote: false, retry: false, showProgress: false } as ConfigOperation), DeleteBranch: { kind: OperationKind.DeleteBranch, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteBranchOperation, DeleteRef: { kind: OperationKind.DeleteRef, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteRefOperation, - DeleteRemoteTag: { kind: OperationKind.DeleteRemoteTag, blocking: false, readOnly: false, remote: true, retry: false, showProgress: true } as DeleteRemoteTagOperation, + DeleteRemoteRef: { kind: OperationKind.DeleteRemoteRef, blocking: false, readOnly: false, remote: true, retry: false, showProgress: true } as DeleteRemoteRefOperation, DeleteTag: { kind: OperationKind.DeleteTag, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteTagOperation, Diff: { kind: OperationKind.Diff, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as DiffOperation, Fetch: (showProgress: boolean) => ({ kind: OperationKind.Fetch, blocking: false, readOnly: false, remote: true, retry: true, showProgress } as FetchOperation), diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 560477c586d5..f659d1089c58 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1667,8 +1667,8 @@ export class Repository implements Disposable { await this.run(Operation.DeleteTag, () => this.repository.deleteTag(name)); } - async deleteRemoteTag(remoteName: string, tagName: string): Promise { - await this.run(Operation.DeleteRemoteTag, () => this.repository.deleteRemoteTag(remoteName, tagName)); + async deleteRemoteRef(remoteName: string, refName: string, options?: { force?: boolean }): Promise { + await this.run(Operation.DeleteRemoteRef, () => this.repository.deleteRemoteRef(remoteName, refName, options)); } async checkout(treeish: string, opts?: { detached?: boolean; pullBeforeCheckout?: boolean }): Promise { From cb3bc7b639ac3077f247ea63141a5a03ee5fa98f Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 5 Feb 2025 15:49:50 +0100 Subject: [PATCH 1229/3587] more `EditingSessionAction` adoption (#239686) --- .../chat/browser/actions/chatExecuteActions.ts | 15 ++++++--------- .../browser/chatEditing/chatEditingActions.ts | 14 +++++--------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 4931801e3048..ca3e4ca57282 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -6,12 +6,12 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { URI } from '../../../../../base/common/uri.js'; +import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { localize, localize2 } from '../../../../../nls.js'; import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; -import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; @@ -95,7 +95,8 @@ export interface IToggleAgentModeArgs { agentMode: boolean; } -export class ToggleAgentModeAction extends Action2 { +export class ToggleAgentModeAction extends EditingSessionAction { + static readonly ID = ToggleAgentModeActionId; constructor() { @@ -132,20 +133,16 @@ export class ToggleAgentModeAction extends Action2 { }); } - override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + override async runEditingSessionAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]) { + const agentService = accessor.get(IChatAgentService); - const chatEditingService = accessor.get(IChatEditingService); const chatService = accessor.get(IChatService); const commandService = accessor.get(ICommandService); const dialogService = accessor.get(IDialogService); - const currentEditingSession = chatEditingService.globalEditingSession; - if (!currentEditingSession) { - return; - } const entries = currentEditingSession.entries.get(); if (entries.length > 0 && entries.some(entry => entry.state.get() === WorkingSetEntryState.Modified)) { - if (!await discardAllEditsWithConfirmation(accessor)) { + if (!await discardAllEditsWithConfirmation(accessor, currentEditingSession)) { // User cancelled return; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 6cf5b9ae1b1b..74c5d98582a6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -314,7 +314,7 @@ export class ChatEditingAcceptAllAction extends EditingSessionAction { } registerAction2(ChatEditingAcceptAllAction); -export class ChatEditingDiscardAllAction extends Action2 { +export class ChatEditingDiscardAllAction extends EditingSessionAction { constructor() { super({ @@ -345,19 +345,15 @@ export class ChatEditingDiscardAllAction extends Action2 { }); } - async run(accessor: ServicesAccessor, ...args: any[]): Promise { - await discardAllEditsWithConfirmation(accessor); + override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]) { + await discardAllEditsWithConfirmation(accessor, editingSession); } } registerAction2(ChatEditingDiscardAllAction); -export async function discardAllEditsWithConfirmation(accessor: ServicesAccessor): Promise { - const chatEditingService = accessor.get(IChatEditingService); +export async function discardAllEditsWithConfirmation(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession): Promise { + const dialogService = accessor.get(IDialogService); - const currentEditingSession = chatEditingService.globalEditingSession; - if (!currentEditingSession) { - return false; - } // Ask for confirmation if there are any edits const entries = currentEditingSession.entries.get(); From a563227a21c235f16a6daa33a1f2e9efae253d9c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 5 Feb 2025 07:23:03 -0800 Subject: [PATCH 1230/3587] Use fig on windows regardless of file extension Fixes #237598 --- .../src/terminalSuggestMain.ts | 34 +++++++++++++------ .../src/test/completions/code.test.ts | 11 +++--- .../src/test/terminalSuggestMain.test.ts | 24 ++++++++++++- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 685ed8410056..71bcfba77ca8 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -239,9 +239,15 @@ export async function getCompletionItemsFromSpecs( let filesRequested = false; let foldersRequested = false; - const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1); - // TODO: Normalize precedingText to ignore file extensions on Windows - // precedingText = precedingText.replace('.cmd', ''); + let precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1); + if (isWindows) { + const spaceIndex = precedingText.indexOf(' '); + const commandEndIndex = spaceIndex === -1 ? precedingText.length : spaceIndex; + const lastDotIndex = precedingText.lastIndexOf('.', commandEndIndex); + if (lastDotIndex > 0) { // Don't treat dotfiles as extensions + precedingText = precedingText.substring(0, lastDotIndex) + precedingText.substring(spaceIndex); + } + } let specificItemsProvided = false; for (const spec of specs) { @@ -252,9 +258,9 @@ export async function getCompletionItemsFromSpecs( } for (const specLabel of specLabels) { - const availableCommand = availableCommands.find(command => specLabel === command.label); - // TODO: Normalize commands to ignore file extensions on Windows https://github.com/microsoft/vscode/issues/237598 - // const availableCommand = availableCommands.find(command => command.label.startsWith(specLabel)); + const availableCommand = (isWindows + ? availableCommands.find(command => command.label.match(new RegExp(`${specLabel}(\\.[^ ]+)?$`))) + : availableCommands.find(command => command.label.startsWith(specLabel))); if (!availableCommand || (token && token.isCancellationRequested)) { continue; } @@ -267,11 +273,14 @@ export async function getCompletionItemsFromSpecs( continue; } - // TODO: Normalize commands to ignore file extensions on Windows https://github.com/microsoft/vscode/issues/237598 - // const commandAndAliases = availableCommands.filter(command => specLabel === (command.definitionCommand ?? command.label).replace('.cmd', '')); - // if (!commandAndAliases.some(e => terminalContext.commandLine.startsWith(`${e.label} `) || terminalContext.commandLine.startsWith(`${e.label}.cmd `))) { - const commandAndAliases = availableCommands.filter(command => specLabel === (command.definitionCommand ?? command.label)); - if (!commandAndAliases.some(e => terminalContext.commandLine.startsWith(`${e.label} `))) { + const commandAndAliases = (isWindows + ? availableCommands.filter(command => specLabel === removeAnyFileExtension(command.definitionCommand ?? command.label)) + : availableCommands.filter(command => specLabel === (command.definitionCommand ?? command.label))); + if ( + !(isWindows + ? commandAndAliases.some(e => precedingText.startsWith(`${removeAnyFileExtension(e.label)} `)) + : commandAndAliases.some(e => precedingText.startsWith(`${e.label} `))) + ) { // the spec label is not the first word in the command line, so do not provide options or args continue; } @@ -469,3 +478,6 @@ function getShell(shellType: TerminalShellType): string | undefined { } } +function removeAnyFileExtension(label: string): string { + return label.replace(/\.[a-zA-Z0-9!#\$%&'\(\)\-@\^_`{}~\+,;=\[\]]+$/, ''); +} diff --git a/extensions/terminal-suggest/src/test/completions/code.test.ts b/extensions/terminal-suggest/src/test/completions/code.test.ts index 50ad0458a224..ecbaef0c130b 100644 --- a/extensions/terminal-suggest/src/test/completions/code.test.ts +++ b/extensions/terminal-suggest/src/test/completions/code.test.ts @@ -7,8 +7,9 @@ import 'mocha'; import codeCompletionSpec from '../../completions/code'; import { testPaths, type ISuiteSpec, type ITestSpec } from '../helpers'; +export const codeSpecOptions = ['-', '--add', '--category', '--diff', '--disable-extension', '--disable-extensions', '--disable-gpu', '--enable-proposed-api', '--extensions-dir', '--goto', '--help', '--inspect-brk-extensions', '--inspect-extensions', '--install-extension', '--list-extensions', '--locale', '--log', '--max-memory', '--merge', '--new-window', '--pre-release', '--prof-startup', '--profile', '--reuse-window', '--show-versions', '--status', '--sync', '--telemetry', '--uninstall-extension', '--user-data-dir', '--verbose', '--version', '--wait', '-a', '-d', '-g', '-h', '-m', '-n', '-r', '-s', '-v', '-w']; + export function createCodeTestSpecs(executable: string): ITestSpec[] { - const codeOptions = ['-', '--add', '--category', '--diff', '--disable-extension', '--disable-extensions', '--disable-gpu', '--enable-proposed-api', '--extensions-dir', '--goto', '--help', '--inspect-brk-extensions', '--inspect-extensions', '--install-extension', '--list-extensions', '--locale', '--log', '--max-memory', '--merge', '--new-window', '--pre-release', '--prof-startup', '--profile', '--reuse-window', '--show-versions', '--status', '--sync', '--telemetry', '--uninstall-extension', '--user-data-dir', '--verbose', '--version', '--wait', '-a', '-d', '-g', '-h', '-m', '-n', '-r', '-s', '-v', '-w']; const localeOptions = ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW']; const categoryOptions = ['azure', 'data science', 'debuggers', 'extension packs', 'education', 'formatters', 'keymaps', 'language packs', 'linters', 'machine learning', 'notebooks', 'programming languages', 'scm providers', 'snippets', 'testing', 'themes', 'visualization', 'other']; const logOptions = ['critical', 'error', 'warn', 'info', 'debug', 'trace', 'off']; @@ -25,7 +26,7 @@ export function createCodeTestSpecs(executable: string): ITestSpec[] { ...typingTests, // Basic arguments - { input: `${executable} |`, expectedCompletions: codeOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: `${executable} |`, expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, { input: `${executable} --locale |`, expectedCompletions: localeOptions }, { input: `${executable} --diff |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } }, { input: `${executable} --diff ./file1 |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } }, @@ -40,13 +41,13 @@ export function createCodeTestSpecs(executable: string): ITestSpec[] { { input: `${executable} --log |`, expectedCompletions: logOptions }, { input: `${executable} --sync |`, expectedCompletions: syncOptions }, { input: `${executable} --extensions-dir |`, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, - { input: `${executable} --list-extensions |`, expectedCompletions: codeOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, - { input: `${executable} --show-versions |`, expectedCompletions: codeOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: `${executable} --list-extensions |`, expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: `${executable} --show-versions |`, expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, { input: `${executable} --category |`, expectedCompletions: categoryOptions }, { input: `${executable} --category a|`, expectedCompletions: categoryOptions.filter(c => c.startsWith('a')) }, // Middle of command - { input: `${executable} | --locale`, expectedCompletions: codeOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: `${executable} | --locale`, expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, ]; } diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index eca864c9727c..189a022101b7 100644 --- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -9,7 +9,7 @@ import { basename } from 'path'; import { asArray, getCompletionItemsFromSpecs } from '../terminalSuggestMain'; import { getTokenType } from '../tokens'; import { cdTestSuiteSpec as cdTestSuite } from './completions/cd.test'; -import { codeTestSuite } from './completions/code.test'; +import { codeSpecOptions, codeTestSuite } from './completions/code.test'; import { testPaths, type ISuiteSpec } from './helpers'; import { codeInsidersTestSuite } from './completions/code-insiders.test'; import { lsTestSuiteSpec } from './completions/upstream/ls.test'; @@ -18,6 +18,8 @@ import { mkdirTestSuiteSpec } from './completions/upstream/mkdir.test'; import { rmTestSuiteSpec } from './completions/upstream/rm.test'; import { rmdirTestSuiteSpec } from './completions/upstream/rmdir.test'; import { touchTestSuiteSpec } from './completions/upstream/touch.test'; +import { osIsWindows } from '../helpers/os'; +import codeCompletionSpec from '../completions/code'; const testSpecs2: ISuiteSpec[] = [ { @@ -46,6 +48,26 @@ const testSpecs2: ISuiteSpec[] = [ touchTestSuiteSpec, ]; +if (osIsWindows()) { + testSpecs2.push({ + name: 'Handle options extensions on Windows', + completionSpecs: [codeCompletionSpec], + availableCommands: [ + 'code.bat', + 'code.cmd', + 'code.exe', + 'code.anything', + ], + testSpecs: [ + { input: 'code |', expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'code.bat |', expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'code.cmd |', expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'code.exe |', expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'code.anything |', expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + ] + }); +} + suite('Terminal Suggest', () => { for (const suiteSpec of testSpecs2) { suite(suiteSpec.name, () => { From 47b33dc58d1577bae96d66488f2a7276d1322a83 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:32:11 +0100 Subject: [PATCH 1231/3587] Git - add command to delete remote branch (#239692) --- extensions/git/package.json | 14 +++++++++ extensions/git/package.nls.json | 1 + extensions/git/src/commands.ts | 54 ++++++++++++++++++++++++++------- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 600196ef2ce2..7b436cb3ca10 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -490,6 +490,12 @@ "category": "Git", "enablement": "!operationInProgress" }, + { + "command": "git.deleteRemoteBranch", + "title": "%command.deleteRemoteBranch%", + "category": "Git", + "enablement": "!operationInProgress" + }, { "command": "git.renameBranch", "title": "%command.renameBranch%", @@ -1224,6 +1230,10 @@ "command": "git.deleteBranch", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, + { + "command": "git.deleteRemoteBranch", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" + }, { "command": "git.renameBranch", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" @@ -2437,6 +2447,10 @@ "command": "git.deleteBranch", "group": "3_modify@2" }, + { + "command": "git.deleteRemoteBranch", + "group": "3_modify@3" + }, { "command": "git.publish", "group": "4_publish@1" diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 5cfb2b1a7b24..06b2a6ac32d8 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -65,6 +65,7 @@ "command.branch": "Create Branch...", "command.branchFrom": "Create Branch From...", "command.deleteBranch": "Delete Branch...", + "command.deleteRemoteBranch": "Delete Remote Branch...", "command.renameBranch": "Rename Branch...", "command.cherryPick": "Cherry Pick...", "command.cherryPickAbort": "Abort Cherry Pick", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 497edcb9e8e4..0957890e09f9 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -155,8 +155,11 @@ class CheckoutTagItem extends RefItem { class BranchDeleteItem extends RefItem { async run(repository: Repository, force?: boolean): Promise { - if (this.ref.name) { - await repository.deleteBranch(this.ref.name, force); + if (this.ref.type === RefType.Head && this.refName) { + await repository.deleteBranch(this.refName, force); + } else if (this.ref.type === RefType.RemoteHead && this.refRemote && this.refName) { + const refName = this.refName.substring(this.refRemote.length + 1); + await repository.deleteRemoteRef(this.refRemote, refName, { force }); } } } @@ -2883,7 +2886,7 @@ export class CommandCenter { @command('git.deleteBranch', { repository: true }) async deleteBranch(repository: Repository, name: string | undefined, force?: boolean): Promise { - await this._deleteBranch(repository, name, force); + await this._deleteBranch(repository, undefined, name, { remote: false, force }); } @command('git.graph.deleteBranch', { repository: true }) @@ -2893,22 +2896,51 @@ export class CommandCenter { return; } - await this._deleteBranch(repository, historyItemRef.name); + await this._deleteBranch(repository, undefined, historyItemRef.name, { remote: false }); + } + + @command('git.deleteRemoteBranch', { repository: true }) + async deleteRemoteBranch(repository: Repository): Promise { + await this._deleteBranch(repository, undefined, undefined, { remote: true }); } - private async _deleteBranch(repository: Repository, name: string | undefined, force?: boolean): Promise { + private async _deleteBranch(repository: Repository, remote: string | undefined, name: string | undefined, options: { remote: boolean; force?: boolean }): Promise { let run: (force?: boolean) => Promise; - if (typeof name === 'string') { + + if (!options.remote && typeof name === 'string') { + // Local branch run = force => repository.deleteBranch(name!, force); + } else if (options.remote && typeof remote === 'string' && typeof name === 'string') { + // Remote branch + run = force => repository.deleteRemoteRef(remote, name!, { force }); } else { const getBranchPicks = async () => { - const refs = await repository.getRefs({ pattern: 'refs/heads' }); - const currentHead = repository.HEAD && repository.HEAD.name; + const pattern = options.remote ? 'refs/remotes' : 'refs/heads'; + const refs = await repository.getRefs({ pattern }); + + const refsToExclude: string[] = []; + if (options.remote) { + refsToExclude.push('origin/HEAD'); - return refs.filter(ref => ref.name !== currentHead).map(ref => new BranchDeleteItem(ref)); + if (repository.HEAD?.upstream) { + // Current branch's upstream + refsToExclude.push(`${repository.HEAD.upstream.remote}/${repository.HEAD.upstream.name}`); + } + } else { + if (repository.HEAD?.name) { + // Current branch + refsToExclude.push(repository.HEAD.name); + } + } + + return refs.filter(ref => ref.name && !refsToExclude.includes(ref.name)) + .map(ref => new BranchDeleteItem(ref)); }; - const placeHolder = l10n.t('Select a branch to delete'); + const placeHolder = !options.remote + ? l10n.t('Select a branch to delete') + : l10n.t('Select a remote branch to delete'); + const choice = await this.pickRef(getBranchPicks(), placeHolder); if (!choice || !choice.refName) { @@ -2919,7 +2951,7 @@ export class CommandCenter { } try { - await run(force); + await run(options.force); } catch (err) { if (err.gitErrorCode !== GitErrorCodes.BranchNotFullyMerged) { throw err; From c887687cadbe3570308cd346c8c63ec8fe3db6b5 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 4 Feb 2025 18:46:14 -0800 Subject: [PATCH 1232/3587] remove redundant service creation in the tests --- .../test/common/promptSyntax/service/promptSyntaxService.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptSyntaxService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptSyntaxService.test.ts index 65c876889897..48716edbd6f9 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptSyntaxService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptSyntaxService.test.ts @@ -111,7 +111,6 @@ suite('PromptSyntaxService', () => { instantiationService = disposables.add(new TestInstantiationService()); instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IConfigurationService, new TestConfigurationService()); - instantiationService.stub(IFileService, new TestConfigurationService()); instantiationService.stub(IFileService, disposables.add(instantiationService.createInstance(FileService))); service = disposables.add(instantiationService.createInstance(PromptSyntaxService)); From 13615eed0afc89270699e3a2aaedefffc1434f7d Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 4 Feb 2025 22:03:12 -0800 Subject: [PATCH 1233/3587] reuse common `createURI` test utility --- .../service/promptSyntaxService.test.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptSyntaxService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptSyntaxService.test.ts index 48716edbd6f9..a62673525525 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptSyntaxService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptSyntaxService.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { createURI } from '../testUtils/createUri.js'; import { URI } from '../../../../../../../base/common/uri.js'; -import { isWindows } from '../../../../../../../base/common/platform.js'; import { Range } from '../../../../../../../editor/common/core/range.js'; import { assertDefined } from '../../../../../../../base/common/types.js'; import { waitRandom } from '../../../../../../../base/test/common/testUtils.js'; @@ -89,18 +89,6 @@ const assertLinks = ( ); }; -/** - * Creates cross-platform URI. On Windows, absolute paths - * are prefixed with the disk name. - */ -const createURI = (linkPath: string): URI => { - if (isWindows && linkPath.startsWith('/')) { - return URI.file('/d:' + linkPath); - } - - return URI.file(linkPath); -}; - suite('PromptSyntaxService', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); From 3834de8f311b7a3227952910088c6b34abd2a2ba Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 5 Feb 2025 17:04:37 +0100 Subject: [PATCH 1234/3587] chore - move globalEditingSession cleanup (#239698) * `ChatDecorationsProvider` doesn't works with all editing sessions * `ChatEditingMultiDiffSourceResolver` doesn't use singleton session anymore https://github.com/microsoft/vscode-copilot/issues/12820 * `ChatEditingSnapshotTextModelContentProvider` and `ChatEditingTextModelContentProvider` don't use global editing session anymore * dispose all session on shutdown * move multifile accept/discard into separate actions --- .../browser/chatEditing/chatEditingActions.ts | 13 +---- .../chatEditing/chatEditingEditorActions.ts | 56 ++++++++++++++++++- .../chatEditingModifiedFileEntry.ts | 4 +- .../chatEditing/chatEditingServiceImpl.ts | 43 ++++++++------ .../browser/chatEditing/chatEditingSession.ts | 2 +- .../chatEditingTextModelContentProviders.ts | 41 ++++++-------- .../contrib/chat/common/chatEditingService.ts | 4 +- 7 files changed, 103 insertions(+), 60 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 74c5d98582a6..1e759cb59871 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -292,12 +292,7 @@ export class ChatEditingAcceptAllAction extends EditingSessionAction { weight: KeybindingWeight.WorkbenchContrib, }, menu: [ - { - when: ContextKeyExpr.equals('resourceScheme', CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME), - id: MenuId.EditorTitle, - order: 0, - group: 'navigation', - }, + { id: MenuId.ChatEditingWidgetToolbar, group: 'navigation', @@ -324,12 +319,6 @@ export class ChatEditingDiscardAllAction extends EditingSessionAction { tooltip: localize('discardAllEdits', 'Discard All Edits'), precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), menu: [ - { - when: ContextKeyExpr.equals('resourceScheme', CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME), - id: MenuId.EditorTitle, - order: 1, - group: 'navigation', - }, { id: MenuId.ChatEditingWidgetToolbar, group: 'navigation', diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts index 072855972ec1..ac33f23c462c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts @@ -14,12 +14,16 @@ import { ChatEditorController, ctxHasEditorModification, ctxReviewModeEnabled } import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; import { ACTIVE_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js'; -import { IChatEditingService } from '../../common/chatEditingService.js'; +import { CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, IChatEditingService } from '../../common/chatEditingService.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { isEqual } from '../../../../../base/common/resources.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { getNotebookEditorFromEditorPane } from '../../../notebook/browser/notebookBrowser.js'; import { ctxNotebookHasEditorModification } from '../../../notebook/browser/contrib/chatEdit/notebookChatEditContext.js'; +import { resolveCommandsContext } from '../../../../browser/parts/editor/editorCommandsContext.js'; +import { IListService } from '../../../../../platform/list/browser/listService.js'; +import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; +import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; abstract class NavigateAction extends Action2 { @@ -320,6 +324,53 @@ export class ReviewChangesAction extends EditorAction2 { } } + +// --- multi file diff + +abstract class MultiDiffAcceptDiscardAction extends Action2 { + + constructor(readonly accept: boolean) { + super({ + id: accept ? 'chatEditing.multidiff.acceptAllFiles' : 'chatEditing.multidiff.discardAllFiles', + title: accept ? localize('accept3', 'Accept All Edits') : localize('discard3', 'Discard All Edits'), + icon: accept ? Codicon.check : Codicon.discard, + menu: { + when: ContextKeyExpr.equals('resourceScheme', CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME), + id: MenuId.EditorTitle, + order: accept ? 0 : 1, + group: 'navigation', + }, + }); + } + + async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + const chatEditingService = accessor.get(IChatEditingService); + const editorService = accessor.get(IEditorService); + const editorGroupsService = accessor.get(IEditorGroupsService); + const listService = accessor.get(IListService); + + const resolvedContext = resolveCommandsContext(args, editorService, editorGroupsService, listService); + + const groupContext = resolvedContext.groupedEditors[0]; + if (!groupContext) { + return; + } + + const editor = groupContext.editors[0]; + if (!(editor instanceof MultiDiffEditorInput) || !editor.resource) { + return; + } + + const session = chatEditingService.getEditingSession(editor.resource.authority); + if (this.accept) { + await session?.accept(); + } else { + await session?.reject(); + } + } +} + + export function registerChatEditorActions() { registerAction2(class NextAction extends NavigateAction { constructor() { super(true); } }); registerAction2(class PrevAction extends NavigateAction { constructor() { super(false); } }); @@ -330,6 +381,9 @@ export function registerChatEditorActions() { registerAction2(RejectHunkAction); registerAction2(OpenDiffAction); + registerAction2(class extends MultiDiffAcceptDiscardAction { constructor() { super(true); } }); + registerAction2(class extends MultiDiffAcceptDiscardAction { constructor() { super(false); } }); + MenuRegistry.appendMenuItem(MenuId.ChatEditingEditorContent, { command: { id: navigationBearingFakeActionId, diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 46068b2e8179..b9a77cddff6b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -180,7 +180,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie modelService.createModel( createTextBufferFactoryFromSnapshot(initialContent ? stringToSnapshot(initialContent) : this.doc.createSnapshot()), languageService.createById(this.doc.getLanguageId()), - ChatEditingTextModelContentProvider.getFileURI(this.entryId, this.modifiedURI.path), + ChatEditingTextModelContentProvider.getFileURI(_telemetryInfo.sessionId, this.entryId, this.modifiedURI.path), false ) ); @@ -270,7 +270,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie return { resource: this.modifiedURI, languageId: this.modifiedModel.getLanguageId(), - snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(requestId, this.modifiedURI.path), + snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(this._telemetryInfo.sessionId, requestId, this.modifiedURI.path), original: this.originalModel.getValue(), current: this.modifiedModel.getValue(), originalToCurrentEdit: this._edit, diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index e49f8637673a..0098bdadb66a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts @@ -10,7 +10,7 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { BugIndicatingError, ErrorNoTelemetry } from '../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { Iterable } from '../../../../../base/common/iterator.js'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { LinkedList } from '../../../../../base/common/linkedList.js'; import { ResourceMap } from '../../../../../base/common/map.js'; import { Schemas } from '../../../../../base/common/network.js'; @@ -104,15 +104,15 @@ export class ChatEditingService extends Disposable implements IChatEditingServic @IProductService productService: IProductService, ) { super(); - this._register(decorationsService.registerDecorationsProvider(_instantiationService.createInstance(ChatDecorationsProvider, this._currentSessionObs))); - this._register(multiDiffSourceResolverService.registerResolver(_instantiationService.createInstance(ChatEditingMultiDiffSourceResolver, this._currentSessionObs))); - this._register(textModelService.registerTextModelContentProvider(ChatEditingTextModelContentProvider.scheme, _instantiationService.createInstance(ChatEditingTextModelContentProvider, this._currentSessionObs))); - this._register(textModelService.registerTextModelContentProvider(ChatEditingSnapshotTextModelContentProvider.scheme, _instantiationService.createInstance(ChatEditingSnapshotTextModelContentProvider, this._currentSessionObs))); + this._register(decorationsService.registerDecorationsProvider(_instantiationService.createInstance(ChatDecorationsProvider, this.editingSessionsObs))); + this._register(multiDiffSourceResolverService.registerResolver(_instantiationService.createInstance(ChatEditingMultiDiffSourceResolver))); + this._register(textModelService.registerTextModelContentProvider(ChatEditingTextModelContentProvider.scheme, _instantiationService.createInstance(ChatEditingTextModelContentProvider))); + this._register(textModelService.registerTextModelContentProvider(ChatEditingSnapshotTextModelContentProvider.scheme, _instantiationService.createInstance(ChatEditingSnapshotTextModelContentProvider))); this._register(this._chatService.onDidDisposeSession((e) => { - if (e.reason === 'cleared' && this._currentSessionObs.get()?.chatSessionId === e.sessionId) { - void this._currentSessionObs.get()?.stop(); + if (e.reason === 'cleared') { + this.getEditingSession(e.sessionId)?.stop(); } })); @@ -156,6 +156,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic override dispose(): void { this._currentSessionObs.get()?.dispose(); + dispose(this._adhocSessionsObs.get()); super.dispose(); } @@ -434,15 +435,18 @@ class ChatDecorationsProvider extends Disposable implements IDecorationsProvider readonly label: string = localize('chat', "Chat Editing"); private readonly _currentEntries = derived(this, (r) => { - const session = this._session.read(r); - if (!session) { + const sessions = this._sessions.read(r); + if (!sessions) { return []; } - const state = session.state.read(r); - if (state === ChatEditingSessionState.Disposed) { - return []; + const result: IModifiedFileEntry[] = []; + for (const session of sessions) { + if (session.state.read(r) !== ChatEditingSessionState.Disposed) { + const entries = session.entries.read(r); + result.push(...entries); + } } - return session.entries.read(r); + return result; }); private readonly _currentlyEditingUris = derived(this, (r) => { @@ -461,7 +465,7 @@ class ChatDecorationsProvider extends Disposable implements IDecorationsProvider ); constructor( - private readonly _session: IObservable, + private readonly _sessions: IObservable, @IChatAgentService private readonly _chatAgentService: IChatAgentService ) { super(); @@ -493,8 +497,8 @@ class ChatDecorationsProvider extends Disposable implements IDecorationsProvider export class ChatEditingMultiDiffSourceResolver implements IMultiDiffSourceResolver { constructor( - private readonly _currentSession: IObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IChatEditingService private readonly _chatEditingService: IChatEditingService ) { } canHandleUri(uri: URI): boolean { @@ -502,7 +506,12 @@ export class ChatEditingMultiDiffSourceResolver implements IMultiDiffSourceResol } async resolveDiffSource(uri: URI): Promise { - return this._instantiationService.createInstance(ChatEditingMultiDiffSource, this._currentSession); + + const thisSession = derived(this, r => { + return this._chatEditingService.editingSessionsObs.read(r).find(candidate => candidate.chatSessionId === uri.authority); + }); + + return this._instantiationService.createInstance(ChatEditingMultiDiffSource, thisSession); } } @@ -532,6 +541,6 @@ class ChatEditingMultiDiffSource implements IResolvedMultiDiffSource { }; constructor( - private readonly _currentSession: IObservable + private readonly _currentSession: IObservable ) { } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 36c7bdcfa033..3dd782fe5f1a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -529,7 +529,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } } const input = MultiDiffEditorInput.fromResourceMultiDiffEditorInput({ - multiDiffSource: getMultiDiffSourceUri(), + multiDiffSource: getMultiDiffSourceUri(this), label: localize('multiDiffEditorInput.name', "Suggested Edits") }, this._instantiationService); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTextModelContentProviders.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTextModelContentProviders.ts index e722b40ff942..4afb878b0012 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTextModelContentProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTextModelContentProviders.ts @@ -3,36 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IObservable } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { ITextModel } from '../../../../../editor/common/model.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; import { ITextModelContentProvider } from '../../../../../editor/common/services/resolverService.js'; +import { IChatEditingService } from '../../common/chatEditingService.js'; import { ChatEditingSession } from './chatEditingSession.js'; -type ChatEditingTextModelContentQueryData = { kind: 'empty' } | { kind: 'doc'; documentId: string }; +type ChatEditingTextModelContentQueryData = { kind: 'doc'; documentId: string; chatSessionId: string }; export class ChatEditingTextModelContentProvider implements ITextModelContentProvider { public static readonly scheme = 'chat-editing-text-model'; - public static getEmptyFileURI(): URI { - return URI.from({ - scheme: ChatEditingTextModelContentProvider.scheme, - query: JSON.stringify({ kind: 'empty' }), - }); - } - - public static getFileURI(documentId: string, path: string): URI { + public static getFileURI(chatSessionId: string, documentId: string, path: string): URI { return URI.from({ scheme: ChatEditingTextModelContentProvider.scheme, path, - query: JSON.stringify({ kind: 'doc', documentId }), + query: JSON.stringify({ kind: 'doc', documentId, chatSessionId } satisfies ChatEditingTextModelContentQueryData), }); } constructor( - private readonly _currentSessionObs: IObservable, - @IModelService private readonly _modelService: IModelService + @IModelService private readonly _modelService: IModelService, + @IChatEditingService private readonly _chatEditingService: IChatEditingService ) { } async provideTextContent(resource: URI): Promise { @@ -42,12 +35,10 @@ export class ChatEditingTextModelContentProvider implements ITextModelContentPro } const data: ChatEditingTextModelContentQueryData = JSON.parse(resource.query); - if (data.kind === 'empty') { - return this._modelService.createModel('', null, resource, false); - } - const session = this._currentSessionObs.get(); - if (!session) { + const session = this._chatEditingService.getEditingSession(data.chatSessionId); + + if (!(session instanceof ChatEditingSession)) { return null; } @@ -55,22 +46,22 @@ export class ChatEditingTextModelContentProvider implements ITextModelContentPro } } -type ChatEditingSnapshotTextModelContentQueryData = { requestId: string | undefined }; +type ChatEditingSnapshotTextModelContentQueryData = { sessionId: string; requestId: string | undefined }; export class ChatEditingSnapshotTextModelContentProvider implements ITextModelContentProvider { public static readonly scheme = 'chat-editing-snapshot-text-model'; - public static getSnapshotFileURI(requestId: string | undefined, path: string): URI { + public static getSnapshotFileURI(chatSessionId: string, requestId: string | undefined, path: string): URI { return URI.from({ scheme: ChatEditingSnapshotTextModelContentProvider.scheme, path, - query: JSON.stringify({ requestId: requestId ?? '' }), + query: JSON.stringify({ sessionId: chatSessionId, requestId: requestId ?? '' } satisfies ChatEditingSnapshotTextModelContentQueryData), }); } constructor( - private readonly _currentSessionObs: IObservable, - @IModelService private readonly _modelService: IModelService + @IModelService private readonly _modelService: IModelService, + @IChatEditingService private readonly _chatEditingService: IChatEditingService ) { } async provideTextContent(resource: URI): Promise { @@ -81,8 +72,8 @@ export class ChatEditingSnapshotTextModelContentProvider implements ITextModelCo const data: ChatEditingSnapshotTextModelContentQueryData = JSON.parse(resource.query); - const session = this._currentSessionObs.get(); - if (!session || !data.requestId) { + const session = this._chatEditingService.getEditingSession(data.sessionId); + if (!(session instanceof ChatEditingSession) || !data.requestId) { return null; } diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index dc2a4a951209..cb704a048088 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -191,9 +191,9 @@ export function isChatEditingActionContext(thing: unknown): thing is IChatEditin return typeof thing === 'object' && !!thing && 'sessionId' in thing; } -export function getMultiDiffSourceUri(): URI { +export function getMultiDiffSourceUri(session: IChatEditingSession): URI { return URI.from({ scheme: CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, - path: '', + authority: session.chatSessionId, }); } From af22e57ed637fdfcbe14d09a50e1ef73b1dd9c02 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 5 Feb 2025 17:36:20 +0100 Subject: [PATCH 1235/3587] Git - add a check so that the active branch cannot be deleted (#239700) --- extensions/git/src/commands.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 0957890e09f9..c7fe7d5616ed 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -2896,6 +2896,11 @@ export class CommandCenter { return; } + if (historyItemRef.id === repository.historyProvider.currentHistoryItemRef?.id) { + window.showInformationMessage(l10n.t('The active branch cannot be deleted.')); + return; + } + await this._deleteBranch(repository, undefined, historyItemRef.name, { remote: false }); } From 5446d62f193ed124a0ff5ad24ec98ab1aa5d54d1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 5 Feb 2025 08:38:04 -0800 Subject: [PATCH 1236/3587] Don't show suggest widget when last input was arrow Fixes #239609 --- .../suggest/browser/terminalSuggestAddon.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index cfaa70a0883a..651bbba27c00 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -258,6 +258,12 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest await this._handleCompletionProviders(this._terminal, token, explicitlyInvoked); } + private _wasLastInputArrowKey(): boolean { + // Never request completions if the last key sequence was up or down as the user was likely + // navigating history + return !!this._lastUserData?.match(/^\x1b[\[O]?[A-D]$/); + } + private _sync(promptInputState: IPromptInputModelState): void { const config = this._configurationService.getValue(terminalSuggestConfigSection); if (!this._mostRecentPromptInputState || promptInputState.cursorIndex > this._mostRecentPromptInputState.cursorIndex) { @@ -268,9 +274,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest if (!this._terminalSuggestWidgetVisibleContextKey.get()) { if (config.quickSuggestions) { if (promptInputState.prefix.match(/[^\s]$/)) { - // Never request completions if the last key sequence was up or down as the user was likely - // navigating history - if (!this._lastUserData?.match(/^\x1b[\[O]?[A-D]$/)) { + if (!this._wasLastInputArrowKey()) { this.requestCompletions(); sent = true; } @@ -289,8 +293,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest // with git branches in particular this._isFilteringDirectories && prefix?.match(/[\\\/]$/) ) { - this.requestCompletions(); - sent = true; + if (!this._wasLastInputArrowKey()) { + this.requestCompletions(); + sent = true; + } } if (!sent) { for (const provider of this._terminalCompletionService.providers) { @@ -299,8 +305,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } for (const char of provider.triggerCharacters) { if (prefix?.endsWith(char)) { - this.requestCompletions(); - sent = true; + if (!this._wasLastInputArrowKey()) { + this.requestCompletions(); + sent = true; + } break; } } From d467ad19ff15c1a3b5bb2a6a858ca59f13a113a0 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 5 Feb 2025 17:55:47 +0100 Subject: [PATCH 1237/3587] debt - move actions around to code editor --- .../parts/editor/editor.contribution.ts | 5 +-- .../browser/parts/editor/editorActions.ts | 30 ------------- .../browser/parts/editor/editorGroupView.ts | 2 +- .../browser/codeEditor.contribution.ts | 1 + .../codeEditor/browser/toggleOvertype.ts | 43 +++++++++++++++++++ 5 files changed, 46 insertions(+), 35 deletions(-) create mode 100644 src/vs/workbench/contrib/codeEditor/browser/toggleOvertype.ts diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 8c75858d9810..1c777faacaf8 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -43,8 +43,7 @@ import { SplitEditorToFirstGroupAction, SplitEditorToLastGroupAction, SplitEditorToLeftGroupAction, SplitEditorToNextGroupAction, SplitEditorToPreviousGroupAction, SplitEditorToRightGroupAction, NavigateForwardInEditsAction, NavigateBackwardsInEditsAction, NavigateForwardInNavigationsAction, NavigateBackwardsInNavigationsAction, NavigatePreviousInNavigationsAction, NavigatePreviousInEditsAction, NavigateToLastNavigationLocationAction, MaximizeGroupHideSidebarAction, MoveEditorToNewWindowAction, CopyEditorToNewindowAction, RestoreEditorsToMainWindowAction, ToggleMaximizeEditorGroupAction, MinimizeOtherGroupsHideSidebarAction, CopyEditorGroupToNewWindowAction, - MoveEditorGroupToNewWindowAction, NewEmptyEditorWindowAction, - ToggleOvertypeInsertMode + MoveEditorGroupToNewWindowAction, NewEmptyEditorWindowAction } from './editorActions.js'; import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_EDITOR_GROUP_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, @@ -172,8 +171,6 @@ quickAccessRegistry.registerQuickAccessProvider({ //#region Actions & Commands -registerAction2(ToggleOvertypeInsertMode); - registerAction2(ChangeLanguageAction); registerAction2(ChangeEOLAction); registerAction2(ChangeEncodingAction); diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index fc9b8ec03e0e..c33f71c958e1 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -38,7 +38,6 @@ import { ICommandActionTitle } from '../../../../platform/action/common/action.j import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { resolveCommandsContext } from './editorCommandsContext.js'; import { IListService } from '../../../../platform/list/browser/listService.js'; -import { InputMode } from '../../../../editor/common/inputMode.js'; class ExecuteCommandAction extends Action2 { @@ -2697,32 +2696,3 @@ export class NewEmptyEditorWindowAction extends Action2 { auxiliaryEditorPart.activeGroup.focus(); } } - -export class ToggleOvertypeInsertMode extends Action2 { - - constructor() { - super({ - id: 'editor.action.toggleOvertypeInsertMode', - title: { - ...localize2('toggleOvertypeInsertMode', "Toggle Overtype/Insert Mode"), - mnemonicTitle: localize({ key: 'mitoggleOvertypeInsertMode', comment: ['&& denotes a mnemonic'] }, "&&Toggle Overtype/Insert Mode"), - }, - metadata: { - description: localize2('toggleOvertypeMode.description', "Toggle between overtype and insert mode"), - }, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyCode.Insert, - mac: { primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.KeyO }, - }, - f1: true, - category: Categories.View - }); - } - - override async run(accessor: ServicesAccessor): Promise { - const oldInputMode = InputMode.getInputMode(); - const newInputMode = oldInputMode === 'insert' ? 'overtype' : 'insert'; - InputMode.setInputMode(newInputMode); - } -} diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 1dff715f7036..4d50123c07da 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -1467,7 +1467,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const options = fillActiveEditorViewState(this, editor, { ...openOptions, pinned: true, // always pin moved editor - sticky: openOptions?.sticky ?? (!keepCopy && this.model.isSticky(editor)) // preserve sticky state only if editor is moved or eplicitly wanted (https://github.com/microsoft/vscode/issues/99035) + sticky: openOptions?.sticky ?? (!keepCopy && this.model.isSticky(editor)) // preserve sticky state only if editor is moved or explicitly wanted (https://github.com/microsoft/vscode/issues/99035) }); // Indicate will move event diff --git a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts index ac45ecadd875..4e42d6cd7c45 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts @@ -16,6 +16,7 @@ import './quickaccess/gotoSymbolQuickAccess.js'; import './saveParticipants.js'; import './toggleColumnSelection.js'; import './toggleMinimap.js'; +import './toggleOvertype.js'; import './toggleMultiCursorModifier.js'; import './toggleRenderControlCharacter.js'; import './toggleRenderWhitespace.js'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleOvertype.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleOvertype.ts new file mode 100644 index 000000000000..b607bf0c8ff2 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleOvertype.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize, localize2 } from '../../../../nls.js'; +import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; +import { InputMode } from '../../../../editor/common/inputMode.js'; + +export class ToggleOvertypeInsertMode extends Action2 { + + constructor() { + super({ + id: 'editor.action.toggleOvertypeInsertMode', + title: { + ...localize2('toggleOvertypeInsertMode', "Toggle Overtype/Insert Mode"), + mnemonicTitle: localize({ key: 'mitoggleOvertypeInsertMode', comment: ['&& denotes a mnemonic'] }, "&&Toggle Overtype/Insert Mode"), + }, + metadata: { + description: localize2('toggleOvertypeMode.description', "Toggle between overtype and insert mode"), + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Insert, + mac: { primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.KeyO }, + }, + f1: true, + category: Categories.View + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const oldInputMode = InputMode.getInputMode(); + const newInputMode = oldInputMode === 'insert' ? 'overtype' : 'insert'; + InputMode.setInputMode(newInputMode); + } +} + +registerAction2(ToggleOvertypeInsertMode); From 39cf1ffb98036dc04eb0a4d70c618c481963d0a7 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 5 Feb 2025 18:58:39 +0100 Subject: [PATCH 1238/3587] Adopt Tree-Sitter 0.25.1 (#239683) * Adopt Tree-Sitter 0.25.1 * Update @vscode/tree-sitter-wasm * Fix incorrect redirect --- build/gulpfile.editor.js | 2 +- package-lock.json | 8 +-- package.json | 2 +- remote/package-lock.json | 8 +-- remote/package.json | 2 +- remote/web/package-lock.json | 8 +-- remote/web/package.json | 2 +- src/vs/editor/common/languages.ts | 2 +- .../treeSitter/treeSitterParserService.ts | 66 ++++++++----------- .../services/treeSitterParserService.ts | 50 +++++++++++++- .../browser/standaloneTreeSitterService.ts | 2 +- .../services/treeSitterParserService.test.ts | 58 +++++++++++----- .../common/services/testTreeSitterService.ts | 2 +- .../inspectEditorTokens.ts | 8 +-- .../browser/themes.test.contribution.ts | 2 +- ...eSitterTokenizationFeature.contribution.ts | 3 +- .../browser/treeSitterTokenizationFeature.ts | 13 ++-- .../treeSitterTokenizationFeature.test.ts | 3 +- 18 files changed, 156 insertions(+), 85 deletions(-) diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 739c37fc677b..9f7ea57465ba 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -80,7 +80,7 @@ const extractEditorSrcTask = task.define('extract-editor-src', () => { importIgnorePattern: /\.css$/, destRoot: path.join(root, 'out-editor-src'), redirects: { - '@vscode/tree-sitter-wasm': '../node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-web', + '@vscode/tree-sitter-wasm': '../node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter', } }); }); diff --git a/package-lock.json b/package-lock.json index 220b5d9879b3..d20dc2d09503 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", "@vscode/sudo-prompt": "9.3.1", - "@vscode/tree-sitter-wasm": "^0.0.5", + "@vscode/tree-sitter-wasm": "^0.1.1", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", @@ -3157,9 +3157,9 @@ } }, "node_modules/@vscode/tree-sitter-wasm": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.5.tgz", - "integrity": "sha512-qA+BkB2UgkfXMQVGsqPeG3vR3pXv0inP6WQ/dq6BALy7dIX9KQvGXvDCiqehdFvZZO4tDFt4qb5DdSsvwR4Y9Q==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.1.1.tgz", + "integrity": "sha512-2KHGbX2krHP/LyfpDB6QnSAqyqIL7N2Md7KTMuoqq8+GhUzrgXwIClhpm7lqL/isNTYVzsEubR7YI6f2YfAqqQ==", "license": "MIT" }, "node_modules/@vscode/v8-heap-parser": { diff --git a/package.json b/package.json index 3f97b4a3d115..2cd4edb0bbcf 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", "@vscode/sudo-prompt": "9.3.1", - "@vscode/tree-sitter-wasm": "^0.0.5", + "@vscode/tree-sitter-wasm": "^0.1.1", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", diff --git a/remote/package-lock.json b/remote/package-lock.json index 787c3c9ba96a..a53da8875922 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -16,7 +16,7 @@ "@vscode/proxy-agent": "^0.31.0", "@vscode/ripgrep": "^1.15.10", "@vscode/spdlog": "^0.15.0", - "@vscode/tree-sitter-wasm": "^0.0.5", + "@vscode/tree-sitter-wasm": "^0.1.1", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", @@ -470,9 +470,9 @@ } }, "node_modules/@vscode/tree-sitter-wasm": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.5.tgz", - "integrity": "sha512-qA+BkB2UgkfXMQVGsqPeG3vR3pXv0inP6WQ/dq6BALy7dIX9KQvGXvDCiqehdFvZZO4tDFt4qb5DdSsvwR4Y9Q==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.1.1.tgz", + "integrity": "sha512-2KHGbX2krHP/LyfpDB6QnSAqyqIL7N2Md7KTMuoqq8+GhUzrgXwIClhpm7lqL/isNTYVzsEubR7YI6f2YfAqqQ==", "license": "MIT" }, "node_modules/@vscode/vscode-languagedetection": { diff --git a/remote/package.json b/remote/package.json index 5cc1ff194680..8dc72f471db9 100644 --- a/remote/package.json +++ b/remote/package.json @@ -11,7 +11,7 @@ "@vscode/proxy-agent": "^0.31.0", "@vscode/ripgrep": "^1.15.10", "@vscode/spdlog": "^0.15.0", - "@vscode/tree-sitter-wasm": "^0.0.5", + "@vscode/tree-sitter-wasm": "^0.1.1", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 45f7724805de..547912df017a 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -11,7 +11,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/tree-sitter-wasm": "^0.0.5", + "@vscode/tree-sitter-wasm": "^0.1.1", "@vscode/vscode-languagedetection": "1.0.21", "@xterm/addon-clipboard": "^0.2.0-beta.79", "@xterm/addon-image": "^0.9.0-beta.96", @@ -76,9 +76,9 @@ "integrity": "sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg==" }, "node_modules/@vscode/tree-sitter-wasm": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.5.tgz", - "integrity": "sha512-qA+BkB2UgkfXMQVGsqPeG3vR3pXv0inP6WQ/dq6BALy7dIX9KQvGXvDCiqehdFvZZO4tDFt4qb5DdSsvwR4Y9Q==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.1.1.tgz", + "integrity": "sha512-2KHGbX2krHP/LyfpDB6QnSAqyqIL7N2Md7KTMuoqq8+GhUzrgXwIClhpm7lqL/isNTYVzsEubR7YI6f2YfAqqQ==", "license": "MIT" }, "node_modules/@vscode/vscode-languagedetection": { diff --git a/remote/web/package.json b/remote/web/package.json index f1f5ce1ecb94..47350e2deaf2 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -6,7 +6,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/tree-sitter-wasm": "^0.0.5", + "@vscode/tree-sitter-wasm": "^0.1.1", "@vscode/vscode-languagedetection": "1.0.21", "@xterm/addon-clipboard": "^0.2.0-beta.79", "@xterm/addon-image": "^0.9.0-beta.96", diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 76daa762217e..a8f1af273f44 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -27,7 +27,7 @@ import { localize } from '../../nls.js'; import { ExtensionIdentifier } from '../../platform/extensions/common/extensions.js'; import { IMarkerData } from '../../platform/markers/common/markers.js'; import { IModelTokensChangedEvent } from './textModelEvents.js'; -import type { Parser } from '@vscode/tree-sitter-wasm'; +import type * as Parser from '@vscode/tree-sitter-wasm'; import { ITextModel } from './model.js'; import { TokenUpdate } from './model/tokenStore.js'; diff --git a/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts index 4ca0e3396de8..fb8c0c912c48 100644 --- a/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts +++ b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { Parser } from '@vscode/tree-sitter-wasm'; +import type * as Parser from '@vscode/tree-sitter-wasm'; import { AppResourcePath, FileAccess, nodeModulesAsarUnpackedPath, nodeModulesPath } from '../../../../base/common/network.js'; -import { EDITOR_EXPERIMENTAL_PREFER_TREESITTER, ITreeSitterParserService, ITreeSitterParseResult, ITextModelTreeSitter, RangeChange, TreeUpdateEvent, TreeParseUpdateEvent } from '../treeSitterParserService.js'; +import { EDITOR_EXPERIMENTAL_PREFER_TREESITTER, ITreeSitterParserService, ITreeSitterParseResult, ITextModelTreeSitter, RangeChange, TreeUpdateEvent, TreeParseUpdateEvent, ITreeSitterImporter } from '../treeSitterParserService.js'; import { IModelService } from '../model.js'; import { Disposable, DisposableMap, DisposableStore, dispose, IDisposable } from '../../../../base/common/lifecycle.js'; import { ITextModel } from '../../model.js'; @@ -15,7 +15,7 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { ILogService } from '../../../../platform/log/common/log.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { setTimeout0 } from '../../../../base/common/platform.js'; -import { canASAR, importAMDNodeModule } from '../../../../amdX.js'; +import { canASAR } from '../../../../amdX.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { CancellationToken, cancelOnDispose } from '../../../../base/common/cancellation.js'; import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; @@ -44,7 +44,7 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit constructor(readonly model: ITextModel, private readonly _treeSitterLanguages: TreeSitterLanguages, - private readonly _treeSitterImporter: TreeSitterImporter, + private readonly _treeSitterImporter: ITreeSitterImporter, private readonly _logService: ILogService, private readonly _telemetryService: ITelemetryService, parseImmediately: boolean = true @@ -155,11 +155,10 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul return this._versionId; } private _isDisposed: boolean = false; - constructor(public readonly parser: Parser, + constructor(public readonly parser: Parser.Parser, public /** exposed for tests **/ readonly language: Parser.Language, private readonly _logService: ILogService, private readonly _telemetryService: ITelemetryService) { - this.parser.setTimeoutMicros(50 * 1000); // 50 ms this.parser.setLanguage(language); } dispose(): void { @@ -228,7 +227,7 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul return false; }; - const getClosestPreviousNodes = (): { old: Parser.SyntaxNode; new: Parser.SyntaxNode } | undefined => { + const getClosestPreviousNodes = (): { old: Parser.Node; new: Parser.Node } | undefined => { // Go up parents until the end of the parent is before the start of the current. const newFindPrev = newTree.walk(); newFindPrev.resetTo(newCursor); @@ -267,10 +266,10 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul const newChildren = newCursor.currentNode.children; const indexChangedChildren: number[] = []; const changedChildren = newChildren.filter((c, index) => { - if (c.hasChanges) { + if (c?.hasChanges) { indexChangedChildren.push(index); } - return c.hasChanges; + return c?.hasChanges; }); if (changedChildren.length >= 1) { next = gotoNthChild(indexChangedChildren[0]); @@ -412,12 +411,13 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul let time: number = 0; let passes: number = 0; const inProgressVersion = this._editVersion; - let newTree: Parser.Tree | undefined; + let newTree: Parser.Tree | null | undefined; + this._lastYieldTime = performance.now(); do { const timer = performance.now(); try { - newTree = this.parser.parse((index: number, position?: Parser.Point) => this._parseCallback(model, index), this._tree); + newTree = this.parser.parse((index: number, position?: Parser.Point) => this._parseCallback(model, index), this._tree, { progressCallback: this._parseProgressCallback.bind(this) }); } catch (e) { // parsing can fail when the timeout is reached, will resume upon next loop } finally { @@ -433,13 +433,23 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul return (newTree && (inProgressVersion === model.getVersionId())) ? newTree : undefined; } - private _parseCallback(textModel: ITextModel, index: number): string | null { + private _lastYieldTime: number = 0; + private _parseProgressCallback(state: Parser.ParseState) { + const now = performance.now(); + if (now - this._lastYieldTime > 50) { + this._lastYieldTime = now; + return true; + } + return false; + } + + private _parseCallback(textModel: ITextModel, index: number): string | undefined { try { return textModel.getTextBuffer().getNearestChunk(index); } catch (e) { this._logService.debug('Error getting chunk for tree-sitter parsing', e); } - return null; + return undefined; } private sendParseTimeTelemetry(parseType: TelemetryParseType, languageId: string, time: number, passes: number): void { @@ -467,7 +477,7 @@ export class TreeSitterLanguages extends Disposable { */ public readonly onDidAddLanguage: Event<{ id: string; language: Parser.Language }> = this._onDidAddLanguage.event; - constructor(private readonly _treeSitterImporter: TreeSitterImporter, + constructor(private readonly _treeSitterImporter: ITreeSitterImporter, private readonly _fileService: IFileService, private readonly _environmentService: IEnvironmentService, private readonly _registeredLanguages: Map, @@ -514,8 +524,8 @@ export class TreeSitterLanguages extends Disposable { } const wasmPath: AppResourcePath = `${languageLocation}/${grammarName}.wasm`; const languageFile = await (this._fileService.readFile(FileAccess.asFileUri(wasmPath))); - const Parser = await this._treeSitterImporter.getParserClass(); - return Parser.Language.load(languageFile.value.buffer); + const Language = await this._treeSitterImporter.getLanguageClass(); + return Language.load(languageFile.value.buffer); } private _getLanguageLocation(languageId: string): AppResourcePath | undefined { @@ -527,24 +537,6 @@ export class TreeSitterLanguages extends Disposable { } } -export class TreeSitterImporter { - private _treeSitterImport: typeof import('@vscode/tree-sitter-wasm') | undefined; - private async _getTreeSitterImport() { - if (!this._treeSitterImport) { - this._treeSitterImport = await importAMDNodeModule('@vscode/tree-sitter-wasm', 'wasm/tree-sitter.js'); - } - return this._treeSitterImport; - } - - private _parserClass: typeof Parser | undefined; - public async getParserClass() { - if (!this._parserClass) { - this._parserClass = (await this._getTreeSitterImport()).Parser; - } - return this._parserClass; - } -} - interface TextModelTreeSitterItem { dispose(): void; textModelTreeSitter: TextModelTreeSitter; @@ -556,7 +548,6 @@ export class TreeSitterTextModelService extends Disposable implements ITreeSitte private _init!: Promise; private _textModelTreeSitters: DisposableMap = this._register(new DisposableMap()); private readonly _registeredLanguages: Map = new Map(); - private readonly _treeSitterImporter: TreeSitterImporter = new TreeSitterImporter(); private readonly _treeSitterLanguages: TreeSitterLanguages; public readonly onDidAddLanguage: Event<{ id: string; language: Parser.Language }>; @@ -570,7 +561,8 @@ export class TreeSitterTextModelService extends Disposable implements ITreeSitte @ITelemetryService private readonly _telemetryService: ITelemetryService, @ILogService private readonly _logService: ILogService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @IEnvironmentService private readonly _environmentService: IEnvironmentService + @IEnvironmentService private readonly _environmentService: IEnvironmentService, + @ITreeSitterImporter private readonly _treeSitterImporter: ITreeSitterImporter ) { super(); this._treeSitterLanguages = this._register(new TreeSitterLanguages(this._treeSitterImporter, fileService, this._environmentService, this._registeredLanguages)); @@ -601,7 +593,7 @@ export class TreeSitterTextModelService extends Disposable implements ITreeSitte if (language) { const parser = new Parser(); parser.setLanguage(language); - return parser.parse(content); + return parser.parse(content) ?? undefined; } return undefined; } diff --git a/src/vs/editor/common/services/treeSitterParserService.ts b/src/vs/editor/common/services/treeSitterParserService.ts index 4d0ddb3f971e..43d480443b26 100644 --- a/src/vs/editor/common/services/treeSitterParserService.ts +++ b/src/vs/editor/common/services/treeSitterParserService.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { Parser } from '@vscode/tree-sitter-wasm'; +import type * as Parser from '@vscode/tree-sitter-wasm'; import { Event } from '../../../base/common/event.js'; import { ITextModel } from '../model.js'; import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import { Range } from '../core/range.js'; +import { importAMDNodeModule } from '../../../amdX.js'; export const EDITOR_EXPERIMENTAL_PREFER_TREESITTER = 'editor.experimental.preferTreeSitter'; @@ -57,3 +58,50 @@ export interface ITextModelTreeSitter { parse(languageId?: string): Promise; dispose(): void; } + +export const ITreeSitterImporter = createDecorator('treeSitterImporter'); + +export interface ITreeSitterImporter { + readonly _serviceBrand: undefined; + getParserClass(): Promise; + getLanguageClass(): Promise; + getQueryClass(): Promise; +} + +export class TreeSitterImporter implements ITreeSitterImporter { + readonly _serviceBrand: undefined; + private _treeSitterImport: typeof import('@vscode/tree-sitter-wasm') | undefined; + + constructor() { } + + private async _getTreeSitterImport() { + if (!this._treeSitterImport) { + this._treeSitterImport = await importAMDNodeModule('@vscode/tree-sitter-wasm', 'wasm/tree-sitter.js'); + } + return this._treeSitterImport; + } + + private _parserClass: typeof Parser.Parser | undefined; + public async getParserClass() { + if (!this._parserClass) { + this._parserClass = (await this._getTreeSitterImport()).Parser; + } + return this._parserClass; + } + + private _languageClass: typeof Parser.Language | undefined; + public async getLanguageClass() { + if (!this._languageClass) { + this._languageClass = (await this._getTreeSitterImport()).Language; + } + return this._languageClass; + } + + private _queryClass: typeof Parser.Query | undefined; + public async getQueryClass() { + if (!this._queryClass) { + this._queryClass = (await this._getTreeSitterImport()).Query; + } + return this._queryClass; + } +} diff --git a/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts b/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts index 20a2f082f3c2..c0f0b1354d97 100644 --- a/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts +++ b/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { Parser } from '@vscode/tree-sitter-wasm'; +import type * as Parser from '@vscode/tree-sitter-wasm'; import { Event } from '../../../base/common/event.js'; import { ITextModel } from '../../common/model.js'; import { ITextModelTreeSitter, ITreeSitterParseResult, ITreeSitterParserService, TreeUpdateEvent } from '../../common/services/treeSitterParserService.js'; diff --git a/src/vs/editor/test/browser/services/treeSitterParserService.test.ts b/src/vs/editor/test/browser/services/treeSitterParserService.test.ts index 5475195b7db4..f234cc6973e6 100644 --- a/src/vs/editor/test/browser/services/treeSitterParserService.test.ts +++ b/src/vs/editor/test/browser/services/treeSitterParserService.test.ts @@ -4,48 +4,57 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { TextModelTreeSitter, TreeSitterImporter, TreeSitterLanguages } from '../../../common/services/treeSitter/treeSitterParserService.js'; -import type { Parser } from '@vscode/tree-sitter-wasm'; +import { TextModelTreeSitter, TreeSitterLanguages } from '../../../common/services/treeSitter/treeSitterParserService.js'; +import type * as Parser from '@vscode/tree-sitter-wasm'; import { createTextModel } from '../../common/testTextModel.js'; import { timeout } from '../../../../base/common/async.js'; import { ConsoleMainLogger, ILogService } from '../../../../platform/log/common/log.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { LogService } from '../../../../platform/log/common/logService.js'; import { mock } from '../../../../base/test/common/mock.js'; +import { ITreeSitterImporter } from '../../../common/services/treeSitterParserService.js'; -class MockParser implements Parser { - static async init(): Promise { } +class MockParser implements Parser.Parser { + language: Parser.Language | null = null; delete(): void { } - parse(input: string | Parser.Input, oldTree?: Parser.Tree, options?: Parser.Options): Parser.Tree { + setLanguage(language: Parser.Language | null) { return this; } + parse(callback: string | Parser.ParseCallback, oldTree?: Parser.Tree | null, options?: Parser.ParseOptions): Parser.Tree | null { return new MockTree(); } + reset(): void { } getIncludedRanges(): Parser.Range[] { return []; } getTimeoutMicros(): number { return 0; } setTimeoutMicros(timeout: number): void { } - reset(): void { } - getLanguage(): Parser.Language { return {} as any; } - setLanguage(): void { } - getLogger(): Parser.Logger { + setLogger(callback: Parser.LogCallback | boolean | null): this { throw new Error('Method not implemented.'); } - setLogger(logFunc?: Parser.Logger | false | null): void { + getLogger(): Parser.LogCallback | null { throw new Error('Method not implemented.'); } } -class MockTreeSitterImporter extends TreeSitterImporter { - public override async getParserClass(): Promise { +class MockTreeSitterImporter implements ITreeSitterImporter { + _serviceBrand: undefined; + getParserClass(): Promise { return MockParser as any; } + getLanguageClass(): Promise { + return MockLanguage as any; + } + getQueryClass(): Promise { + throw new Error('Method not implemented.'); + } + } class MockTree implements Parser.Tree { + language: Parser.Language = new MockLanguage(); editorLanguage: string = ''; editorContents: string = ''; - rootNode: Parser.SyntaxNode = {} as any; - rootNodeWithOffset(offsetBytes: number, offsetExtent: Parser.Point): Parser.SyntaxNode { + rootNode: Parser.Node = {} as any; + rootNodeWithOffset(offsetBytes: number, offsetExtent: Parser.Point): Parser.Node { throw new Error('Method not implemented.'); } copy(): Parser.Tree { @@ -73,6 +82,23 @@ class MockTree implements Parser.Tree { } class MockLanguage implements Parser.Language { + types: string[] = []; + fields: (string | null)[] = []; + get name(): string | null { + throw new Error('Method not implemented.'); + } + get abiVersion(): number { + throw new Error('Method not implemented.'); + } + get metadata(): Parser.LanguageMetadata | null { + throw new Error('Method not implemented.'); + } + get supertypes(): number[] { + throw new Error('Method not implemented.'); + } + subtypes(supertype: number): number[] { + throw new Error('Method not implemented.'); + } version: number = 0; fieldCount: number = 0; stateCount: number = 0; @@ -101,14 +127,14 @@ class MockLanguage implements Parser.Language { query(source: string): Parser.Query { throw new Error('Method not implemented.'); } - lookaheadIterator(stateId: number): Parser.LookaheadIterable | null { + lookaheadIterator(stateId: number): Parser.LookaheadIterator | null { throw new Error('Method not implemented.'); } languageId: string = ''; } suite('TreeSitterParserService', function () { - const treeSitterImporter: TreeSitterImporter = new MockTreeSitterImporter(); + const treeSitterImporter: ITreeSitterImporter = new MockTreeSitterImporter(); let logService: ILogService; let telemetryService: ITelemetryService; setup(function () { diff --git a/src/vs/editor/test/common/services/testTreeSitterService.ts b/src/vs/editor/test/common/services/testTreeSitterService.ts index e2ca50462ecc..a41369c53e0c 100644 --- a/src/vs/editor/test/common/services/testTreeSitterService.ts +++ b/src/vs/editor/test/common/services/testTreeSitterService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { Parser } from '@vscode/tree-sitter-wasm'; +import type * as Parser from '@vscode/tree-sitter-wasm'; import { Event } from '../../../../base/common/event.js'; import { ITextModel } from '../../../common/model.js'; import { ITreeSitterParserService, ITreeSitterParseResult, ITextModelTreeSitter, TreeUpdateEvent } from '../../../common/services/treeSitterParserService.js'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index 4da76c311442..ff2710dcd2b2 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -32,7 +32,7 @@ import { SEMANTIC_HIGHLIGHTING_SETTING_ID, IEditorSemanticHighlightingOptions } import { Schemas } from '../../../../../base/common/network.js'; import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; import { ITreeSitterParserService } from '../../../../../editor/common/services/treeSitterParserService.js'; -import type { Parser } from '@vscode/tree-sitter-wasm'; +import type * as Parser from '@vscode/tree-sitter-wasm'; const $ = dom.$; @@ -646,11 +646,11 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { return null; } - private _walkTreeforPosition(cursor: Parser.TreeCursor, pos: Position): Parser.SyntaxNode | null { + private _walkTreeforPosition(cursor: Parser.TreeCursor, pos: Position): Parser.Node | null { const offset = this._model.getOffsetAt(pos); cursor.gotoFirstChild(); let goChild: boolean = false; - let lastGoodNode: Parser.SyntaxNode | null = null; + let lastGoodNode: Parser.Node | null = null; do { if (cursor.currentNode.startIndex <= offset && offset < cursor.currentNode.endIndex) { goChild = true; @@ -662,7 +662,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { return lastGoodNode; } - private _getTreeSitterTokenAtPosition(tree: Parser.Tree, pos: Position): Parser.SyntaxNode | null { + private _getTreeSitterTokenAtPosition(tree: Parser.Tree, pos: Position): Parser.Node | null { const cursor = tree.walk(); return this._walkTreeforPosition(cursor, pos); diff --git a/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts index 57e6c944465b..496922ef3913 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from '../../../../base/common/uri.js'; -import type { Parser } from '@vscode/tree-sitter-wasm'; +import type * as Parser from '@vscode/tree-sitter-wasm'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.contribution.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.contribution.ts index 21701ec617c5..6c06c1b189c1 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.contribution.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.contribution.ts @@ -6,7 +6,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; import { TreeSitterTextModelService } from '../../../../editor/common/services/treeSitter/treeSitterParserService.js'; -import { ITreeSitterParserService } from '../../../../editor/common/services/treeSitterParserService.js'; +import { ITreeSitterImporter, ITreeSitterParserService, TreeSitterImporter } from '../../../../editor/common/services/treeSitterParserService.js'; import { ITreeSitterTokenizationFeature } from './treeSitterTokenizationFeature.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; @@ -28,6 +28,7 @@ class TreeSitterTokenizationInstantiator implements IWorkbenchContribution { ) { } } +registerSingleton(ITreeSitterImporter, TreeSitterImporter, InstantiationType.Eager); registerSingleton(ITreeSitterParserService, TreeSitterTextModelService, InstantiationType.Eager); registerWorkbenchContribution2(TreeSitterTokenizationInstantiator.ID, TreeSitterTokenizationInstantiator, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts index 34bcb6b7536c..139cd64d5c33 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { Parser } from '@vscode/tree-sitter-wasm'; +import type * as Parser from '@vscode/tree-sitter-wasm'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; import { AppResourcePath, FileAccess } from '../../../../base/common/network.js'; import { ILanguageIdCodec, ITreeSitterTokenizationSupport, LazyTokenizationSupport, QueryCapture, TreeSitterTokenizationRegistry } from '../../../../editor/common/languages.js'; import { ITextModel } from '../../../../editor/common/model.js'; -import { EDITOR_EXPERIMENTAL_PREFER_TREESITTER, ITreeSitterParserService, ITreeSitterParseResult, TreeUpdateEvent, RangeChange } from '../../../../editor/common/services/treeSitterParserService.js'; +import { EDITOR_EXPERIMENTAL_PREFER_TREESITTER, ITreeSitterParserService, ITreeSitterParseResult, TreeUpdateEvent, RangeChange, ITreeSitterImporter } from '../../../../editor/common/services/treeSitterParserService.js'; import { IModelTokensChangedEvent } from '../../../../editor/common/textModelEvents.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../platform/files/common/files.js'; @@ -45,6 +45,7 @@ export class TreeSitterTokenizationFeature extends Disposable implements ITreeSi private readonly _tokenizersRegistrations: DisposableMap = this._register(new DisposableMap()); constructor( + @ITreeSitterImporter private readonly _treeSitterImporter: ITreeSitterImporter, @ILanguageService private readonly _languageService: ILanguageService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -92,7 +93,8 @@ export class TreeSitterTokenizationFeature extends Disposable implements ITreeSi private async _createTokenizationSupport(languageId: string): Promise { const queries = await this._fetchQueries(languageId); - return this._instantiationService.createInstance(TreeSitterTokenizationSupport, queries, languageId, this._languageService.languageIdCodec); + const Query = await this._treeSitterImporter.getQueryClass(); + return this._instantiationService.createInstance(TreeSitterTokenizationSupport, queries, Query, languageId, this._languageService.languageIdCodec); } } @@ -105,6 +107,7 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi constructor( private readonly _queries: TreeSitterQueries, + private readonly Query: typeof Parser.Query, private readonly _languageId: string, private readonly _languageIdCodec: ILanguageIdCodec, @ITreeSitterParserService private readonly _treeSitterService: ITreeSitterParserService, @@ -314,12 +317,12 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi if (!language) { if (!this._languageAddedListener) { this._languageAddedListener = this._register(Event.onceIf(this._treeSitterService.onDidAddLanguage, e => e.id === this._languageId)((e) => { - this._query = e.language.query(this._queries); + this._query = new this.Query(e.language, this._queries); })); } return; } - this._query = language.query(this._queries); + this._query = new this.Query(language, this._queries); } return this._query; } diff --git a/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts b/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts index 1eab214ca26a..c246b1e8568a 100644 --- a/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts +++ b/src/vs/workbench/test/electron-main/treeSitterTokenizationFeature.test.ts @@ -20,7 +20,7 @@ import { IEnvironmentService } from '../../../platform/environment/common/enviro import { ModelService } from '../../../editor/common/services/modelService.js'; // eslint-disable-next-line local/code-layering, local/code-import-patterns import { TreeSitterTokenizationFeature } from '../../services/treeSitter/browser/treeSitterTokenizationFeature.js'; -import { ITreeSitterParserService, TreeUpdateEvent } from '../../../editor/common/services/treeSitterParserService.js'; +import { ITreeSitterImporter, ITreeSitterParserService, TreeSitterImporter, TreeUpdateEvent } from '../../../editor/common/services/treeSitterParserService.js'; import { ITreeSitterTokenizationSupport, TreeSitterTokenizationRegistry } from '../../../editor/common/languages.js'; import { FileService } from '../../../platform/files/common/fileService.js'; import { Schemas } from '../../../base/common/network.js'; @@ -131,6 +131,7 @@ suite('Tree Sitter TokenizationFeature', function () { instantiationService.set(ITextResourcePropertiesService, textResourcePropertiesService); languageConfigurationService = disposables.add(instantiationService.createInstance(TestLanguageConfigurationService)); instantiationService.set(ILanguageConfigurationService, languageConfigurationService); + instantiationService.set(ITreeSitterImporter, instantiationService.createInstance(TreeSitterImporter)); fileService = disposables.add(instantiationService.createInstance(FileService)); const diskFileSystemProvider = disposables.add(new DiskFileSystemProvider(logService)); From 2035725aca7c3a4961f60cb894488875015dd6c4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 5 Feb 2025 10:50:04 -0800 Subject: [PATCH 1239/3587] Add generic fig tests Part of #239515 --- .../terminal-suggest/src/test/fig.test.ts | 133 ++++++++++++++++++ .../src/test/terminalSuggestMain.test.ts | 3 + 2 files changed, 136 insertions(+) create mode 100644 extensions/terminal-suggest/src/test/fig.test.ts diff --git a/extensions/terminal-suggest/src/test/fig.test.ts b/extensions/terminal-suggest/src/test/fig.test.ts new file mode 100644 index 000000000000..d6c6b2682849 --- /dev/null +++ b/extensions/terminal-suggest/src/test/fig.test.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { testPaths, type ISuiteSpec } from './helpers'; + +export const figGenericTestSuites: ISuiteSpec[] = [ + { + name: 'Fig name and description only', + completionSpecs: [ + { + name: 'foo', + description: 'Foo', + } + ], + availableCommands: 'foo', + testSpecs: [ + // Typing a path + { input: '|', expectedCompletions: ['foo'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'f|', expectedCompletions: ['foo'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'fo|', expectedCompletions: ['foo'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: 'foo|', expectedCompletions: ['foo'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + + // Basic arguments (fallback) + { input: 'foo |', expectedCompletions: [], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } } + ] + }, + { + name: 'Fig top-level args files only', + completionSpecs: [ + { + name: 'foo', + description: 'Foo', + args: { + template: 'filepaths', + isVariadic: true, + } + } + ], + availableCommands: 'foo', + testSpecs: [ + { input: 'foo |', expectedCompletions: [], expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } }, + ] + }, + { + name: 'Fig top-level args folders only', + completionSpecs: [ + { + name: 'foo', + description: 'Foo', + args: { + template: 'folders', + isVariadic: true, + } + } + ], + availableCommands: 'foo', + testSpecs: [ + { input: 'foo |', expectedCompletions: [], expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, + ] + }, + { + name: 'Fig top-level args files and folders', + completionSpecs: [ + { + name: 'foo', + description: 'Foo', + args: { + template: ['filepaths', 'folders'], + isVariadic: true, + } + } + ], + availableCommands: 'foo', + testSpecs: [ + { input: 'foo |', expectedCompletions: [], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + ] + }, + { + name: 'Fig top-level options', + completionSpecs: [ + { + name: 'foo', + description: 'Foo', + options: [ + { name: '--bar', description: 'Bar' }, + { name: '--baz', description: 'Baz' } + ] + } + ], + availableCommands: 'foo', + testSpecs: [ + { input: 'foo |', expectedCompletions: ['--bar', '--baz'] }, + { input: 'foo bar|', expectedCompletions: ['--bar', '--baz'] }, + // TODO: Duplicate options should not be presented https://github.com/microsoft/vscode/issues/239607 + // { input: 'foo --bar |', expectedCompletions: ['--baz'] }, + // { input: 'foo --baz |', expectedCompletions: ['--bar'] }, + ] + }, + { + name: 'Fig top-level option values', + completionSpecs: [ + { + name: 'foo', + description: 'Foo', + options: [ + { + name: '--bar', + description: 'Bar', + args: { + name: 'baz', + suggestions: [ + 'a', + 'b', + 'c', + ], + } + } + ] + } + ], + availableCommands: 'foo', + testSpecs: [ + { input: 'foo |', expectedCompletions: ['--bar'] }, + { input: 'foo --bar |', expectedCompletions: ['a', 'b', 'c'] }, + // TODO: All options should be suggested here? https://github.com/microsoft/vscode/issues/239713 + // { input: 'foo --bar a|', expectedCompletions: ['a', 'b', 'c'] }, + // { input: 'foo --bar b|', expectedCompletions: ['a', 'b', 'c'] }, + // { input: 'foo --bar c|', expectedCompletions: ['a', 'b', 'c'] }, + ] + } +]; diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index 189a022101b7..58c07c7a9c10 100644 --- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -20,6 +20,7 @@ import { rmdirTestSuiteSpec } from './completions/upstream/rmdir.test'; import { touchTestSuiteSpec } from './completions/upstream/touch.test'; import { osIsWindows } from '../helpers/os'; import codeCompletionSpec from '../completions/code'; +import { figGenericTestSuites } from './fig.test'; const testSpecs2: ISuiteSpec[] = [ { @@ -34,6 +35,8 @@ const testSpecs2: ISuiteSpec[] = [ ] }, + ...figGenericTestSuites, + // completions/ cdTestSuite, codeTestSuite, From cd52101a4b2b05bd8845a75eb0a54a269f1c0efb Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 5 Feb 2025 11:09:18 -0800 Subject: [PATCH 1240/3587] Register terminal variables from vscode (#239653) * Register terminal variables from vscode * Simplify terminalLastCommand * Bump chatParticipantPrivate version just to ensure that moving variables from the extension to core doesn't lead to dupes or missing vars --- .../common/extensionsApiProposals.ts | 2 +- .../contrib/chat/browser/chat.contribution.ts | 2 + .../browser/contrib/chatInputCompletions.ts | 2 - .../chat/browser/variables/variables.ts | 90 +++++++++++++++++++ ...scode.proposed.chatParticipantPrivate.d.ts | 2 +- 5 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/variables/variables.ts diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 79fc43225b96..bf5936b2a707 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -33,7 +33,7 @@ const _allApiProposals = { }, chatParticipantPrivate: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts', - version: 2 + version: 3 }, chatProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 47c41d56362b..bb2b7f1e6e91 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -91,6 +91,7 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IPromptSyntaxService } from '../common/promptSyntax/service/types.js'; import { PromptSyntaxService } from '../common/promptSyntax/service/promptSyntaxService.js'; +import { BuiltinVariablesContribution } from './variables/variables.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -392,6 +393,7 @@ registerWorkbenchContribution2(ChatGettingStartedContribution.ID, ChatGettingSta registerWorkbenchContribution2(ChatSetupContribution.ID, ChatSetupContribution, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatQuotasStatusBarEntry.ID, ChatQuotasStatusBarEntry, WorkbenchPhase.Eventually); registerWorkbenchContribution2(BuiltinToolsContribution.ID, BuiltinToolsContribution, WorkbenchPhase.Eventually); +registerWorkbenchContribution2(BuiltinVariablesContribution.ID, BuiltinVariablesContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatAgentSettingContribution.ID, ChatAgentSettingContribution, WorkbenchPhase.BlockRestore); registerChatActions(); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 9ab466c60a44..4e3fc6dd1e7d 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -22,7 +22,6 @@ import { IOutlineModelService } from '../../../../../editor/contrib/documentSymb import { localize } from '../../../../../nls.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; @@ -835,7 +834,6 @@ class VariableCompletions extends Disposable { @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, - @IConfigurationService configService: IConfigurationService, @ILanguageModelToolsService toolsService: ILanguageModelToolsService ) { super(); diff --git a/src/vs/workbench/contrib/chat/browser/variables/variables.ts b/src/vs/workbench/contrib/chat/browser/variables/variables.ts new file mode 100644 index 000000000000..e2e59860f300 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/variables/variables.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { ProviderResult } from '../../../../../editor/common/languages.js'; +import { localize } from '../../../../../nls.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; +import { IWorkbenchContribution } from '../../../../common/contributions.js'; +import { ITerminalService } from '../../../terminal/browser/terminal.js'; +import { IChatModel } from '../../common/chatModel.js'; +import { IChatRequestVariableValue, IChatVariableResolverProgress, IChatVariablesService } from '../../common/chatVariables.js'; + +export class BuiltinVariablesContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'chat.builtinVariables'; + + constructor( + @IChatVariablesService varsService: IChatVariablesService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + const terminalSelectionVar = instantiationService.createInstance(TerminalSelectionVariable); + varsService.registerVariable({ + id: 'copilot.terminalSelection', + name: 'terminalSelection', + fullName: localize('termSelection', "Terminal Selection"), + description: localize('termSelectionDescription', "The active terminal's selection"), + icon: Codicon.terminal, + }, async (messageText: string, arg: string | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise => { + return await terminalSelectionVar.resolve(token); + }); + + const terminalLastCommandVar = instantiationService.createInstance(TerminalLastCommandVariable); + varsService.registerVariable({ + id: 'copilot.terminalLastCommand', + name: 'terminalLastCommand', + fullName: localize('terminalLastCommand', "Terminal Last Command"), + description: localize('termLastCommandDesc', "The active terminal's last run command"), + icon: Codicon.terminal, + }, async (messageText: string, arg: string | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise => { + return await terminalLastCommandVar.resolve(token); + }); + } +} + +class TerminalSelectionVariable { + constructor( + @ITerminalService private readonly terminalService: ITerminalService + ) { } + + resolve(token: CancellationToken): ProviderResult { + const terminalSelection = this.terminalService.activeInstance?.selection; + return terminalSelection ? `The active terminal's selection:\n${terminalSelection}` : undefined; + } +} + +class TerminalLastCommandVariable { + constructor( + @ITerminalService private readonly terminalService: ITerminalService + ) { } + + resolve(token: CancellationToken): ProviderResult { + const lastCommand = this.terminalService.activeInstance?.capabilities.get(TerminalCapability.CommandDetection)?.commands.at(-1); + if (!lastCommand) { + return; + } + + const userPrompt: string[] = []; + userPrompt.push(`The following is the last command run in the terminal:`); + userPrompt.push(lastCommand.command); + if (lastCommand.cwd) { + userPrompt.push(`It was run in the directory:`); + userPrompt.push(lastCommand.cwd); + } + const output = lastCommand.getOutput(); + if (output) { + userPrompt.push(`It has the following output:`); + userPrompt.push(output); + } + + const prompt = userPrompt.join('\n'); + return `The active terminal's last run command:\n${prompt}`; + } +} diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index 0aff2d2091b1..10a4ce36ce52 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 2 +// version: 3 declare module 'vscode' { From c77b0b46a8e4081030b62e82aab669fb878a5568 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 6 Feb 2025 06:17:38 +1100 Subject: [PATCH 1241/3587] Remove assumption there's just one editing session (#239711) --- .../contrib/chatEdit/notebookCellDecorators.ts | 12 +++++------- .../chatEdit/notebookChatActionsOverlay.ts | 8 ++++++-- .../chatEdit/notebookChatEditController.ts | 6 +++--- .../contrib/chatEdit/notebookSynchronizer.ts | 7 ++----- .../chatEdit/notebookSynchronizerService.ts | 16 ++++++---------- 5 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts index f7647dda6a75..50bfee22f313 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts @@ -86,15 +86,13 @@ export class NotebookCellDiffDecorator extends DisposableStore { })); const shouldBeReadOnly = derived(this, r => { - const editor = editorObs.read(r); - if (!editor) { - return false; - } - const value = this._chatEditingService.globalEditingSessionObs.read(r); - if (!value || value.state.read(r) !== ChatEditingSessionState.StreamingEdits) { + const editorUri = editorObs.read(r)?.getModel()?.uri; + if (!editorUri) { return false; } - return value.entries.read(r).some(e => isEqual(e.modifiedURI, editor.getModel()?.uri)); + const sessions = this._chatEditingService.editingSessionsObs.read(r); + const session = sessions.find(s => s.entries.read(r).some(e => isEqual(e.modifiedURI, editorUri))); + return session?.state.read(r) === ChatEditingSessionState.StreamingEdits; }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts index f9c9ed3a545f..33bc1f15d1d4 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts @@ -33,9 +33,13 @@ export class NotebookChatActionsOverlayController extends Disposable { const notebookModel = observableFromEvent(this.notebookEditor.onDidChangeModel, e => e); this._register(autorunWithStore((r, store) => { - const session = this._chatEditingService.globalEditingSessionObs.read(r); const model = notebookModel.read(r); - if (!model || !session) { + if (!model) { + return; + } + const sessions = this._chatEditingService.editingSessionsObs.read(r); + const session = sessions.find(s => s.readEntry(model.uri, r)); + if (!session) { return; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts index c6d9db8501f0..b62807826523 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts @@ -80,12 +80,12 @@ class NotebookChatEditorController extends Disposable { let notebookSynchronizer: IReference; const entryObs = derived((r) => { - const session = this._chatEditingService.globalEditingSessionObs.read(r); const model = notebookModel.read(r); - if (!model || !session) { + if (!model) { return; } - return session.readEntry(model.uri, r); + const sessions = this._chatEditingService.editingSessionsObs.read(r); + return sessions.map(s => s.readEntry(model.uri, r)).find(r => !!r); }).recomputeInitiallyAndOnChange(this._store); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts index 2c376b3b0309..b22e053cad73 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts @@ -91,11 +91,8 @@ export class NotebookModelSynchronizer extends Disposable { super(); const entryObs = derived((r) => { - const session = _chatEditingService.globalEditingSessionObs.read(r); - if (!session) { - return; - } - return session.readEntry(model.uri, r); + const sessions = _chatEditingService.editingSessionsObs.read(r); + return sessions.map(s => s.readEntry(model.uri, r)).find(r => !!r); }).recomputeInitiallyAndOnChange(this._store); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts index 8f93143cccd0..d42aa1b1230a 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts @@ -28,8 +28,8 @@ class NotebookSynchronizerSaveParticipant extends NotebookSaveParticipant { } override async participate(workingCopy: IStoredFileWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress, token: CancellationToken): Promise { - const session = this._chatEditingService.globalEditingSessionObs.get(); - + const sessions = this._chatEditingService.editingSessionsObs.get(); + const session = sessions.find(s => s.getEntry(workingCopy.resource)); if (!session) { return; } @@ -74,14 +74,10 @@ export class NotebookSynchronizerService extends Disposable implements INotebook async revert(workingCopy: IStoredFileWorkingCopy | IUntitledFileWorkingCopy) { // check if we have mirror document const resource = workingCopy.resource; - - const session = this._chatEditingService.globalEditingSessionObs.get(); - - if (session) { - const entry = session.getEntry(resource); - if (entry instanceof ChatEditingModifiedNotebookEntry) { - await entry.revertMirrorDocument(); - } + const sessions = this._chatEditingService.editingSessionsObs.get(); + const entry = sessions.map(s => s.getEntry(resource)).find(r => !!r); + if (entry instanceof ChatEditingModifiedNotebookEntry) { + await entry.revertMirrorDocument(); } } } From 9f448306f040600e5c8ee6b2f833d1a6412aceca Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Wed, 5 Feb 2025 11:19:06 -0800 Subject: [PATCH 1242/3587] Hide vertical scrollbar in Getting Started pages and adjust video element attributes (#239716) --- .../contrib/welcomeGettingStarted/browser/gettingStarted.ts | 5 +++-- .../browser/gettingStartedDetailsRenderer.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 6346f20f11f9..39313994cd0d 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -72,6 +72,7 @@ import { GettingStartedIndexList } from './gettingStartedList.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; import { AccessibleViewAction } from '../../accessibility/browser/accessibleViewActions.js'; import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; +import { ScrollbarVisibility } from '../../../../base/common/scrollable.js'; const SLIDE_TRANSITION_TIME_MS = 250; const configurationKey = 'workbench.startupEditor'; @@ -820,8 +821,8 @@ export class GettingStartedPage extends EditorPane { this.stepsContent = $('.gettingStartedDetailsContent', {}); - this.detailsPageScrollbar = this._register(new DomScrollableElement(this.stepsContent, { className: 'full-height-scrollable' })); - this.categoriesPageScrollbar = this._register(new DomScrollableElement(this.categoriesSlide, { className: 'full-height-scrollable categoriesScrollbar' })); + this.detailsPageScrollbar = this._register(new DomScrollableElement(this.stepsContent, { className: 'full-height-scrollable', vertical: ScrollbarVisibility.Hidden })); + this.categoriesPageScrollbar = this._register(new DomScrollableElement(this.categoriesSlide, { className: 'full-height-scrollable categoriesScrollbar', vertical: ScrollbarVisibility.Hidden })); this.stepsSlide.appendChild(this.detailsPageScrollbar.getDomNode()); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts index d2be9cd03095..9490776109bd 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts @@ -218,7 +218,7 @@ export class GettingStartedDetailsRenderer { - ${escape(localize('show', "show"))} + ${escape(localize('downloadExtensionData', "Download Extension Data"))}
 				
diff --git a/src/vs/workbench/contrib/issue/browser/issueReporterService.ts b/src/vs/workbench/contrib/issue/browser/issueReporterService.ts
index ae2d6106f757..abfc9943e175 100644
--- a/src/vs/workbench/contrib/issue/browser/issueReporterService.ts
+++ b/src/vs/workbench/contrib/issue/browser/issueReporterService.ts
@@ -4,9 +4,11 @@
  *--------------------------------------------------------------------------------------------*/
 import { IProductConfiguration } from '../../../../base/common/product.js';
 import { localize } from '../../../../nls.js';
+import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
+import { IFileService } from '../../../../platform/files/common/files.js';
 import { IThemeService } from '../../../../platform/theme/common/themeService.js';
-import { BaseIssueReporterService } from './baseIssueReporterService.js';
 import { IIssueFormService, IssueReporterData } from '../common/issue.js';
+import { BaseIssueReporterService } from './baseIssueReporterService.js';
 
 // GitHub has let us know that we could up our limit here to 8k. We chose 7500 to play it safe.
 // ref https://github.com/microsoft/vscode/issues/159191
@@ -23,9 +25,11 @@ export class IssueWebReporter extends BaseIssueReporterService {
 		product: IProductConfiguration,
 		window: Window,
 		@IIssueFormService issueFormService: IIssueFormService,
-		@IThemeService themeService: IThemeService
+		@IThemeService themeService: IThemeService,
+		@IFileService fileService: IFileService,
+		@IFileDialogService fileDialogService: IFileDialogService
 	) {
-		super(disableExtensions, data, os, product, window, true, issueFormService, themeService);
+		super(disableExtensions, data, os, product, window, true, issueFormService, themeService, fileService, fileDialogService);
 
 		const target = this.window.document.querySelector('.block-system .block-info');
 
diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts
index bc6b696e1048..a21991a5d0b1 100644
--- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts
+++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts
@@ -8,8 +8,10 @@ import { IProductConfiguration } from '../../../../base/common/product.js';
 import { URI } from '../../../../base/common/uri.js';
 import { localize } from '../../../../nls.js';
 import { isRemoteDiagnosticError } from '../../../../platform/diagnostics/common/diagnostics.js';
-import { IProcessMainService } from '../../../../platform/process/common/process.js';
+import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
+import { IFileService } from '../../../../platform/files/common/files.js';
 import { INativeHostService } from '../../../../platform/native/common/native.js';
+import { IProcessMainService } from '../../../../platform/process/common/process.js';
 import { IThemeService } from '../../../../platform/theme/common/themeService.js';
 import { applyZoom } from '../../../../platform/window/electron-sandbox/window.js';
 import { BaseIssueReporterService } from '../browser/baseIssueReporterService.js';
@@ -40,9 +42,11 @@ export class IssueReporter extends BaseIssueReporterService {
 		@INativeHostService private readonly nativeHostService: INativeHostService,
 		@IIssueFormService issueFormService: IIssueFormService,
 		@IProcessMainService processMainService: IProcessMainService,
-		@IThemeService themeService: IThemeService
+		@IThemeService themeService: IThemeService,
+		@IFileService fileService: IFileService,
+		@IFileDialogService fileDialogService: IFileDialogService
 	) {
-		super(disableExtensions, data, os, product, window, false, issueFormService, themeService);
+		super(disableExtensions, data, os, product, window, false, issueFormService, themeService, fileService, fileDialogService);
 		this.processMainService = processMainService;
 		this.processMainService.$getSystemInfo().then(info => {
 			this.issueReporterModel.update({ systemInfo: info });
@@ -90,7 +94,6 @@ export class IssueReporter extends BaseIssueReporterService {
 
 	public override async submitToGitHub(issueTitle: string, issueBody: string, gitHubDetails: { owner: string; repositoryName: string }): Promise {
 		if (issueBody.length > MAX_GITHUB_API_LENGTH) {
-			console.error('Issue body is too long.');
 			return false;
 		}
 		const url = `https://api.github.com/repos/${gitHubDetails.owner}/${gitHubDetails.repositoryName}/issues`;

From b96caf191e8107e992c564b88e600da1e1d9597d Mon Sep 17 00:00:00 2001
From: Rob Lourens 
Date: Wed, 5 Feb 2025 17:34:23 -0800
Subject: [PATCH 1264/3587] Move variable types that completions need into
 additions.d.ts (#239752)

---
 ...ode.proposed.chatParticipantAdditions.d.ts | 26 ++++++++++++++++++
 .../vscode.proposed.chatVariableResolver.d.ts | 27 -------------------
 2 files changed, 26 insertions(+), 27 deletions(-)

diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts
index 3fafc42e7a45..814274a48dd6 100644
--- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts
+++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts
@@ -397,4 +397,30 @@ declare module 'vscode' {
 	export namespace lm {
 		export function fileIsIgnored(uri: Uri, token: CancellationToken): Thenable;
 	}
+
+	export interface ChatVariableValue {
+		/**
+		 * The detail level of this chat variable value. If possible, variable resolvers should try to offer shorter values that will consume fewer tokens in an LLM prompt.
+		 */
+		level: ChatVariableLevel;
+
+		/**
+		 * The variable's value, which can be included in an LLM prompt as-is, or the chat participant may decide to read the value and do something else with it.
+		 */
+		value: string | Uri;
+
+		/**
+		 * A description of this value, which could be provided to the LLM as a hint.
+		 */
+		description?: string;
+	}
+
+	/**
+	 * The detail level of this chat variable value.
+	 */
+	export enum ChatVariableLevel {
+		Short = 1,
+		Medium = 2,
+		Full = 3
+	}
 }
diff --git a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts
index ec386ec928c8..eca26b694b97 100644
--- a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts
+++ b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts
@@ -21,23 +21,6 @@ declare module 'vscode' {
 		export function registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: ChatVariableResolver, fullName?: string, icon?: ThemeIcon): Disposable;
 	}
 
-	export interface ChatVariableValue {
-		/**
-		 * The detail level of this chat variable value. If possible, variable resolvers should try to offer shorter values that will consume fewer tokens in an LLM prompt.
-		 */
-		level: ChatVariableLevel;
-
-		/**
-		 * The variable's value, which can be included in an LLM prompt as-is, or the chat participant may decide to read the value and do something else with it.
-		 */
-		value: string | Uri;
-
-		/**
-		 * A description of this value, which could be provided to the LLM as a hint.
-		 */
-		description?: string;
-	}
-
 	// TODO@API align with ChatRequest
 	export interface ChatVariableContext {
 		/**
@@ -67,16 +50,6 @@ declare module 'vscode' {
 		resolve2?(name: string, context: ChatVariableContext, stream: ChatVariableResolverResponseStream, token: CancellationToken): ProviderResult;
 	}
 
-
-	/**
-	 * The detail level of this chat variable value.
-	 */
-	export enum ChatVariableLevel {
-		Short = 1,
-		Medium = 2,
-		Full = 3
-	}
-
 	export interface ChatVariableResolverResponseStream {
 		/**
 		 * Push a progress part to this stream. Short-hand for

From 4aa863ca5602bfc326c2e4ed76f4b019d5a1a514 Mon Sep 17 00:00:00 2001
From: Rob Lourens 
Date: Wed, 5 Feb 2025 17:34:43 -0800
Subject: [PATCH 1265/3587] Move chat participant detection to private.d.ts
 (#239754)

---
 .../workbench/api/common/extHost.api.impl.ts  |  2 +-
 ...ode.proposed.chatParticipantAdditions.d.ts | 17 -----------------
 ...scode.proposed.chatParticipantPrivate.d.ts | 19 +++++++++++++++++++
 3 files changed, 20 insertions(+), 18 deletions(-)

diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts
index 7766c78231fc..789d30aec57f 100644
--- a/src/vs/workbench/api/common/extHost.api.impl.ts
+++ b/src/vs/workbench/api/common/extHost.api.impl.ts
@@ -1454,7 +1454,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
 				return extHostChatAgents2.createDynamicChatAgent(extension, id, dynamicProps, handler);
 			},
 			registerChatParticipantDetectionProvider(provider: vscode.ChatParticipantDetectionProvider) {
-				checkProposedApiEnabled(extension, 'chatParticipantAdditions');
+				checkProposedApiEnabled(extension, 'chatParticipantPrivate');
 				return extHostChatAgents2.registerChatParticipantDetectionProvider(extension, provider);
 			},
 			registerRelatedFilesProvider(provider: vscode.ChatRelatedFilesProvider, metadata: vscode.ChatRelatedFilesProviderMetadata) {
diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts
index 814274a48dd6..ed5da524ceae 100644
--- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts
+++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts
@@ -278,23 +278,6 @@ declare module 'vscode' {
 		 * Create a chat participant with the extended progress type
 		 */
 		export function createChatParticipant(id: string, handler: ChatExtendedRequestHandler): ChatParticipant;
-
-		export function registerChatParticipantDetectionProvider(participantDetectionProvider: ChatParticipantDetectionProvider): Disposable;
-	}
-
-	export interface ChatParticipantMetadata {
-		participant: string;
-		command?: string;
-		disambiguation: { category: string; description: string; examples: string[] }[];
-	}
-
-	export interface ChatParticipantDetectionResult {
-		participant: string;
-		command?: string;
-	}
-
-	export interface ChatParticipantDetectionProvider {
-		provideParticipantDetection(chatRequest: ChatRequest, context: ChatContext, options: { participants?: ChatParticipantMetadata[]; location: ChatLocation }, token: CancellationToken): ProviderResult;
 	}
 
 	/*
diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts
index 10a4ce36ce52..b5b7c170df34 100644
--- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts
+++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts
@@ -127,4 +127,23 @@ declare module 'vscode' {
 		pastTenseMessage?: string | MarkdownString;
 		tooltip?: string | MarkdownString;
 	}
+
+	export interface ChatParticipantMetadata {
+		participant: string;
+		command?: string;
+		disambiguation: { category: string; description: string; examples: string[] }[];
+	}
+
+	export interface ChatParticipantDetectionResult {
+		participant: string;
+		command?: string;
+	}
+
+	export interface ChatParticipantDetectionProvider {
+		provideParticipantDetection(chatRequest: ChatRequest, context: ChatContext, options: { participants?: ChatParticipantMetadata[]; location: ChatLocation }, token: CancellationToken): ProviderResult;
+	}
+
+	export namespace chat {
+		export function registerChatParticipantDetectionProvider(participantDetectionProvider: ChatParticipantDetectionProvider): Disposable;
+	}
 }

From 520c873a12341807461437e61b817781853ef462 Mon Sep 17 00:00:00 2001
From: Bhavya U 
Date: Wed, 5 Feb 2025 18:03:09 -0800
Subject: [PATCH 1266/3587] refactor: clean up startup page logic (#239757)

* refactor: remove unused log service and clean up startup page logic

* More logging
---
 .../browser/startupPage.ts                    | 28 ++++++-------------
 1 file changed, 9 insertions(+), 19 deletions(-)

diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts
index fed108054460..1b5a6a1f92cc 100644
--- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts
+++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts
@@ -24,11 +24,11 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
 import { getTelemetryLevel } from '../../../../platform/telemetry/common/telemetryUtils.js';
 import { TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js';
 import { IProductService } from '../../../../platform/product/common/productService.js';
-import { ILogService } from '../../../../platform/log/common/log.js';
 import { INotificationService } from '../../../../platform/notification/common/notification.js';
 import { localize } from '../../../../nls.js';
 import { IEditorResolverService, RegisteredEditorPriority } from '../../../services/editor/common/editorResolverService.js';
 import { TerminalCommandId } from '../../terminal/common/terminal.js';
+import { ILogService } from '../../../../platform/log/common/log.js';
 
 export const restoreWalkthroughsConfigurationKey = 'workbench.welcomePage.restorableWalkthroughs';
 export type RestoreWalkthroughsConfigurationValue = { folder: string; category?: string; step?: string };
@@ -114,15 +114,13 @@ export class StartupPageRunnerContribution extends Disposable implements IWorkbe
 			&& !this.storageService.get(telemetryOptOutStorageKey, StorageScope.PROFILE)
 		) {
 			this.storageService.store(telemetryOptOutStorageKey, true, StorageScope.PROFILE, StorageTarget.USER);
-			await this.openGettingStarted(true);
-			return;
 		}
 
 		if (this.tryOpenWalkthroughForFolder()) {
 			return;
 		}
 
-		const enabled = isStartupPageEnabled(this.configurationService, this.contextService, this.environmentService);
+		const enabled = isStartupPageEnabled(this.configurationService, this.contextService, this.environmentService, this.logService);
 		if (enabled && this.lifecycleService.startupKind !== StartupKind.ReloadedWindow) {
 			const hasBackups = await this.workingCopyBackupService.hasBackups();
 			if (hasBackups) { return; }
@@ -131,19 +129,7 @@ export class StartupPageRunnerContribution extends Disposable implements IWorkbe
 			if (!this.editorService.activeEditor || this.layoutService.openedDefaultEditors) {
 				const startupEditorSetting = this.configurationService.inspect(configurationKey);
 
-
-				const isStartupEditorReadme = startupEditorSetting.value === 'readme';
-				const isStartupEditorUserReadme = startupEditorSetting.userValue === 'readme';
-				const isStartupEditorDefaultReadme = startupEditorSetting.defaultValue === 'readme';
-
-				// 'readme' should not be set in workspace settings to prevent tracking,
-				// but it can be set as a default (as in codespaces or from configurationDefaults) or a user setting
-				if (isStartupEditorReadme && (!isStartupEditorUserReadme || !isStartupEditorDefaultReadme)) {
-					this.logService.warn(`Warning: 'workbench.startupEditor: readme' setting ignored due to being set somewhere other than user or default settings (user=${startupEditorSetting.userValue}, default=${startupEditorSetting.defaultValue})`);
-				}
-
-				const openWithReadme = isStartupEditorReadme && (isStartupEditorUserReadme || isStartupEditorDefaultReadme);
-				if (openWithReadme) {
+				if (startupEditorSetting.value === 'readme') {
 					await this.openReadme();
 				} else if (startupEditorSetting.value === 'welcomePage' || startupEditorSetting.value === 'welcomePageInEmptyWorkbench') {
 					await this.openGettingStarted();
@@ -222,7 +208,7 @@ export class StartupPageRunnerContribution extends Disposable implements IWorkbe
 	}
 }
 
-function isStartupPageEnabled(configurationService: IConfigurationService, contextService: IWorkspaceContextService, environmentService: IWorkbenchEnvironmentService) {
+function isStartupPageEnabled(configurationService: IConfigurationService, contextService: IWorkspaceContextService, environmentService: IWorkbenchEnvironmentService, logService: ILogService) {
 	if (environmentService.skipWelcome) {
 		return false;
 	}
@@ -235,8 +221,12 @@ function isStartupPageEnabled(configurationService: IConfigurationService, conte
 		}
 	}
 
+	if (startupEditor.value !== startupEditor.userRemoteValue) {
+		logService.info(`Startup editor is configured to be "${startupEditor.value}". This setting will be overridden by "${startupEditor.userRemoteValue}".`);
+	}
+
 	return startupEditor.value === 'welcomePage'
-		|| startupEditor.value === 'readme' && (startupEditor.userValue === 'readme' || startupEditor.defaultValue === 'readme')
+		|| startupEditor.value === 'readme'
 		|| (contextService.getWorkbenchState() === WorkbenchState.EMPTY && startupEditor.value === 'welcomePageInEmptyWorkbench')
 		|| startupEditor.value === 'terminal';
 }

From edd39c48ec3c81b5f8c6ddf1893a95acf8662187 Mon Sep 17 00:00:00 2001
From: Tyler James Leonhardt 
Date: Wed, 5 Feb 2025 18:06:30 -0800
Subject: [PATCH 1267/3587] Use `textbox` instead of `combobox` (#239758)

To get screenreaders to read the input.

Fixes https://github.com/microsoft/vscode/issues/237324
---
 src/vs/platform/quickinput/browser/quickInputBox.ts | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/vs/platform/quickinput/browser/quickInputBox.ts b/src/vs/platform/quickinput/browser/quickInputBox.ts
index e7868b89316a..010637adb9ed 100644
--- a/src/vs/platform/quickinput/browser/quickInputBox.ts
+++ b/src/vs/platform/quickinput/browser/quickInputBox.ts
@@ -29,10 +29,9 @@ export class QuickInputBox extends Disposable {
 		this.container = dom.append(this.parent, $('.quick-input-box'));
 		this.findInput = this._register(new FindInput(this.container, undefined, { label: '', inputBoxStyles, toggleStyles }));
 		const input = this.findInput.inputBox.inputElement;
-		input.role = 'combobox';
+		input.role = 'textbox';
 		input.ariaHasPopup = 'menu';
 		input.ariaAutoComplete = 'list';
-		input.ariaExpanded = 'true';
 	}
 
 	onKeyDown = (handler: (event: StandardKeyboardEvent) => void): IDisposable => {

From 8fde20fdb1b19fec24908e787aa4cf3b4f196283 Mon Sep 17 00:00:00 2001
From: Connor Peet 
Date: Wed, 5 Feb 2025 21:30:26 -0800
Subject: [PATCH 1268/3587] debug: fix toolbar X value goes off screen causing
 odd DND behavior (#239746)

* debug: fix toolbar X value goes off screen causing odd DND behavior

And respects the controls container better. I hate it but this seems to be the way. Could be generalized.

Fixes #239654

* Update src/vs/workbench/contrib/debug/browser/debugToolBar.ts

Co-authored-by: Tyler James Leonhardt 

* give up on smarter titlebar unflow avoidance for now

---------

Co-authored-by: Tyler James Leonhardt 
---
 .../contrib/debug/browser/debugToolBar.ts         | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts
index 683bdf081209..41f28eabfc27 100644
--- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts
+++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts
@@ -183,7 +183,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution {
 			const activeWindow = dom.getWindow(this.layoutService.activeContainer);
 			const originEvent = new StandardMouseEvent(activeWindow, e);
 
-			const originX = this.getCurrentXPercent();
+			const originX = this.computeCurrentXPercent();
 			const originY = this.getCurrentYPosition();
 
 			const mouseMoveListener = dom.addDisposableGenericMouseMoveListener(activeWindow, (e: MouseEvent) => {
@@ -220,10 +220,23 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution {
 		}));
 	}
 
+	/**
+	 * Computes the x percent position at which the toolbar is currently displayed.
+	 */
+	private computeCurrentXPercent(): number {
+		const { left, width } = this.$el.getBoundingClientRect();
+		return (left + width / 2) / dom.getWindow(this.$el).innerWidth;
+	}
+
+	/**
+	 * Gets the x position set in the style of the toolbar. This may not be its
+	 * actual position on screen depending on toolbar locations.
+	 */
 	private getCurrentXPercent(): number {
 		return Number(this.$el.style.getPropertyValue('--x-position'));
 	}
 
+	/** Gets the y position set in the style of the toolbar */
 	private getCurrentYPosition(): number {
 		return parseInt(this.$el.style.getPropertyValue('--y-position'));
 	}

From 3cb31a973dc0601885266626ff2d6474596d9d89 Mon Sep 17 00:00:00 2001
From: Joyce Er 
Date: Thu, 6 Feb 2025 14:06:11 +0800
Subject: [PATCH 1269/3587] refactor: reduce more global editing session
 references (#239764)

---
 .../contrib/chat/browser/actions/chatExecuteActions.ts   | 9 ++++++++-
 .../browser/chatContentParts/chatMarkdownContentPart.ts  | 4 ++--
 .../chat/browser/chatEditing/chatEditingActions.ts       | 4 +---
 .../chat/browser/contrib/chatInputRelatedFilesContrib.ts | 6 +++---
 4 files changed, 14 insertions(+), 9 deletions(-)

diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts
index ca3e4ca57282..59befe2e8344 100644
--- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts
+++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts
@@ -435,9 +435,16 @@ class SendToChatEditingAction extends EditingSessionAction {
 		}
 
 		const { widget: editingWidget } = await viewsService.openView(EditsViewId) as ChatViewPane;
+		if (!editingWidget.viewModel?.sessionId) {
+			return;
+		}
+		const chatEditingSession = chatEditingService.getEditingSession(editingWidget.viewModel.sessionId);
+		if (!chatEditingSession) {
+			return;
+		}
 		for (const attachment of widget.attachmentModel.attachments) {
 			if (attachment.isFile && URI.isUri(attachment.value)) {
-				chatEditingService.globalEditingSessionObs.get()?.addFileToWorkingSet(attachment.value);
+				chatEditingSession.addFileToWorkingSet(attachment.value);
 			} else {
 				editingWidget.attachmentModel.addContext(attachment);
 			}
diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts
index f00b4c0d3e93..3cb208d85856 100644
--- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts
+++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts
@@ -295,7 +295,7 @@ class CollapsedCodeBlock extends Disposable {
 	private readonly _progressStore = this._store.add(new DisposableStore());
 
 	constructor(
-		sessionId: string,
+		private readonly sessionId: string,
 		requestId: string,
 		@ILabelService private readonly labelService: ILabelService,
 		@IEditorService private readonly editorService: IEditorService,
@@ -335,7 +335,7 @@ class CollapsedCodeBlock extends Disposable {
 		this._uri = uri;
 
 		const iconText = this.labelService.getUriBasenameLabel(uri);
-		const modifiedEntry = this.chatEditingService.globalEditingSession?.getEntry(uri);
+		const modifiedEntry = this.chatEditingService.getEditingSession(this.sessionId)?.getEntry(uri);
 		const isComplete = !modifiedEntry?.isCurrentlyBeingModified.get();
 
 		let iconClasses: string[] = [];
diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts
index 1e759cb59871..4e8d0d41abd5 100644
--- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts
+++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts
@@ -508,8 +508,6 @@ registerAction2(class RemoveAction extends Action2 {
 			return;
 		}
 
-
-
 		const configurationService = accessor.get(IConfigurationService);
 		const dialogService = accessor.get(IDialogService);
 		const chatEditingService = accessor.get(IChatEditingService);
@@ -534,7 +532,7 @@ registerAction2(class RemoveAction extends Action2 {
 
 			const requestsToRemove = chatRequests.slice(itemIndex);
 			const requestIdsToRemove = new Set(requestsToRemove.map(request => request.id));
-			const entriesModifiedInRequestsToRemove = chatEditingService.globalEditingSessionObs.get()?.entries.get().filter((entry) => requestIdsToRemove.has(entry.lastModifyingRequestId)) ?? [];
+			const entriesModifiedInRequestsToRemove = session.entries.get().filter((entry) => requestIdsToRemove.has(entry.lastModifyingRequestId)) ?? [];
 			const shouldPrompt = entriesModifiedInRequestsToRemove.length > 0 && configurationService.getValue('chat.editing.confirmEditRequestRemoval') === true;
 
 			let message: string;
diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts
index 6451b341dcdc..6e7c665e9bbc 100644
--- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts
+++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts
@@ -51,12 +51,12 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben
 
 		this._currentRelatedFilesRetrievalOperation = this.chatEditingService.getRelatedFiles(currentEditingSession.chatSessionId, widget.getInput(), CancellationToken.None)
 			.then((files) => {
-				if (!files?.length) {
+				if (!files?.length || !widget.viewModel?.sessionId) {
 					return;
 				}
 
-				const currentEditingSession = this.chatEditingService.globalEditingSessionObs.get();
-				if (!currentEditingSession || currentEditingSession.chatSessionId !== widget.viewModel?.sessionId || currentEditingSession.entries.get().length) {
+				const currentEditingSession = this.chatEditingService.getEditingSession(widget.viewModel.sessionId);
+				if (!currentEditingSession || currentEditingSession.entries.get().length) {
 					return; // Might have disposed while we were calculating
 				}
 

From 013ebeaece6c2e2c955cdd0e9d9d3ee3d5103688 Mon Sep 17 00:00:00 2001
From: Justin Chen <54879025+justschen@users.noreply.github.com>
Date: Wed, 5 Feb 2025 22:59:23 -0800
Subject: [PATCH 1270/3587] issue reporter handling when body too large
 (#239766)

handle case when at submission, body is too large
---
 .../issue/browser/baseIssueReporterService.ts |  3 +-
 .../electron-sandbox/issueReporterService.ts  | 29 ++++++++++++++++++-
 2 files changed, 30 insertions(+), 2 deletions(-)

diff --git a/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts b/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts
index 5b141801e762..38da042c4095 100644
--- a/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts
+++ b/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts
@@ -35,7 +35,8 @@ const MAX_URL_LENGTH = 7500;
 // Github API and issues on web has a limit of 65536. If extension data is too large, we will allow users to downlaod and attach it as a file.
 // We round down to be safe.
 // ref https://github.com/github/issues/issues/12858
-const MAX_EXTENSION_DATA_LENGTH = 55000;
+
+const MAX_EXTENSION_DATA_LENGTH = 60000;
 
 interface SearchResult {
 	html_url: string;
diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts
index a21991a5d0b1..e6be6f94d322 100644
--- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts
+++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts
@@ -3,8 +3,11 @@
  *  Licensed under the MIT License. See License.txt in the project root for license information.
  *--------------------------------------------------------------------------------------------*/
 import { $, reset } from '../../../../base/browser/dom.js';
+import { VSBuffer } from '../../../../base/common/buffer.js';
 import { CancellationError } from '../../../../base/common/errors.js';
+import { Schemas } from '../../../../base/common/network.js';
 import { IProductConfiguration } from '../../../../base/common/product.js';
+import { joinPath } from '../../../../base/common/resources.js';
 import { URI } from '../../../../base/common/uri.js';
 import { localize } from '../../../../nls.js';
 import { isRemoteDiagnosticError } from '../../../../platform/diagnostics/common/diagnostics.js';
@@ -94,7 +97,31 @@ export class IssueReporter extends BaseIssueReporterService {
 
 	public override async submitToGitHub(issueTitle: string, issueBody: string, gitHubDetails: { owner: string; repositoryName: string }): Promise {
 		if (issueBody.length > MAX_GITHUB_API_LENGTH) {
-			return false;
+			const extensionData = this.issueReporterModel.getData().extensionData;
+			if (extensionData) {
+				issueBody = issueBody.replace(extensionData, '');
+				const date = new Date();
+				const formattedDate = date.toISOString().split('T')[0]; // YYYY-MM-DD
+				const formattedTime = date.toTimeString().split(' ')[0].replace(/:/g, '-'); // HH-MM-SS
+				const fileName = `extensionData_${formattedDate}_${formattedTime}.md`;
+				try {
+					const downloadPath = await this.fileDialogService.showSaveDialog({
+						title: localize('saveExtensionData', "Save Extension Data"),
+						availableFileSystems: [Schemas.file],
+						defaultUri: joinPath(await this.fileDialogService.defaultFilePath(Schemas.file), fileName),
+					});
+
+					if (downloadPath) {
+						await this.fileService.writeFile(downloadPath, VSBuffer.fromString(extensionData));
+					}
+				} catch (e) {
+					console.error('Writing extension data to file failed');
+					return false;
+				}
+			} else {
+				console.error('Issue body too large to submit to GitHub');
+				return false;
+			}
 		}
 		const url = `https://api.github.com/repos/${gitHubDetails.owner}/${gitHubDetails.repositoryName}/issues`;
 		const init = {

From 8f36bf3d117212c2d79f0af7648d77a299eb6a48 Mon Sep 17 00:00:00 2001
From: Benjamin Pasero 
Date: Thu, 6 Feb 2025 08:10:01 +0100
Subject: [PATCH 1271/3587] debt - fix leaking disposables

---
 .../workbench/contrib/testing/browser/testingDecorations.ts   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts
index f65839ebff53..ef1483188f53 100644
--- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts
+++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts
@@ -372,8 +372,8 @@ export class TestingDecorations extends Disposable implements IEditorContributio
 	public get currentUri() { return this._currentUri; }
 
 	private _currentUri?: URI;
-	private readonly expectedWidget = new MutableDisposable();
-	private readonly actualWidget = new MutableDisposable();
+	private readonly expectedWidget = this._register(new MutableDisposable());
+	private readonly actualWidget = this._register(new MutableDisposable());
 
 	private readonly errorContentWidgets = this._register(new DisposableMap());
 	private readonly loggedMessageDecorations = new Map
Date: Thu, 6 Feb 2025 08:15:49 +0100
Subject: [PATCH 1272/3587] Pinned editor lost when closing a floating window
 after editors move to main window (fix #239549)

---
 .../workbench/browser/parts/editor/editor.ts  | 47 ++++++++++++++++++-
 .../browser/parts/editor/editorActions.ts     | 25 ++++++----
 .../browser/parts/editor/editorCommands.ts    |  6 ++-
 .../browser/parts/editor/editorDropTarget.ts  | 17 ++-----
 .../browser/parts/editor/editorPart.ts        | 22 +++++++--
 .../parts/editor/multiEditorTabsControl.ts    |  8 ++--
 6 files changed, 91 insertions(+), 34 deletions(-)

diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts
index d6f477d64f05..ac8943cf017e 100644
--- a/src/vs/workbench/browser/parts/editor/editor.ts
+++ b/src/vs/workbench/browser/parts/editor/editor.ts
@@ -3,7 +3,7 @@
  *  Licensed under the MIT License. See License.txt in the project root for license information.
  *--------------------------------------------------------------------------------------------*/
 
-import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorIdentifier, IEditorCloseEvent, IEditorPartOptions, IEditorPartOptionsChangeEvent, SideBySideEditor, EditorCloseContext, IEditorPane, IEditorPartLimitOptions, IEditorPartDecorationOptions, IEditorWillOpenEvent } from '../../../common/editor.js';
+import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorIdentifier, IEditorCloseEvent, IEditorPartOptions, IEditorPartOptionsChangeEvent, SideBySideEditor, EditorCloseContext, IEditorPane, IEditorPartLimitOptions, IEditorPartDecorationOptions, IEditorWillOpenEvent, EditorInputWithOptions } from '../../../common/editor.js';
 import { EditorInput } from '../../../common/editor/editorInput.js';
 import { IEditorGroup, GroupDirection, IMergeGroupOptions, GroupsOrder, GroupsArrangement, IAuxiliaryEditorPart, IEditorPart } from '../../../services/editor/common/editorGroupsService.js';
 import { IDisposable } from '../../../../base/common/lifecycle.js';
@@ -19,6 +19,7 @@ import { IWindowsConfiguration } from '../../../../platform/window/common/window
 import { BooleanVerifier, EnumVerifier, NumberVerifier, ObjectVerifier, SetVerifier, verifyObject } from '../../../../base/common/verifier.js';
 import { IAuxiliaryWindowOpenOptions } from '../../../services/auxiliaryWindow/browser/auxiliaryWindowService.js';
 import { ContextKeyValue, IContextKey, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
+import { coalesce } from '../../../../base/common/arrays.js';
 
 export interface IEditorPartCreationOptions {
 	readonly restorePreviousState: boolean;
@@ -300,6 +301,50 @@ export function fillActiveEditorViewState(group: IEditorGroup, expectedActiveEdi
 	return presetOptions || Object.create(null);
 }
 
+export function prepareMoveCopyEditors(sourceGroup: IEditorGroup, editors: EditorInput[], preserveFocus?: boolean): EditorInputWithOptions[] {
+	if (editors.length === 0) {
+		return [];
+	}
+
+	const editorsWithOptions: EditorInputWithOptions[] = [];
+
+	let activeEditor: EditorInput | undefined;
+	const inactiveEditors: EditorInput[] = [];
+	for (const editor of editors) {
+		if (!activeEditor && sourceGroup.isActive(editor)) {
+			activeEditor = editor;
+		} else {
+			inactiveEditors.push(editor);
+		}
+	}
+
+	if (!activeEditor) {
+		activeEditor = inactiveEditors.shift(); // just take the first editor as active if none is active
+	}
+
+	// ensure inactive editors are then sorted by inverse visual order
+	// so that we can preserve the order in the target group. we inverse
+	// because editors will open to the side of the active editor as
+	// inactive editors, and the active editor is always the reference
+	inactiveEditors.sort((a, b) => sourceGroup.getIndexOfEditor(b) - sourceGroup.getIndexOfEditor(a));
+
+	const sortedEditors = coalesce([activeEditor, ...inactiveEditors]);
+	for (let i = 0; i < sortedEditors.length; i++) {
+		const editor = sortedEditors[i];
+		editorsWithOptions.push({
+			editor,
+			options: {
+				pinned: true,
+				sticky: sourceGroup.isSticky(editor),
+				inactive: i > 0,
+				preserveFocus
+			}
+		});
+	}
+
+	return editorsWithOptions;
+}
+
 /**
  * A sub-interface of IEditorService to hide some workbench-core specific
  * events from clients.
diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts
index c33f71c958e1..c922c06e8177 100644
--- a/src/vs/workbench/browser/parts/editor/editorActions.ts
+++ b/src/vs/workbench/browser/parts/editor/editorActions.ts
@@ -38,6 +38,7 @@ import { ICommandActionTitle } from '../../../../platform/action/common/action.j
 import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js';
 import { resolveCommandsContext } from './editorCommandsContext.js';
 import { IListService } from '../../../../platform/list/browser/listService.js';
+import { prepareMoveCopyEditors } from './editor.js';
 
 class ExecuteCommandAction extends Action2 {
 
@@ -65,9 +66,11 @@ abstract class AbstractSplitEditorAction extends Action2 {
 	override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise {
 		const editorGroupsService = accessor.get(IEditorGroupsService);
 		const configurationService = accessor.get(IConfigurationService);
+		const editorService = accessor.get(IEditorService);
+		const listService = accessor.get(IListService);
 
 		const direction = this.getDirection(configurationService);
-		const commandContext = resolveCommandsContext(args, accessor.get(IEditorService), editorGroupsService, accessor.get(IListService));
+		const commandContext = resolveCommandsContext(args, editorService, editorGroupsService, listService);
 
 		splitEditor(editorGroupsService, direction, commandContext);
 	}
@@ -1173,8 +1176,10 @@ export class ToggleMaximizeEditorGroupAction extends Action2 {
 
 	override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise {
 		const editorGroupsService = accessor.get(IEditorGroupsService);
+		const editorService = accessor.get(IEditorService);
+		const listService = accessor.get(IListService);
 
-		const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), editorGroupsService, accessor.get(IListService));
+		const resolvedContext = resolveCommandsContext(args, editorService, editorGroupsService, listService);
 		if (resolvedContext.groupedEditors.length) {
 			editorGroupsService.toggleMaximizeGroup(resolvedContext.groupedEditors[0].group);
 		}
@@ -2543,19 +2548,19 @@ abstract class BaseMoveCopyEditorToNewWindowAction extends Action2 {
 	}
 
 	override async run(accessor: ServicesAccessor, ...args: unknown[]) {
-		const editorGroupService = accessor.get(IEditorGroupsService);
-		const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), editorGroupService, accessor.get(IListService));
+		const editorGroupsService = accessor.get(IEditorGroupsService);
+		const editorService = accessor.get(IEditorService);
+		const listService = accessor.get(IListService);
+
+		const resolvedContext = resolveCommandsContext(args, editorService, editorGroupsService, listService);
 		if (!resolvedContext.groupedEditors.length) {
 			return;
 		}
 
-		const auxiliaryEditorPart = await editorGroupService.createAuxiliaryEditorPart();
-
-		// only single group supported for move/copy for now
-		const { group, editors } = resolvedContext.groupedEditors[0];
-		const options = { preserveFocus: resolvedContext.preserveFocus };
-		const editorsWithOptions = editors.map(editor => ({ editor, options }));
+		const auxiliaryEditorPart = await editorGroupsService.createAuxiliaryEditorPart();
 
+		const { group, editors } = resolvedContext.groupedEditors[0]; // only single group supported for move/copy for now
+		const editorsWithOptions = prepareMoveCopyEditors(group, editors, resolvedContext.preserveFocus);
 		if (this.move) {
 			group.moveEditors(editorsWithOptions, auxiliaryEditorPart.activeGroup);
 		} else {
diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts
index 20c084c96032..83a724a79b1f 100644
--- a/src/vs/workbench/browser/parts/editor/editorCommands.ts
+++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts
@@ -40,6 +40,7 @@ import { IPathService } from '../../../services/path/common/pathService.js';
 import { IUntitledTextEditorService } from '../../../services/untitled/common/untitledTextEditorService.js';
 import { DIFF_FOCUS_OTHER_SIDE, DIFF_FOCUS_PRIMARY_SIDE, DIFF_FOCUS_SECONDARY_SIDE, DIFF_OPEN_SIDE, registerDiffEditorCommands } from './diffEditorCommands.js';
 import { IResolvedEditorCommandsContext, resolveCommandsContext } from './editorCommandsContext.js';
+import { prepareMoveCopyEditors } from './editor.js';
 
 export const CLOSE_SAVED_EDITORS_COMMAND_ID = 'workbench.action.closeUnmodifiedEditors';
 export const CLOSE_EDITORS_IN_GROUP_COMMAND_ID = 'workbench.action.closeEditorsInGroup';
@@ -307,10 +308,11 @@ function registerActiveEditorMoveCopyCommand(): void {
 		}
 
 		if (targetGroup) {
+			const editorsWithOptions = prepareMoveCopyEditors(sourceGroup, editors);
 			if (isMove) {
-				sourceGroup.moveEditors(editors.map(editor => ({ editor })), targetGroup);
+				sourceGroup.moveEditors(editorsWithOptions, targetGroup);
 			} else if (sourceGroup.id !== targetGroup.id) {
-				sourceGroup.copyEditors(editors.map(editor => ({ editor })), targetGroup);
+				sourceGroup.copyEditors(editorsWithOptions, targetGroup);
 			}
 
 			targetGroup.focus();
diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts
index ed286ff7f17a..244ef9cb8906 100644
--- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts
+++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts
@@ -20,7 +20,7 @@ import { IThemeService, Themable } from '../../../../platform/theme/common/theme
 import { isTemporaryWorkspace, IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
 import { CodeDataTransfers, containsDragType, Extensions as DragAndDropExtensions, IDragAndDropContributionRegistry, LocalSelectionTransfer } from '../../../../platform/dnd/browser/dnd.js';
 import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, extractTreeDropData, ResourcesDropHandler } from '../../dnd.js';
-import { fillActiveEditorViewState, IEditorGroupView } from './editor.js';
+import { IEditorGroupView, prepareMoveCopyEditors } from './editor.js';
 import { EditorInputCapabilities, IEditorIdentifier, IUntypedEditorInput } from '../../../common/editor.js';
 import { EDITOR_DRAG_AND_DROP_BACKGROUND, EDITOR_DROP_INTO_PROMPT_BACKGROUND, EDITOR_DROP_INTO_PROMPT_BORDER, EDITOR_DROP_INTO_PROMPT_FOREGROUND } from '../../../common/theme.js';
 import { GroupDirection, IEditorDropTargetDelegate, IEditorGroup, IEditorGroupsService, IMergeGroupOptions, MergeGroupMode } from '../../../services/editor/common/editorGroupsService.js';
@@ -329,20 +329,11 @@ class DropOverlay extends Themable {
 							return;
 						}
 
-						const editors = draggedEditors.map(draggedEditor => (
-							{
-								editor: draggedEditor.identifier.editor,
-								options: fillActiveEditorViewState(sourceGroup, draggedEditor.identifier.editor, {
-									pinned: true,													// always pin dropped editor
-									sticky: sourceGroup.isSticky(draggedEditor.identifier.editor)	// preserve sticky state
-								})
-							}
-						));
-
+						const editorsWithOptions = prepareMoveCopyEditors(this.groupView, draggedEditors.map(editor => editor.identifier.editor));
 						if (!copyEditor) {
-							sourceGroup.moveEditors(editors, targetGroup);
+							sourceGroup.moveEditors(editorsWithOptions, targetGroup);
 						} else {
-							sourceGroup.copyEditors(editors, targetGroup);
+							sourceGroup.copyEditors(editorsWithOptions, targetGroup);
 						}
 					}
 
diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts
index b472ab4e8f32..c463f617c79a 100644
--- a/src/vs/workbench/browser/parts/editor/editorPart.ts
+++ b/src/vs/workbench/browser/parts/editor/editorPart.ts
@@ -879,12 +879,26 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView {
 		let index = (options && typeof options.index === 'number') ? options.index : targetView.count;
 		for (const editor of sourceView.editors) {
 			const inactive = !sourceView.isActive(editor) || this._activeGroup !== sourceView;
-			const sticky = sourceView.isSticky(editor);
-			const options = { index: !sticky ? index : undefined /* do not set index to preserve sticky flag */, inactive, preserveFocus: inactive };
 
-			editors.push({ editor, options });
+			let actualIndex: number | undefined;
+			if (targetView.contains(editor) && targetView.isSticky(editor)) {
+				// Do not configure an `index` for editors that are sticky in
+				// the target, otherwise there is a chance of losing that state
+				// when the editor is moved.
+				// See https://github.com/microsoft/vscode/issues/239549
+			} else {
+				actualIndex = index;
+				index++;
+			}
 
-			index++;
+			editors.push({
+				editor,
+				options: {
+					index: actualIndex,
+					inactive,
+					preserveFocus: inactive
+				}
+			});
 		}
 
 		// Move/Copy editors over into target
diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts
index fdf85346726a..708f4d9f6d56 100644
--- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts
+++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts
@@ -34,7 +34,7 @@ import { INotificationService } from '../../../../platform/notification/common/n
 import { MergeGroupMode, IMergeGroupOptions } from '../../../services/editor/common/editorGroupsService.js';
 import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent, getWindow } from '../../../../base/browser/dom.js';
 import { localize } from '../../../../nls.js';
-import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView } from './editor.js';
+import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView, prepareMoveCopyEditors } from './editor.js';
 import { CloseEditorTabAction, UnpinEditorAction } from './editorActions.js';
 import { assertAllDefined, assertIsDefined } from '../../../../base/common/types.js';
 import { IEditorService } from '../../../services/editor/common/editorService.js';
@@ -1157,11 +1157,11 @@ export class MultiEditorTabsControl extends EditorTabsControl {
 				}
 
 				const targetGroup = auxiliaryEditorPart.activeGroup;
-				const editors = draggedEditors.map(de => ({ editor: de.identifier.editor }));
+				const editorsWithOptions = prepareMoveCopyEditors(this.groupView, draggedEditors.map(editor => editor.identifier.editor));
 				if (this.isMoveOperation(lastDragEvent ?? e, targetGroup.id, draggedEditors[0].identifier.editor)) {
-					this.groupView.moveEditors(editors, targetGroup);
+					this.groupView.moveEditors(editorsWithOptions, targetGroup);
 				} else {
-					this.groupView.copyEditors(editors, targetGroup);
+					this.groupView.copyEditors(editorsWithOptions, targetGroup);
 				}
 
 				targetGroup.focus();

From e4fdde7391dcd0b1c7bd775c98c6eaf478420a72 Mon Sep 17 00:00:00 2001
From: Benjamin Pasero 
Date: Thu, 6 Feb 2025 08:48:31 +0100
Subject: [PATCH 1273/3587] editors - add and use `preserveExistingIndex` for
 group merging

---
 .../parts/editor/auxiliaryEditorPart.ts       |  7 ++++++-
 .../browser/parts/editor/editorPart.ts        | 21 ++++++++++++-------
 .../browser/parts/editor/editorParts.ts       |  4 ++--
 .../editor/common/editorGroupsService.ts      |  7 +++++++
 .../test/browser/workbenchTestServices.ts     |  2 +-
 5 files changed, 30 insertions(+), 11 deletions(-)

diff --git a/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts b/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts
index 4b3c1fd8bda2..220bdf1cc65a 100644
--- a/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts
+++ b/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts
@@ -312,7 +312,12 @@ class AuxiliaryEditorPartImpl extends EditorPart implements IAuxiliaryEditorPart
 			targetGroup = this.editorPartsView.mainPart.addGroup(this.editorPartsView.mainPart.activeGroup, this.partOptions.openSideBySideDirection === 'right' ? GroupDirection.RIGHT : GroupDirection.DOWN);
 		}
 
-		const result = this.mergeAllGroups(targetGroup);
+		const result = this.mergeAllGroups(targetGroup, {
+			// Try to reduce the impact of closing the auxiliary window
+			// as much as possible by not changing existing editors
+			// in the main window. 
+			preserveExistingIndex: true
+		});
 		targetGroup.focus();
 
 		return result;
diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts
index c463f617c79a..1246406e0d0c 100644
--- a/src/vs/workbench/browser/parts/editor/editorPart.ts
+++ b/src/vs/workbench/browser/parts/editor/editorPart.ts
@@ -881,11 +881,18 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView {
 			const inactive = !sourceView.isActive(editor) || this._activeGroup !== sourceView;
 
 			let actualIndex: number | undefined;
-			if (targetView.contains(editor) && targetView.isSticky(editor)) {
-				// Do not configure an `index` for editors that are sticky in
-				// the target, otherwise there is a chance of losing that state
-				// when the editor is moved.
-				// See https://github.com/microsoft/vscode/issues/239549
+			if (targetView.contains(editor) &&
+				(
+					// Do not configure an `index` for editors that are sticky in
+					// the target, otherwise there is a chance of losing that state
+					// when the editor is moved.
+					// See https://github.com/microsoft/vscode/issues/239549
+					targetView.isSticky(editor) ||
+					// Do not configure an `index` when we are explicitly instructed
+					options?.preserveExistingIndex
+				)
+			) {
+				// leave `index` as `undefined`
 			} else {
 				actualIndex = index;
 				index++;
@@ -917,7 +924,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView {
 		return result;
 	}
 
-	mergeAllGroups(target: IEditorGroupView | GroupIdentifier): boolean {
+	mergeAllGroups(target: IEditorGroupView | GroupIdentifier, options?: IMergeGroupOptions): boolean {
 		const targetView = this.assertGroupView(target);
 
 		let result = true;
@@ -926,7 +933,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView {
 				continue; // keep target
 			}
 
-			const merged = this.mergeGroup(group, targetView);
+			const merged = this.mergeGroup(group, targetView, options);
 			if (!merged) {
 				result = false;
 			}
diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts
index 3684686bdf22..5d8dd28ea474 100644
--- a/src/vs/workbench/browser/parts/editor/editorParts.ts
+++ b/src/vs/workbench/browser/parts/editor/editorParts.ts
@@ -648,8 +648,8 @@ export class EditorParts extends MultiWindowParts implements IEditor
 		return this.getPart(group).mergeGroup(group, target, options);
 	}
 
-	mergeAllGroups(target: IEditorGroupView | GroupIdentifier): boolean {
-		return this.activePart.mergeAllGroups(target);
+	mergeAllGroups(target: IEditorGroupView | GroupIdentifier, options?: IMergeGroupOptions): boolean {
+		return this.activePart.mergeAllGroups(target, options);
 	}
 
 	copyGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView {
diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts
index 381943646191..b340a83f1a38 100644
--- a/src/vs/workbench/services/editor/common/editorGroupsService.ts
+++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts
@@ -101,6 +101,13 @@ export const enum MergeGroupMode {
 export interface IMergeGroupOptions {
 	mode?: MergeGroupMode;
 	readonly index?: number;
+
+	/**
+	 * Set this to prevent editors already present in the
+	 * target group from moving to a different index as
+	 * they are in the source group.
+	 */
+	readonly preserveExistingIndex?: boolean;
 }
 
 export interface ICloseEditorOptions {
diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts
index 463276cd547b..499dac81ec7c 100644
--- a/src/vs/workbench/test/browser/workbenchTestServices.ts
+++ b/src/vs/workbench/test/browser/workbenchTestServices.ts
@@ -889,7 +889,7 @@ export class TestEditorGroupsService implements IEditorGroupsService {
 	removeGroup(_group: number | IEditorGroup): void { }
 	moveGroup(_group: number | IEditorGroup, _location: number | IEditorGroup, _direction: GroupDirection): IEditorGroup { throw new Error('not implemented'); }
 	mergeGroup(_group: number | IEditorGroup, _target: number | IEditorGroup, _options?: IMergeGroupOptions): boolean { throw new Error('not implemented'); }
-	mergeAllGroups(_group: number | IEditorGroup): boolean { throw new Error('not implemented'); }
+	mergeAllGroups(_group: number | IEditorGroup, _options?: IMergeGroupOptions): boolean { throw new Error('not implemented'); }
 	copyGroup(_group: number | IEditorGroup, _location: number | IEditorGroup, _direction: GroupDirection): IEditorGroup { throw new Error('not implemented'); }
 	centerLayout(active: boolean): void { }
 	isLayoutCentered(): boolean { return false; }

From b69ec451152bb0913ae853f22a76a83f1b9a4c46 Mon Sep 17 00:00:00 2001
From: Benjamin Pasero 
Date: Thu, 6 Feb 2025 09:03:23 +0100
Subject: [PATCH 1274/3587] debt - fix leaks for `ActionRunner`

---
 .../diffEditor/features/gutterFeature.ts       |  4 ++--
 .../actions/browser/menuEntryActionViewItem.ts |  2 +-
 .../contextview/browser/contextMenuHandler.ts  |  2 +-
 .../browser/parts/titlebar/titlebarPart.ts     |  7 ++-----
 .../contrib/find/notebookFindReplaceWidget.ts  |  2 +-
 .../contrib/remote/browser/tunnelView.ts       |  2 +-
 .../electron-sandbox/contextmenuService.ts     | 18 ++++++++++--------
 7 files changed, 18 insertions(+), 19 deletions(-)

diff --git a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts
index 17ff41896b41..a9aaeab304d5 100644
--- a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts
+++ b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts
@@ -245,7 +245,7 @@ class DiffToolBar extends Disposable implements IGutterItemView {
 				},
 				overflowBehavior: { maxItems: this._isSmall.read(reader) ? 1 : 3 },
 				hiddenItemStrategy: HiddenItemStrategy.Ignore,
-				actionRunner: new ActionRunnerWithContext(() => {
+				actionRunner: store.add(new ActionRunnerWithContext(() => {
 					const item = this._item.get();
 					const mapping = item.mapping;
 					return {
@@ -254,7 +254,7 @@ class DiffToolBar extends Disposable implements IGutterItemView {
 						originalUri: item.originalUri,
 						modifiedUri: item.modifiedUri,
 					} satisfies DiffEditorSelectionHunkToolbarContext;
-				}),
+				})),
 				menuOptions: {
 					shouldForwardArgs: true,
 				},
diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts
index f90896f91a3b..0581f04a6441 100644
--- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts
+++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts
@@ -468,7 +468,7 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
 			...options,
 			menuAsChild: options?.menuAsChild ?? true,
 			classNames: options?.classNames ?? ['codicon', 'codicon-chevron-down'],
-			actionRunner: options?.actionRunner ?? new ActionRunner(),
+			actionRunner: options?.actionRunner ?? this._register(new ActionRunner()),
 		};
 
 		this._dropdown = new DropdownMenuActionViewItem(submenuAction, submenuAction.actions, this._contextMenuService, dropdownOptions);
diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.ts b/src/vs/platform/contextview/browser/contextMenuHandler.ts
index 162360af93aa..03666b228078 100644
--- a/src/vs/platform/contextview/browser/contextMenuHandler.ts
+++ b/src/vs/platform/contextview/browser/contextMenuHandler.ts
@@ -81,7 +81,7 @@ export class ContextMenuHandler {
 
 				const menuDisposables = new DisposableStore();
 
-				const actionRunner = delegate.actionRunner || new ActionRunner();
+				const actionRunner = delegate.actionRunner || menuDisposables.add(new ActionRunner());
 				actionRunner.onWillRun(evt => this.onActionRun(evt, !delegate.skipTelemetry), this, menuDisposables);
 				actionRunner.onDidRun(this.onDidActionRun, this, menuDisposables);
 				menu = new Menu(container, actions, {
diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts
index 32dad57a2183..f417bd2d7112 100644
--- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts
+++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts
@@ -663,14 +663,11 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart {
 			if (this.editorActionsEnabled && this.editorService.activeEditor !== undefined) {
 				const context: IEditorCommandsContext = { groupId: this.editorGroupsContainer.activeGroup.id };
 
-				this.actionToolBar.actionRunner = new EditorCommandsContextActionRunner(context);
+				this.actionToolBar.actionRunner = this.editorToolbarMenuDisposables.add(new EditorCommandsContextActionRunner(context));
 				this.actionToolBar.context = context;
-				this.editorToolbarMenuDisposables.add(this.actionToolBar.actionRunner);
 			} else {
-				this.actionToolBar.actionRunner = new ActionRunner();
+				this.actionToolBar.actionRunner = this.editorToolbarMenuDisposables.add(new ActionRunner());
 				this.actionToolBar.context = undefined;
-
-				this.editorToolbarMenuDisposables.add(this.actionToolBar.actionRunner);
 			}
 		}
 
diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts
index 74a3bc881b5c..d700b962cd94 100644
--- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts
+++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts
@@ -223,7 +223,7 @@ export class NotebookFindInputFilterButton extends Disposable {
 		this._actionbar = this._register(new ActionBar(container, {
 			actionViewItemProvider: (action, options) => {
 				if (action.id === this._filtersAction.id) {
-					return this.instantiationService.createInstance(NotebookFindFilterActionViewItem, this.filters, action, options, new ActionRunner());
+					return this.instantiationService.createInstance(NotebookFindFilterActionViewItem, this.filters, action, options, this._register(new ActionRunner()));
 				}
 				return undefined;
 			}
diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts
index a274fff1252a..abb7d7884820 100644
--- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts
+++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts
@@ -902,7 +902,7 @@ export class TunnelPanel extends ViewPane {
 			}
 		) as WorkbenchTable;
 
-		const actionRunner: ActionRunner = new ActionRunner();
+		const actionRunner: ActionRunner = this.tableDisposables.add(new ActionRunner());
 		actionBarRenderer.actionRunner = actionRunner;
 
 		this.tableDisposables.add(this.table);
diff --git a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts
index c3328242ee56..5a0d83a9686d 100644
--- a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts
+++ b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts
@@ -3,7 +3,7 @@
  *  Licensed under the MIT License. See License.txt in the project root for license information.
  *--------------------------------------------------------------------------------------------*/
 
-import { IAction, IActionRunner, ActionRunner, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, Separator, SubmenuAction } from '../../../../base/common/actions.js';
+import { IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, Separator, SubmenuAction } from '../../../../base/common/actions.js';
 import * as dom from '../../../../base/browser/dom.js';
 import { IContextMenuMenuDelegate, IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
 import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
@@ -179,11 +179,10 @@ class NativeContextMenuService extends Disposable implements IContextMenuService
 	}
 
 	private createMenu(delegate: IContextMenuDelegate, entries: readonly IAction[], onHide: () => void, submenuIds = new Set()): IContextMenuItem[] {
-		const actionRunner = delegate.actionRunner || new ActionRunner();
-		return coalesce(entries.map(entry => this.createMenuItem(delegate, entry, actionRunner, onHide, submenuIds)));
+		return coalesce(entries.map(entry => this.createMenuItem(delegate, entry, onHide, submenuIds)));
 	}
 
-	private createMenuItem(delegate: IContextMenuDelegate, entry: IAction, actionRunner: IActionRunner, onHide: () => void, submenuIds: Set): IContextMenuItem | undefined {
+	private createMenuItem(delegate: IContextMenuDelegate, entry: IAction, onHide: () => void, submenuIds: Set): IContextMenuItem | undefined {
 		// Separator
 		if (entry instanceof Separator) {
 			return { type: 'separator' };
@@ -226,7 +225,7 @@ class NativeContextMenuService extends Disposable implements IContextMenuService
 					onHide();
 
 					// Run action which will close the menu
-					this.runAction(actionRunner, entry, delegate, event);
+					this.runAction(entry, delegate, event);
 				}
 			};
 
@@ -247,16 +246,19 @@ class NativeContextMenuService extends Disposable implements IContextMenuService
 		}
 	}
 
-	private async runAction(actionRunner: IActionRunner, actionToRun: IAction, delegate: IContextMenuDelegate, event: IContextMenuEvent): Promise {
+	private async runAction(actionToRun: IAction, delegate: IContextMenuDelegate, event: IContextMenuEvent): Promise {
 		if (!delegate.skipTelemetry) {
 			this.telemetryService.publicLog2('workbenchActionExecuted', { id: actionToRun.id, from: 'contextMenu' });
 		}
 
 		const context = delegate.getActionsContext ? delegate.getActionsContext(event) : undefined;
 
-		const runnable = actionRunner.run(actionToRun, context);
 		try {
-			await runnable;
+			if (delegate.actionRunner) {
+				await delegate.actionRunner.run(actionToRun, context);
+			} else if (actionToRun.enabled) {
+				await actionToRun.run(context);
+			}
 		} catch (error) {
 			this.notificationService.error(error);
 		}

From ed2f9fda7a6087b317414bbbf0274dc31572324c Mon Sep 17 00:00:00 2001
From: Benjamin Pasero 
Date: Thu, 6 Feb 2025 09:41:10 +0100
Subject: [PATCH 1275/3587] chat: update label for adding more models action
 (#239774)

---
 src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts
index fb5ed5c8a1ac..0dcc74fe0cf9 100644
--- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts
+++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts
@@ -1707,7 +1707,7 @@ class ModelPickerActionViewItem extends MenuEntryActionViewItem {
 
 				if (this._contextKeyService.getContextKeyValue(ChatContextKeys.Setup.limited.key) === true) {
 					actions.push(new Separator());
-					actions.push(toAction({ id: 'moreModels', label: localize('chat.moreModels', "Enable More Models..."), run: () => this._commandService.executeCommand('workbench.action.chat.upgradePlan') }));
+					actions.push(toAction({ id: 'moreModels', label: localize('chat.moreModels', "Add More Models..."), run: () => this._commandService.executeCommand('workbench.action.chat.upgradePlan') }));
 				}
 
 				return actions;

From 2af422737386e792c3fcde7884f9bf47a1aff2f5 Mon Sep 17 00:00:00 2001
From: Benjamin Pasero 
Date: Thu, 6 Feb 2025 10:08:43 +0100
Subject: [PATCH 1276/3587] debt - fix disposable leaks (#239775)

---
 .../workbench/contrib/chat/browser/variables/variables.ts | 8 ++++----
 .../languageFeatures/promptPathAutocompletion.ts          | 2 +-
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/vs/workbench/contrib/chat/browser/variables/variables.ts b/src/vs/workbench/contrib/chat/browser/variables/variables.ts
index e2e59860f300..b30ecc91019f 100644
--- a/src/vs/workbench/contrib/chat/browser/variables/variables.ts
+++ b/src/vs/workbench/contrib/chat/browser/variables/variables.ts
@@ -26,7 +26,7 @@ export class BuiltinVariablesContribution extends Disposable implements IWorkben
 		super();
 
 		const terminalSelectionVar = instantiationService.createInstance(TerminalSelectionVariable);
-		varsService.registerVariable({
+		this._register(varsService.registerVariable({
 			id: 'copilot.terminalSelection',
 			name: 'terminalSelection',
 			fullName: localize('termSelection', "Terminal Selection"),
@@ -34,10 +34,10 @@ export class BuiltinVariablesContribution extends Disposable implements IWorkben
 			icon: Codicon.terminal,
 		}, async (messageText: string, arg: string | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise => {
 			return await terminalSelectionVar.resolve(token);
-		});
+		}));
 
 		const terminalLastCommandVar = instantiationService.createInstance(TerminalLastCommandVariable);
-		varsService.registerVariable({
+		this._register(varsService.registerVariable({
 			id: 'copilot.terminalLastCommand',
 			name: 'terminalLastCommand',
 			fullName: localize('terminalLastCommand', "Terminal Last Command"),
@@ -45,7 +45,7 @@ export class BuiltinVariablesContribution extends Disposable implements IWorkben
 			icon: Codicon.terminal,
 		}, async (messageText: string, arg: string | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise => {
 			return await terminalLastCommandVar.resolve(token);
-		});
+		}));
 	}
 }
 
diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptPathAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptPathAutocompletion.ts
index 1dac14fd1e06..ceee66a3ffbc 100644
--- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptPathAutocompletion.ts
+++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptPathAutocompletion.ts
@@ -106,7 +106,7 @@ export class PromptPathAutocompletion extends Disposable implements CompletionIt
 	) {
 		super();
 
-		this.languageService.completionProvider.register(LANGUAGE_SELECTOR, this);
+		this._register(this.languageService.completionProvider.register(LANGUAGE_SELECTOR, this));
 	}
 
 	/**

From ea2bfe3906d0327dab819f90e9eb5638d8c5ff60 Mon Sep 17 00:00:00 2001
From: Martin Aeschlimann 
Date: Thu, 6 Feb 2025 10:28:41 +0100
Subject: [PATCH 1277/3587] allow to edit untitled files

---
 src/vs/workbench/contrib/chat/common/tools/editFileTool.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts
index 4c758db27d31..dd9f089530f4 100644
--- a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts
+++ b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts
@@ -55,7 +55,7 @@ export const EditToolData: IToolData = {
 			},
 			filePath: {
 				type: 'string',
-				description: 'An absolute path to the file to edit',
+				description: 'An absolute path to the file to edit, or the URI of a untitled, not yet named, file, such as `untitled:Untitled-1.',
 			},
 			code: {
 				type: 'string',
@@ -180,10 +180,10 @@ export class EditToolInputProcessor implements IToolInputProcessor {
 			// Tool name collision, or input wasn't properly validated upstream
 			return input as any;
 		}
-
+		const filePath = input.filePath;
 		// Runs in EH, will be mapped
 		return {
-			file: URI.file(input.filePath),
+			file: filePath.startsWith('untitled:') ? URI.parse(filePath) : URI.file(filePath),
 			explanation: input.explanation,
 			code: input.code,
 		};

From ffa2cc7276e522a4ee63c0df33fc7ba060ad0239 Mon Sep 17 00:00:00 2001
From: Johannes Rieken 
Date: Thu, 6 Feb 2025 10:34:36 +0100
Subject: [PATCH 1278/3587] :lipstick: (#239779)

---
 .../browser/chatEditing/chatEditingSession.ts | 19 ++++++++-----------
 1 file changed, 8 insertions(+), 11 deletions(-)

diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts
index 3dd782fe5f1a..4d0b1545ce7a 100644
--- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts
+++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts
@@ -762,11 +762,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
 	 *
 	 * @returns The modified file entry.
 	 */
-	private async _getOrCreateModifiedFileEntry(resource: URI, responseModel: IModifiedEntryTelemetryInfo): Promise {
+	private async _getOrCreateModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo): Promise {
 		const existingEntry = this._entriesObs.get().find(e => isEqual(e.modifiedURI, resource));
 		if (existingEntry) {
-			if (responseModel.requestId !== existingEntry.telemetryInfo.requestId) {
-				existingEntry.updateTelemetryInfo(responseModel);
+			if (telemetryInfo.requestId !== existingEntry.telemetryInfo.requestId) {
+				existingEntry.updateTelemetryInfo(telemetryInfo);
 			}
 			return existingEntry;
 		}
@@ -778,7 +778,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
 		} else {
 			const initialContent = this._initialFileContents.get(resource);
 			// This gets manually disposed in .dispose() or in .restoreSnapshot()
-			entry = await this._createModifiedFileEntry(resource, responseModel, false, initialContent);
+			entry = await this._createModifiedFileEntry(resource, telemetryInfo, false, initialContent);
 			if (!initialContent) {
 				this._initialFileContents.set(resource, entry.initialContent);
 			}
@@ -810,14 +810,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
 		return entry;
 	}
 
-	private async _createModifiedFileEntry(resource: URI, responseModel: IModifiedEntryTelemetryInfo, mustExist = false, initialContent: string | undefined): Promise {
+	private async _createModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo, mustExist = false, initialContent: string | undefined): Promise {
 		try {
 			const ref = await this._textModelService.createModelReference(resource);
-
-			if (this._notebookService.hasSupportedNotebooks(resource)) {
-				return this._instantiationService.createInstance(ChatEditingModifiedNotebookEntry, ref, { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) }, responseModel, mustExist ? ChatEditKind.Created : ChatEditKind.Modified, initialContent);
-			}
-			return this._instantiationService.createInstance(ChatEditingModifiedFileEntry, ref, { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) }, responseModel, mustExist ? ChatEditKind.Created : ChatEditKind.Modified, initialContent);
+			const ctor = this._notebookService.hasSupportedNotebooks(resource) ? ChatEditingModifiedNotebookEntry : ChatEditingModifiedFileEntry;
+			return this._instantiationService.createInstance(ctor, ref, { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) }, telemetryInfo, mustExist ? ChatEditKind.Created : ChatEditKind.Modified, initialContent);
 		} catch (err) {
 			if (mustExist) {
 				throw err;
@@ -825,7 +822,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
 			// this file does not exist yet, create it and try again
 			await this._bulkEditService.apply({ edits: [{ newResource: resource }] });
 			this._editorService.openEditor({ resource, options: { inactive: true, preserveFocus: true, pinned: true } });
-			return this._createModifiedFileEntry(resource, responseModel, true, initialContent);
+			return this._createModifiedFileEntry(resource, telemetryInfo, true, initialContent);
 		}
 	}
 

From c44bdecf5b2249c88af7703126fdb53aa8778f66 Mon Sep 17 00:00:00 2001
From: Alex Ross 
Date: Thu, 6 Feb 2025 12:08:12 +0100
Subject: [PATCH 1279/3587] Leaking disposable pass over comment widget
 (#239786)

Fixes #239771
---
 .../contrib/comments/browser/commentMenus.ts  | 10 ++-
 .../contrib/comments/browser/commentNode.ts   | 73 +++++++++----------
 .../contrib/comments/browser/commentReply.ts  |  4 +-
 .../comments/browser/commentThreadHeader.ts   |  8 +-
 4 files changed, 48 insertions(+), 47 deletions(-)

diff --git a/src/vs/workbench/contrib/comments/browser/commentMenus.ts b/src/vs/workbench/contrib/comments/browser/commentMenus.ts
index 504739f7db1e..6cd59dc30073 100644
--- a/src/vs/workbench/contrib/comments/browser/commentMenus.ts
+++ b/src/vs/workbench/contrib/comments/browser/commentMenus.ts
@@ -5,7 +5,7 @@
 
 import { IDisposable } from '../../../../base/common/lifecycle.js';
 import { Comment } from '../../../../editor/common/languages.js';
-import { IMenu, IMenuCreateOptions, IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';
+import { IMenu, IMenuActionOptions, IMenuCreateOptions, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from '../../../../platform/actions/common/actions.js';
 import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
 
 export class CommentMenus implements IDisposable {
@@ -37,14 +37,18 @@ export class CommentMenus implements IDisposable {
 		return this.getMenu(MenuId.CommentActions, contextKeyService);
 	}
 
-	getCommentThreadTitleContextActions(contextKeyService: IContextKeyService): IMenu {
-		return this.getMenu(MenuId.CommentThreadTitleContext, contextKeyService);
+	getCommentThreadTitleContextActions(contextKeyService: IContextKeyService) {
+		return this.getActions(MenuId.CommentThreadTitleContext, contextKeyService, { shouldForwardArgs: true });
 	}
 
 	private getMenu(menuId: MenuId, contextKeyService: IContextKeyService, options?: IMenuCreateOptions): IMenu {
 		return this.menuService.createMenu(menuId, contextKeyService, options);
 	}
 
+	private getActions(menuId: MenuId, contextKeyService: IContextKeyService, options?: IMenuActionOptions): Array {
+		return this.menuService.getMenuActions(menuId, contextKeyService, options).map((value) => value[1]).flat();
+	}
+
 	dispose(): void {
 
 	}
diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts
index 89418b813182..77f5e4117a25 100644
--- a/src/vs/workbench/contrib/comments/browser/commentNode.ts
+++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts
@@ -7,10 +7,10 @@ import * as nls from '../../../../nls.js';
 import * as dom from '../../../../base/browser/dom.js';
 import * as languages from '../../../../editor/common/languages.js';
 import { ActionsOrientation, ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
-import { Action, IActionRunner, IAction, Separator, ActionRunner } from '../../../../base/common/actions.js';
-import { Disposable, IDisposable, IReference, dispose } from '../../../../base/common/lifecycle.js';
+import { Action, IAction, Separator, ActionRunner } from '../../../../base/common/actions.js';
+import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable, dispose } from '../../../../base/common/lifecycle.js';
 import { URI, UriComponents } from '../../../../base/common/uri.js';
-import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
+import { IMarkdownRenderResult, MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
 import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
 import { ICommentService } from './commentService.js';
 import { LayoutableEditor, MIN_EDITOR_HEIGHT, SimpleCommentEditor, calculateEditorHeight } from './simpleCommentEditor.js';
@@ -61,7 +61,7 @@ export class CommentNode extends Disposable {
 	private _domNode: HTMLElement;
 	private _body: HTMLElement;
 	private _avatar: HTMLElement;
-	private _md: HTMLElement | undefined;
+	private readonly _md: MutableDisposable = this._register(new MutableDisposable());
 	private _plainText: HTMLElement | undefined;
 	private _clearTimeout: any;
 
@@ -69,7 +69,8 @@ export class CommentNode extends Disposable {
 	private _commentEditContainer: HTMLElement | null = null;
 	private _commentDetailsContainer: HTMLElement;
 	private _actionsToolbarContainer!: HTMLElement;
-	private _reactionsActionBar?: ActionBar;
+	private readonly _reactionsActionBar: MutableDisposable = this._register(new MutableDisposable());
+	private readonly _reactionActions: DisposableStore = this._register(new DisposableStore());
 	private _reactionActionsContainer?: HTMLElement;
 	private _commentEditor: SimpleCommentEditor | null = null;
 	private _commentEditorDisposables: IDisposable[] = [];
@@ -86,8 +87,8 @@ export class CommentNode extends Disposable {
 	private _scrollable!: Scrollable;
 	private _scrollableElement!: SmoothScrollableElement;
 
-	protected actionRunner?: IActionRunner;
-	protected toolbar: ToolBar | undefined;
+	private readonly _actionRunner: CommentsActionRunner = this._register(new CommentsActionRunner());
+	private readonly toolbar: MutableDisposable = this._register(new MutableDisposable());
 	private _commentFormActions: CommentFormActions | null = null;
 	private _commentEditorActions: CommentFormActions | null = null;
 
@@ -173,11 +174,11 @@ export class CommentNode extends Disposable {
 	}
 
 	private createScroll(container: HTMLElement, body: HTMLElement) {
-		this._scrollable = new Scrollable({
+		this._scrollable = this._register(new Scrollable({
 			forceIntegerValues: true,
 			smoothScrollDuration: 125,
 			scheduleAtNextAnimationFrame: cb => dom.scheduleAtNextAnimationFrame(dom.getWindow(container), cb)
-		});
+		}));
 		this._scrollableElement = this._register(new SmoothScrollableElement(body, {
 			horizontal: ScrollbarVisibility.Visible,
 			vertical: ScrollbarVisibility.Visible
@@ -208,14 +209,14 @@ export class CommentNode extends Disposable {
 
 	private updateCommentBody(body: string | IMarkdownString) {
 		this._body.innerText = '';
-		this._md = undefined;
+		this._md.clear();
 		this._plainText = undefined;
 		if (typeof body === 'string') {
 			this._plainText = dom.append(this._body, dom.$('.comment-body-plainstring'));
 			this._plainText.innerText = body;
 		} else {
-			this._md = this.markdownRenderer.render(body).element;
-			this._body.appendChild(this._md);
+			this._md.value = this.markdownRenderer.render(body);
+			this._body.appendChild(this._md.value.element);
 		}
 	}
 
@@ -305,7 +306,7 @@ export class CommentNode extends Disposable {
 	}
 
 	private createToolbar() {
-		this.toolbar = new ToolBar(this._actionsToolbarContainer, this.contextMenuService, {
+		this.toolbar.value = new ToolBar(this._actionsToolbarContainer, this.contextMenuService, {
 			actionViewItemProvider: (action, options) => {
 				if (action.id === ToggleReactionsAction.ID) {
 					return new DropdownMenuActionViewItem(
@@ -315,7 +316,6 @@ export class CommentNode extends Disposable {
 						{
 							...options,
 							actionViewItemProvider: (action, options) => this.actionViewItemProvider(action as Action, options),
-							actionRunner: this.actionRunner,
 							classNames: ['toolbar-toggle-pickReactions', ...ThemeIcon.asClassNameArray(Codicon.reactions)],
 							anchorAlignmentProvider: () => AnchorAlignment.RIGHT
 						}
@@ -326,11 +326,10 @@ export class CommentNode extends Disposable {
 			orientation: ActionsOrientation.HORIZONTAL
 		});
 
-		this.toolbar.context = this.commentNodeContext;
-		this.toolbar.actionRunner = new CommentsActionRunner();
+		this.toolbar.value.context = this.commentNodeContext;
+		this.toolbar.value.actionRunner = this._actionRunner;
 
 		this.registerActionBarListeners(this._actionsToolbarContainer);
-		this._register(this.toolbar);
 	}
 
 	private createActionsToolbar() {
@@ -352,7 +351,7 @@ export class CommentNode extends Disposable {
 			if (toggleReactionAction) {
 				primary.unshift(toggleReactionAction);
 			}
-			this.toolbar!.setActions(primary, secondary);
+			this.toolbar.value!.setActions(primary, secondary);
 		}));
 
 		const { primary, secondary } = this.getToolbarActions(menu);
@@ -360,7 +359,7 @@ export class CommentNode extends Disposable {
 
 		if (actions.length || secondary.length) {
 			this.createToolbar();
-			this.toolbar!.setActions(actions, secondary);
+			this.toolbar.value!.setActions(actions, secondary);
 		}
 	}
 
@@ -392,14 +391,14 @@ export class CommentNode extends Disposable {
 	}
 
 	private createReactionPicker(reactionGroup: languages.CommentReaction[]): ToggleReactionsAction {
-		const toggleReactionAction = this._register(new ToggleReactionsAction(() => {
+		const toggleReactionAction = this._reactionActions.add(new ToggleReactionsAction(() => {
 			toggleReactionActionViewItem?.show();
 		}, nls.localize('commentToggleReaction', "Toggle Reaction")));
 
 		let reactionMenuActions: Action[] = [];
 		if (reactionGroup && reactionGroup.length) {
 			reactionMenuActions = reactionGroup.map((reaction) => {
-				return new Action(`reaction.command.${reaction.label}`, `${reaction.label}`, '', true, async () => {
+				return this._reactionActions.add(new Action(`reaction.command.${reaction.label}`, `${reaction.label}`, '', true, async () => {
 					try {
 						await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread, this.comment, reaction);
 					} catch (e) {
@@ -408,13 +407,13 @@ export class CommentNode extends Disposable {
 							: nls.localize('commentToggleReactionDefaultError', "Toggling the comment reaction failed");
 						this.notificationService.error(error);
 					}
-				});
+				}));
 			});
 		}
 
 		toggleReactionAction.menuActions = reactionMenuActions;
 
-		const toggleReactionActionViewItem: DropdownMenuActionViewItem = new DropdownMenuActionViewItem(
+		const toggleReactionActionViewItem: DropdownMenuActionViewItem = this._reactionActions.add(new DropdownMenuActionViewItem(
 			toggleReactionAction,
 			(toggleReactionAction).menuActions,
 			this.contextMenuService,
@@ -425,18 +424,21 @@ export class CommentNode extends Disposable {
 					}
 					return this.actionViewItemProvider(action as Action, options);
 				},
-				actionRunner: this.actionRunner,
 				classNames: 'toolbar-toggle-pickReactions',
 				anchorAlignmentProvider: () => AnchorAlignment.RIGHT
 			}
-		);
+		));
 
 		return toggleReactionAction;
 	}
 
 	private createReactionsContainer(commentDetailsContainer: HTMLElement): void {
+		this._reactionActionsContainer?.remove();
+		this._reactionsActionBar.clear();
+		this._reactionActions.clear();
+
 		this._reactionActionsContainer = dom.append(commentDetailsContainer, dom.$('div.comment-reactions'));
-		this._reactionsActionBar = new ActionBar(this._reactionActionsContainer, {
+		this._reactionsActionBar.value = new ActionBar(this._reactionActionsContainer, {
 			actionViewItemProvider: (action, options) => {
 				if (action.id === ToggleReactionsAction.ID) {
 					return new DropdownMenuActionViewItem(
@@ -445,7 +447,6 @@ export class CommentNode extends Disposable {
 						this.contextMenuService,
 						{
 							actionViewItemProvider: (action, options) => this.actionViewItemProvider(action as Action, options),
-							actionRunner: this.actionRunner,
 							classNames: ['toolbar-toggle-pickReactions', ...ThemeIcon.asClassNameArray(Codicon.reactions)],
 							anchorAlignmentProvider: () => AnchorAlignment.RIGHT
 						}
@@ -454,11 +455,10 @@ export class CommentNode extends Disposable {
 				return this.actionViewItemProvider(action as Action, options);
 			}
 		});
-		this._register(this._reactionsActionBar);
 
 		const hasReactionHandler = this.commentService.hasReactionHandler(this.owner);
 		this.comment.commentReactions?.filter(reaction => !!reaction.count).map(reaction => {
-			const action = new ReactionAction(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && (reaction.canEdit || hasReactionHandler) ? 'active' : '', (reaction.canEdit || hasReactionHandler), async () => {
+			const action = this._reactionActions.add(new ReactionAction(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && (reaction.canEdit || hasReactionHandler) ? 'active' : '', (reaction.canEdit || hasReactionHandler), async () => {
 				try {
 					await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread, this.comment, reaction);
 				} catch (e) {
@@ -475,14 +475,14 @@ export class CommentNode extends Disposable {
 					}
 					this.notificationService.error(error);
 				}
-			}, reaction.reactors, reaction.iconPath, reaction.count);
+			}, reaction.reactors, reaction.iconPath, reaction.count));
 
-			this._reactionsActionBar?.push(action, { label: true, icon: true });
+			this._reactionsActionBar.value?.push(action, { label: true, icon: true });
 		});
 
 		if (hasReactionHandler) {
 			const toggleReactionAction = this.createReactionPicker(this.comment.commentReactions || []);
-			this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true });
+			this._reactionsActionBar.value?.push(toggleReactionAction, { label: false, icon: true });
 		}
 	}
 
@@ -623,7 +623,6 @@ export class CommentNode extends Disposable {
 		this.createCommentWidgetFormActions(otherActions);
 		const editorActions = dom.append(formActions, dom.$('.editor-actions'));
 		this.createCommentWidgetEditorActions(editorActions);
-
 	}
 
 	private createCommentWidgetFormActions(container: HTMLElement) {
@@ -738,10 +737,6 @@ export class CommentNode extends Disposable {
 		}
 
 		// update comment reactions
-		this._reactionActionsContainer?.remove();
-
-		this._reactionsActionBar?.clear();
-
 		this.createReactionsContainer(this._commentDetailsContainer);
 
 		if (this.comment.contextValue) {
@@ -755,16 +750,14 @@ export class CommentNode extends Disposable {
 		}
 	}
 
-
 	private onContextMenu(e: MouseEvent) {
 		const event = new StandardMouseEvent(dom.getWindow(this._domNode), e);
-
 		this.contextMenuService.showContextMenu({
 			getAnchor: () => event,
 			menuId: MenuId.CommentThreadCommentContext,
 			menuActionOptions: { shouldForwardArgs: true },
 			contextKeyService: this._contextKeyService,
-			actionRunner: new CommentsActionRunner(),
+			actionRunner: this._actionRunner,
 			getActionsContext: () => {
 				return this.commentNodeContext;
 			},
diff --git a/src/vs/workbench/contrib/comments/browser/commentReply.ts b/src/vs/workbench/contrib/comments/browser/commentReply.ts
index 37bb741dc681..917b6a64239b 100644
--- a/src/vs/workbench/contrib/comments/browser/commentReply.ts
+++ b/src/vs/workbench/contrib/comments/browser/commentReply.ts
@@ -359,11 +359,11 @@ export class CommentReply extends Disposable {
 		this._register(dom.addDisposableListener(this._reviewThreadReplyButton, 'click', _ => this.clearAndExpandReplyArea()));
 		this._register(dom.addDisposableListener(this._reviewThreadReplyButton, 'focus', _ => this.clearAndExpandReplyArea()));
 
-		commentEditor.onDidBlurEditorWidget(() => {
+		this._register(commentEditor.onDidBlurEditorWidget(() => {
 			if (commentEditor.getModel()!.getValueLength() === 0 && commentForm.classList.contains('expand')) {
 				commentForm.classList.remove('expand');
 			}
-		});
+		}));
 	}
 
 	override dispose(): void {
diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts
index cb721233760a..91a30f2fafb7 100644
--- a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts
+++ b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts
@@ -38,6 +38,7 @@ export class CommentThreadHeader extends Disposable {
 	private _headingLabel!: HTMLElement;
 	private _actionbarWidget!: ActionBar;
 	private _collapseAction!: Action;
+	private _contextMenuActionRunner: ActionRunner | undefined;
 
 	constructor(
 		container: HTMLElement,
@@ -131,15 +132,18 @@ export class CommentThreadHeader extends Disposable {
 	}
 
 	private onContextMenu(e: MouseEvent) {
-		const actions = this._commentMenus.getCommentThreadTitleContextActions(this._contextKeyService).getActions({ shouldForwardArgs: true }).map((value) => value[1]).flat();
+		const actions = this._commentMenus.getCommentThreadTitleContextActions(this._contextKeyService);
 		if (!actions.length) {
 			return;
 		}
 		const event = new StandardMouseEvent(dom.getWindow(this._headElement), e);
+		if (!this._contextMenuActionRunner) {
+			this._contextMenuActionRunner = this._register(new ActionRunner());
+		}
 		this._contextMenuService.showContextMenu({
 			getAnchor: () => event,
 			getActions: () => actions,
-			actionRunner: new ActionRunner(),
+			actionRunner: this._contextMenuActionRunner,
 			getActionsContext: (): MarshalledCommentThread => {
 				return {
 					commentControlHandle: this._commentThread.controllerHandle,

From 18191a16c8e5fdaee0c4d26c1f35423cda4be843 Mon Sep 17 00:00:00 2001
From: Alex Ross 
Date: Thu, 6 Feb 2025 12:11:49 +0100
Subject: [PATCH 1280/3587] Fix leaking action view item in view pane (#239787)

Fixed in the view pane, rather than each view, since each view pane will otherwise need to deal with this.
---
 src/vs/workbench/browser/parts/views/viewPane.ts | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts
index 0e3e70d07eec..f848ff261ab4 100644
--- a/src/vs/workbench/browser/parts/views/viewPane.ts
+++ b/src/vs/workbench/browser/parts/views/viewPane.ts
@@ -10,7 +10,7 @@ import { asCssVariable, foreground } from '../../../../platform/theme/common/col
 import { after, append, $, trackFocus, EventType, addDisposableListener, Dimension, reset, isAncestorOfActiveElement, isActiveElement } from '../../../../base/browser/dom.js';
 import { createCSSRule } from '../../../../base/browser/domStylesheets.js';
 import { asCssValueWithDefault, asCSSUrl } from '../../../../base/browser/cssValue.js';
-import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
+import { DisposableMap, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
 import { Action, IAction, IActionRunner } from '../../../../base/common/actions.js';
 import { ActionsOrientation, IActionViewItem, prepareActions } from '../../../../base/browser/ui/actionbar/actionbar.js';
 import { Registry } from '../../../../platform/registry/common/platform.js';
@@ -367,6 +367,8 @@ export abstract class ViewPane extends Pane implements IView {
 	protected twistiesContainer?: HTMLElement;
 	private viewWelcomeController!: ViewWelcomeController;
 
+	private readonly headerActionViewItems: DisposableMap = this._register(new DisposableMap());
+
 	protected readonly scopedContextKeyService: IContextKeyService;
 
 	constructor(
@@ -458,7 +460,13 @@ export abstract class ViewPane extends Pane implements IView {
 		actions.classList.toggle('show-expanded', this.showActions === ViewPaneShowActions.WhenExpanded);
 		this.toolbar = this.instantiationService.createInstance(WorkbenchToolBar, actions, {
 			orientation: ActionsOrientation.HORIZONTAL,
-			actionViewItemProvider: (action, options) => this.getActionViewItem(action, options),
+			actionViewItemProvider: (action, options) => {
+				const item = this.getActionViewItem(action, options);
+				if (item) {
+					this.headerActionViewItems.set(item.action.id, item);
+				}
+				return item;
+			},
 			ariaLabel: nls.localize('viewToolbarAriaLabel', "{0} actions", this.title),
 			getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id),
 			renderDropdownAsChildElement: true,

From 330e6bfeb9a4e00dd07849ce6ade65e9f7a85117 Mon Sep 17 00:00:00 2001
From: Johannes Rieken 
Date: Thu, 6 Feb 2025 12:22:23 +0100
Subject: [PATCH 1281/3587] more inline chat controller alignment (#239785)

* remove `InlineChatController#onWillStartSession`

* more inline chat controller alignment

* more inline chat controller alignment

* fix compile errors
---
 .../browser/actions/chatCodeblockActions.ts   | 13 +--
 .../browser/inlineChatAccessibleView.ts       |  2 +-
 .../browser/inlineChatController.ts           | 32 +++----
 .../browser/inlineChatController2.ts          | 92 +++++++++++--------
 .../browser/inlineChatCurrentLine.ts          | 24 ++---
 .../inlineChat/browser/inlineChatNotebook.ts  |  2 +-
 .../browser/inlineChatSessionServiceImpl.ts   | 15 ++-
 .../inlineChat/browser/inlineChatWidget.ts    |  6 +-
 .../test/browser/inlineChatController.test.ts |  6 +-
 .../browser/viewModel/baseCellViewModel.ts    | 16 ++--
 .../browser/viewModel/codeCellViewModel.ts    |  6 +-
 .../browser/viewModel/markupCellViewModel.ts  |  6 +-
 12 files changed, 114 insertions(+), 106 deletions(-)

diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts
index 5448cdeed929..baf3f39e7c0b 100644
--- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts
+++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts
@@ -27,7 +27,7 @@ import { IWorkbenchContribution } from '../../../../common/contributions.js';
 import { IUntitledTextResourceEditorInput } from '../../../../common/editor.js';
 import { IEditorService } from '../../../../services/editor/common/editorService.js';
 import { accessibleViewInCodeBlock } from '../../../accessibility/browser/accessibilityConfiguration.js';
-import { InlineChatController, reviewEdits } from '../../../inlineChat/browser/inlineChatController.js';
+import { reviewEdits } from '../../../inlineChat/browser/inlineChatController.js';
 import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from '../../../terminal/browser/terminal.js';
 import { ChatAgentLocation } from '../../common/chatAgents.js';
 import { ChatContextKeys } from '../../common/chatContextKeys.js';
@@ -599,13 +599,10 @@ export function registerChatCodeCompareBlockActions() {
 
 			const editorToApply = await editorService.openCodeEditor({ resource: item.uri }, null);
 			if (editorToApply) {
-				const inlineChatController = InlineChatController.get(editorToApply);
-				if (inlineChatController) {
-					editorToApply.revealLineInCenterIfOutsideViewport(firstEdit.range.startLineNumber);
-					instaService.invokeFunction(reviewEdits, editorToApply, textEdits, CancellationToken.None);
-					response.setEditApplied(item, 1);
-					return true;
-				}
+				editorToApply.revealLineInCenterIfOutsideViewport(firstEdit.range.startLineNumber);
+				instaService.invokeFunction(reviewEdits, editorToApply, textEdits, CancellationToken.None);
+				response.setEditApplied(item, 1);
+				return true;
 			}
 			return false;
 		}
diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts
index 3f733e337486..0736974c5c1a 100644
--- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts
+++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts
@@ -30,7 +30,7 @@ export class InlineChatAccessibleView implements IAccessibleViewImplementation {
 		if (!controller) {
 			return;
 		}
-		const responseContent = controller?.getMessage();
+		const responseContent = controller.widget.responseContent;
 		if (!responseContent) {
 			return;
 		}
diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts
index f3c7605e6e52..95b2f9072922 100644
--- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts
+++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts
@@ -50,6 +50,7 @@ import { HunkInformation, Session, StashedSession } from './inlineChatSession.js
 import { IInlineChatSessionService } from './inlineChatSessionService.js';
 import { InlineChatError } from './inlineChatSessionServiceImpl.js';
 import { HunkAction, IEditObserver, LiveStrategy, ProgressingEditsOptions } from './inlineChatStrategies.js';
+import { EditorBasedInlineChatWidget } from './inlineChatWidget.js';
 import { InlineChatZoneWidget } from './inlineChatZoneWidget.js';
 
 export const enum State {
@@ -116,10 +117,6 @@ export class InlineChatController implements IEditorContribution {
 
 	private readonly _messages = this._store.add(new Emitter());
 	protected readonly _onDidEnterState = this._store.add(new Emitter());
-	readonly onDidEnterState = this._onDidEnterState.event;
-
-	private readonly _onWillStartSession = this._store.add(new Emitter());
-	readonly onWillStartSession = this._onWillStartSession.event;
 
 	get chatWidget() {
 		return this._ui.value.widget.chatWidget;
@@ -209,7 +206,7 @@ export class InlineChatController implements IEditorContribution {
 		this._store.add(this._inlineChatSessionService.onDidEndSession(e => {
 			if (e.session === this._session && e.endedByExternalCause) {
 				this._log('session ENDED by external cause');
-				this.finishExistingSession();
+				this.acceptSession();
 			}
 		}));
 
@@ -242,8 +239,8 @@ export class InlineChatController implements IEditorContribution {
 		}
 	}
 
-	getMessage(): string | undefined {
-		return this._ui.value.widget.responseContent;
+	get widget(): EditorBasedInlineChatWidget {
+		return this._ui.value.widget;
 	}
 
 	getId(): string {
@@ -256,9 +253,13 @@ export class InlineChatController implements IEditorContribution {
 
 	private _currentRun?: Promise;
 
-	async run(options: InlineChatRunOptions | undefined = {}): Promise {
+	async run(options: InlineChatRunOptions | undefined = {}): Promise {
+
+		let lastState: State | undefined;
+		const d = this._onDidEnterState.event(e => lastState = e);
+
 		try {
-			this.finishExistingSession();
+			this.acceptSession();
 			if (this._currentRun) {
 				await this._currentRun;
 			}
@@ -266,7 +267,6 @@ export class InlineChatController implements IEditorContribution {
 				this._editor.setSelection(options.initialSelection);
 			}
 			this._stashedSession.clear();
-			this._onWillStartSession.fire();
 			this._currentRun = this._nextState(State.CREATE_SESSION, options);
 			await this._currentRun;
 
@@ -281,7 +281,10 @@ export class InlineChatController implements IEditorContribution {
 
 		} finally {
 			this._currentRun = undefined;
+			d.dispose();
 		}
+
+		return lastState !== State.CANCEL;
 	}
 
 	// ---- state machine
@@ -427,7 +430,7 @@ export class InlineChatController implements IEditorContribution {
 
 			if (shouldFinishSession) {
 				this._log('text changed outside of whole range, FINISH session');
-				this.finishExistingSession();
+				this.acceptSession();
 			}
 		}));
 
@@ -1077,13 +1080,6 @@ export class InlineChatController implements IEditorContribution {
 		this._messages.fire(Message.CANCEL_SESSION);
 	}
 
-	finishExistingSession(): void {
-		if (this._session) {
-			this._log('finishing existing session, using APPLY');
-			this.acceptSession();
-		}
-	}
-
 	reportIssue() {
 		const response = this._session?.chatModel.lastRequest?.response;
 		if (response) {
diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts
index 23bb36062f48..458683cab4b4 100644
--- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts
+++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController2.ts
@@ -5,10 +5,11 @@
 
 import { CancellationToken } from '../../../../base/common/cancellation.js';
 import { Codicon } from '../../../../base/common/codicons.js';
+import { Event } from '../../../../base/common/event.js';
 import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
 import { Lazy } from '../../../../base/common/lazy.js';
 import { DisposableStore } from '../../../../base/common/lifecycle.js';
-import { autorun, autorunWithStore, constObservable, derived, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from '../../../../base/common/observable.js';
+import { autorun, autorunWithStore, constObservable, derived, IObservable, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from '../../../../base/common/observable.js';
 import { isEqual } from '../../../../base/common/resources.js';
 import { assertType } from '../../../../base/common/types.js';
 import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js';
@@ -17,6 +18,7 @@ import { observableCodeEditor } from '../../../../editor/browser/observableCodeE
 import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
 import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js';
 import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diffEditor/embeddedDiffEditorWidget.js';
+import { Position } from '../../../../editor/common/core/position.js';
 import { IEditorContribution } from '../../../../editor/common/editorCommon.js';
 import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';
 import { localize, localize2 } from '../../../../nls.js';
@@ -55,6 +57,8 @@ export class InlineChatController2 implements IEditorContribution {
 	private readonly _isActiveController = observableValue(this, false);
 	private readonly _zone: Lazy;
 
+	private readonly _currentSession: IObservable;
+
 	get widget(): EditorBasedInlineChatWidget {
 		return this._zone.value.widget;
 	}
@@ -63,7 +67,7 @@ export class InlineChatController2 implements IEditorContribution {
 		private readonly _editor: ICodeEditor,
 		@IInstantiationService private readonly _instaService: IInstantiationService,
 		@INotebookEditorService private readonly _notebookEditorService: INotebookEditorService,
-		@IInlineChatSessionService inlineChatSessions: IInlineChatSessionService,
+		@IInlineChatSessionService private readonly _inlineChatSessions: IInlineChatSessionService,
 		@IContextKeyService contextKeyService: IContextKeyService,
 	) {
 
@@ -120,18 +124,18 @@ export class InlineChatController2 implements IEditorContribution {
 
 		const editorObs = observableCodeEditor(_editor);
 
-		const sessionsSignal = observableSignalFromEvent(this, inlineChatSessions.onDidChangeSessions);
+		const sessionsSignal = observableSignalFromEvent(this, _inlineChatSessions.onDidChangeSessions);
 
-		const sessionObs = derived(r => {
+		this._currentSession = derived(r => {
 			sessionsSignal.read(r);
 			const model = editorObs.model.read(r);
-			const value = model && inlineChatSessions.getSession2(model.uri);
+			const value = model && _inlineChatSessions.getSession2(model.uri);
 			return value ?? undefined;
 		});
 
 
 		this._store.add(autorunWithStore((r, store) => {
-			const session = sessionObs.read(r);
+			const session = this._currentSession.read(r);
 
 			if (!session) {
 				ctxHasSession.set(undefined);
@@ -147,7 +151,7 @@ export class InlineChatController2 implements IEditorContribution {
 
 		this._store.add(autorunWithStore((r, store) => {
 
-			const session = sessionObs.read(r);
+			const session = this._currentSession.read(r);
 			const isActive = this._isActiveController.read(r);
 
 			if (!session || !isActive) {
@@ -198,7 +202,7 @@ export class InlineChatController2 implements IEditorContribution {
 		this._store.add(autorun(r => {
 
 			const overlay = ChatEditorOverlayController.get(_editor)!;
-			const session = sessionObs.read(r);
+			const session = this._currentSession.read(r);
 			const model = editorObs.model.read(r);
 			if (!session || !model) {
 				overlay.hide();
@@ -229,13 +233,52 @@ export class InlineChatController2 implements IEditorContribution {
 		this._showWidgetOverrideObs.set(!value, undefined);
 	}
 
-	markActiveController() {
-		this._isActiveController.set(true, undefined);
+
+	getWidgetPosition(): Position | undefined {
+		return this._zone.rawValue?.position;
 	}
 
 	focus() {
 		this._zone.rawValue?.widget.focus();
 	}
+
+	markActiveController() {
+		this._isActiveController.set(true, undefined);
+	}
+
+	async run(arg?: InlineChatRunOptions): Promise {
+		assertType(this._editor.hasModel());
+
+		const uri = this._editor.getModel().uri;
+		const session = await this._inlineChatSessions.createSession2(this._editor, uri, CancellationToken.None);
+
+		this.markActiveController();
+
+		if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) {
+			if (arg.initialRange) {
+				this._editor.revealRange(arg.initialRange);
+			}
+			if (arg.initialSelection) {
+				this._editor.setSelection(arg.initialSelection);
+			}
+			if (arg.message) {
+				this._zone.value.widget.chatWidget.setInput(arg.message);
+				if (arg.autoSend) {
+					await this._zone.value.widget.chatWidget.acceptInput();
+				}
+			}
+		}
+
+		await Event.toPromise(session.editingSession.onDidDispose);
+
+		const rejected = session.editingSession.getEntry(uri)?.state.get() === WorkingSetEntryState.Rejected;
+		return !rejected;
+	}
+
+	acceptSession() {
+		const value = this._currentSession.get();
+		value?.editingSession.accept();
+	}
 }
 
 export class StartSessionAction2 extends EditorAction2 {
@@ -268,35 +311,8 @@ export class StartSessionAction2 extends EditorAction2 {
 	}
 
 	override async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]) {
-		const inlineChatSessions = accessor.get(IInlineChatSessionService);
-		if (!editor.hasModel()) {
-			return;
-		}
 		const ctrl = InlineChatController2.get(editor);
-		if (!ctrl) {
-			return;
-		}
-
-		const textModel = editor.getModel();
-		await inlineChatSessions.createSession2(editor, textModel.uri, CancellationToken.None);
-
-		ctrl.markActiveController();
-
-		const arg = args[0];
-		if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) {
-			if (arg.initialRange) {
-				editor.revealRange(arg.initialRange);
-			}
-			if (arg.initialSelection) {
-				editor.setSelection(arg.initialSelection);
-			}
-			if (arg.message) {
-				ctrl.widget.chatWidget.setInput(arg.message);
-				if (arg.autoSend) {
-					await ctrl.widget.chatWidget.acceptInput();
-				}
-			}
-		}
+		ctrl?.run();
 	}
 }
 
diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts
index 08c9911fdb80..5c6cee1541fd 100644
--- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts
+++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts
@@ -8,7 +8,7 @@ import { ICodeEditor, MouseTargetType } from '../../../../editor/browser/editorB
 import { IEditorContribution } from '../../../../editor/common/editorCommon.js';
 import { localize, localize2 } from '../../../../nls.js';
 import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
-import { InlineChatController, State } from './inlineChatController.js';
+import { InlineChatController } from './inlineChatController.js';
 import { ACTION_START, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js';
 import { EditorAction2, ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
 import { EditOperation } from '../../../../editor/common/core/editOperation.js';
@@ -83,22 +83,14 @@ export class InlineChatExpandLineAction extends EditorAction2 {
 			return null;
 		});
 
-		let lastState: State | undefined;
-		const d = ctrl.onDidEnterState(e => lastState = e);
-
-		try {
-			// trigger chat
-			await ctrl.run({
-				autoSend: true,
-				message: lineContent.trim(),
-				position: new Position(lineNumber, startColumn)
-			});
-
-		} finally {
-			d.dispose();
-		}
+		// trigger chat
+		const accepted = await ctrl.run({
+			autoSend: true,
+			message: lineContent.trim(),
+			position: new Position(lineNumber, startColumn)
+		});
 
-		if (lastState === State.CANCEL) {
+		if (!accepted) {
 			model.pushEditOperations(null, undoEdits, () => null);
 		}
 	}
diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts
index 48b9e6ca0e21..95c38b1b78ef 100644
--- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts
+++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts
@@ -89,7 +89,7 @@ export class InlineChatNotebookContribution {
 						// cancel all sibling sessions
 						for (const editor of editors) {
 							if (editor !== newSessionEditor) {
-								InlineChatController.get(editor)?.finishExistingSession();
+								InlineChatController.get(editor)?.acceptSession();
 							}
 						}
 						break;
diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts
index fe4a356ad200..8be4db0711c9 100644
--- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts
+++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts
@@ -350,9 +350,18 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
 		store.add(chatModel);
 
 		store.add(autorun(r => {
-			const entry = editingSession.readEntry(uri, r);
-			const state = entry?.state.read(r);
-			if (state === WorkingSetEntryState.Accepted || state === WorkingSetEntryState.Rejected) {
+
+			const entries = editingSession.entries.read(r);
+			if (entries.length === 0) {
+				return;
+			}
+
+			const allSettled = entries.every(entry => {
+				const state = entry.state.read(r);
+				return state === WorkingSetEntryState.Accepted || state === WorkingSetEntryState.Rejected;
+			});
+
+			if (allSettled) {
 				// self terminate
 				store.dispose();
 			}
diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts
index 131cb7587662..d3d0b420edf2 100644
--- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts
+++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts
@@ -8,7 +8,6 @@ import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/ac
 import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
 import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
 import { IAction } from '../../../../base/common/actions.js';
-import { isNonEmptyArray } from '../../../../base/common/arrays.js';
 import { Emitter, Event } from '../../../../base/common/event.js';
 import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
 import { constObservable, derived, IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js';
@@ -424,10 +423,7 @@ export class InlineChatWidget {
 
 	get responseContent(): string | undefined {
 		const requests = this._chatWidget.viewModel?.model.getRequests();
-		if (!isNonEmptyArray(requests)) {
-			return undefined;
-		}
-		return requests.at(-1)?.response?.response.toString();
+		return requests?.at(-1)?.response?.response.toString();
 	}
 
 
diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts
index 44fe68ae3509..e1d5f3ef7593 100644
--- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts
+++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts
@@ -470,7 +470,7 @@ suite('InlineChatController', function () {
 
 		editor.executeEdits('test', [EditOperation.insert(model.getFullModelRange().getEndPosition(), 'MANUAL')]);
 
-		ctrl.finishExistingSession();
+		ctrl.acceptSession();
 		await r;
 		assert.ok(model.getValue().includes('GENERATED'));
 		assert.ok(model.getValue().includes('MANUAL'));
@@ -509,7 +509,7 @@ suite('InlineChatController', function () {
 		assert.strictEqual(await p2, undefined);
 
 		assert.strictEqual(model.getValue(), 'PROMPT_2');
-		ctrl.finishExistingSession();
+		ctrl.acceptSession();
 		await r;
 	});
 
@@ -558,7 +558,7 @@ suite('InlineChatController', function () {
 
 		assert.strictEqual(model.getValue(), 'drei-eins-');
 
-		ctrl.finishExistingSession();
+		ctrl.acceptSession();
 		await r;
 
 	});
diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts
index dc6f21656751..cae7e893d81d 100644
--- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts
+++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts
@@ -19,13 +19,13 @@ import { IResolvedTextEditorModel, ITextModelService } from '../../../../../edit
 import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
 import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js';
 import { IWordWrapTransientState, readTransientState, writeTransientState } from '../../../codeEditor/browser/toggleWordWrap.js';
-import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js';
 import { CellEditState, CellFocusMode, CellLayoutChangeEvent, CursorAtBoundary, CursorAtLineBoundary, IEditableCellViewModel, INotebookCellDecorationOptions } from '../notebookBrowser.js';
 import { NotebookOptionsChangeEvent } from '../notebookOptions.js';
 import { CellViewModelStateChangeEvent } from '../notebookViewEvents.js';
 import { ViewContext } from './viewContext.js';
 import { NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js';
 import { CellKind, INotebookCellStatusBarItem, INotebookFindOptions } from '../../common/notebookCommon.js';
+import { IInlineChatSessionService } from '../../../inlineChat/browser/inlineChatSessionService.js';
 
 export abstract class BaseCellViewModel extends Disposable {
 
@@ -190,6 +190,7 @@ export abstract class BaseCellViewModel extends Disposable {
 		private readonly _modelService: ITextModelService,
 		private readonly _undoRedoService: IUndoRedoService,
 		private readonly _codeEditorService: ICodeEditorService,
+		private readonly _inlineChatSessionService: IInlineChatSessionService
 		// private readonly _keymapService: INotebookKeymapService
 	) {
 		super();
@@ -315,14 +316,11 @@ export abstract class BaseCellViewModel extends Disposable {
 		});
 
 		this._editorListeners.push(editor.onDidChangeCursorSelection(() => { this._onDidChangeState.fire({ selectionChanged: true }); }));
-		const inlineChatController = InlineChatController.get(this._textEditor);
-		if (inlineChatController) {
-			this._editorListeners.push(inlineChatController.onWillStartSession(() => {
-				if (this.textBuffer.getLength() === 0) {
-					this.enableAutoLanguageDetection();
-				}
-			}));
-		}
+		this._editorListeners.push(this._inlineChatSessionService.onWillStartSession((e) => {
+			if (e === this._textEditor && this.textBuffer.getLength() === 0) {
+				this.enableAutoLanguageDetection();
+			}
+		}));
 
 		this._onDidChangeState.fire({ selectionChanged: true });
 		this._onDidChangeEditorAttachState.fire();
diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts
index 0765526c8b30..a1ce3e95da4d 100644
--- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts
+++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts
@@ -23,6 +23,7 @@ import { CellKind, INotebookFindOptions, NotebookCellOutputsSplice } from '../..
 import { ICellExecutionError, ICellExecutionStateChangedEvent } from '../../common/notebookExecutionStateService.js';
 import { INotebookService } from '../../common/notebookService.js';
 import { BaseCellViewModel } from './baseCellViewModel.js';
+import { IInlineChatSessionService } from '../../../inlineChat/browser/inlineChatSessionService.js';
 
 export const outputDisplayLimit = 500;
 
@@ -145,9 +146,10 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod
 		@INotebookService private readonly _notebookService: INotebookService,
 		@ITextModelService modelService: ITextModelService,
 		@IUndoRedoService undoRedoService: IUndoRedoService,
-		@ICodeEditorService codeEditorService: ICodeEditorService
+		@ICodeEditorService codeEditorService: ICodeEditorService,
+		@IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService
 	) {
-		super(viewType, model, UUID.generateUuid(), viewContext, configurationService, modelService, undoRedoService, codeEditorService);
+		super(viewType, model, UUID.generateUuid(), viewContext, configurationService, modelService, undoRedoService, codeEditorService, inlineChatSessionService);
 		this._outputViewModels = this.model.outputs.map(output => new CellOutputViewModel(this, output, this._notebookService));
 
 		this._register(this.model.onDidChangeOutputs((splice) => {
diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts
index c7d85a24cca5..875d0f18c491 100644
--- a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts
+++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts
@@ -17,6 +17,7 @@ import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRe
 import { NotebookOptionsChangeEvent } from '../notebookOptions.js';
 import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
 import { NotebookCellStateChangedEvent, NotebookLayoutInfo } from '../notebookViewEvents.js';
+import { IInlineChatSessionService } from '../../../inlineChat/browser/inlineChatSessionService.js';
 
 export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewModel {
 
@@ -120,9 +121,10 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM
 		@IConfigurationService configurationService: IConfigurationService,
 		@ITextModelService textModelService: ITextModelService,
 		@IUndoRedoService undoRedoService: IUndoRedoService,
-		@ICodeEditorService codeEditorService: ICodeEditorService
+		@ICodeEditorService codeEditorService: ICodeEditorService,
+		@IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService
 	) {
-		super(viewType, model, UUID.generateUuid(), viewContext, configurationService, textModelService, undoRedoService, codeEditorService);
+		super(viewType, model, UUID.generateUuid(), viewContext, configurationService, textModelService, undoRedoService, codeEditorService, inlineChatSessionService);
 
 		const { bottomToolbarGap } = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType);
 

From 6057cd558306acb602b1db2cb173168e74f1dec7 Mon Sep 17 00:00:00 2001
From: Johannes Rieken 
Date: Thu, 6 Feb 2025 12:44:36 +0100
Subject: [PATCH 1282/3587] debt - more `globalEditingSession` cleanup
 (#239778)

* make `ContinueEditingAction` work with correct editing session

https://github.com/microsoft/vscode-copilot/issues/12820

* remove `globalEditingSession`

https://github.com/microsoft/vscode-copilot/issues/12820

* remove unused import

* fix one more bad import
---
 .../chat/browser/actions/chatTitleActions.ts  | 14 +++++---
 .../chatEditing/chatEditingServiceImpl.ts     | 34 ++++++++-----------
 .../contrib/chat/common/chatEditingService.ts |  7 ++--
 .../test/browser/inlineChatController.test.ts |  3 +-
 .../test/browser/inlineChatSession.test.ts    |  3 +-
 5 files changed, 27 insertions(+), 34 deletions(-)

diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts
index 3f757e90ed44..bf5663ee0c8f 100644
--- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts
+++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts
@@ -8,7 +8,7 @@ import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
 import { DisposableStore } from '../../../../../base/common/lifecycle.js';
 import { ResourceSet } from '../../../../../base/common/map.js';
 import { marked } from '../../../../../base/common/marked/marked.js';
-import { waitForState } from '../../../../../base/common/observable.js';
+import { observableFromEvent, waitForState } from '../../../../../base/common/observable.js';
 import { basename } from '../../../../../base/common/resources.js';
 import { URI } from '../../../../../base/common/uri.js';
 import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
@@ -36,6 +36,7 @@ import { IChatRequestModel } from '../../common/chatModel.js';
 import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatService } from '../../common/chatService.js';
 import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js';
 import { ChatTreeItem, EditsViewId, IChatWidgetService } from '../chat.js';
+import { ChatViewPane } from '../chatViewPane.js';
 import { CHAT_CATEGORY } from './chatActions.js';
 
 export const MarkUnhelpfulActionId = 'workbench.action.chat.markUnhelpful';
@@ -461,13 +462,16 @@ export function registerChatTitleActions() {
 				return;
 			}
 
-			await viewsService.openView(EditsViewId);
+			const editsView = await viewsService.openView(EditsViewId);
 
-			let editingSession = chatEditingService.globalEditingSessionObs.get();
-			if (!editingSession) {
-				editingSession = await waitForState(chatEditingService.globalEditingSessionObs);
+			if (!(editsView instanceof ChatViewPane)) {
+				return;
 			}
 
+			const viewModelObs = observableFromEvent(this, editsView.widget.onDidChangeViewModel, () => editsView.widget.viewModel);
+			const chatSessionId = (await waitForState(viewModelObs)).sessionId;
+			const editingSession = chatEditingService.getEditingSession(chatSessionId);
+
 			if (!editingSession) {
 				return;
 			}
diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts
index 0098bdadb66a..acbfadb936e3 100644
--- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts
+++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts
@@ -17,7 +17,7 @@ import { Schemas } from '../../../../../base/common/network.js';
 import { derived, IObservable, observableValue, observableValueOpts, runOnChange, ValueWithChangeEventFromObservable } from '../../../../../base/common/observable.js';
 import { compare } from '../../../../../base/common/strings.js';
 import { ThemeIcon } from '../../../../../base/common/themables.js';
-import { isString } from '../../../../../base/common/types.js';
+import { assertType, isString } from '../../../../../base/common/types.js';
 import { URI } from '../../../../../base/common/uri.js';
 import { TextEdit } from '../../../../../editor/common/languages.js';
 import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
@@ -53,10 +53,10 @@ export class ChatEditingService extends Disposable implements IChatEditingServic
 	private readonly _currentSessionObs = observableValue(this, null);
 	private readonly _currentSessionDisposables = this._register(new DisposableStore());
 
-	private readonly _adhocSessionsObs = observableValueOpts>({ equalsFn: (a, b) => false }, new LinkedList());
+	private readonly _sessionsObs = observableValueOpts>({ equalsFn: (a, b) => false }, new LinkedList());
 
 	readonly editingSessionsObs: IObservable = derived(r => {
-		const result = Array.from(this._adhocSessionsObs.read(r));
+		const result = Array.from(this._sessionsObs.read(r));
 		const globalSession = this._currentSessionObs.read(r);
 		if (globalSession) {
 			result.push(globalSession);
@@ -64,14 +64,6 @@ export class ChatEditingService extends Disposable implements IChatEditingServic
 		return result;
 	});
 
-	get globalEditingSession(): IChatEditingSession | null {
-		return this._currentSessionObs.get();
-	}
-
-	get globalEditingSessionObs(): IObservable {
-		return this._currentSessionObs;
-	}
-
 	private _editingSessionFileLimitPromise: Promise;
 	private _editingSessionFileLimit: number | undefined;
 	get editingSessionFileLimit() {
@@ -156,7 +148,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic
 
 	override dispose(): void {
 		this._currentSessionObs.get()?.dispose();
-		dispose(this._adhocSessionsObs.get());
+		dispose(this._sessionsObs.get());
 		super.dispose();
 	}
 
@@ -214,11 +206,14 @@ export class ChatEditingService extends Disposable implements IChatEditingServic
 			.find(candidate => candidate.chatSessionId === chatSessionId);
 	}
 
-	async createEditingSession(chatSessionId: string): Promise {
+	async createEditingSession(chatSessionId: string): Promise {
+
+		assertType(this.getEditingSession(chatSessionId) === undefined, 'CANNOT have more than one editing session per chat session');
+
 		const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, false, this._editingSessionFileLimitPromise, this._lookupEntry.bind(this));
 		await session.init();
 
-		const list = this._adhocSessionsObs.get();
+		const list = this._sessionsObs.get();
 		const removeSession = list.unshift(session);
 
 		const store = new DisposableStore();
@@ -228,12 +223,11 @@ export class ChatEditingService extends Disposable implements IChatEditingServic
 
 		store.add(session.onDidDispose(e => {
 			removeSession();
-			this._adhocSessionsObs.set(list, undefined);
-			this._store.deleteAndLeak(store);
-			store.dispose();
+			this._sessionsObs.set(list, undefined);
+			this._store.delete(store);
 		}));
 
-		this._adhocSessionsObs.set(list, undefined);
+		this._sessionsObs.set(list, undefined);
 
 		return session;
 	}
@@ -387,8 +381,8 @@ export class ChatEditingService extends Disposable implements IChatEditingServic
 	}
 
 	async getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined> {
-		const currentSession = this._currentSessionObs.get();
-		if (!currentSession || chatSessionId !== currentSession.chatSessionId) {
+		const currentSession = this.getEditingSession(chatSessionId);
+		if (!currentSession) {
 			return undefined;
 		}
 		const userAddedWorkingSetEntries: URI[] = [];
diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts
index cb704a048088..c4df4c82ed85 100644
--- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts
+++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts
@@ -24,9 +24,6 @@ export interface IChatEditingService {
 
 	_serviceBrand: undefined;
 
-	readonly globalEditingSessionObs: IObservable;
-
-	readonly globalEditingSession: IChatEditingSession | null;
 
 	startOrContinueGlobalEditingSession(chatSessionId: string): Promise;
 
@@ -40,7 +37,7 @@ export interface IChatEditingService {
 	/**
 	 * Creates a new short lived editing session
 	 */
-	createEditingSession(chatSessionId: string): Promise;
+	createEditingSession(chatSessionId: string): Promise;
 
 	readonly editingSessionFileLimit: number;
 
@@ -78,7 +75,7 @@ export interface WorkingSetDisplayMetadata {
 	isMarkedReadonly?: boolean;
 }
 
-export interface IChatEditingSession {
+export interface IChatEditingSession extends IDisposable {
 	readonly isGlobalEditingSession: boolean;
 	readonly chatSessionId: string;
 	readonly onDidChange: Event;
diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts
index e1d5f3ef7593..20282488d486 100644
--- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts
+++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts
@@ -68,7 +68,7 @@ import { IChatEditingService, IChatEditingSession } from '../../../chat/common/c
 import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
 import { TextModelResolverService } from '../../../../services/textmodelResolver/common/textModelResolverService.js';
 import { ChatInputBoxContentProvider } from '../../../chat/browser/chatEdinputInputContentProvider.js';
-import { constObservable, IObservable, observableValue } from '../../../../../base/common/observable.js';
+import { constObservable, IObservable } from '../../../../../base/common/observable.js';
 import { ILanguageModelToolsService } from '../../../chat/common/languageModelToolsService.js';
 import { MockLanguageModelToolsService } from '../../../chat/test/common/mockLanguageModelToolsService.js';
 
@@ -163,7 +163,6 @@ suite('InlineChatController', function () {
 			[IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)],
 			[ICommandService, new SyncDescriptor(TestCommandService)],
 			[IChatEditingService, new class extends mock() {
-				override globalEditingSessionObs: IObservable = observableValue(this, null);
 				override editingSessionsObs: IObservable = constObservable([]);
 			}],
 			[IEditorProgressService, new class extends mock() {
diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts
index c7a67a79d95b..f2d8cc86365b 100644
--- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts
+++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts
@@ -60,7 +60,7 @@ import { ILanguageModelToolsService } from '../../../chat/common/languageModelTo
 import { MockLanguageModelToolsService } from '../../../chat/test/common/mockLanguageModelToolsService.js';
 import { IChatRequestModel } from '../../../chat/common/chatModel.js';
 import { assertSnapshot } from '../../../../../base/test/common/snapshot.js';
-import { IObservable, observableValue, constObservable } from '../../../../../base/common/observable.js';
+import { IObservable, constObservable } from '../../../../../base/common/observable.js';
 import { IChatEditingService, IChatEditingSession } from '../../../chat/common/chatEditingService.js';
 
 suite('InlineChatSession', function () {
@@ -106,7 +106,6 @@ suite('InlineChatSession', function () {
 				}
 			}],
 			[IChatEditingService, new class extends mock() {
-				override globalEditingSessionObs: IObservable = observableValue(this, null);
 				override editingSessionsObs: IObservable = constObservable([]);
 			}],
 			[IChatAccessibilityService, new class extends mock() {

From 907518a25c6d6b9467cbcc57132c6adb7e7396b0 Mon Sep 17 00:00:00 2001
From: Alex Ross 
Date: Thu, 6 Feb 2025 13:13:16 +0100
Subject: [PATCH 1283/3587] Fix spread in typescript tree-sitter (#239790)

---
 src/vs/editor/common/languages/highlights/typescript.scm | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/vs/editor/common/languages/highlights/typescript.scm b/src/vs/editor/common/languages/highlights/typescript.scm
index 5f6a6796883d..d031559fe428 100644
--- a/src/vs/editor/common/languages/highlights/typescript.scm
+++ b/src/vs/editor/common/languages/highlights/typescript.scm
@@ -306,7 +306,8 @@
 
 (rest_pattern) @keyword.operator.rest
 
-(spread_element) @keyword.operator.spread
+(spread_element
+  ("...") @keyword.operator.spread)
 
 ; Language constants
 

From 1467228f0c917b3b646f8c2dad676fe1db021b66 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Thu, 6 Feb 2025 04:40:41 -0800
Subject: [PATCH 1284/3587] Add simple caching in for _getFontInfo

Fixes #239792
---
 .../suggest/browser/terminalSuggestAddon.ts           | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
index 9c93bbc13fd3..cb2a59f2b374 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
@@ -57,6 +57,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 	private _container?: HTMLElement;
 	private _screen?: HTMLElement;
 	private _suggestWidget?: SimpleSuggestWidget;
+	private _cachedFontInfo: ISimpleSuggestWidgetFontInfo | undefined;
 	private _enableWidget: boolean = true;
 	private _pathSeparator: string = sep;
 	private _isFilteringDirectories: boolean = false;
@@ -131,6 +132,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 				this._promptInputModel = undefined;
 			}
 		}));
+		this._register(this._terminalConfigurationService.onConfigChanged(() => this._cachedFontInfo = undefined));
 	}
 
 	activate(xterm: Terminal): void {
@@ -384,7 +386,12 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 	}
 
 	private _getFontInfo(): ISimpleSuggestWidgetFontInfo {
-		const font = this._terminalConfigurationService.getFont(dom.getActiveWindow());
+		if (this._cachedFontInfo) {
+			return this._cachedFontInfo;
+		}
+
+		const core = (this._terminal as any)._core as IXtermCore;
+		const font = this._terminalConfigurationService.getFont(dom.getActiveWindow(), core);
 		let lineHeight: number = font.lineHeight;
 		const fontSize: number = font.fontSize;
 		const fontFamily: string = font.fontFamily;
@@ -412,6 +419,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 			fontFamily
 		};
 
+		this._cachedFontInfo = fontInfo;
+
 		return fontInfo;
 	}
 

From 0b786957e2b2f2a3db48eb9711454c5135f60e96 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Thu, 6 Feb 2025 04:59:50 -0800
Subject: [PATCH 1285/3587] Be less strict when checking line continuation

Fixes #239694
---
 .../capabilities/commandDetection/promptInputModel.ts | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts b/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts
index eda4c7b93d24..1495bb43a1f5 100644
--- a/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts
+++ b/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts
@@ -456,17 +456,22 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
 	}
 
 	private _lineContainsContinuationPrompt(lineText: string): boolean {
-		return !!(this._continuationPrompt && lineText.startsWith(this._continuationPrompt));
+		return !!(this._continuationPrompt && lineText.startsWith(this._continuationPrompt.trimEnd()));
 	}
 
 	private _getContinuationPromptCellWidth(line: IBufferLine, lineText: string): number {
-		if (!this._continuationPrompt || !lineText.startsWith(this._continuationPrompt)) {
+		if (!this._continuationPrompt || !lineText.startsWith(this._continuationPrompt.trimEnd())) {
 			return 0;
 		}
 		let buffer = '';
 		let x = 0;
+		let cell: IBufferCell | undefined;
 		while (buffer !== this._continuationPrompt) {
-			buffer += line.getCell(x++)!.getChars();
+			cell = line.getCell(x++);
+			if (!cell) {
+				break;
+			}
+			buffer += cell.getChars();
 		}
 		return x;
 	}

From 72190d3a93754176b93c8602c82d1a1ede944feb Mon Sep 17 00:00:00 2001
From: Alex Ross 
Date: Thu, 6 Feb 2025 14:12:45 +0100
Subject: [PATCH 1286/3587] getActionViewItem -> createActionViewItem (#239797)

---
 src/vs/workbench/browser/parts/views/viewPane.ts       |  4 ++--
 .../workbench/browser/parts/views/viewPaneContainer.ts |  2 +-
 src/vs/workbench/contrib/debug/browser/repl.ts         |  4 ++--
 .../contrib/scm/browser/scmHistoryViewPane.ts          |  4 ++--
 .../workbench/contrib/terminal/browser/terminalView.ts |  4 ++--
 .../contrib/testing/browser/testingExplorerView.ts     | 10 +++++-----
 6 files changed, 14 insertions(+), 14 deletions(-)

diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts
index f848ff261ab4..66be180891f5 100644
--- a/src/vs/workbench/browser/parts/views/viewPane.ts
+++ b/src/vs/workbench/browser/parts/views/viewPane.ts
@@ -461,7 +461,7 @@ export abstract class ViewPane extends Pane implements IView {
 		this.toolbar = this.instantiationService.createInstance(WorkbenchToolBar, actions, {
 			orientation: ActionsOrientation.HORIZONTAL,
 			actionViewItemProvider: (action, options) => {
-				const item = this.getActionViewItem(action, options);
+				const item = this.createActionViewItem(action, options);
 				if (item) {
 					this.headerActionViewItems.set(item.action.id, item);
 				}
@@ -695,7 +695,7 @@ export abstract class ViewPane extends Pane implements IView {
 		this._onDidChangeTitleArea.fire();
 	}
 
-	getActionViewItem(action: IAction, options?: IDropdownMenuActionViewItemOptions): IActionViewItem | undefined {
+	createActionViewItem(action: IAction, options?: IDropdownMenuActionViewItemOptions): IActionViewItem | undefined {
 		if (action.id === VIEWPANE_FILTER_ACTION.id) {
 			const that = this;
 			return new class extends BaseActionViewItem {
diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts
index bdac0037f3f8..4d97f8d05720 100644
--- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts
+++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts
@@ -603,7 +603,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer {
 
 	getActionViewItem(action: IAction, options: IBaseActionViewItemOptions): IActionViewItem | undefined {
 		if (this.isViewMergedWithContainer()) {
-			return this.paneItems[0].pane.getActionViewItem(action, options);
+			return this.paneItems[0].pane.createActionViewItem(action, options);
 		}
 		return createActionViewItem(this.instantiationService, action, options);
 	}
diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts
index 04823d1f137c..18ebfe7da043 100644
--- a/src/vs/workbench/contrib/debug/browser/repl.ts
+++ b/src/vs/workbench/contrib/debug/browser/repl.ts
@@ -569,13 +569,13 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget {
 		this.replInput.focus();
 	}
 
-	override getActionViewItem(action: IAction): IActionViewItem | undefined {
+	override createActionViewItem(action: IAction): IActionViewItem | undefined {
 		if (action.id === selectReplCommandId) {
 			const session = (this.tree ? this.tree.getInput() : undefined) ?? this.debugService.getViewModel().focusedSession;
 			return this.instantiationService.createInstance(SelectReplActionViewItem, action, session);
 		}
 
-		return super.getActionViewItem(action);
+		return super.createActionViewItem(action);
 	}
 
 	private get isMultiSessionView(): boolean {
diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts
index 0d20c1d6de1d..43895d363315 100644
--- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts
+++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts
@@ -1414,7 +1414,7 @@ export class SCMHistoryViewPane extends ViewPane {
 		return this._treeViewModel?.repository.get()?.provider;
 	}
 
-	override getActionViewItem(action: IAction, options?: IDropdownMenuActionViewItemOptions): IActionViewItem | undefined {
+	override createActionViewItem(action: IAction, options?: IDropdownMenuActionViewItemOptions): IActionViewItem | undefined {
 		if (action.id === PICK_REPOSITORY_ACTION_ID) {
 			const repository = this._treeViewModel?.repository.get();
 			if (repository) {
@@ -1428,7 +1428,7 @@ export class SCMHistoryViewPane extends ViewPane {
 			}
 		}
 
-		return super.getActionViewItem(action, options);
+		return super.createActionViewItem(action, options);
 	}
 
 	override focus(): void {
diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts
index 38e6d3429421..7629255bc58a 100644
--- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts
+++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts
@@ -250,7 +250,7 @@ export class TerminalViewPane extends ViewPane {
 		this._terminalTabbedView?.layout(width, height);
 	}
 
-	override getActionViewItem(action: Action, options: IBaseActionViewItemOptions): IActionViewItem | undefined {
+	override createActionViewItem(action: Action, options: IBaseActionViewItemOptions): IActionViewItem | undefined {
 		switch (action.id) {
 			case TerminalCommandId.Split: {
 				// Split needs to be special cased to force splitting within the panel, not the editor
@@ -292,7 +292,7 @@ export class TerminalViewPane extends ViewPane {
 				}
 			}
 		}
-		return super.getActionViewItem(action, options);
+		return super.createActionViewItem(action, options);
 	}
 
 	/**
diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts
index dea0b909d049..1383e0061ec3 100644
--- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts
+++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts
@@ -323,7 +323,7 @@ export class TestingExplorerView extends ViewPane {
 	}
 
 	/** @override  */
-	public override getActionViewItem(action: IAction, options: IActionViewItemOptions): IActionViewItem | undefined {
+	public override createActionViewItem(action: IAction, options: IActionViewItemOptions): IActionViewItem | undefined {
 		switch (action.id) {
 			case TestCommandId.FilterAction:
 				this.filter.value = this.instantiationService.createInstance(TestingExplorerFilter, action, options);
@@ -337,7 +337,7 @@ export class TestingExplorerView extends ViewPane {
 			case TestCommandId.StopContinousRun:
 				return this.getContinuousRunDropdown(action, options);
 			default:
-				return super.getActionViewItem(action, options);
+				return super.createActionViewItem(action, options);
 		}
 	}
 
@@ -444,7 +444,7 @@ export class TestingExplorerView extends ViewPane {
 	private getRunGroupDropdown(group: TestRunProfileBitset, defaultAction: IAction, options: IActionViewItemOptions) {
 		const dropdownActions = this.getTestConfigGroupActions(group);
 		if (dropdownActions.numberOfProfiles < 2) {
-			return super.getActionViewItem(defaultAction, options);
+			return super.createActionViewItem(defaultAction, options);
 		}
 
 		const primaryAction = this.instantiationService.createInstance(MenuItemAction, {
@@ -476,7 +476,7 @@ export class TestingExplorerView extends ViewPane {
 		})];
 
 		if (allProfiles.length <= 1) {
-			return super.getActionViewItem(defaultAction, options);
+			return super.createActionViewItem(defaultAction, options);
 		}
 
 		const primaryAction = this.instantiationService.createInstance(MenuItemAction, {
@@ -530,7 +530,7 @@ export class TestingExplorerView extends ViewPane {
 
 	private createFilterActionBar() {
 		const bar = new ActionBar(this.treeHeader, {
-			actionViewItemProvider: (action, options) => this.getActionViewItem(action, options),
+			actionViewItemProvider: (action, options) => this.createActionViewItem(action, options),
 			triggerKeys: { keyDown: false, keys: [] },
 		});
 		bar.push(new Action(TestCommandId.FilterAction));

From cd7fcd95a9505cfd229f28dd95f2a45d1721c0a5 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Thu, 6 Feb 2025 05:43:39 -0800
Subject: [PATCH 1287/3587] Allow up to 5s for shellType when requesting
 completions

Fixes #239799
---
 .../suggest/browser/terminalSuggestAddon.ts   | 28 +++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
index 9c93bbc13fd3..c4ec9524bc52 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
@@ -34,6 +34,7 @@ import { MenuId } from '../../../../../platform/actions/common/actions.js';
 import { ISimpleSuggestWidgetFontInfo } from '../../../../services/suggest/browser/simpleSuggestWidgetRenderer.js';
 import { ITerminalConfigurationService } from '../../../terminal/browser/terminal.js';
 import { GOLDEN_LINE_HEIGHT_RATIO, MINIMUM_LINE_HEIGHT } from '../../../../../editor/common/config/fontInfo.js';
+import { IntervalTimer, TimeoutTimer } from '../../../../../base/common/async.js';
 
 export interface ISuggestController {
 	isPasting: boolean;
@@ -74,6 +75,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 
 	isPasting: boolean = false;
 	shellType: TerminalShellType | undefined;
+	private readonly _shellTypeInit: Promise;
 
 	private readonly _onBell = this._register(new Emitter());
 	readonly onBell = this._onBell.event;
@@ -107,7 +109,28 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 	) {
 		super();
 
+		// Initialize shell type, including a promise that completions can await for that resolves:
+		// - immediately if shell type
+		// - after a short delay if shell type gets set
+		// - after a long delay if it doesn't get set
 		this.shellType = shellType;
+		if (this.shellType) {
+			this._shellTypeInit = Promise.resolve();
+		} else {
+			const intervalTimer = this._register(new IntervalTimer());
+			const timeoutTimer = this._register(new TimeoutTimer());
+			this._shellTypeInit = new Promise(r => {
+				intervalTimer.cancelAndSet(() => {
+					if (this.shellType) {
+						r();
+					}
+				}, 50);
+				timeoutTimer.cancelAndSet(r, 5000);
+			}).then(() => {
+				this._store.delete(intervalTimer);
+				this._store.delete(timeoutTimer);
+			});
+		}
 
 		this._register(Event.runAndSubscribe(Event.any(
 			this._capabilities.onDidAddCapabilityType,
@@ -139,6 +162,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 			this._lastUserData = e;
 			this._lastUserDataTimestamp = Date.now();
 		}));
+
 	}
 
 	private async _handleCompletionProviders(terminal: Terminal | undefined, token: CancellationToken, explicitlyInvoked?: boolean): Promise {
@@ -152,6 +176,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 			return;
 		}
 
+		// Require a shell type for completions. This will wait a short period after launching to
+		// wait for the shell type to initialize. This prevents user requests sometimes getting lost
+		// if requested shortly after the terminal is created.
+		await this._shellTypeInit;
 		if (!this.shellType) {
 			return;
 		}

From 2060e48e0c7993d7848029f65f024dffd71130f1 Mon Sep 17 00:00:00 2001
From: Sandeep Somavarapu 
Date: Thu, 6 Feb 2025 15:00:08 +0100
Subject: [PATCH 1288/3587] remove search and ppe URLs (#239777)

---
 src/vs/base/common/product.ts                              | 2 --
 .../extensionManagement/common/extensionGalleryService.ts  | 7 ++-----
 2 files changed, 2 insertions(+), 7 deletions(-)

diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts
index ebd9e4381b25..52c4c943d97d 100644
--- a/src/vs/base/common/product.ts
+++ b/src/vs/base/common/product.ts
@@ -95,8 +95,6 @@ export interface IProductConfiguration {
 
 	readonly extensionsGallery?: {
 		readonly serviceUrl: string;
-		readonly servicePPEUrl?: string;
-		readonly searchUrl?: string;
 		readonly itemUrl: string;
 		readonly publisherUrl: string;
 		readonly resourceUrlTemplate: string;
diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts
index 83b0491704bf..a173fd7b5cac 100644
--- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts
+++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts
@@ -616,7 +616,6 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
 	declare readonly _serviceBrand: undefined;
 
 	private readonly extensionsGalleryUrl: string | undefined;
-	private readonly extensionsGallerySearchUrl: string | undefined;
 	private readonly extensionsControlUrl: string | undefined;
 	private readonly extensionUrlTemplate: string | undefined;
 
@@ -635,9 +634,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
 		@IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService,
 	) {
 		const config = productService.extensionsGallery;
-		const isPPEEnabled = config?.servicePPEUrl && configurationService.getValue('_extensionsGallery.enablePPE');
-		this.extensionsGalleryUrl = isPPEEnabled ? config.servicePPEUrl : config?.serviceUrl;
-		this.extensionsGallerySearchUrl = isPPEEnabled ? undefined : config?.searchUrl;
+		this.extensionsGalleryUrl = config?.serviceUrl;
 		this.extensionsControlUrl = config?.controlUrl;
 		this.extensionUrlTemplate = config?.extensionUrlTemplate;
 		this.extensionsEnabledWithApiProposalVersion = productService.extensionsEnabledWithApiProposalVersion?.map(id => id.toLowerCase()) ?? [];
@@ -1187,7 +1184,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
 		try {
 			context = await this.requestService.request({
 				type: 'POST',
-				url: this.extensionsGallerySearchUrl && query.criteria.some(c => c.filterType === FilterType.SearchText) ? this.extensionsGallerySearchUrl : this.api('/extensionquery'),
+				url: this.api('/extensionquery'),
 				data,
 				headers
 			}, token);

From 1802fa1343197a011761c7c6286d0cb458a65a6e Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Thu, 6 Feb 2025 06:18:51 -0800
Subject: [PATCH 1289/3587] Allow firstMatchCanBeWeak in terminal suggest

Fixes #236651
---
 .../suggest/browser/simpleCompletionModel.ts   |  5 ++++-
 .../test/browser/simpleCompletionModel.test.ts | 18 ++++++++++++++++++
 2 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts
index eff68db51be1..13245b800383 100644
--- a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts
+++ b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts
@@ -31,7 +31,10 @@ export class SimpleCompletionModel {
 	private _stats?: ISimpleCompletionStats;
 	private _filteredItems?: SimpleCompletionItem[];
 	private _refilterKind: Refilter = Refilter.All;
-	private _fuzzyScoreOptions: FuzzyScoreOptions | undefined = FuzzyScoreOptions.default;
+	private _fuzzyScoreOptions: FuzzyScoreOptions | undefined = {
+		...FuzzyScoreOptions.default,
+		firstMatchCanBeWeak: true
+	};
 
 	// TODO: Pass in options
 	private _options: {
diff --git a/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts b/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts
index 216f8c91b78a..943f75e5ae06 100644
--- a/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts
+++ b/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts
@@ -77,6 +77,24 @@ suite('SimpleCompletionModel', function () {
 		assert.strictEqual(model.items[2].completion.label, 'z');
 	});
 
+	test('fuzzy matching', () => {
+		const initial = [
+			'.\\.eslintrc',
+			'.\\resources\\',
+			'.\\scripts\\',
+			'.\\src\\',
+		];
+		const expected = [
+			'.\\scripts\\',
+			'.\\src\\',
+			'.\\.eslintrc',
+			'.\\resources\\',
+		];
+		model = new SimpleCompletionModel(initial.map(e => (createItem({ label: e }))), new LineContext('s', 0));
+
+		assertItems(model, expected);
+	});
+
 	suite('files and folders', () => {
 		test('should deprioritize files that start with underscore', function () {
 			const initial = ['_a', 'a', 'z'];

From 922615def693ff52c632c9648eaedb4aad3f07e7 Mon Sep 17 00:00:00 2001
From: Sandeep Somavarapu 
Date: Thu, 6 Feb 2025 15:26:10 +0100
Subject: [PATCH 1290/3587] checking .vsix file extension should be case
 insensitive (#239804)

---
 .../sharedProcess/contrib/defaultExtensionsInitializer.ts       | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/vs/code/electron-utility/sharedProcess/contrib/defaultExtensionsInitializer.ts b/src/vs/code/electron-utility/sharedProcess/contrib/defaultExtensionsInitializer.ts
index 69f0511553e3..739c184a1072 100644
--- a/src/vs/code/electron-utility/sharedProcess/contrib/defaultExtensionsInitializer.ts
+++ b/src/vs/code/electron-utility/sharedProcess/contrib/defaultExtensionsInitializer.ts
@@ -50,7 +50,7 @@ export class DefaultExtensionsInitializer extends Disposable {
 			return;
 		}
 
-		const vsixs = stat.children.filter(child => child.name.endsWith('.vsix'));
+		const vsixs = stat.children.filter(child => child.name.toLowerCase().endsWith('.vsix'));
 		if (vsixs.length === 0) {
 			this.logService.debug('There are no default extensions to initialize', extensionsLocation.toString());
 			return;

From b02f84ecaca3652e7a55a2200650f94209f863da Mon Sep 17 00:00:00 2001
From: Sandeep Somavarapu 
Date: Thu, 6 Feb 2025 15:38:32 +0100
Subject: [PATCH 1291/3587] update distro (#239805)

---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 2cd4edb0bbcf..fb24916dfad3 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "code-oss-dev",
   "version": "1.98.0",
-  "distro": "4e8c47ac95d95aa744f78305db0f163dcd407126",
+  "distro": "41de7f370f100ee0374fe780e73a995803da6a9c",
   "author": {
     "name": "Microsoft Corporation"
   },
@@ -237,4 +237,4 @@
   "optionalDependencies": {
     "windows-foreground-love": "0.5.0"
   }
-}
+}
\ No newline at end of file

From e092e2cafbdf38c3e36001470dc39212424b12d4 Mon Sep 17 00:00:00 2001
From: Sandeep Somavarapu 
Date: Thu, 6 Feb 2025 15:41:36 +0100
Subject: [PATCH 1292/3587] report version with cdn fallback event (#239807)

---
 .../common/extensionGalleryService.ts         | 20 ++++++++++---------
 1 file changed, 11 insertions(+), 9 deletions(-)

diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts
index a173fd7b5cac..eb1126e2f130 100644
--- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts
+++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts
@@ -1338,7 +1338,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
 		} : extension.assets.download;
 
 		const headers: IHeaders | undefined = extension.queryContext?.[ACTIVITY_HEADER_NAME] ? { [ACTIVITY_HEADER_NAME]: extension.queryContext[ACTIVITY_HEADER_NAME] } : undefined;
-		const context = await this.getAsset(extension.identifier.id, downloadAsset, AssetType.VSIX, headers ? { headers } : undefined);
+		const context = await this.getAsset(extension.identifier.id, downloadAsset, AssetType.VSIX, extension.version, headers ? { headers } : undefined);
 
 		try {
 			await this.fileService.writeFile(location, context.stream);
@@ -1371,7 +1371,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
 
 		this.logService.trace('ExtensionGalleryService#downloadSignatureArchive', extension.identifier.id);
 
-		const context = await this.getAsset(extension.identifier.id, extension.assets.signature, AssetType.Signature);
+		const context = await this.getAsset(extension.identifier.id, extension.assets.signature, AssetType.Signature, extension.version);
 		try {
 			await this.fileService.writeFile(location, context.stream);
 		} catch (error) {
@@ -1388,7 +1388,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
 
 	async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise {
 		if (extension.assets.readme) {
-			const context = await this.getAsset(extension.identifier.id, extension.assets.readme, AssetType.Details, {}, token);
+			const context = await this.getAsset(extension.identifier.id, extension.assets.readme, AssetType.Details, extension.version, {}, token);
 			const content = await asTextOrError(context);
 			return content || '';
 		}
@@ -1397,7 +1397,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
 
 	async getManifest(extension: IGalleryExtension, token: CancellationToken): Promise {
 		if (extension.assets.manifest) {
-			const context = await this.getAsset(extension.identifier.id, extension.assets.manifest, AssetType.Manifest, {}, token);
+			const context = await this.getAsset(extension.identifier.id, extension.assets.manifest, AssetType.Manifest, extension.version, {}, token);
 			const text = await asTextOrError(context);
 			return text ? JSON.parse(text) : null;
 		}
@@ -1410,14 +1410,14 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
 			throw new Error('Manifest was not found');
 		}
 		const headers = { 'Accept-Encoding': 'gzip' };
-		const context = await this.getAsset(extension, manifestAsset, AssetType.Manifest, { headers });
+		const context = await this.getAsset(extension, manifestAsset, AssetType.Manifest, rawExtensionVersion.version, { headers });
 		return await asJson(context);
 	}
 
 	async getCoreTranslation(extension: IGalleryExtension, languageId: string): Promise {
 		const asset = extension.assets.coreTranslations.filter(t => t[0] === languageId.toUpperCase())[0];
 		if (asset) {
-			const context = await this.getAsset(extension.identifier.id, asset[1], asset[0]);
+			const context = await this.getAsset(extension.identifier.id, asset[1], asset[0], extension.version);
 			const text = await asTextOrError(context);
 			return text ? JSON.parse(text) : null;
 		}
@@ -1426,7 +1426,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
 
 	async getChangelog(extension: IGalleryExtension, token: CancellationToken): Promise {
 		if (extension.assets.changelog) {
-			const context = await this.getAsset(extension.identifier.id, extension.assets.changelog, AssetType.Changelog, {}, token);
+			const context = await this.getAsset(extension.identifier.id, extension.assets.changelog, AssetType.Changelog, extension.version, {}, token);
 			const content = await asTextOrError(context);
 			return content || '';
 		}
@@ -1489,7 +1489,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
 		return result;
 	}
 
-	private async getAsset(extension: string, asset: IGalleryExtensionAsset, assetType: string, options: IRequestOptions = {}, token: CancellationToken = CancellationToken.None): Promise {
+	private async getAsset(extension: string, asset: IGalleryExtensionAsset, assetType: string, version: string, options: IRequestOptions = {}, token: CancellationToken = CancellationToken.None): Promise {
 		const commonHeaders = await this.commonHeadersPromise;
 		const baseOptions = { type: 'GET' };
 		const headers = { ...commonHeaders, ...(options.headers || {}) };
@@ -1518,13 +1518,15 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
 				extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension name' };
 				assetType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'asset that failed' };
 				message: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error message' };
+				version: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'version' };
 			};
 			type GalleryServiceCDNFallbackEvent = {
 				extension: string;
 				assetType: string;
 				message: string;
+				version: string;
 			};
-			this.telemetryService.publicLog2('galleryService:cdnFallback', { extension, assetType, message });
+			this.telemetryService.publicLog2('galleryService:cdnFallback', { extension, assetType, message, version });
 
 			const fallbackOptions = { ...options, url: fallbackUrl };
 			return this.requestService.request(fallbackOptions, token);

From 12d6f33eb38292e8bf4a6bb45e1cc1047429d647 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Thu, 6 Feb 2025 07:12:04 -0800
Subject: [PATCH 1293/3587] Move terminal bits out of simpleCompletionModel

Part of #238986
---
 .../browser/terminalCompletionModel.ts        | 117 ++++++++++++++++++
 .../suggest/browser/terminalSuggestAddon.ts   |  13 +-
 .../browser/terminalCompletionModel.test.ts}  |  29 ++---
 .../suggest/browser/simpleCompletionItem.ts   |   2 +
 .../suggest/browser/simpleCompletionModel.ts  | 111 +----------------
 5 files changed, 145 insertions(+), 127 deletions(-)
 create mode 100644 src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts
 rename src/vs/workbench/{services/suggest/test/browser/simpleCompletionModel.test.ts => contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts} (84%)

diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts
new file mode 100644
index 000000000000..6addd673260c
--- /dev/null
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts
@@ -0,0 +1,117 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { isWindows } from '../../../../../base/common/platform.js';
+import { count } from '../../../../../base/common/strings.js';
+import type { SimpleCompletionItem } from '../../../../services/suggest/browser/simpleCompletionItem.js';
+import { SimpleCompletionModel, type LineContext } from '../../../../services/suggest/browser/simpleCompletionModel.js';
+
+export class TerminalCompletionModel extends SimpleCompletionModel {
+	constructor(
+		items: SimpleCompletionItem[],
+		lineContext: LineContext
+	) {
+		super(items, lineContext, compareCompletionsFn);
+	}
+}
+
+const compareCompletionsFn = (leadingLineContent: string, a: SimpleCompletionItem, b: SimpleCompletionItem) => {
+	// Sort by the score
+	let score = b.score[0] - a.score[0];
+	if (score !== 0) {
+		return score;
+	}
+
+	// Sort by underscore penalty (eg. `__init__/` should be penalized)
+	if (a.underscorePenalty !== b.underscorePenalty) {
+		return a.underscorePenalty - b.underscorePenalty;
+	}
+
+	// Sort files of the same name by extension
+	const isArg = leadingLineContent.includes(' ');
+	if (!isArg && a.labelLowExcludeFileExt === b.labelLowExcludeFileExt) {
+		// Then by label length ascending (excluding file extension if it's a file)
+		score = a.labelLowExcludeFileExt.length - b.labelLowExcludeFileExt.length;
+		if (score !== 0) {
+			return score;
+		}
+		// If they're files at the start of the command line, boost extensions depending on the operating system
+		score = fileExtScore(b.fileExtLow) - fileExtScore(a.fileExtLow);
+		if (score !== 0) {
+			return score;
+		}
+		// Then by file extension length ascending
+		score = a.fileExtLow.length - b.fileExtLow.length;
+		if (score !== 0) {
+			return score;
+		}
+	}
+
+	// Sort by folder depth (eg. `vscode/` should come before `vscode-.../`)
+	if (a.labelLowNormalizedPath && b.labelLowNormalizedPath) {
+		// Directories
+		// Count depth of path (number of / or \ occurrences)
+		score = count(a.labelLowNormalizedPath, '/') - count(b.labelLowNormalizedPath, '/');
+		if (score !== 0) {
+			return score;
+		}
+
+		// Ensure shorter prefixes appear first
+		if (b.labelLowNormalizedPath.startsWith(a.labelLowNormalizedPath)) {
+			return -1; // `a` is a prefix of `b`, so `a` should come first
+		}
+		if (a.labelLowNormalizedPath.startsWith(b.labelLowNormalizedPath)) {
+			return 1; // `b` is a prefix of `a`, so `b` should come first
+		}
+	}
+
+	// Sort alphabetically, ignoring punctuation causes dot files to be mixed in rather than
+	// all at the top
+	return a.labelLow.localeCompare(b.labelLow, undefined, { ignorePunctuation: true });
+};
+
+// TODO: This should be based on the process OS, not the local OS
+// File score boosts for specific file extensions on Windows. This only applies when the file is the
+// _first_ part of the command line.
+const fileExtScores = new Map(isWindows ? [
+	// Windows - .ps1 > .exe > .bat > .cmd. This is the command precedence when running the files
+	//           without an extension, tested manually in pwsh v7.4.4
+	['ps1', 0.09],
+	['exe', 0.08],
+	['bat', 0.07],
+	['cmd', 0.07],
+	['msi', 0.06],
+	['com', 0.06],
+	// Non-Windows
+	['sh', -0.05],
+	['bash', -0.05],
+	['zsh', -0.05],
+	['fish', -0.05],
+	['csh', -0.06], // C shell
+	['ksh', -0.06], // Korn shell
+	// Scripting language files are excluded here as the standard behavior on Windows will just open
+	// the file in a text editor, not run the file
+] : [
+	// Pwsh
+	['ps1', 0.05],
+	// Windows
+	['bat', -0.05],
+	['cmd', -0.05],
+	['exe', -0.05],
+	// Non-Windows
+	['sh', 0.05],
+	['bash', 0.05],
+	['zsh', 0.05],
+	['fish', 0.05],
+	['csh', 0.04], // C shell
+	['ksh', 0.04], // Korn shell
+	// Scripting languages
+	['py', 0.05], // Python
+	['pl', 0.05], // Perl
+]);
+
+function fileExtScore(ext: string): number {
+	return fileExtScores.get(ext) || 0;
+}
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
index 9c93bbc13fd3..6c9201e97280 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
@@ -23,7 +23,7 @@ import type { IXtermCore } from '../../../terminal/browser/xterm-private.js';
 import { TerminalStorageKeys } from '../../../terminal/common/terminalStorageKeys.js';
 import { terminalSuggestConfigSection, TerminalSuggestSettingId, type ITerminalSuggestConfiguration } from '../common/terminalSuggestConfiguration.js';
 import { SimpleCompletionItem } from '../../../../services/suggest/browser/simpleCompletionItem.js';
-import { LineContext, SimpleCompletionModel } from '../../../../services/suggest/browser/simpleCompletionModel.js';
+import { LineContext } from '../../../../services/suggest/browser/simpleCompletionModel.js';
 import { ISimpleSelectedSuggestion, SimpleSuggestWidget } from '../../../../services/suggest/browser/simpleSuggestWidget.js';
 import { ITerminalCompletionService, TerminalCompletionItemKind } from './terminalCompletionService.js';
 import { TerminalSettingId, TerminalShellType } from '../../../../../platform/terminal/common/terminal.js';
@@ -34,6 +34,7 @@ import { MenuId } from '../../../../../platform/actions/common/actions.js';
 import { ISimpleSuggestWidgetFontInfo } from '../../../../services/suggest/browser/simpleSuggestWidgetRenderer.js';
 import { ITerminalConfigurationService } from '../../../terminal/browser/terminal.js';
 import { GOLDEN_LINE_HEIGHT_RATIO, MINIMUM_LINE_HEIGHT } from '../../../../../editor/common/config/fontInfo.js';
+import { TerminalCompletionModel } from './terminalCompletionModel.js';
 
 export interface ISuggestController {
 	isPasting: boolean;
@@ -52,7 +53,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 
 	private _mostRecentPromptInputState?: IPromptInputModelState;
 	private _currentPromptInputState?: IPromptInputModelState;
-	private _model?: SimpleCompletionModel;
+	private _model?: TerminalCompletionModel;
 
 	private _container?: HTMLElement;
 	private _screen?: HTMLElement;
@@ -215,7 +216,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 			}
 		}
 		const lineContext = new LineContext(normalizedLeadingLineContent, this._cursorIndexDelta);
-		const model = new SimpleCompletionModel(completions.filter(c => !!c.label).map(c => new SimpleCompletionItem(c)), lineContext);
+		const model = new TerminalCompletionModel(
+			completions.filter(c => !!c.label).map(c => new SimpleCompletionItem(c)),
+			lineContext
+		);
 		if (token.isCancellationRequested) {
 			return;
 		}
@@ -415,7 +419,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 		return fontInfo;
 	}
 
-	private _showCompletions(model: SimpleCompletionModel, explicitlyInvoked?: boolean): void {
+	private _showCompletions(model: TerminalCompletionModel, explicitlyInvoked?: boolean): void {
 		if (!this._terminal?.element) {
 			return;
 		}
@@ -636,6 +640,7 @@ class PersistedWidgetSize {
 		this._storageService.remove(this._key, StorageScope.PROFILE);
 	}
 }
+
 export function normalizePathSeparator(path: string, sep: string): string {
 	if (sep === '/') {
 		return path.replaceAll('\\', '/');
diff --git a/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts
similarity index 84%
rename from src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts
rename to src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts
index 216f8c91b78a..2da7ff1fac99 100644
--- a/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts
@@ -3,9 +3,10 @@
  *  Licensed under the MIT License. See License.txt in the project root for license information.
  *--------------------------------------------------------------------------------------------*/
 import assert from 'assert';
-import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
-import { LineContext, SimpleCompletionModel } from '../../browser/simpleCompletionModel.js';
-import { SimpleCompletionItem, type ISimpleCompletion } from '../../browser/simpleCompletionItem.js';
+import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
+import { SimpleCompletionItem, type ISimpleCompletion } from '../../../../../services/suggest/browser/simpleCompletionItem.js';
+import { TerminalCompletionModel } from '../../browser/terminalCompletionModel.js';
+import { LineContext } from '../../../../../services/suggest/browser/simpleCompletionModel.js';
 
 function createItem(options: Partial): SimpleCompletionItem {
 	return new SimpleCompletionItem({
@@ -21,8 +22,8 @@ function createFileItems(...labels: string[]): SimpleCompletionItem[] {
 	return labels.map(label => createItem({ label, isFile: true }));
 }
 
-function createFileItemsModel(...labels: string[]): SimpleCompletionModel {
-	return new SimpleCompletionModel(
+function createFileItemsModel(...labels: string[]): TerminalCompletionModel {
+	return new TerminalCompletionModel(
 		createFileItems(...labels),
 		new LineContext('', 0)
 	);
@@ -32,31 +33,31 @@ function createFolderItems(...labels: string[]): SimpleCompletionItem[] {
 	return labels.map(label => createItem({ label, isDirectory: true }));
 }
 
-function createFolderItemsModel(...labels: string[]): SimpleCompletionModel {
-	return new SimpleCompletionModel(
+function createFolderItemsModel(...labels: string[]): TerminalCompletionModel {
+	return new TerminalCompletionModel(
 		createFolderItems(...labels),
 		new LineContext('', 0)
 	);
 }
 
-function assertItems(model: SimpleCompletionModel, labels: string[]): void {
+function assertItems(model: TerminalCompletionModel, labels: string[]): void {
 	assert.deepStrictEqual(model.items.map(i => i.completion.label), labels);
 	assert.strictEqual(model.items.length, labels.length); // sanity check
 }
 
-suite('SimpleCompletionModel', function () {
+suite('TerminalCompletionModel', function () {
 	ensureNoDisposablesAreLeakedInTestSuite();
 
-	let model: SimpleCompletionModel;
+	let model: TerminalCompletionModel;
 
 	test('should handle an empty list', function () {
-		model = new SimpleCompletionModel([], new LineContext('', 0));
+		model = new TerminalCompletionModel([], new LineContext('', 0));
 
 		assert.strictEqual(model.items.length, 0);
 	});
 
 	test('should handle a list with one item', function () {
-		model = new SimpleCompletionModel([
+		model = new TerminalCompletionModel([
 			createItem({ label: 'a' }),
 		], new LineContext('', 0));
 
@@ -65,7 +66,7 @@ suite('SimpleCompletionModel', function () {
 	});
 
 	test('should sort alphabetically', function () {
-		model = new SimpleCompletionModel([
+		model = new TerminalCompletionModel([
 			createItem({ label: 'b' }),
 			createItem({ label: 'z' }),
 			createItem({ label: 'a' }),
@@ -146,7 +147,7 @@ suite('SimpleCompletionModel', function () {
 					'tsfmt.json',
 				)
 			];
-			const model = new SimpleCompletionModel(items, new LineContext('', 0));
+			const model = new TerminalCompletionModel(items, new LineContext('', 0));
 			assertItems(model, [
 				'.build',
 				'build',
diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts
index d3590fbf873c..4dcc8713e043 100644
--- a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts
+++ b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts
@@ -37,10 +37,12 @@ export interface ISimpleCompletion {
 	 * first by extension length and then certain extensions will get a boost based on the OS.
 	 */
 	isFile?: boolean;
+
 	/**
 	 * Whether the completion is a directory.
 	 */
 	isDirectory?: boolean;
+
 	/**
 	 * Whether the completion is a keyword.
 	 */
diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts
index eff68db51be1..83d2298a1e74 100644
--- a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts
+++ b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts
@@ -7,8 +7,6 @@ import { SimpleCompletionItem } from './simpleCompletionItem.js';
 import { quickSelect } from '../../../../base/common/arrays.js';
 import { CharCode } from '../../../../base/common/charCode.js';
 import { FuzzyScore, fuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScoreOptions, FuzzyScorer } from '../../../../base/common/filters.js';
-import { isWindows } from '../../../../base/common/platform.js';
-import { count } from '../../../../base/common/strings.js';
 
 export interface ISimpleCompletionStats {
 	pLabelLen: number;
@@ -41,6 +39,7 @@ export class SimpleCompletionModel {
 	constructor(
 		private readonly _items: SimpleCompletionItem[],
 		private _lineContext: LineContext,
+		private readonly _rawCompareFn?: (leadingLineContent: string, a: SimpleCompletionItem, b: SimpleCompletionItem) => number,
 	) {
 	}
 
@@ -179,69 +178,7 @@ export class SimpleCompletionModel {
 			labelLengths.push(item.completion.label.length);
 		}
 
-		this._filteredItems = target.sort((a, b) => {
-			// Keywords should always appear at the bottom when they are not an exact match
-			let score = 0;
-			if (a.completion.isKeyword && a.labelLow !== wordLow || b.completion.isKeyword && b.labelLow !== wordLow) {
-				score = (a.completion.isKeyword ? 1 : 0) - (b.completion.isKeyword ? 1 : 0);
-				if (score !== 0) {
-					return score;
-				}
-			}
-
-			// Sort by the score
-			score = b.score[0] - a.score[0];
-			if (score !== 0) {
-				return score;
-			}
-
-			// Sort by underscore penalty (eg. `__init__/` should be penalized)
-			if (a.underscorePenalty !== b.underscorePenalty) {
-				return a.underscorePenalty - b.underscorePenalty;
-			}
-
-			// Sort files of the same name by extension
-			const isArg = leadingLineContent.includes(' ');
-			if (!isArg && a.labelLowExcludeFileExt === b.labelLowExcludeFileExt) {
-				// Then by label length ascending (excluding file extension if it's a file)
-				score = a.labelLowExcludeFileExt.length - b.labelLowExcludeFileExt.length;
-				if (score !== 0) {
-					return score;
-				}
-				// If they're files at the start of the command line, boost extensions depending on the operating system
-				score = fileExtScore(b.fileExtLow) - fileExtScore(a.fileExtLow);
-				if (score !== 0) {
-					return score;
-				}
-				// Then by file extension length ascending
-				score = a.fileExtLow.length - b.fileExtLow.length;
-				if (score !== 0) {
-					return score;
-				}
-			}
-
-			// Sort by folder depth (eg. `vscode/` should come before `vscode-.../`)
-			if (a.labelLowNormalizedPath && b.labelLowNormalizedPath) {
-				// Directories
-				// Count depth of path (number of / or \ occurrences)
-				score = count(a.labelLowNormalizedPath, '/') - count(b.labelLowNormalizedPath, '/');
-				if (score !== 0) {
-					return score;
-				}
-
-				// Ensure shorter prefixes appear first
-				if (b.labelLowNormalizedPath.startsWith(a.labelLowNormalizedPath)) {
-					return -1; // `a` is a prefix of `b`, so `a` should come first
-				}
-				if (a.labelLowNormalizedPath.startsWith(b.labelLowNormalizedPath)) {
-					return 1; // `b` is a prefix of `a`, so `b` should come first
-				}
-			}
-
-			// Sort alphabetically, ignoring punctuation causes dot files to be mixed in rather than
-			// all at the top
-			return a.labelLow.localeCompare(b.labelLow, undefined, { ignorePunctuation: true });
-		});
+		this._filteredItems = target.sort(this._rawCompareFn?.bind(undefined, leadingLineContent));
 		this._refilterKind = Refilter.Nothing;
 
 		this._stats = {
@@ -251,47 +188,3 @@ export class SimpleCompletionModel {
 		};
 	}
 }
-
-// TODO: This should be based on the process OS, not the local OS
-// File score boosts for specific file extensions on Windows. This only applies when the file is the
-// _first_ part of the command line.
-const fileExtScores = new Map(isWindows ? [
-	// Windows - .ps1 > .exe > .bat > .cmd. This is the command precedence when running the files
-	//           without an extension, tested manually in pwsh v7.4.4
-	['ps1', 0.09],
-	['exe', 0.08],
-	['bat', 0.07],
-	['cmd', 0.07],
-	['msi', 0.06],
-	['com', 0.06],
-	// Non-Windows
-	['sh', -0.05],
-	['bash', -0.05],
-	['zsh', -0.05],
-	['fish', -0.05],
-	['csh', -0.06], // C shell
-	['ksh', -0.06], // Korn shell
-	// Scripting language files are excluded here as the standard behavior on Windows will just open
-	// the file in a text editor, not run the file
-] : [
-	// Pwsh
-	['ps1', 0.05],
-	// Windows
-	['bat', -0.05],
-	['cmd', -0.05],
-	['exe', -0.05],
-	// Non-Windows
-	['sh', 0.05],
-	['bash', 0.05],
-	['zsh', 0.05],
-	['fish', 0.05],
-	['csh', 0.04], // C shell
-	['ksh', 0.04], // Korn shell
-	// Scripting languages
-	['py', 0.05], // Python
-	['pl', 0.05], // Perl
-]);
-
-function fileExtScore(ext: string): number {
-	return fileExtScores.get(ext) || 0;
-}

From 3d64ea2d8d76f57d9263280f1d2144b42c67f24b Mon Sep 17 00:00:00 2001
From: Benjamin Christopher Simmonds
 <44439583+benibenj@users.noreply.github.com>
Date: Thu, 6 Feb 2025 16:27:34 +0100
Subject: [PATCH 1294/3587] Fix leaks in hover, timeline and menubar  (#239808)

fix leaks
---
 .../services/hoverService/hoverWidget.ts       |  9 +++++----
 .../browser/parts/titlebar/menubarControl.ts   | 18 ++++++++++++------
 .../contrib/timeline/browser/timelinePane.ts   |  4 ++++
 3 files changed, 21 insertions(+), 10 deletions(-)

diff --git a/src/vs/editor/browser/services/hoverService/hoverWidget.ts b/src/vs/editor/browser/services/hoverService/hoverWidget.ts
index 15bda1211ac9..c33296a34e65 100644
--- a/src/vs/editor/browser/services/hoverService/hoverWidget.ts
+++ b/src/vs/editor/browser/services/hoverService/hoverWidget.ts
@@ -4,7 +4,7 @@
  *--------------------------------------------------------------------------------------------*/
 
 import './hover.css';
-import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
+import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
 import { Event, Emitter } from '../../../../base/common/event.js';
 import * as dom from '../../../../base/browser/dom.js';
 import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
@@ -165,7 +165,7 @@ export class HoverWidget extends Widget implements IHoverWidget {
 				{ codeBlockFontFamily: this._configurationService.getValue('editor').fontFamily || EDITOR_FONT_DEFAULTS.fontFamily }
 			);
 
-			const { element } = mdRenderer.render(markdown, {
+			const { element, dispose } = mdRenderer.render(markdown, {
 				actionHandler: {
 					callback: (content) => this._linkHandler(content),
 					disposables: this._messageListeners
@@ -178,6 +178,7 @@ export class HoverWidget extends Widget implements IHoverWidget {
 				}
 			});
 			contentsElement.appendChild(element);
+			this._register(toDisposable(dispose));
 		}
 		rowElement.appendChild(contentsElement);
 		this._hover.contentsDomNode.appendChild(rowElement);
@@ -188,7 +189,7 @@ export class HoverWidget extends Widget implements IHoverWidget {
 			options.actions.forEach(action => {
 				const keybinding = this._keybindingService.lookupKeybinding(action.commandId);
 				const keybindingLabel = keybinding ? keybinding.getLabel() : null;
-				HoverAction.render(actionsElement, {
+				this._register(HoverAction.render(actionsElement, {
 					label: action.label,
 					commandId: action.commandId,
 					run: e => {
@@ -196,7 +197,7 @@ export class HoverWidget extends Widget implements IHoverWidget {
 						this.dispose();
 					},
 					iconClass: action.iconClass
-				}, keybindingLabel);
+				}, keybindingLabel));
 			});
 			statusBarElement.appendChild(actionsElement);
 			this._hover.containerDomNode.appendChild(statusBarElement);
diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts
index 50e683186508..142f360c1dd9 100644
--- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts
+++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts
@@ -565,6 +565,7 @@ export class CustomMenubarControl extends MenubarControl {
 	}
 
 	private readonly reinstallDisposables = this._register(new DisposableStore());
+	private readonly updateActionsDisposables = this._register(new DisposableStore());
 	private setupCustomMenubar(firstTime: boolean): void {
 		// If there is no container, we cannot setup the menubar
 		if (!this.container) {
@@ -620,7 +621,7 @@ export class CustomMenubarControl extends MenubarControl {
 		}
 
 		// Update the menu actions
-		const updateActions = (menuActions: readonly IAction[], target: IAction[], topLevelTitle: string) => {
+		const updateActions = (menuActions: readonly IAction[], target: IAction[], topLevelTitle: string, store: DisposableStore) => {
 			target.splice(0);
 
 			for (const menuItem of menuActions) {
@@ -636,7 +637,7 @@ export class CustomMenubarControl extends MenubarControl {
 
 					if (menuItem instanceof SubmenuItemAction) {
 						const submenuActions: SubmenuAction[] = [];
-						updateActions(menuItem.actions, submenuActions, topLevelTitle);
+						updateActions(menuItem.actions, submenuActions, topLevelTitle, store);
 
 						if (submenuActions.length > 0) {
 							target.push(new SubmenuAction(menuItem.id, mnemonicMenuLabel(title), submenuActions));
@@ -646,7 +647,7 @@ export class CustomMenubarControl extends MenubarControl {
 							title = menuItem.item.toggled.mnemonicTitle ?? menuItem.item.toggled.title ?? title;
 						}
 
-						const newAction = new Action(menuItem.id, mnemonicMenuLabel(title), menuItem.class, menuItem.enabled, () => this.commandService.executeCommand(menuItem.id));
+						const newAction = store.add(new Action(menuItem.id, mnemonicMenuLabel(title), menuItem.class, menuItem.enabled, () => this.commandService.executeCommand(menuItem.id)));
 						newAction.tooltip = menuItem.tooltip;
 						newAction.checked = menuItem.checked;
 						target.push(newAction);
@@ -667,20 +668,24 @@ export class CustomMenubarControl extends MenubarControl {
 		for (const title of Object.keys(this.topLevelTitles)) {
 			const menu = this.menus[title];
 			if (firstTime && menu) {
+				const menuChangedDisposable = this.reinstallDisposables.add(new DisposableStore());
 				this.reinstallDisposables.add(menu.onDidChange(() => {
 					if (!this.focusInsideMenubar) {
 						const actions: IAction[] = [];
-						updateActions(this.toActionsArray(menu), actions, title);
+						menuChangedDisposable.clear();
+						updateActions(this.toActionsArray(menu), actions, title, menuChangedDisposable);
 						this.menubar?.updateMenu({ actions, label: mnemonicMenuLabel(this.topLevelTitles[title]) });
 					}
 				}));
 
 				// For the file menu, we need to update if the web nav menu updates as well
 				if (menu === this.menus.File) {
+					const webMenuChangedDisposable = this.reinstallDisposables.add(new DisposableStore());
 					this.reinstallDisposables.add(this.webNavigationMenu.onDidChange(() => {
 						if (!this.focusInsideMenubar) {
 							const actions: IAction[] = [];
-							updateActions(this.toActionsArray(menu), actions, title);
+							webMenuChangedDisposable.clear();
+							updateActions(this.toActionsArray(menu), actions, title, webMenuChangedDisposable);
 							this.menubar?.updateMenu({ actions, label: mnemonicMenuLabel(this.topLevelTitles[title]) });
 						}
 					}));
@@ -689,7 +694,8 @@ export class CustomMenubarControl extends MenubarControl {
 
 			const actions: IAction[] = [];
 			if (menu) {
-				updateActions(this.toActionsArray(menu), actions, title);
+				this.updateActionsDisposables.clear();
+				updateActions(this.toActionsArray(menu), actions, title, this.updateActionsDisposables);
 			}
 
 			if (this.menubar) {
diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts
index 7e18e20482c2..810f79817671 100644
--- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts
+++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts
@@ -1234,6 +1234,10 @@ class TimelineTreeRenderer implements ITreeRenderer, index: number, templateData: TimelineElementTemplate, height: number | undefined): void {
+		templateData.actionBar.actionRunner.dispose();
+	}
+
 	disposeTemplate(template: TimelineElementTemplate): void {
 		template.dispose();
 	}

From 4b6515c99547d5d6e23a1e2dfe8e59125f219831 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Thu, 6 Feb 2025 07:36:00 -0800
Subject: [PATCH 1295/3587] Move terminal parts out of simple suggest item

---
 .../browser/pwshCompletionProviderAddon.ts    |  3 +-
 .../browser/terminalCompletionModel.ts        | 94 ++++++++++++++++++-
 .../browser/terminalCompletionService.ts      | 16 +---
 .../suggest/browser/terminalSuggestAddon.ts   | 17 ++--
 .../browser/terminalCompletionModel.test.ts   | 11 +--
 .../browser/terminalCompletionService.test.ts |  3 +-
 .../suggest/browser/simpleCompletionItem.ts   | 68 +-------------
 .../suggest/browser/simpleCompletionModel.ts  | 12 +--
 .../suggest/browser/simpleSuggestWidget.ts    | 42 ++++-----
 9 files changed, 139 insertions(+), 127 deletions(-)

diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts
index 53a0eb48bccc..b0a0f6964946 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts
@@ -3,7 +3,7 @@
  *  Licensed under the MIT License. See License.txt in the project root for license information.
  *--------------------------------------------------------------------------------------------*/
 
-import { ITerminalCompletion, ITerminalCompletionProvider } from './terminalCompletionService.js';
+import { ITerminalCompletionProvider } from './terminalCompletionService.js';
 import { Disposable } from '../../../../../base/common/lifecycle.js';
 import type { ITerminalAddon, Terminal } from '@xterm/xterm';
 import { Event, Emitter } from '../../../../../base/common/event.js';
@@ -21,6 +21,7 @@ import { ITerminalCapabilityStore, TerminalCapability } from '../../../../../pla
 import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
 import { DeferredPromise } from '../../../../../base/common/async.js';
 import { CancellationToken } from '../../../../../base/common/cancellation.js';
+import type { ITerminalCompletion } from './terminalCompletionModel.js';
 
 export const enum VSCodeSuggestOscPt {
 	Completions = 'Completions',
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts
index 6addd673260c..6fe9370beaf2 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts
@@ -3,21 +3,22 @@
  *  Licensed under the MIT License. See License.txt in the project root for license information.
  *--------------------------------------------------------------------------------------------*/
 
+import { basename } from '../../../../../base/common/path.js';
 import { isWindows } from '../../../../../base/common/platform.js';
 import { count } from '../../../../../base/common/strings.js';
-import type { SimpleCompletionItem } from '../../../../services/suggest/browser/simpleCompletionItem.js';
+import { ISimpleCompletion, SimpleCompletionItem } from '../../../../services/suggest/browser/simpleCompletionItem.js';
 import { SimpleCompletionModel, type LineContext } from '../../../../services/suggest/browser/simpleCompletionModel.js';
 
-export class TerminalCompletionModel extends SimpleCompletionModel {
+export class TerminalCompletionModel extends SimpleCompletionModel {
 	constructor(
-		items: SimpleCompletionItem[],
+		items: TerminalCompletionItem[],
 		lineContext: LineContext
 	) {
 		super(items, lineContext, compareCompletionsFn);
 	}
 }
 
-const compareCompletionsFn = (leadingLineContent: string, a: SimpleCompletionItem, b: SimpleCompletionItem) => {
+const compareCompletionsFn = (leadingLineContent: string, a: TerminalCompletionItem, b: TerminalCompletionItem) => {
 	// Sort by the score
 	let score = b.score[0] - a.score[0];
 	if (score !== 0) {
@@ -115,3 +116,88 @@ const fileExtScores = new Map(isWindows ? [
 function fileExtScore(ext: string): number {
 	return fileExtScores.get(ext) || 0;
 }
+
+export enum TerminalCompletionItemKind {
+	File = 0,
+	Folder = 1,
+	Flag = 2,
+	Method = 3,
+	Argument = 4,
+	Alias = 5,
+}
+
+export interface ITerminalCompletion extends ISimpleCompletion {
+
+	kind?: TerminalCompletionItemKind;
+
+	/**
+	 * Whether the completion is a file. Files with the same score will be sorted against each other
+	 * first by extension length and then certain extensions will get a boost based on the OS.
+	 */
+	isFile?: boolean;
+
+	/**
+	 * Whether the completion is a directory.
+	 */
+	isDirectory?: boolean;
+
+	/**
+	 * Whether the completion is a keyword.
+	 */
+	isKeyword?: boolean;
+}
+
+export class TerminalCompletionItem extends SimpleCompletionItem {
+	/**
+	 * {@link labelLow} without the file extension.
+	 */
+	readonly labelLowExcludeFileExt: string;
+
+	/**
+	 * The lowercase label, when the completion is a file or directory this has  normalized path
+	 * separators (/) on Windows and no trailing separator for directories.
+	 */
+	readonly labelLowNormalizedPath: string;
+
+	/**
+	 * A penalty that applies to files or folders starting with the underscore character.
+	 */
+	readonly underscorePenalty: 0 | 1 = 0;
+
+	/**
+	 * The file extension part from {@link labelLow}.
+	 */
+	readonly fileExtLow: string = '';
+
+	constructor(
+		override readonly completion: ITerminalCompletion
+	) {
+		super(completion);
+
+		// ensure lower-variants (perf)
+		this.labelLowExcludeFileExt = this.labelLow;
+		this.labelLowNormalizedPath = this.labelLow;
+
+		if (completion.isFile) {
+			if (isWindows) {
+				this.labelLow = this.labelLow.replaceAll('/', '\\');
+			}
+			// Don't include dotfiles as extensions when sorting
+			const extIndex = this.labelLow.lastIndexOf('.');
+			if (extIndex > 0) {
+				this.labelLowExcludeFileExt = this.labelLow.substring(0, extIndex);
+				this.fileExtLow = this.labelLow.substring(extIndex + 1);
+			}
+		}
+
+		if (completion.isFile || completion.isDirectory) {
+			if (isWindows) {
+				this.labelLowNormalizedPath = this.labelLow.replaceAll('\\', '/');
+			}
+			if (completion.isDirectory) {
+				this.labelLowNormalizedPath = this.labelLowNormalizedPath.replace(/\/$/, '');
+			}
+			this.underscorePenalty = basename(this.labelLowNormalizedPath).startsWith('_') ? 1 : 0;
+		}
+	}
+}
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts
index e91ff1ba9632..a34e537a8410 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts
@@ -13,25 +13,11 @@ import { IFileService } from '../../../../../platform/files/common/files.js';
 import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
 import { TerminalCapability, type ITerminalCapabilityStore } from '../../../../../platform/terminal/common/capabilities/capabilities.js';
 import { GeneralShellType, TerminalShellType } from '../../../../../platform/terminal/common/terminal.js';
-import { ISimpleCompletion } from '../../../../services/suggest/browser/simpleCompletionItem.js';
 import { TerminalSuggestSettingId } from '../common/terminalSuggestConfiguration.js';
+import { TerminalCompletionItemKind, type ITerminalCompletion } from './terminalCompletionModel.js';
 
 export const ITerminalCompletionService = createDecorator('terminalCompletionService');
 
-export enum TerminalCompletionItemKind {
-	File = 0,
-	Folder = 1,
-	Flag = 2,
-	Method = 3,
-	Argument = 4,
-	Alias = 5,
-}
-
-export interface ITerminalCompletion extends ISimpleCompletion {
-	kind?: TerminalCompletionItemKind;
-}
-
-
 /**
  * Represents a collection of {@link CompletionItem completion items} to be presented
  * in the editor.
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
index ae423c80e8f0..d0306357b453 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
@@ -22,10 +22,9 @@ import { activeContrastBorder } from '../../../../../platform/theme/common/color
 import type { IXtermCore } from '../../../terminal/browser/xterm-private.js';
 import { TerminalStorageKeys } from '../../../terminal/common/terminalStorageKeys.js';
 import { terminalSuggestConfigSection, TerminalSuggestSettingId, type ITerminalSuggestConfiguration } from '../common/terminalSuggestConfiguration.js';
-import { SimpleCompletionItem } from '../../../../services/suggest/browser/simpleCompletionItem.js';
 import { LineContext } from '../../../../services/suggest/browser/simpleCompletionModel.js';
 import { ISimpleSelectedSuggestion, SimpleSuggestWidget } from '../../../../services/suggest/browser/simpleSuggestWidget.js';
-import { ITerminalCompletionService, TerminalCompletionItemKind } from './terminalCompletionService.js';
+import { ITerminalCompletionService } from './terminalCompletionService.js';
 import { TerminalSettingId, TerminalShellType } from '../../../../../platform/terminal/common/terminal.js';
 import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';
 import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
@@ -34,7 +33,7 @@ import { MenuId } from '../../../../../platform/actions/common/actions.js';
 import { ISimpleSuggestWidgetFontInfo } from '../../../../services/suggest/browser/simpleSuggestWidgetRenderer.js';
 import { ITerminalConfigurationService } from '../../../terminal/browser/terminal.js';
 import { GOLDEN_LINE_HEIGHT_RATIO, MINIMUM_LINE_HEIGHT } from '../../../../../editor/common/config/fontInfo.js';
-import { TerminalCompletionModel } from './terminalCompletionModel.js';
+import { TerminalCompletionItem, TerminalCompletionItemKind, TerminalCompletionModel } from './terminalCompletionModel.js';
 import { IntervalTimer, TimeoutTimer } from '../../../../../base/common/async.js';
 
 export interface ISuggestController {
@@ -43,7 +42,7 @@ export interface ISuggestController {
 	selectPreviousPageSuggestion(): void;
 	selectNextSuggestion(): void;
 	selectNextPageSuggestion(): void;
-	acceptSelectedSuggestion(suggestion?: Pick): void;
+	acceptSelectedSuggestion(suggestion?: Pick, 'item' | 'model'>): void;
 	hideSuggestWidget(): void;
 }
 export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggestController {
@@ -58,7 +57,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 
 	private _container?: HTMLElement;
 	private _screen?: HTMLElement;
-	private _suggestWidget?: SimpleSuggestWidget;
+	private _suggestWidget?: SimpleSuggestWidget;
 	private _cachedFontInfo: ISimpleSuggestWidgetFontInfo | undefined;
 	private _enableWidget: boolean = true;
 	private _pathSeparator: string = sep;
@@ -247,7 +246,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 		}
 		const lineContext = new LineContext(normalizedLeadingLineContent, this._cursorIndexDelta);
 		const model = new TerminalCompletionModel(
-			completions.filter(c => !!c.label).map(c => new SimpleCompletionItem(c)),
+			completions.filter(c => !!c.label).map(c => new TerminalCompletionItem(c)),
 			lineContext
 		);
 		if (token.isCancellationRequested) {
@@ -481,7 +480,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 	}
 
 
-	private _ensureSuggestWidget(terminal: Terminal): SimpleSuggestWidget {
+	private _ensureSuggestWidget(terminal: Terminal): SimpleSuggestWidget {
 		if (!this._suggestWidget) {
 			this._suggestWidget = this._register(this._instantiationService.createInstance(
 				SimpleSuggestWidget,
@@ -493,7 +492,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 				},
 				this._getFontInfo.bind(this),
 				this._onDidFontConfigurationChange.event.bind(this)
-			));
+			)) as any as SimpleSuggestWidget;
 			this._suggestWidget.list.style(getListStyles({
 				listInactiveFocusBackground: editorSuggestWidgetSelectedBackground,
 				listInactiveFocusOutline: activeContrastBorder
@@ -548,7 +547,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 		this._suggestWidget?.selectNextPage();
 	}
 
-	acceptSelectedSuggestion(suggestion?: Pick, respectRunOnEnter?: boolean): void {
+	acceptSelectedSuggestion(suggestion?: Pick, 'item' | 'model'>, respectRunOnEnter?: boolean): void {
 		if (!suggestion) {
 			suggestion = this._suggestWidget?.getFocusedItem();
 		}
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts
index 96144196dcad..566764c3f2a7 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts
@@ -4,12 +4,11 @@
  *--------------------------------------------------------------------------------------------*/
 import assert from 'assert';
 import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
-import { SimpleCompletionItem, type ISimpleCompletion } from '../../../../../services/suggest/browser/simpleCompletionItem.js';
-import { TerminalCompletionModel } from '../../browser/terminalCompletionModel.js';
+import { TerminalCompletionItem, TerminalCompletionModel, type ITerminalCompletion } from '../../browser/terminalCompletionModel.js';
 import { LineContext } from '../../../../../services/suggest/browser/simpleCompletionModel.js';
 
-function createItem(options: Partial): SimpleCompletionItem {
-	return new SimpleCompletionItem({
+function createItem(options: Partial): TerminalCompletionItem {
+	return new TerminalCompletionItem({
 		...options,
 		label: options.label || 'defaultLabel',
 		provider: options.provider || 'defaultProvider',
@@ -18,7 +17,7 @@ function createItem(options: Partial): SimpleCompletionItem {
 	});
 }
 
-function createFileItems(...labels: string[]): SimpleCompletionItem[] {
+function createFileItems(...labels: string[]): TerminalCompletionItem[] {
 	return labels.map(label => createItem({ label, isFile: true }));
 }
 
@@ -29,7 +28,7 @@ function createFileItemsModel(...labels: string[]): TerminalCompletionModel {
 	);
 }
 
-function createFolderItems(...labels: string[]): SimpleCompletionItem[] {
+function createFolderItems(...labels: string[]): TerminalCompletionItem[] {
 	return labels.map(label => createItem({ label, isDirectory: true }));
 }
 
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts
index 5d540c8fd41a..76a9038c4cce 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts
@@ -5,7 +5,7 @@
 
 import { URI } from '../../../../../../base/common/uri.js';
 import { IFileService, IFileStatWithMetadata, IResolveMetadataFileOptions } from '../../../../../../platform/files/common/files.js';
-import { TerminalCompletionService, TerminalCompletionItemKind, TerminalResourceRequestConfig, ITerminalCompletion } from '../../browser/terminalCompletionService.js';
+import { TerminalCompletionService, TerminalResourceRequestConfig } from '../../browser/terminalCompletionService.js';
 import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
 import assert from 'assert';
 import { isWindows } from '../../../../../../base/common/platform.js';
@@ -16,6 +16,7 @@ import { IConfigurationService } from '../../../../../../platform/configuration/
 import { TerminalCapabilityStore } from '../../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js';
 import { ShellEnvDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/shellEnvDetectionCapability.js';
 import { TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js';
+import { ITerminalCompletion, TerminalCompletionItemKind } from '../../browser/terminalCompletionModel.js';
 
 const pathSeparator = isWindows ? '\\' : '/';
 
diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts
index 4dcc8713e043..fc16fb647a6e 100644
--- a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts
+++ b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts
@@ -5,8 +5,6 @@
 
 import { FuzzyScore } from '../../../../base/common/filters.js';
 import { MarkdownString } from '../../../../base/common/htmlContent.js';
-import { basename } from '../../../../base/common/path.js';
-import { isWindows } from '../../../../base/common/platform.js';
 import { ThemeIcon } from '../../../../base/common/themables.js';
 
 export interface ISimpleCompletion {
@@ -14,14 +12,17 @@ export interface ISimpleCompletion {
 	 * The completion's label which appears on the left beside the icon.
 	 */
 	label: string;
+
 	/**
 	 * The ID of the provider the completion item came from
 	 */
 	provider: string;
+
 	/**
 	 * The completion's icon to show on the left of the suggest widget.
 	 */
 	icon?: ThemeIcon;
+
 	/**
 	 * The completion's detail which appears on the right of the list.
 	 */
@@ -32,22 +33,6 @@ export interface ISimpleCompletion {
 	 */
 	documentation?: string | MarkdownString;
 
-	/**
-	 * Whether the completion is a file. Files with the same score will be sorted against each other
-	 * first by extension length and then certain extensions will get a boost based on the OS.
-	 */
-	isFile?: boolean;
-
-	/**
-	 * Whether the completion is a directory.
-	 */
-	isDirectory?: boolean;
-
-	/**
-	 * Whether the completion is a keyword.
-	 */
-	isKeyword?: boolean;
-
 	/**
 	 * The start of the replacement.
 	 */
@@ -63,28 +48,7 @@ export class SimpleCompletionItem {
 	/**
 	 * The lowercase label, normalized to `\` path separators on Windows.
 	 */
-	readonly labelLow: string;
-
-	/**
-	 * {@link labelLow} without the file extension.
-	 */
-	readonly labelLowExcludeFileExt: string;
-
-	/**
-	 * The lowercase label, when the completion is a file or directory this has  normalized path
-	 * separators (/) on Windows and no trailing separator for directories.
-	 */
-	readonly labelLowNormalizedPath: string;
-
-	/**
-	 * A penalty that applies to files or folders starting with the underscore character.
-	 */
-	readonly underscorePenalty: 0 | 1 = 0;
-
-	/**
-	 * The file extension part from {@link labelLow}.
-	 */
-	readonly fileExtLow: string = '';
+	labelLow: string;
 
 	// sorting, filtering
 	score: FuzzyScore = FuzzyScore.Default;
@@ -96,29 +60,5 @@ export class SimpleCompletionItem {
 	) {
 		// ensure lower-variants (perf)
 		this.labelLow = this.completion.label.toLowerCase();
-		this.labelLowExcludeFileExt = this.labelLow;
-		this.labelLowNormalizedPath = this.labelLow;
-
-		if (completion.isFile) {
-			if (isWindows) {
-				this.labelLow = this.labelLow.replaceAll('/', '\\');
-			}
-			// Don't include dotfiles as extensions when sorting
-			const extIndex = this.labelLow.lastIndexOf('.');
-			if (extIndex > 0) {
-				this.labelLowExcludeFileExt = this.labelLow.substring(0, extIndex);
-				this.fileExtLow = this.labelLow.substring(extIndex + 1);
-			}
-		}
-
-		if (completion.isFile || completion.isDirectory) {
-			if (isWindows) {
-				this.labelLowNormalizedPath = this.labelLow.replaceAll('\\', '/');
-			}
-			if (completion.isDirectory) {
-				this.labelLowNormalizedPath = this.labelLowNormalizedPath.replace(/\/$/, '');
-			}
-			this.underscorePenalty = basename(this.labelLowNormalizedPath).startsWith('_') ? 1 : 0;
-		}
 	}
 }
diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts
index e3834c833f5e..a2138ea2bb08 100644
--- a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts
+++ b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts
@@ -25,9 +25,9 @@ const enum Refilter {
 	Incr = 2
 }
 
-export class SimpleCompletionModel {
+export class SimpleCompletionModel {
 	private _stats?: ISimpleCompletionStats;
-	private _filteredItems?: SimpleCompletionItem[];
+	private _filteredItems?: T[];
 	private _refilterKind: Refilter = Refilter.All;
 	private _fuzzyScoreOptions: FuzzyScoreOptions | undefined = {
 		...FuzzyScoreOptions.default,
@@ -40,13 +40,13 @@ export class SimpleCompletionModel {
 	} = {};
 
 	constructor(
-		private readonly _items: SimpleCompletionItem[],
+		private readonly _items: T[],
 		private _lineContext: LineContext,
-		private readonly _rawCompareFn?: (leadingLineContent: string, a: SimpleCompletionItem, b: SimpleCompletionItem) => number,
+		private readonly _rawCompareFn?: (leadingLineContent: string, a: T, b: T) => number,
 	) {
 	}
 
-	get items(): SimpleCompletionItem[] {
+	get items(): T[] {
 		this._ensureCachedState();
 		return this._filteredItems!;
 	}
@@ -87,7 +87,7 @@ export class SimpleCompletionModel {
 
 		// incrementally filter less
 		const source = this._refilterKind === Refilter.All ? this._items : this._filteredItems!;
-		const target: SimpleCompletionItem[] = [];
+		const target: T[] = [];
 
 		// picks a score function based on the number of
 		// items that we have to score/filter and based on the
diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts
index 28afee4b2c29..745b94de123b 100644
--- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts
+++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts
@@ -35,10 +35,10 @@ const enum State {
 	Details
 }
 
-export interface ISimpleSelectedSuggestion {
-	item: SimpleCompletionItem;
+export interface ISimpleSelectedSuggestion {
+	item: T;
 	index: number;
-	model: SimpleCompletionModel;
+	model: SimpleCompletionModel;
 }
 
 interface IPersistedWidgetSizeDelegate {
@@ -69,13 +69,13 @@ export interface IWorkbenchSuggestWidgetOptions {
 	showStatusBarSettingId?: string;
 }
 
-export class SimpleSuggestWidget extends Disposable {
+export class SimpleSuggestWidget, TItem extends SimpleCompletionItem> extends Disposable {
 
 	private static LOADING_MESSAGE: string = localize('suggestWidget.loading', "Loading...");
 	private static NO_SUGGESTIONS_MESSAGE: string = localize('suggestWidget.noSuggestions', "No suggestions.");
 
 	private _state: State = State.Hidden;
-	private _completionModel?: SimpleCompletionModel;
+	private _completionModel?: TModel;
 	private _cappedHeight?: { wanted: number; capped: number };
 	private _forceRenderingAbove: boolean = false;
 	private _explainMode: boolean = false;
@@ -84,29 +84,29 @@ export class SimpleSuggestWidget extends Disposable {
 	private readonly _pendingShowDetails = this._register(new MutableDisposable());
 	private readonly _pendingLayout = this._register(new MutableDisposable());
 	private _currentSuggestionDetails?: CancelablePromise;
-	private _focusedItem?: SimpleCompletionItem;
+	private _focusedItem?: TItem;
 	private _ignoreFocusEvents: boolean = false;
 	readonly element: ResizableHTMLElement;
 	private readonly _messageElement: HTMLElement;
 	private readonly _listElement: HTMLElement;
-	private readonly _list: List;
+	private readonly _list: List;
 	private _status?: SuggestWidgetStatus;
 	private readonly _details: SimpleSuggestDetailsOverlay;
 
 	private readonly _showTimeout = this._register(new TimeoutTimer());
 
-	private readonly _onDidSelect = this._register(new Emitter());
-	readonly onDidSelect: Event = this._onDidSelect.event;
+	private readonly _onDidSelect = this._register(new Emitter>());
+	readonly onDidSelect: Event> = this._onDidSelect.event;
 	private readonly _onDidHide = this._register(new Emitter());
 	readonly onDidHide: Event = this._onDidHide.event;
 	private readonly _onDidShow = this._register(new Emitter());
 	readonly onDidShow: Event = this._onDidShow.event;
-	private readonly _onDidFocus = new PauseableEmitter();
-	readonly onDidFocus: Event = this._onDidFocus.event;
+	private readonly _onDidFocus = new PauseableEmitter>();
+	readonly onDidFocus: Event> = this._onDidFocus.event;
 	private readonly _onDidBlurDetails = this._register(new Emitter());
 	readonly onDidBlurDetails = this._onDidBlurDetails.event;
 
-	get list(): List { return this._list; }
+	get list(): List { return this._list; }
 
 	private readonly _ctxSuggestWidgetHasFocusedSuggestion: IContextKey;
 
@@ -181,9 +181,9 @@ export class SimpleSuggestWidget extends Disposable {
 		const renderer = new SimpleSuggestWidgetItemRenderer(this._getFontInfo.bind(this), this._onDidFontConfigurationChange.bind(this));
 		this._register(renderer);
 		this._listElement = dom.append(this.element.domNode, $('.tree'));
-		this._list = this._register(new List('SuggestWidget', this._listElement, {
-			getHeight: (_element: SimpleCompletionItem): number => this._getLayoutInfo().itemHeight,
-			getTemplateId: (_element: SimpleCompletionItem): string => 'suggestion'
+		this._list = this._register(new List('SuggestWidget', this._listElement, {
+			getHeight: (): number => this._getLayoutInfo().itemHeight,
+			getTemplateId: (): string => 'suggestion'
 		}, [renderer], {
 			alwaysConsumeMouseWheel: true,
 			useShadows: false,
@@ -268,7 +268,7 @@ export class SimpleSuggestWidget extends Disposable {
 		}));
 	}
 
-	private _onListFocus(e: IListEvent): void {
+	private _onListFocus(e: IListEvent): void {
 		if (this._ignoreFocusEvents) {
 			return;
 		}
@@ -367,7 +367,7 @@ export class SimpleSuggestWidget extends Disposable {
 
 	private _cursorPosition?: { top: number; left: number; height: number };
 
-	setCompletionModel(completionModel: SimpleCompletionModel) {
+	setCompletionModel(completionModel: TModel) {
 		this._completionModel = completionModel;
 	}
 
@@ -787,7 +787,7 @@ export class SimpleSuggestWidget extends Disposable {
 		};
 	}
 
-	private _onListMouseDownOrTap(e: IListMouseEvent | IListGestureEvent): void {
+	private _onListMouseDownOrTap(e: IListMouseEvent | IListGestureEvent): void {
 		if (typeof e.element === 'undefined' || typeof e.index === 'undefined') {
 			return;
 		}
@@ -799,13 +799,13 @@ export class SimpleSuggestWidget extends Disposable {
 		this._select(e.element, e.index);
 	}
 
-	private _onListSelection(e: IListEvent): void {
+	private _onListSelection(e: IListEvent): void {
 		if (e.elements.length) {
 			this._select(e.elements[0], e.indexes[0]);
 		}
 	}
 
-	private _select(item: SimpleCompletionItem, index: number): void {
+	private _select(item: TItem, index: number): void {
 		const completionModel = this._completionModel;
 		if (completionModel) {
 			this._onDidSelect.fire({ item, index, model: completionModel });
@@ -848,7 +848,7 @@ export class SimpleSuggestWidget extends Disposable {
 		return true;
 	}
 
-	getFocusedItem(): ISimpleSelectedSuggestion | undefined {
+	getFocusedItem(): ISimpleSelectedSuggestion | undefined {
 		if (this._completionModel) {
 			return {
 				item: this._list.getFocusedElements()[0],

From 5e6f020d731666e65ca6f11c16ba45ef1660027d Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Thu, 6 Feb 2025 07:37:53 -0800
Subject: [PATCH 1296/3587] Move terminal completion item impl into its own
 file

---
 .../browser/pwshCompletionProviderAddon.ts    |  2 +-
 .../suggest/browser/terminalCompletionItem.ts | 93 +++++++++++++++++++
 .../browser/terminalCompletionModel.ts        | 88 +-----------------
 .../browser/terminalCompletionService.ts      |  2 +-
 .../suggest/browser/terminalSuggestAddon.ts   |  3 +-
 .../browser/terminalCompletionModel.test.ts   |  3 +-
 .../browser/terminalCompletionService.test.ts |  2 +-
 7 files changed, 101 insertions(+), 92 deletions(-)
 create mode 100644 src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionItem.ts

diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts
index b0a0f6964946..a21096ed4e37 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts
@@ -21,7 +21,7 @@ import { ITerminalCapabilityStore, TerminalCapability } from '../../../../../pla
 import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
 import { DeferredPromise } from '../../../../../base/common/async.js';
 import { CancellationToken } from '../../../../../base/common/cancellation.js';
-import type { ITerminalCompletion } from './terminalCompletionModel.js';
+import type { ITerminalCompletion } from './terminalCompletionItem.js';
 
 export const enum VSCodeSuggestOscPt {
 	Completions = 'Completions',
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionItem.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionItem.ts
new file mode 100644
index 000000000000..59e515557238
--- /dev/null
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionItem.ts
@@ -0,0 +1,93 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { basename } from '../../../../../base/common/path.js';
+import { isWindows } from '../../../../../base/common/platform.js';
+import { ISimpleCompletion, SimpleCompletionItem } from '../../../../services/suggest/browser/simpleCompletionItem.js';
+
+export enum TerminalCompletionItemKind {
+	File = 0,
+	Folder = 1,
+	Flag = 2,
+	Method = 3,
+	Argument = 4,
+	Alias = 5,
+}
+
+export interface ITerminalCompletion extends ISimpleCompletion {
+
+	kind?: TerminalCompletionItemKind;
+
+	/**
+	 * Whether the completion is a file. Files with the same score will be sorted against each other
+	 * first by extension length and then certain extensions will get a boost based on the OS.
+	 */
+	isFile?: boolean;
+
+	/**
+	 * Whether the completion is a directory.
+	 */
+	isDirectory?: boolean;
+
+	/**
+	 * Whether the completion is a keyword.
+	 */
+	isKeyword?: boolean;
+}
+
+export class TerminalCompletionItem extends SimpleCompletionItem {
+	/**
+	 * {@link labelLow} without the file extension.
+	 */
+	readonly labelLowExcludeFileExt: string;
+
+	/**
+	 * The lowercase label, when the completion is a file or directory this has  normalized path
+	 * separators (/) on Windows and no trailing separator for directories.
+	 */
+	readonly labelLowNormalizedPath: string;
+
+	/**
+	 * A penalty that applies to files or folders starting with the underscore character.
+	 */
+	readonly underscorePenalty: 0 | 1 = 0;
+
+	/**
+	 * The file extension part from {@link labelLow}.
+	 */
+	readonly fileExtLow: string = '';
+
+	constructor(
+		override readonly completion: ITerminalCompletion
+	) {
+		super(completion);
+
+		// ensure lower-variants (perf)
+		this.labelLowExcludeFileExt = this.labelLow;
+		this.labelLowNormalizedPath = this.labelLow;
+
+		if (completion.isFile) {
+			if (isWindows) {
+				this.labelLow = this.labelLow.replaceAll('/', '\\');
+			}
+			// Don't include dotfiles as extensions when sorting
+			const extIndex = this.labelLow.lastIndexOf('.');
+			if (extIndex > 0) {
+				this.labelLowExcludeFileExt = this.labelLow.substring(0, extIndex);
+				this.fileExtLow = this.labelLow.substring(extIndex + 1);
+			}
+		}
+
+		if (completion.isFile || completion.isDirectory) {
+			if (isWindows) {
+				this.labelLowNormalizedPath = this.labelLow.replaceAll('\\', '/');
+			}
+			if (completion.isDirectory) {
+				this.labelLowNormalizedPath = this.labelLowNormalizedPath.replace(/\/$/, '');
+			}
+			this.underscorePenalty = basename(this.labelLowNormalizedPath).startsWith('_') ? 1 : 0;
+		}
+	}
+}
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts
index 6fe9370beaf2..2122e9d22625 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts
@@ -3,11 +3,10 @@
  *  Licensed under the MIT License. See License.txt in the project root for license information.
  *--------------------------------------------------------------------------------------------*/
 
-import { basename } from '../../../../../base/common/path.js';
 import { isWindows } from '../../../../../base/common/platform.js';
 import { count } from '../../../../../base/common/strings.js';
-import { ISimpleCompletion, SimpleCompletionItem } from '../../../../services/suggest/browser/simpleCompletionItem.js';
 import { SimpleCompletionModel, type LineContext } from '../../../../services/suggest/browser/simpleCompletionModel.js';
+import type { TerminalCompletionItem } from './terminalCompletionItem.js';
 
 export class TerminalCompletionModel extends SimpleCompletionModel {
 	constructor(
@@ -116,88 +115,3 @@ const fileExtScores = new Map(isWindows ? [
 function fileExtScore(ext: string): number {
 	return fileExtScores.get(ext) || 0;
 }
-
-export enum TerminalCompletionItemKind {
-	File = 0,
-	Folder = 1,
-	Flag = 2,
-	Method = 3,
-	Argument = 4,
-	Alias = 5,
-}
-
-export interface ITerminalCompletion extends ISimpleCompletion {
-
-	kind?: TerminalCompletionItemKind;
-
-	/**
-	 * Whether the completion is a file. Files with the same score will be sorted against each other
-	 * first by extension length and then certain extensions will get a boost based on the OS.
-	 */
-	isFile?: boolean;
-
-	/**
-	 * Whether the completion is a directory.
-	 */
-	isDirectory?: boolean;
-
-	/**
-	 * Whether the completion is a keyword.
-	 */
-	isKeyword?: boolean;
-}
-
-export class TerminalCompletionItem extends SimpleCompletionItem {
-	/**
-	 * {@link labelLow} without the file extension.
-	 */
-	readonly labelLowExcludeFileExt: string;
-
-	/**
-	 * The lowercase label, when the completion is a file or directory this has  normalized path
-	 * separators (/) on Windows and no trailing separator for directories.
-	 */
-	readonly labelLowNormalizedPath: string;
-
-	/**
-	 * A penalty that applies to files or folders starting with the underscore character.
-	 */
-	readonly underscorePenalty: 0 | 1 = 0;
-
-	/**
-	 * The file extension part from {@link labelLow}.
-	 */
-	readonly fileExtLow: string = '';
-
-	constructor(
-		override readonly completion: ITerminalCompletion
-	) {
-		super(completion);
-
-		// ensure lower-variants (perf)
-		this.labelLowExcludeFileExt = this.labelLow;
-		this.labelLowNormalizedPath = this.labelLow;
-
-		if (completion.isFile) {
-			if (isWindows) {
-				this.labelLow = this.labelLow.replaceAll('/', '\\');
-			}
-			// Don't include dotfiles as extensions when sorting
-			const extIndex = this.labelLow.lastIndexOf('.');
-			if (extIndex > 0) {
-				this.labelLowExcludeFileExt = this.labelLow.substring(0, extIndex);
-				this.fileExtLow = this.labelLow.substring(extIndex + 1);
-			}
-		}
-
-		if (completion.isFile || completion.isDirectory) {
-			if (isWindows) {
-				this.labelLowNormalizedPath = this.labelLow.replaceAll('\\', '/');
-			}
-			if (completion.isDirectory) {
-				this.labelLowNormalizedPath = this.labelLowNormalizedPath.replace(/\/$/, '');
-			}
-			this.underscorePenalty = basename(this.labelLowNormalizedPath).startsWith('_') ? 1 : 0;
-		}
-	}
-}
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts
index a34e537a8410..f3a2b4b250e0 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts
@@ -14,7 +14,7 @@ import { createDecorator } from '../../../../../platform/instantiation/common/in
 import { TerminalCapability, type ITerminalCapabilityStore } from '../../../../../platform/terminal/common/capabilities/capabilities.js';
 import { GeneralShellType, TerminalShellType } from '../../../../../platform/terminal/common/terminal.js';
 import { TerminalSuggestSettingId } from '../common/terminalSuggestConfiguration.js';
-import { TerminalCompletionItemKind, type ITerminalCompletion } from './terminalCompletionModel.js';
+import { TerminalCompletionItemKind, type ITerminalCompletion } from './terminalCompletionItem.js';
 
 export const ITerminalCompletionService = createDecorator('terminalCompletionService');
 
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
index d0306357b453..6250ab4109c2 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
@@ -33,7 +33,8 @@ import { MenuId } from '../../../../../platform/actions/common/actions.js';
 import { ISimpleSuggestWidgetFontInfo } from '../../../../services/suggest/browser/simpleSuggestWidgetRenderer.js';
 import { ITerminalConfigurationService } from '../../../terminal/browser/terminal.js';
 import { GOLDEN_LINE_HEIGHT_RATIO, MINIMUM_LINE_HEIGHT } from '../../../../../editor/common/config/fontInfo.js';
-import { TerminalCompletionItem, TerminalCompletionItemKind, TerminalCompletionModel } from './terminalCompletionModel.js';
+import { TerminalCompletionModel } from './terminalCompletionModel.js';
+import { TerminalCompletionItem, TerminalCompletionItemKind } from './terminalCompletionItem.js';
 import { IntervalTimer, TimeoutTimer } from '../../../../../base/common/async.js';
 
 export interface ISuggestController {
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts
index 566764c3f2a7..e10b0e7db304 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts
@@ -4,8 +4,9 @@
  *--------------------------------------------------------------------------------------------*/
 import assert from 'assert';
 import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
-import { TerminalCompletionItem, TerminalCompletionModel, type ITerminalCompletion } from '../../browser/terminalCompletionModel.js';
+import { TerminalCompletionModel } from '../../browser/terminalCompletionModel.js';
 import { LineContext } from '../../../../../services/suggest/browser/simpleCompletionModel.js';
+import { TerminalCompletionItem, type ITerminalCompletion } from '../../browser/terminalCompletionItem.js';
 
 function createItem(options: Partial): TerminalCompletionItem {
 	return new TerminalCompletionItem({
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts
index 76a9038c4cce..5245af388594 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts
@@ -16,7 +16,7 @@ import { IConfigurationService } from '../../../../../../platform/configuration/
 import { TerminalCapabilityStore } from '../../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js';
 import { ShellEnvDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/shellEnvDetectionCapability.js';
 import { TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js';
-import { ITerminalCompletion, TerminalCompletionItemKind } from '../../browser/terminalCompletionModel.js';
+import { ITerminalCompletion, TerminalCompletionItemKind } from '../../browser/terminalCompletionItem.js';
 
 const pathSeparator = isWindows ? '\\' : '/';
 

From 33f9460110dbe1c9bb847386d1992eab08a19929 Mon Sep 17 00:00:00 2001
From: Alex Ross 
Date: Thu, 6 Feb 2025 16:55:13 +0100
Subject: [PATCH 1297/3587] Refresh tree sitter nodes with errors (#239814)

---
 .../common/services/treeSitter/treeSitterParserService.ts  | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts
index fb8c0c912c48..75f89c5f6128 100644
--- a/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts
+++ b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts
@@ -271,9 +271,8 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul
 					}
 					return c?.hasChanges;
 				});
-				if (changedChildren.length >= 1) {
-					next = gotoNthChild(indexChangedChildren[0]);
-				} else if (changedChildren.length === 0) {
+				// If we have changes and we *had* an error, the whole node should be refreshed.
+				if ((changedChildren.length === 0) || oldCursor.currentNode.hasError) {
 					// walk up again until we get to the first one that's named as unnamed nodes can be too granular
 					while (newCursor.currentNode.parent && !newCursor.currentNode.isNamed && next) {
 						next = gotoParent();
@@ -293,6 +292,8 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul
 
 					changedRanges.push({ newStartPosition, newEndPosition, oldStartIndex, oldEndIndex, newNodeId: newNode.id, newStartIndex, newEndIndex: newNode.endIndex });
 					next = nextSiblingOrParentSibling();
+				} else if (changedChildren.length >= 1) {
+					next = gotoNthChild(indexChangedChildren[0]);
 				}
 			} else {
 				next = nextSiblingOrParentSibling();

From 92919ba1d1827318b5c9443c38292e9fcb019f42 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Thu, 6 Feb 2025 08:22:12 -0800
Subject: [PATCH 1298/3587] Also hide when cursorIndex is -1

Fixes #239816
---
 .../terminalContrib/suggest/browser/terminalSuggestAddon.ts     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
index e7106757d443..5f1eef74f7fe 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
@@ -371,7 +371,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 		// requested, but since extensions are expected to allow the client-side to filter, they are
 		// only invalidated when whitespace is encountered.
 		if (this._currentPromptInputState && this._currentPromptInputState.cursorIndex < this._leadingLineContent.length) {
-			if (this._currentPromptInputState.cursorIndex === 0 || this._currentPromptInputState.value[this._currentPromptInputState.cursorIndex - 1].match(/\s/)) {
+			if (this._currentPromptInputState.cursorIndex <= 0 || this._currentPromptInputState.value[this._currentPromptInputState.cursorIndex - 1].match(/\s/)) {
 				this.hideSuggestWidget();
 				return;
 			}

From 287a31aca533995144553f4202a580d22fb84d4a Mon Sep 17 00:00:00 2001
From: Alex Ross 
Date: Thu, 6 Feb 2025 17:30:30 +0100
Subject: [PATCH 1299/3587] Comments panel: Line numbers too far to the right
 (#239815)

Fixes #239221
---
 .../workbench/contrib/comments/browser/media/panel.css | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/vs/workbench/contrib/comments/browser/media/panel.css b/src/vs/workbench/contrib/comments/browser/media/panel.css
index 867d5aba90cc..e6c66b601742 100644
--- a/src/vs/workbench/contrib/comments/browser/media/panel.css
+++ b/src/vs/workbench/contrib/comments/browser/media/panel.css
@@ -90,9 +90,17 @@
 .comments-panel .comments-panel-container .tree-container .comment-thread-container .text *,
 .comments-panel .comments-panel-container .tree-container .comment-thread-container .range * {
 	margin: 0;
+	padding-right: 5px;
+}
+
+.comments-panel .comments-panel-container .tree-container .comment-thread-container .text * {
 	text-overflow: ellipsis;
 	overflow: hidden;
-	padding-right: 5px;
+}
+
+.comments-panel .comments-panel-container .tree-container .comment-thread-container .range * {
+	overflow: visible;
+	white-space: nowrap;
 }
 
 .comments-panel .comments-panel-container .tree-container .comment-thread-container .range {

From 8705d85839a75b4d3743a30576dc419fe5097b6d Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Thu, 6 Feb 2025 08:57:38 -0800
Subject: [PATCH 1300/3587] Remove isDirectory

Part of #238986
---
 .../browser/pwshCompletionProviderAddon.ts        |  7 ++++---
 .../suggest/browser/terminalCompletionItem.ts     | 15 ++++++---------
 .../suggest/browser/terminalCompletionService.ts  |  8 --------
 .../suggest/browser/terminalSuggestAddon.ts       |  6 +++---
 .../test/browser/terminalCompletionModel.test.ts  |  7 ++++---
 5 files changed, 17 insertions(+), 26 deletions(-)

diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts
index a21096ed4e37..98c36db94ea4 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts
@@ -21,7 +21,7 @@ import { ITerminalCapabilityStore, TerminalCapability } from '../../../../../pla
 import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
 import { DeferredPromise } from '../../../../../base/common/async.js';
 import { CancellationToken } from '../../../../../base/common/cancellation.js';
-import type { ITerminalCompletion } from './terminalCompletionItem.js';
+import { ITerminalCompletion, TerminalCompletionItemKind } from './terminalCompletionItem.js';
 
 export const enum VSCodeSuggestOscPt {
 	Completions = 'Completions',
@@ -208,7 +208,7 @@ export class PwshCompletionProviderAddon extends Disposable implements ITerminal
 			}
 		}
 
-		if (this._mostRecentCompletion?.isDirectory && completions.every(c => c.isDirectory)) {
+		if (this._mostRecentCompletion?.kind === TerminalCompletionItemKind.Folder && completions.every(c => c.kind === TerminalCompletionItemKind.Folder)) {
 			completions.push(this._mostRecentCompletion);
 		}
 		this._mostRecentCompletion = undefined;
@@ -352,8 +352,9 @@ function rawCompletionToITerminalCompletion(rawCompletion: PwshCompletion, repla
 		provider: PwshCompletionProviderAddon.ID,
 		icon,
 		detail,
+		// HACK: This isn't complete, but we'll remove it soon
+		kind: rawCompletion.ResultType === 3 ? TerminalCompletionItemKind.File : TerminalCompletionItemKind.Folder,
 		isFile: rawCompletion.ResultType === 3,
-		isDirectory: rawCompletion.ResultType === 4,
 		isKeyword: rawCompletion.ResultType === 12,
 		replacementIndex,
 		replacementLength
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionItem.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionItem.ts
index 59e515557238..20ff77eb207b 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionItem.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionItem.ts
@@ -17,8 +17,10 @@ export enum TerminalCompletionItemKind {
 }
 
 export interface ITerminalCompletion extends ISimpleCompletion {
-
-	kind?: TerminalCompletionItemKind;
+	/**
+	 * The kind of terminal completion item.
+	 */
+	kind: TerminalCompletionItemKind;
 
 	/**
 	 * Whether the completion is a file. Files with the same score will be sorted against each other
@@ -26,11 +28,6 @@ export interface ITerminalCompletion extends ISimpleCompletion {
 	 */
 	isFile?: boolean;
 
-	/**
-	 * Whether the completion is a directory.
-	 */
-	isDirectory?: boolean;
-
 	/**
 	 * Whether the completion is a keyword.
 	 */
@@ -80,11 +77,11 @@ export class TerminalCompletionItem extends SimpleCompletionItem {
 			}
 		}
 
-		if (completion.isFile || completion.isDirectory) {
+		if (completion.isFile || completion.kind === TerminalCompletionItemKind.Folder) {
 			if (isWindows) {
 				this.labelLowNormalizedPath = this.labelLow.replaceAll('\\', '/');
 			}
-			if (completion.isDirectory) {
+			if (completion.kind === TerminalCompletionItemKind.Folder) {
 				this.labelLowNormalizedPath = this.labelLowNormalizedPath.replace(/\/$/, '');
 			}
 			this.underscorePenalty = basename(this.labelLowNormalizedPath).startsWith('_') ? 1 : 0;
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts
index f3a2b4b250e0..57641199c3fb 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts
@@ -168,7 +168,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
 			const completionItems = Array.isArray(completions) ? completions : completions.items ?? [];
 			for (const completion of completionItems) {
 				completion.isFile ??= completion.kind === TerminalCompletionItemKind.File || (shellType === GeneralShellType.PowerShell && completion.kind === TerminalCompletionItemKind.Method && completion.replacementIndex === 0);
-				completion.isDirectory ??= completion.kind === TerminalCompletionItemKind.Folder;
 			}
 			if (provider.isBuiltin) {
 				//TODO: why is this needed?
@@ -243,7 +242,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
 					label: lastWordFolder,
 					provider,
 					kind: TerminalCompletionItemKind.Folder,
-					isDirectory: true,
 					isFile: false,
 					detail: getFriendlyPath(resolvedFolder, resourceRequestConfig.pathSeparator),
 					replacementIndex: cursorPosition - lastWord.length,
@@ -278,7 +276,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
 					label: lastWordFolder,
 					provider,
 					kind: TerminalCompletionItemKind.Folder,
-					isDirectory: true,
 					isFile: false,
 					detail: getFriendlyPath(lastWordResource, resourceRequestConfig.pathSeparator),
 					replacementIndex: cursorPosition - lastWord.length,
@@ -314,7 +311,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
 					provider,
 					kind,
 					detail: getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator, kind),
-					isDirectory: child.isDirectory,
 					isFile: child.isFile,
 					replacementIndex: cursorPosition - lastWord.length,
 					replacementLength: lastWord.length
@@ -342,7 +338,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
 					label: lastWordFolder.length === 0 ? '.' : lastWordFolder,
 					provider,
 					kind: TerminalCompletionItemKind.Folder,
-					isDirectory: true,
 					isFile: false,
 					detail: getFriendlyPath(cwd, resourceRequestConfig.pathSeparator),
 					replacementIndex: cursorPosition - lastWord.length,
@@ -392,7 +387,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
 					provider,
 					kind,
 					detail: getFriendlyPath(stat.resource, resourceRequestConfig.pathSeparator, kind),
-					isDirectory,
 					isFile: kind === TerminalCompletionItemKind.File,
 					replacementIndex: cursorPosition - lastWord.length,
 					replacementLength: lastWord.length
@@ -421,7 +415,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
 											label,
 											provider,
 											kind: TerminalCompletionItemKind.Folder,
-											isDirectory: child.isDirectory,
 											isFile: child.isFile,
 											detail,
 											replacementIndex: cursorPosition - lastWord.length,
@@ -450,7 +443,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
 					provider,
 					kind: TerminalCompletionItemKind.Folder,
 					detail: getFriendlyPath(parentDir, resourceRequestConfig.pathSeparator),
-					isDirectory: true,
 					isFile: false,
 					replacementIndex: cursorPosition - lastWord.length,
 					replacementLength: lastWord.length
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
index 6250ab4109c2..6a37f67742c4 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
@@ -234,9 +234,9 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 		// - Using `\` or `/` will request new completions. It's important that this only occurs
 		//   when a directory is present, if not completions like git branches could be requested
 		//   which leads to flickering
-		this._isFilteringDirectories = completions.some(e => e.isDirectory);
+		this._isFilteringDirectories = completions.some(e => e.kind === TerminalCompletionItemKind.Folder);
 		if (this._isFilteringDirectories) {
-			const firstDir = completions.find(e => e.isDirectory);
+			const firstDir = completions.find(e => e.kind === TerminalCompletionItemKind.Folder);
 			this._pathSeparator = firstDir?.label.match(/(?[\\\/])/)?.groups?.sep ?? sep;
 			normalizedLeadingLineContent = normalizePathSeparator(normalizedLeadingLineContent, this._pathSeparator);
 		}
@@ -582,7 +582,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 
 		const completion = suggestion.item.completion;
 		let completionText = completion.label;
-		if ((completion.isDirectory || completion.isFile) && completionText.includes(' ')) {
+		if ((completion.kind === TerminalCompletionItemKind.Folder || completion.isFile) && completionText.includes(' ')) {
 			// Escape spaces in files or folders so they're valid paths
 			completionText = completionText.replaceAll(' ', '\\ ');
 		}
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts
index e10b0e7db304..81181cdf453a 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts
@@ -6,11 +6,12 @@ import assert from 'assert';
 import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
 import { TerminalCompletionModel } from '../../browser/terminalCompletionModel.js';
 import { LineContext } from '../../../../../services/suggest/browser/simpleCompletionModel.js';
-import { TerminalCompletionItem, type ITerminalCompletion } from '../../browser/terminalCompletionItem.js';
+import { TerminalCompletionItem, TerminalCompletionItemKind, type ITerminalCompletion } from '../../browser/terminalCompletionItem.js';
 
 function createItem(options: Partial): TerminalCompletionItem {
 	return new TerminalCompletionItem({
 		...options,
+		kind: options.kind ?? TerminalCompletionItemKind.Method,
 		label: options.label || 'defaultLabel',
 		provider: options.provider || 'defaultProvider',
 		replacementIndex: options.replacementIndex || 0,
@@ -19,7 +20,7 @@ function createItem(options: Partial): TerminalCompletionIt
 }
 
 function createFileItems(...labels: string[]): TerminalCompletionItem[] {
-	return labels.map(label => createItem({ label, isFile: true }));
+	return labels.map(label => createItem({ label, isFile: true, kind: TerminalCompletionItemKind.File }));
 }
 
 function createFileItemsModel(...labels: string[]): TerminalCompletionModel {
@@ -30,7 +31,7 @@ function createFileItemsModel(...labels: string[]): TerminalCompletionModel {
 }
 
 function createFolderItems(...labels: string[]): TerminalCompletionItem[] {
-	return labels.map(label => createItem({ label, isDirectory: true }));
+	return labels.map(label => createItem({ label, kind: TerminalCompletionItemKind.Folder }));
 }
 
 function createFolderItemsModel(...labels: string[]): TerminalCompletionModel {

From 4dcc7e914011e393484ca8ba100c5917ad1b9585 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Thu, 6 Feb 2025 08:59:50 -0800
Subject: [PATCH 1301/3587] Change isFile to isFileOverride

Fixes #238986
---
 .../browser/pwshCompletionProviderAddon.ts      |  1 -
 .../suggest/browser/terminalCompletionItem.ts   | 17 +++++++++++------
 .../browser/terminalCompletionService.ts        | 13 ++++---------
 .../suggest/browser/terminalSuggestAddon.ts     |  4 ++--
 .../browser/terminalCompletionModel.test.ts     |  2 +-
 5 files changed, 18 insertions(+), 19 deletions(-)

diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts
index 98c36db94ea4..92053dce6611 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts
@@ -354,7 +354,6 @@ function rawCompletionToITerminalCompletion(rawCompletion: PwshCompletion, repla
 		detail,
 		// HACK: This isn't complete, but we'll remove it soon
 		kind: rawCompletion.ResultType === 3 ? TerminalCompletionItemKind.File : TerminalCompletionItemKind.Folder,
-		isFile: rawCompletion.ResultType === 3,
 		isKeyword: rawCompletion.ResultType === 12,
 		replacementIndex,
 		replacementLength
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionItem.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionItem.ts
index 20ff77eb207b..d7d7dbd17975 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionItem.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionItem.ts
@@ -20,13 +20,14 @@ export interface ITerminalCompletion extends ISimpleCompletion {
 	/**
 	 * The kind of terminal completion item.
 	 */
-	kind: TerminalCompletionItemKind;
+	kind?: TerminalCompletionItemKind;
 
 	/**
-	 * Whether the completion is a file. Files with the same score will be sorted against each other
-	 * first by extension length and then certain extensions will get a boost based on the OS.
+	 * A flag that can be used to override the kind check and treat this completion as a file when
+	 * it comes to sorting. For some pwsh completions come through as methods despite being files,
+	 * this makes sure they're sorted correctly.
 	 */
-	isFile?: boolean;
+	isFileOverride?: boolean;
 
 	/**
 	 * Whether the completion is a keyword.
@@ -65,7 +66,7 @@ export class TerminalCompletionItem extends SimpleCompletionItem {
 		this.labelLowExcludeFileExt = this.labelLow;
 		this.labelLowNormalizedPath = this.labelLow;
 
-		if (completion.isFile) {
+		if (isFile(completion)) {
 			if (isWindows) {
 				this.labelLow = this.labelLow.replaceAll('/', '\\');
 			}
@@ -77,7 +78,7 @@ export class TerminalCompletionItem extends SimpleCompletionItem {
 			}
 		}
 
-		if (completion.isFile || completion.kind === TerminalCompletionItemKind.Folder) {
+		if (isFile(completion) || completion.kind === TerminalCompletionItemKind.Folder) {
 			if (isWindows) {
 				this.labelLowNormalizedPath = this.labelLow.replaceAll('\\', '/');
 			}
@@ -88,3 +89,7 @@ export class TerminalCompletionItem extends SimpleCompletionItem {
 		}
 	}
 }
+
+function isFile(completion: ITerminalCompletion): boolean {
+	return !!(completion.kind === TerminalCompletionItemKind.File || completion.isFileOverride);
+}
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts
index 57641199c3fb..b41dada125b3 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts
@@ -166,8 +166,10 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
 				return undefined;
 			}
 			const completionItems = Array.isArray(completions) ? completions : completions.items ?? [];
-			for (const completion of completionItems) {
-				completion.isFile ??= completion.kind === TerminalCompletionItemKind.File || (shellType === GeneralShellType.PowerShell && completion.kind === TerminalCompletionItemKind.Method && completion.replacementIndex === 0);
+			if (shellType === GeneralShellType.PowerShell) {
+				for (const completion of completionItems) {
+					completion.isFileOverride ??= completion.kind === TerminalCompletionItemKind.Method && completion.replacementIndex === 0;
+				}
 			}
 			if (provider.isBuiltin) {
 				//TODO: why is this needed?
@@ -242,7 +244,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
 					label: lastWordFolder,
 					provider,
 					kind: TerminalCompletionItemKind.Folder,
-					isFile: false,
 					detail: getFriendlyPath(resolvedFolder, resourceRequestConfig.pathSeparator),
 					replacementIndex: cursorPosition - lastWord.length,
 					replacementLength: lastWord.length
@@ -276,7 +277,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
 					label: lastWordFolder,
 					provider,
 					kind: TerminalCompletionItemKind.Folder,
-					isFile: false,
 					detail: getFriendlyPath(lastWordResource, resourceRequestConfig.pathSeparator),
 					replacementIndex: cursorPosition - lastWord.length,
 					replacementLength: lastWord.length
@@ -311,7 +311,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
 					provider,
 					kind,
 					detail: getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator, kind),
-					isFile: child.isFile,
 					replacementIndex: cursorPosition - lastWord.length,
 					replacementLength: lastWord.length
 				});
@@ -338,7 +337,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
 					label: lastWordFolder.length === 0 ? '.' : lastWordFolder,
 					provider,
 					kind: TerminalCompletionItemKind.Folder,
-					isFile: false,
 					detail: getFriendlyPath(cwd, resourceRequestConfig.pathSeparator),
 					replacementIndex: cursorPosition - lastWord.length,
 					replacementLength: lastWord.length
@@ -387,7 +385,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
 					provider,
 					kind,
 					detail: getFriendlyPath(stat.resource, resourceRequestConfig.pathSeparator, kind),
-					isFile: kind === TerminalCompletionItemKind.File,
 					replacementIndex: cursorPosition - lastWord.length,
 					replacementLength: lastWord.length
 				});
@@ -415,7 +412,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
 											label,
 											provider,
 											kind: TerminalCompletionItemKind.Folder,
-											isFile: child.isFile,
 											detail,
 											replacementIndex: cursorPosition - lastWord.length,
 											replacementLength: lastWord.length
@@ -443,7 +439,6 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
 					provider,
 					kind: TerminalCompletionItemKind.Folder,
 					detail: getFriendlyPath(parentDir, resourceRequestConfig.pathSeparator),
-					isFile: false,
 					replacementIndex: cursorPosition - lastWord.length,
 					replacementLength: lastWord.length
 				});
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
index 6a37f67742c4..178e88d3a587 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts
@@ -582,7 +582,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 
 		const completion = suggestion.item.completion;
 		let completionText = completion.label;
-		if ((completion.kind === TerminalCompletionItemKind.Folder || completion.isFile) && completionText.includes(' ')) {
+		if ((completion.kind === TerminalCompletionItemKind.Folder || completion.isFileOverride) && completionText.includes(' ')) {
 			// Escape spaces in files or folders so they're valid paths
 			completionText = completionText.replaceAll(' ', '\\ ');
 		}
@@ -600,7 +600,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
 				}
 				case 'exactMatchIgnoreExtension': {
 					runOnEnter = replacementText.toLowerCase() === completionText.toLowerCase();
-					if (completion.isFile) {
+					if (completion.isFileOverride) {
 						runOnEnter ||= replacementText.toLowerCase() === completionText.toLowerCase().replace(/\.[^\.]+$/, '');
 					}
 					break;
diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts
index 81181cdf453a..a875125e9040 100644
--- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts
+++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionModel.test.ts
@@ -20,7 +20,7 @@ function createItem(options: Partial): TerminalCompletionIt
 }
 
 function createFileItems(...labels: string[]): TerminalCompletionItem[] {
-	return labels.map(label => createItem({ label, isFile: true, kind: TerminalCompletionItemKind.File }));
+	return labels.map(label => createItem({ label, kind: TerminalCompletionItemKind.File }));
 }
 
 function createFileItemsModel(...labels: string[]): TerminalCompletionModel {

From cf46a56bd49fbe0abf64fa284f4ae394380cc8d1 Mon Sep 17 00:00:00 2001
From: Alex Ross 
Date: Thu, 6 Feb 2025 18:18:24 +0100
Subject: [PATCH 1302/3587] Disable changes
 "workbench.action.toggleSidebarPosition" command key binding and verifies it
 (#239820)

---
 test/smoke/src/areas/preferences/preferences.test.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/smoke/src/areas/preferences/preferences.test.ts b/test/smoke/src/areas/preferences/preferences.test.ts
index 14f618ef30a9..02d5cdeac052 100644
--- a/test/smoke/src/areas/preferences/preferences.test.ts
+++ b/test/smoke/src/areas/preferences/preferences.test.ts
@@ -22,7 +22,7 @@ export function setup(logger: Logger) {
 			await app.code.waitForElements('.line-numbers', false, elements => !elements || elements.length === 0);
 		});
 
-		it('changes "workbench.action.toggleSidebarPosition" command key binding and verifies it', async function () {
+		it.skip('changes "workbench.action.toggleSidebarPosition" command key binding and verifies it', async function () {
 			const app = this.app as Application;
 
 			await app.workbench.activitybar.waitForActivityBar(ActivityBarPosition.LEFT);

From 0493ae67238ec7f754afd91a0b73243c2d200c09 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Thu, 6 Feb 2025 09:25:41 -0800
Subject: [PATCH 1303/3587] Remove pwsh scripts completions except for non
 file/folder args

Part of #239822
---
 .../common/scripts/shellIntegration.ps1       | 214 +-----------------
 .../browser/pwshCompletionProviderAddon.ts    |  31 ++-
 2 files changed, 31 insertions(+), 214 deletions(-)

diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1
index 4c5a7059a512..32b1e8038d42 100644
--- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1
+++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1
@@ -180,11 +180,6 @@ function Set-MappedKeyHandler {
 	}
 }
 
-function Get-KeywordCompletionResult {
-	param ($Keyword, $Description = $Keyword)
-	[System.Management.Automation.CompletionResult]::new($Keyword, $Keyword, [System.Management.Automation.CompletionResultType]::Keyword, $Description)
-}
-
 function Set-MappedKeyHandlers {
 	Set-MappedKeyHandler -Chord Ctrl+Spacebar -Sequence 'F12,a'
 	Set-MappedKeyHandler -Chord Alt+Spacebar -Sequence 'F12,b'
@@ -200,134 +195,6 @@ function Set-MappedKeyHandlers {
 		Set-PSReadLineKeyHandler -Chord 'F12,e' -ScriptBlock {
 			Send-Completions
 		}
-
-		# VS Code send global completions request
-		Set-PSReadLineKeyHandler -Chord 'F12,f' -ScriptBlock {
-			# Get commands, convert to string array to reduce the payload size and send as JSON
-			$commands = @(
-				[System.Management.Automation.CompletionCompleters]::CompleteCommand('')
-				Get-KeywordCompletionResult -Keyword 'begin'
-				Get-KeywordCompletionResult -Keyword 'break'
-				Get-KeywordCompletionResult -Keyword 'catch' -Description "catch [[][',' ]*] {}"
-				Get-KeywordCompletionResult -Keyword 'class' -Description @"
-class  [: [][,]] {
-    [[] [hidden] [static]  ...]
-    [([])
-      {} ...]
-    [[] [hidden] [static]  ...]
-}
-"@
-				Get-KeywordCompletionResult -Keyword 'clean'
-				Get-KeywordCompletionResult -Keyword 'continue'
-				Get-KeywordCompletionResult -Keyword 'data' -Description @"
-data [] [-supportedCommand ] {
-    
-}
-"@
-				Get-KeywordCompletionResult -Keyword 'do' -Description @"
-do {} while ()
-do {} until ()
-"@
-				Get-KeywordCompletionResult -Keyword 'dynamicparam' -Description "dynamicparam {}"
-				Get-KeywordCompletionResult -Keyword 'else' -Description @"
-if ()
-    {}
-[elseif ()
-    {}]
-[else
-    {}]
-"@
-				Get-KeywordCompletionResult -Keyword 'elseif' -Description @"
-if ()
-    {}
-[elseif ()
-    {}]
-[else
-    {}]
-"@
-				Get-KeywordCompletionResult -Keyword 'end'
-				Get-KeywordCompletionResult -Keyword 'enum' -Description @"
-[[]...] [Flag()] enum [ : ] {
-