Merge branch 'main' into ben/voice

This commit is contained in:
Benjamin Pasero
2023-08-21 12:57:51 +02:00
53 changed files with 1137 additions and 710 deletions
+12 -2
View File
@@ -56,7 +56,11 @@
{
"command": "ipynb.cleanInvalidImageAttachment",
"title": "%cleanInvalidImageAttachment.title%"
}
},
{
"command": "notebook.cellOutput.copyToClipboard",
"title": "%copyOutputToClipboard.title%"
}
],
"notebooks": [
{
@@ -99,7 +103,13 @@
"command": "ipynb.cleanInvalidImageAttachment",
"when": "false"
}
]
],
"webview/context": [
{
"command": "notebook.cellOutput.copyToClipboard",
"when": "webviewId == 'notebook.output' && webviewSection == 'image'"
}
]
}
},
"scripts": {
+1
View File
@@ -6,6 +6,7 @@
"newUntitledIpynb.shortTitle": "Jupyter Notebook",
"openIpynbInNotebookEditor.title": "Open IPYNB File In Notebook Editor",
"cleanInvalidImageAttachment.title": "Clean Invalid Image Attachment Reference",
"copyOutputToClipboard.title": "Copy Output to Clipboard",
"markdownAttachmentRenderer.displayName": {
"message": "Markdown-It ipynb Cell Attachment renderer",
"comment": [
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExtensionContext, OutputChannel, window, workspace, l10n } from 'vscode';
import { ExtensionContext, OutputChannel, window, workspace, l10n, env } from 'vscode';
import { startClient, LanguageClientConstructor, SchemaRequestService, languageServerDescription } from '../jsonClient';
import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient, BaseLanguageClient } from 'vscode-languageclient/node';
@@ -129,7 +129,10 @@ async function getSchemaRequestService(context: ExtensionContext, log: Log): Pro
const isXHRResponse = (error: any): error is XHRResponse => typeof error?.status === 'number';
const request = async (uri: string, etag?: string): Promise<string> => {
const headers: Headers = { 'Accept-Encoding': 'gzip, deflate' };
const headers: Headers = {
'Accept-Encoding': 'gzip, deflate',
'User-Agent': `${env.appName} (${env.appHost})`
};
if (etag) {
headers['If-None-Match'] = etag;
}
@@ -2,9 +2,9 @@
"displayName": "JSON Language Features",
"description": "Provides rich language support for JSON files.",
"json.schemas.desc": "Associate schemas to JSON files in the current project.",
"json.schemas.url.desc": "A URL to a schema or a relative path to a schema in the current directory",
"json.schemas.fileMatch.desc": "An array of file patterns to match against when resolving JSON files to schemas. `*` can be used as a wildcard. Exclusion patterns can also be defined and start with '!'. A file matches when there is at least one matching pattern and the last matching pattern is not an exclusion pattern.",
"json.schemas.fileMatch.item.desc": "A file pattern that can contain '*' to match against when resolving JSON files to schemas.",
"json.schemas.url.desc": "A URL or absolute file path to a schema. Can be a relative path in workspace and workspace folder settings.",
"json.schemas.fileMatch.desc": "An array of file patterns to match against when resolving JSON files to schemas. `*` and '**' can be used as a wildcard. Exclusion patterns can also be defined and start with '!'. A file matches when there is at least one matching pattern and the last matching pattern is not an exclusion pattern.",
"json.schemas.fileMatch.item.desc": "A file pattern that can contain '*' and '**' to match against when resolving JSON files to schemas. When beginning with '!', it defines an exclusion pattern.",
"json.schemas.schema.desc": "The schema definition for the given URL. The schema only needs to be provided to avoid accesses to the schema URL.",
"json.format.enable.desc": "Enable/disable default JSON formatter",
"json.format.keepLines.desc" : "Keep all existing new lines when formatting.",
@@ -37,6 +37,7 @@ function renderImage(outputInfo: OutputItem, element: HTMLElement): IDisposable
if (alt) {
image.alt = alt;
}
image.setAttribute('data-vscode-context', JSON.stringify({ webviewSection: 'image', outputId: outputInfo.id, 'preventDefaultContextMenuItems': true }));
const display = document.createElement('div');
display.classList.add('display');
display.appendChild(image);
+3 -3
View File
@@ -95,14 +95,14 @@
"vscode-oniguruma": "1.7.0",
"vscode-regexpp": "^3.1.0",
"vscode-textmate": "9.0.0",
"xterm": "5.3.0-beta.42",
"xterm": "5.3.0-beta.46",
"xterm-addon-canvas": "0.5.0-beta.9",
"xterm-addon-image": "0.6.0-beta.2",
"xterm-addon-search": "0.13.0-beta.8",
"xterm-addon-serialize": "0.11.0-beta.8",
"xterm-addon-unicode11": "0.5.0",
"xterm-addon-webgl": "0.16.0-beta.14",
"xterm-headless": "5.3.0-beta.42",
"xterm-addon-webgl": "0.16.0-beta.16",
"xterm-headless": "5.3.0-beta.46",
"yauzl": "^2.9.2",
"yazl": "^2.4.3"
},
+3 -3
View File
@@ -27,14 +27,14 @@
"vscode-oniguruma": "1.7.0",
"vscode-regexpp": "^3.1.0",
"vscode-textmate": "9.0.0",
"xterm": "5.3.0-beta.42",
"xterm": "5.3.0-beta.46",
"xterm-addon-canvas": "0.5.0-beta.9",
"xterm-addon-image": "0.6.0-beta.2",
"xterm-addon-search": "0.13.0-beta.8",
"xterm-addon-serialize": "0.11.0-beta.8",
"xterm-addon-unicode11": "0.5.0",
"xterm-addon-webgl": "0.16.0-beta.14",
"xterm-headless": "5.3.0-beta.42",
"xterm-addon-webgl": "0.16.0-beta.16",
"xterm-headless": "5.3.0-beta.46",
"yauzl": "^2.9.2",
"yazl": "^2.4.3"
}
+2 -2
View File
@@ -11,11 +11,11 @@
"tas-client-umd": "0.1.8",
"vscode-oniguruma": "1.7.0",
"vscode-textmate": "9.0.0",
"xterm": "5.3.0-beta.42",
"xterm": "5.3.0-beta.46",
"xterm-addon-canvas": "0.5.0-beta.9",
"xterm-addon-image": "0.6.0-beta.2",
"xterm-addon-search": "0.13.0-beta.8",
"xterm-addon-unicode11": "0.5.0",
"xterm-addon-webgl": "0.16.0-beta.14"
"xterm-addon-webgl": "0.16.0-beta.16"
}
}
+8 -8
View File
@@ -88,12 +88,12 @@ xterm-addon-unicode11@0.5.0:
resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.5.0.tgz#41c0d96acc1e3bb6c6596eee64e163b6bca74be7"
integrity sha512-Jm4/g4QiTxiKiTbYICQgC791ubhIZyoIwxAIgOW8z8HWFNY+lwk+dwaKEaEeGBfM48Vk8fklsUW9u/PlenYEBg==
xterm-addon-webgl@0.16.0-beta.14:
version "0.16.0-beta.14"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.16.0-beta.14.tgz#de5bf6c97e31f62d13907a875ffc6be154bc4a02"
integrity sha512-cj+rQWqOeZYJ6JMjMuTTKu3OnP+1D+sqSZPuwZAd6s4NkY4dFXFQlKxJ2yvDHOU/jrgpIYR1c3Cnu0AOWZpS3w==
xterm-addon-webgl@0.16.0-beta.16:
version "0.16.0-beta.16"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.16.0-beta.16.tgz#2c31308f8c7f636576720adca529297f8fff3224"
integrity sha512-k0ZSwpBVtxXaqUc29CmlfFgIK7LEYAmwcDC3QVp2ESZo2JHYuQrWFpImjbzbYu0ON6qqcfbX8SnbzRid/XfGUg==
xterm@5.3.0-beta.42:
version "5.3.0-beta.42"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0-beta.42.tgz#f0806ebe9b033530c54ac5b03955827fb729c38f"
integrity sha512-3OLXTfwtSy9UKo8Uzm3/kd7qBg0JD/qFpotg8iKJ2aB9zx0fkJnysl4aIuNleUJzgvuxzBUHkcRZUmEw1XCKdw==
xterm@5.3.0-beta.46:
version "5.3.0-beta.46"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0-beta.46.tgz#9bd2b2a588c88ae64f1989ca3bf5bd9116554b5a"
integrity sha512-keB3C6sXm56ug1+hylAQiNyZP4YtPgd2W+Gu/ZKwJMk5haD25DEnenBYL++Cl3+sNGYxOGS42QlmYj6Mrq1LPw==
+12 -12
View File
@@ -902,20 +902,20 @@ xterm-addon-unicode11@0.5.0:
resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.5.0.tgz#41c0d96acc1e3bb6c6596eee64e163b6bca74be7"
integrity sha512-Jm4/g4QiTxiKiTbYICQgC791ubhIZyoIwxAIgOW8z8HWFNY+lwk+dwaKEaEeGBfM48Vk8fklsUW9u/PlenYEBg==
xterm-addon-webgl@0.16.0-beta.14:
version "0.16.0-beta.14"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.16.0-beta.14.tgz#de5bf6c97e31f62d13907a875ffc6be154bc4a02"
integrity sha512-cj+rQWqOeZYJ6JMjMuTTKu3OnP+1D+sqSZPuwZAd6s4NkY4dFXFQlKxJ2yvDHOU/jrgpIYR1c3Cnu0AOWZpS3w==
xterm-addon-webgl@0.16.0-beta.16:
version "0.16.0-beta.16"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.16.0-beta.16.tgz#2c31308f8c7f636576720adca529297f8fff3224"
integrity sha512-k0ZSwpBVtxXaqUc29CmlfFgIK7LEYAmwcDC3QVp2ESZo2JHYuQrWFpImjbzbYu0ON6qqcfbX8SnbzRid/XfGUg==
xterm-headless@5.3.0-beta.42:
version "5.3.0-beta.42"
resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.3.0-beta.42.tgz#714d167dc0af010067420956ffca80149dddeca9"
integrity sha512-3xhyy9DaPpadd1iiuKjOn4Zvntnx8WSBMEXPJbrORmy3ZbIwmLf/HYvg7iZfVreSDH+UFlm06uq3ictXpv7YvA==
xterm-headless@5.3.0-beta.46:
version "5.3.0-beta.46"
resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.3.0-beta.46.tgz#3f941f673e3c61aad2705e4b34b42fbe467aed48"
integrity sha512-e/VbZKrfyD1TTxOY5/jah3dUih6q8nTO5v5GQV7YOASp+yyI0XHKiDjLrCyFPQOLTKxCcRutMHvr+CoCcPpcHg==
xterm@5.3.0-beta.42:
version "5.3.0-beta.42"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0-beta.42.tgz#f0806ebe9b033530c54ac5b03955827fb729c38f"
integrity sha512-3OLXTfwtSy9UKo8Uzm3/kd7qBg0JD/qFpotg8iKJ2aB9zx0fkJnysl4aIuNleUJzgvuxzBUHkcRZUmEw1XCKdw==
xterm@5.3.0-beta.46:
version "5.3.0-beta.46"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0-beta.46.tgz#9bd2b2a588c88ae64f1989ca3bf5bd9116554b5a"
integrity sha512-keB3C6sXm56ug1+hylAQiNyZP4YtPgd2W+Gu/ZKwJMk5haD25DEnenBYL++Cl3+sNGYxOGS42QlmYj6Mrq1LPw==
yallist@^4.0.0:
version "4.0.0"
@@ -105,40 +105,36 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo
this._register(model.modified.onDidChangeContent((e) => {
const diff = this._diff.get();
if (!diff) {
return;
}
const textEdits = TextEditInfo.fromModelContentChanges(e.changes);
const result = applyModifiedEdits(this._lastDiff!, textEdits, model.original, model.modified);
if (result) {
this._lastDiff = result;
transaction(tx => {
this._diff.set(DiffState.fromDiffResult(this._lastDiff!), tx);
updateUnchangedRegions(result, tx);
const currentSyncedMovedText = this.syncedMovedTexts.get();
this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff!.moves.find(m => m.lineRangeMapping.modified.intersect(currentSyncedMovedText.lineRangeMapping.modified)) : undefined, tx);
});
if (diff) {
const textEdits = TextEditInfo.fromModelContentChanges(e.changes);
const result = applyModifiedEdits(this._lastDiff!, textEdits, model.original, model.modified);
if (result) {
this._lastDiff = result;
transaction(tx => {
this._diff.set(DiffState.fromDiffResult(this._lastDiff!), tx);
updateUnchangedRegions(result, tx);
const currentSyncedMovedText = this.syncedMovedTexts.get();
this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff!.moves.find(m => m.lineRangeMapping.modified.intersect(currentSyncedMovedText.lineRangeMapping.modified)) : undefined, tx);
});
}
}
debouncer.schedule();
}));
this._register(model.original.onDidChangeContent((e) => {
const diff = this._diff.get();
if (!diff) {
return;
}
const textEdits = TextEditInfo.fromModelContentChanges(e.changes);
const result = applyOriginalEdits(this._lastDiff!, textEdits, model.original, model.modified);
if (result) {
this._lastDiff = result;
transaction(tx => {
this._diff.set(DiffState.fromDiffResult(this._lastDiff!), tx);
updateUnchangedRegions(result, tx);
const currentSyncedMovedText = this.syncedMovedTexts.get();
this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff!.moves.find(m => m.lineRangeMapping.modified.intersect(currentSyncedMovedText.lineRangeMapping.modified)) : undefined, tx);
});
if (diff) {
const textEdits = TextEditInfo.fromModelContentChanges(e.changes);
const result = applyOriginalEdits(this._lastDiff!, textEdits, model.original, model.modified);
if (result) {
this._lastDiff = result;
transaction(tx => {
this._diff.set(DiffState.fromDiffResult(this._lastDiff!), tx);
updateUnchangedRegions(result, tx);
const currentSyncedMovedText = this.syncedMovedTexts.get();
this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff!.moves.find(m => m.lineRangeMapping.modified.intersect(currentSyncedMovedText.lineRangeMapping.modified)) : undefined, tx);
});
}
}
debouncer.schedule();
@@ -175,7 +171,6 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo
result = applyOriginalEdits(result, originalTextEditInfos, model.original, model.modified) ?? result;
result = applyModifiedEdits(result, modifiedTextEditInfos, model.original, model.modified) ?? result;
transaction(tx => {
updateUnchangedRegions(result, tx);
@@ -908,13 +908,13 @@ class UnfoldAllRegionsAction extends FoldingAction<void> {
}
}
class FoldAllRegionsExceptAction extends FoldingAction<void> {
class FoldAllExceptAction extends FoldingAction<void> {
constructor() {
super({
id: 'editor.foldAllExcept',
label: nls.localize('foldAllExcept.label', "Fold All Regions Except Selected"),
alias: 'Fold All Regions Except Selected',
label: nls.localize('foldAllExcept.label', "Fold All Except Selected"),
alias: 'Fold All Except Selected',
precondition: CONTEXT_FOLDING_ENABLED,
kbOpts: {
kbExpr: EditorContextKeys.editorTextFocus,
@@ -931,13 +931,13 @@ class FoldAllRegionsExceptAction extends FoldingAction<void> {
}
class UnfoldAllRegionsExceptAction extends FoldingAction<void> {
class UnfoldAllExceptAction extends FoldingAction<void> {
constructor() {
super({
id: 'editor.unfoldAllExcept',
label: nls.localize('unfoldAllExcept.label', "Unfold All Regions Except Selected"),
alias: 'Unfold All Regions Except Selected',
label: nls.localize('unfoldAllExcept.label', "Unfold All Except Selected"),
alias: 'Unfold All Except Selected',
precondition: CONTEXT_FOLDING_ENABLED,
kbOpts: {
kbExpr: EditorContextKeys.editorTextFocus,
@@ -1194,8 +1194,8 @@ registerEditorAction(UnfoldAllAction);
registerEditorAction(FoldAllBlockCommentsAction);
registerEditorAction(FoldAllRegionsAction);
registerEditorAction(UnfoldAllRegionsAction);
registerEditorAction(FoldAllRegionsExceptAction);
registerEditorAction(UnfoldAllRegionsExceptAction);
registerEditorAction(FoldAllExceptAction);
registerEditorAction(UnfoldAllExceptAction);
registerEditorAction(ToggleFoldAction);
registerEditorAction(GotoParentFoldAction);
registerEditorAction(GotoPreviousFoldAction);
@@ -1264,8 +1264,7 @@ export class QuickWidget extends QuickInput implements IQuickWidget {
const visibilities: Visibilities = {
title: !!this.title || !!this.step || !!this.buttons.length,
description: !!this.description || !!this.step,
progressBar: true
description: !!this.description || !!this.step
};
this.ui.setVisibilities(visibilities);
@@ -125,6 +125,19 @@ export interface IQuickAccessProviderHelp {
* The command to bring up this quick access provider.
*/
readonly commandId?: string;
/**
* The order of help entries in the Command Center.
* Lower values will be placed above higher values.
* No value will hide this help entry from the Command Center.
*/
readonly commandCenterOrder?: number;
/**
* An optional label to use for the Command Center entry. If not set
* the description will be used instead.
*/
readonly commandCenterLabel?: string;
}
export interface IQuickAccessProviderDescriptor {
+54 -48
View File
@@ -8,15 +8,24 @@ import { streamToBuffer } from 'vs/base/common/buffer';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IRequestOptions } from 'vs/base/parts/request/common/request';
import { IRequestService } from 'vs/platform/request/common/request';
import * as https from 'https';
import { AbstractOneDataSystemAppender, IAppInsightsCore } from 'vs/platform/telemetry/common/1dsAppender';
type OnCompleteFunc = (status: number, headers: { [headerName: string]: string }, response?: string) => void;
interface IResponseData {
headers: { [headerName: string]: string };
statusCode: number;
responseData: string;
}
/**
* Completes a request to submit telemetry to the server utilizing the request service
* @param options The options which will be used to make the request
* @param requestService The request service
* @returns An object containing the headers, statusCode, and responseData
*/
async function makeTelemetryRequest(options: IRequestOptions, requestService: IRequestService) {
async function makeTelemetryRequest(options: IRequestOptions, requestService: IRequestService): Promise<IResponseData> {
const response = await requestService.request(options, CancellationToken.None);
const responseData = (await streamToBuffer(response.stream)).toString();
const statusCode = response.res.statusCode ?? 200;
@@ -31,30 +40,57 @@ async function makeTelemetryRequest(options: IRequestOptions, requestService: IR
/**
* Complete a request to submit telemetry to the server utilizing the https module. Only used when the request service is not available
* @param options The options which will be used to make the request
* @param httpsModule The https node module
* @returns An object containing the headers, statusCode, and responseData
*/
function makeLegacyTelemetryRequest(options: IRequestOptions, httpsModule: typeof import('https')) {
async function makeLegacyTelemetryRequest(options: IRequestOptions): Promise<IResponseData> {
const httpsOptions = {
method: options.type,
headers: options.headers
};
const req = httpsModule.request(options.url ?? '', httpsOptions, res => {
res.on('data', function (responseData) {
return {
headers: res.headers as Record<string, any>,
statusCode: res.statusCode ?? 200,
responseData: responseData.toString()
};
const responsePromise = new Promise<IResponseData>((resolve, reject) => {
const req = https.request(options.url ?? '', httpsOptions, res => {
res.on('data', function (responseData) {
resolve({
headers: res.headers as Record<string, any>,
statusCode: res.statusCode ?? 200,
responseData: responseData.toString()
});
});
// On response with error send status of 0 and a blank response to oncomplete so we can retry events
res.on('error', function (err) {
reject(err);
});
});
// On response with error send status of 0 and a blank response to oncomplete so we can retry events
res.on('error', function (err) {
throw err;
req.write(options.data, (err) => {
if (err) {
reject(err);
}
});
req.end();
});
req.write(options.data);
req.end();
return;
return responsePromise;
}
async function sendPostAsync(requestService: IRequestService | undefined, payload: IPayloadData, oncomplete: OnCompleteFunc) {
const telemetryRequestData = typeof payload.data === 'string' ? payload.data : new TextDecoder().decode(payload.data);
const requestOptions: IRequestOptions = {
type: 'POST',
headers: {
...payload.headers,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload.data).toString()
},
url: payload.urlString,
data: telemetryRequestData
};
try {
const responseData = requestService ? await makeTelemetryRequest(requestOptions, requestService) : await makeLegacyTelemetryRequest(requestOptions);
oncomplete(responseData.statusCode, responseData.headers, responseData.responseData);
} catch {
// If it errors out, send status of 0 and a blank response to oncomplete so we can retry events
oncomplete(0, {});
}
}
@@ -67,41 +103,11 @@ export class OneDataSystemAppender extends AbstractOneDataSystemAppender {
defaultData: { [key: string]: any } | null,
iKeyOrClientFactory: string | (() => IAppInsightsCore), // allow factory function for testing
) {
let httpsModule: typeof import('https') | undefined;
if (!requestService) {
httpsModule = require('https');
}
// Override the way events get sent since node doesn't have XHTMLRequest
const customHttpXHROverride: IXHROverride = {
sendPOST: (payload: IPayloadData, oncomplete) => {
const telemetryRequestData = typeof payload.data === 'string' ? payload.data : new TextDecoder().decode(payload.data);
const requestOptions: IRequestOptions = {
type: 'POST',
headers: {
...payload.headers,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload.data).toString()
},
url: payload.urlString,
data: telemetryRequestData
};
try {
if (requestService) {
makeTelemetryRequest(requestOptions, requestService).then(({ statusCode, headers, responseData }) => {
oncomplete(statusCode, headers, responseData);
});
} else {
if (!httpsModule) {
throw new Error('https module is undefined');
}
makeLegacyTelemetryRequest(requestOptions, httpsModule);
}
} catch {
// If it errors out, send status of 0 and a blank response to oncomplete so we can retry events
oncomplete(0, {});
}
// Fire off the async request without awaiting it
sendPostAsync(requestService, payload, oncomplete);
}
};
@@ -1217,7 +1217,6 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase {
const normalizedResult = Array.isArray(result) ? result : result.items;
const commands = this._isAdditionsProposedApiEnabled ? Array.isArray(result) ? [] : result.commands || [] : [];
const suppressSuggestions = this._isAdditionsProposedApiEnabled && !Array.isArray(result) ? result.suppressSuggestions : undefined;
const enableForwardStability = this._isAdditionsProposedApiEnabled && !Array.isArray(result) ? result.enableForwardStability : undefined;
let disposableStore: DisposableStore | undefined = undefined;
@@ -1255,7 +1254,7 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase {
}
return this._commands.toInternal(c, disposableStore);
}),
suppressSuggestions,
suppressSuggestions: false,
enableForwardStability,
};
}
@@ -4,18 +4,31 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { Extensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration';
export const accessibilityHelpIsShown = new RawContextKey<boolean>('accessibilityHelpIsShown', false, true);
export const accessibleViewIsShown = new RawContextKey<boolean>('accessibleViewIsShown', false, true);
export const accessibleViewSupportsNavigation = new RawContextKey<boolean>('accessibleViewSupportsNavigation', false, true);
export const accessibleViewVerbosityEnabled = new RawContextKey<boolean>('accessibleViewVerbosityEnabled', false, true);
export const accessibleViewGoToSymbolSupported = new RawContextKey<boolean>('accessibleViewGoToSymbolSupported', false, true);
export const accessibleViewCurrentProviderId = new RawContextKey<string>('accessibleViewCurrentProviderId', undefined, undefined);
export const enum AccessibilitySettingId {
UnfocusedViewOpacity = 'accessibility.unfocusedViewOpacity'
/**
* Miscellaneous settings tagged with accessibility and implemented in the accessibility contrib but
* were better to live under workbench for discoverability.
*/
export const enum AccessibilityWorkbenchSettingId {
ViewDimUnfocusedEnabled = 'workbench.view.dimUnfocused.enabled',
ViewDimUnfocusedOpacity = 'workbench.view.dimUnfocused.opacity'
}
export const enum ViewDimUnfocusedOpacityProperties {
Default = 0.75,
Minimum = 0.2,
Maximum = 1
}
export const enum AccessibilityVerbositySettingId {
@@ -82,19 +95,33 @@ const configuration: IConfigurationNode = {
[AccessibilityVerbositySettingId.EditorUntitledHint]: {
description: localize('verbosity.editor.untitledhint', 'Provide information about relevant actions in an untitled text editor.'),
...baseProperty
},
[AccessibilitySettingId.UnfocusedViewOpacity]: {
description: localize('unfocusedViewOpacity', 'The opacity fraction (0.2 to 1.0) to use for unfocused editors and terminals. This will dim inactive views to make the focused views more obvious.'),
type: 'number',
minimum: 0.2,
maximum: 1,
default: 1,
tags: ['accessibility']
}
}
};
export function registerAccessibilityConfiguration() {
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
configurationRegistry.registerConfiguration(configuration);
const registry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
registry.registerConfiguration(configuration);
registry.registerConfiguration({
...workbenchConfigurationNodeBase,
properties: {
[AccessibilityWorkbenchSettingId.ViewDimUnfocusedEnabled]: {
description: localize('dimUnfocusedEnabled', 'Whether to dim unfocused editors and terminals, making the focused view more obvious.'),
type: 'boolean',
default: false,
tags: ['accessibility'],
scope: ConfigurationScope.MACHINE,
},
[AccessibilityWorkbenchSettingId.ViewDimUnfocusedOpacity]: {
description: localize('dimUnfocusedOpacity', 'The opacity fraction (0.2 to 1.0) to use for unfocused editors and terminals. This will only take effect when {0} is enabled.', `\`#${AccessibilityWorkbenchSettingId.ViewDimUnfocusedEnabled}#\``),
type: 'number',
minimum: ViewDimUnfocusedOpacityProperties.Minimum,
maximum: ViewDimUnfocusedOpacityProperties.Maximum,
default: ViewDimUnfocusedOpacityProperties.Default,
tags: ['accessibility'],
scope: ConfigurationScope.MACHINE,
}
}
});
}
@@ -54,7 +54,7 @@ export class EditorAccessibilityHelpContribution extends Disposable {
codeEditor = codeEditorService.getActiveCodeEditor()!;
}
accessibleViewService.show(instantiationService.createInstance(AccessibilityHelpProvider, codeEditor));
}));
}, EditorContextKeys.focus));
}
}
@@ -282,6 +282,7 @@ export class InlineCompletionsAccessibleViewContribution extends Disposable {
this._register(AccessibleViewAction.addImplementation(95, 'inline-completions', accessor => {
const accessibleViewService = accessor.get(IAccessibleViewService);
const codeEditorService = accessor.get(ICodeEditorService);
const contextViewService = accessor.get(IContextViewService);
const show = () => {
const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor();
if (!editor) {
@@ -310,33 +311,19 @@ export class InlineCompletionsAccessibleViewContribution extends Disposable {
editor.focus();
},
next() {
model.next().then(() => show());
contextViewService.hideContextView();
setTimeout(() => model.next().then(() => show()), 50);
},
previous() {
model.previous().then(() => show());
contextViewService.hideContextView();
setTimeout(() => model.previous().then(() => show()), 50);
},
actions: [
{
id: 'inlineCompletions.accept',
label: localize('inlineCompletions.accept', "Accept Completion"),
tooltip: localize('inlineCompletions.accept', "Accept Completion"),
run: () => {
model.accept(editor).then(() => {
alert('Accepted');
model.stop();
editor.focus();
});
},
class: ThemeIcon.asClassName(Codicon.check),
enabled: true
}
],
options: this._options
});
return true;
};
}; ContextKeyExpr.and(InlineCompletionContextKeys.inlineSuggestionVisible);
return show();
}, ContextKeyExpr.and(InlineCompletionContextKeys.inlineSuggestionVisible, EditorContextKeys.focus, EditorContextKeys.hasCodeActionsProvider)
},
)
);
}
@@ -32,7 +32,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands';
import { AccessibilityVerbositySettingId, accessibilityHelpIsShown, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { AccessibilityVerbositySettingId, accessibilityHelpIsShown, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';
import { IAction } from 'vs/base/common/actions';
import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
@@ -101,6 +101,7 @@ class AccessibleView extends Disposable {
private _accessibleViewSupportsNavigation: IContextKey<boolean>;
private _accessibleViewVerbosityEnabled: IContextKey<boolean>;
private _accessibleViewGoToSymbolSupported: IContextKey<boolean>;
private _accessibleViewCurrentProviderId: IContextKey<string>;
get editorWidget() { return this._editorWidget; }
private _editorContainer: HTMLElement;
@@ -126,6 +127,7 @@ class AccessibleView extends Disposable {
this._accessibleViewSupportsNavigation = accessibleViewSupportsNavigation.bindTo(this._contextKeyService);
this._accessibleViewVerbosityEnabled = accessibleViewVerbosityEnabled.bindTo(this._contextKeyService);
this._accessibleViewGoToSymbolSupported = accessibleViewGoToSymbolSupported.bindTo(this._contextKeyService);
this._accessibleViewCurrentProviderId = accessibleViewCurrentProviderId.bindTo(this._contextKeyService);
this._editorContainer = document.createElement('div');
this._editorContainer.classList.add('accessible-view');
@@ -182,6 +184,7 @@ class AccessibleView extends Disposable {
onHide: () => {
if (!showAccessibleViewHelp) {
this._currentProvider = undefined;
this._accessibleViewCurrentProviderId.reset();
}
}
};
@@ -291,6 +294,7 @@ class AccessibleView extends Disposable {
if (!showAccessibleViewHelp) {
// don't overwrite the current provider
this._currentProvider = provider;
this._accessibleViewCurrentProviderId.set(provider.verbositySettingKey.replaceAll('accessibility.verbosity.', ''));
}
this._updateContextKeys(provider, true);
const value = this._configurationService.getValue(provider.verbositySettingKey);
@@ -11,8 +11,11 @@ import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/act
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands';
import { accessibilityHelpIsShown, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { AccessibilityVerbositySettingId, accessibilityHelpIsShown, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController';
import { alert } from 'vs/base/browser/ui/aria/aria';
const accessibleViewMenu = {
id: MenuId.AccessibleView,
@@ -33,7 +36,12 @@ class AccessibleViewNextAction extends Action2 {
primary: KeyMod.Alt | KeyCode.BracketRight,
weight: KeybindingWeight.WorkbenchContrib
},
menu: [commandPalette, accessibleViewMenu],
menu: [
commandPalette,
{
...accessibleViewMenu,
when: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewSupportsNavigation),
}],
icon: Codicon.chevronRight,
title: localize('editor.action.accessibleViewNext', "Show Next in Accessible View")
});
@@ -55,7 +63,13 @@ class AccessibleViewPreviousAction extends Action2 {
weight: KeybindingWeight.WorkbenchContrib
},
icon: Codicon.chevronLeft,
menu: [commandPalette, accessibleViewMenu],
menu: [
commandPalette,
{
...accessibleViewMenu,
when: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewSupportsNavigation),
}
],
title: localize('editor.action.accessibleViewPrevious', "Show Previous in Accessible View")
});
}
@@ -73,10 +87,17 @@ class AccessibleViewGoToSymbolAction extends Action2 {
precondition: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewGoToSymbolSupported),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyO,
secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Period],
weight: KeybindingWeight.WorkbenchContrib + 10
},
icon: Codicon.symbolField,
menu: [commandPalette, accessibleViewMenu],
menu: [
commandPalette,
{
...accessibleViewMenu,
when: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewSupportsNavigation),
}
],
title: localize('editor.action.accessibleViewGoToSymbol', "Go To Symbol in Accessible View")
});
}
@@ -140,11 +161,14 @@ class AccessibleViewDisableHintAction extends Action2 {
weight: KeybindingWeight.WorkbenchContrib
},
icon: Codicon.treeFilterClear,
menu: [commandPalette,
menu: [
commandPalette,
{
id: MenuId.AccessibleView,
group: 'navigation'
}],
group: 'navigation',
when: ContextKeyExpr.and(ContextKeyExpr.or(accessibleViewIsShown, accessibilityHelpIsShown), accessibleViewVerbosityEnabled),
}
],
title: localize('editor.action.accessibleViewDisableHint', "Disable Accessible View Hint")
});
}
@@ -153,3 +177,44 @@ class AccessibleViewDisableHintAction extends Action2 {
}
}
registerAction2(AccessibleViewDisableHintAction);
class AccessibleViewAcceptInlineCompletionAction extends Action2 {
constructor() {
super({
id: AccessibilityCommandId.AccessibleViewAcceptInlineCompletion,
precondition: ContextKeyExpr.and(accessibleViewIsShown, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibilityVerbositySettingId.InlineCompletions)),
keybinding: {
primary: KeyMod.CtrlCmd | KeyCode.Slash,
mac: { primary: KeyMod.WinCtrl | KeyCode.Slash },
weight: KeybindingWeight.WorkbenchContrib
},
icon: Codicon.check,
menu: [
commandPalette,
{
id: MenuId.AccessibleView,
group: 'navigation',
order: 0,
when: ContextKeyExpr.and(accessibleViewIsShown, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibilityVerbositySettingId.InlineCompletions))
}],
title: localize('editor.action.accessibleViewAcceptInlineCompletionAction', "Accept Inline Completion")
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const codeEditorService = accessor.get(ICodeEditorService);
const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor();
if (!editor) {
return;
}
const model = InlineCompletionsController.get(editor)?.model.get();
const state = model?.state.get();
if (!model || !state) {
return;
}
await model.accept(editor);
alert('Accepted');
model.stop();
editor.focus();
}
}
registerAction2(AccessibleViewAcceptInlineCompletionAction);
@@ -8,38 +8,35 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { clamp } from 'vs/base/common/numbers';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { AccessibilitySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { AccessibilityWorkbenchSettingId, ViewDimUnfocusedOpacityProperties } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
export class UnfocusedViewDimmingContribution extends Disposable implements IWorkbenchContribution {
private _styleElement?: HTMLStyleElement;
constructor(
@IConfigurationService configurationService: IConfigurationService,
) {
super();
const elStyle = document.createElement('style');
elStyle.className = 'accessibilityUnfocusedViewOpacity';
document.head.appendChild(elStyle);
this._register(toDisposable(() => elStyle.remove()));
this._register(toDisposable(() => this._removeStyleElement()));
this._register(Event.runAndSubscribe(configurationService.onDidChangeConfiguration, e => {
if (e && !e.affectsConfiguration(AccessibilitySettingId.UnfocusedViewOpacity)) {
if (e && !e.affectsConfiguration(AccessibilityWorkbenchSettingId.ViewDimUnfocusedEnabled) && !e.affectsConfiguration(AccessibilityWorkbenchSettingId.ViewDimUnfocusedOpacity)) {
return;
}
let opacity: number;
const opacityConfig = configurationService.getValue(AccessibilitySettingId.UnfocusedViewOpacity);
if (typeof opacityConfig !== 'number') {
opacity = 1;
} else {
opacity = clamp(opacityConfig, 0.2, 1);
}
let cssTextContent = '';
// Only add the styles if the feature is used
if (opacity !== 1) {
const rules = new Set<string>();
const enabled = ensureBoolean(configurationService.getValue(AccessibilityWorkbenchSettingId.ViewDimUnfocusedEnabled), false);
if (enabled) {
const opacity = clamp(
ensureNumber(configurationService.getValue(AccessibilityWorkbenchSettingId.ViewDimUnfocusedOpacity), ViewDimUnfocusedOpacityProperties.Default),
ViewDimUnfocusedOpacityProperties.Minimum,
ViewDimUnfocusedOpacityProperties.Maximum
);
if (opacity !== 1) {
const rules = new Set<string>();
const filterRule = `filter: opacity(${opacity});`;
// Terminal tabs
rules.add(`.monaco-workbench .pane-body.integrated-terminal:not(:focus-within) .tabs-container { ${filterRule} }`);
@@ -49,11 +46,39 @@ export class UnfocusedViewDimmingContribution extends Disposable implements IWor
rules.add(`.monaco-workbench .editor-instance:not(:focus-within) .monaco-editor { ${filterRule} }`);
// Terminal editors
rules.add(`.monaco-workbench .editor-instance:not(:focus-within) .terminal-wrapper { ${filterRule} }`);
cssTextContent = [...rules].join('\n');
}
cssTextContent = [...rules].join('\n');
}
elStyle.textContent = cssTextContent;
if (cssTextContent.length === 0) {
this._removeStyleElement();
} else {
this._getStyleElement().textContent = cssTextContent;
}
}));
}
private _getStyleElement(): HTMLStyleElement {
if (!this._styleElement) {
this._styleElement = document.createElement('style');
this._styleElement.className = 'accessibilityUnfocusedViewOpacity';
document.head.appendChild(this._styleElement);
}
return this._styleElement;
}
private _removeStyleElement(): void {
this._styleElement?.remove();
this._styleElement = undefined;
}
}
function ensureBoolean(value: unknown, defaultValue: boolean): boolean {
return typeof value === 'boolean' ? value : defaultValue;
}
function ensureNumber(value: unknown, defaultValue: number): number {
return typeof value === 'number' ? value : defaultValue;
}
@@ -9,5 +9,6 @@ export const enum AccessibilityCommandId {
DisableVerbosityHint = 'editor.action.accessibleViewDisableHint',
GoToSymbol = 'editor.action.accessibleViewGoToSymbol',
ShowNext = 'editor.action.accessibleViewNext',
ShowPrevious = 'editor.action.accessibleViewPrevious'
ShowPrevious = 'editor.action.accessibleViewPrevious',
AccessibleViewAcceptInlineCompletion = 'editor.action.accessibleViewAcceptInlineCompletion'
}
@@ -3,32 +3,69 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Codicon } from 'vs/base/common/codicons';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { ThemeIcon } from 'vs/base/common/themables';
import { localize } from 'vs/nls';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ContextKeyExpr, IContextKeyService, IScopedContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IQuickInputService, IQuickWidget } from 'vs/platform/quickinput/common/quickInput';
import { editorBackground, editorForeground, inputBackground } from 'vs/platform/theme/common/colorRegistry';
import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane';
import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget';
import { IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat';
import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
export const ASK_QUICK_QUESTION_ACTION_ID = 'chat.action.askQuickQuestion';
export const ASK_QUICK_QUESTION_ACTION_ID = 'workbench.action.quickchat.toggle';
export function registerQuickChatActions() {
registerAction2(QuickChatGlobalAction);
registerAction2(class OpenInChatViewAction extends Action2 {
constructor() {
super({
id: 'workbench.action.quickchat.openInChatView',
title: {
value: localize('chat.openInChatView.label', "Open in Chat View"),
original: 'Open in Chat View'
},
f1: false,
category: CHAT_CATEGORY,
icon: Codicon.commentDiscussion,
menu: {
id: MenuId.ChatInputSide,
group: 'navigation',
order: 10
}
});
}
run(accessor: ServicesAccessor) {
const quickChatService = accessor.get(IQuickChatService);
quickChatService.openInChatView();
}
});
registerAction2(class CloseQuickChatAction extends Action2 {
constructor() {
super({
id: 'workbench.action.quickchat.close',
title: {
value: localize('chat.closeQuickChat.label', "Close Quick Chat"),
original: 'Close Quick Chat'
},
f1: false,
category: CHAT_CATEGORY,
icon: Codicon.close,
menu: {
id: MenuId.ChatInputSide,
group: 'navigation',
order: 20
}
});
}
run(accessor: ServicesAccessor) {
const quickChatService = accessor.get(IQuickChatService);
quickChatService.close();
}
});
}
class QuickChatGlobalAction extends Action2 {
@@ -46,24 +83,13 @@ class QuickChatGlobalAction extends Action2 {
linux: {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KeyI
}
},
menu: {
id: MenuId.LayoutControlMenu,
group: '0_workbench_toggles',
when: ContextKeyExpr.notEquals('config.chat.experimental.defaultMode', 'chatView'),
order: 0
}
});
}
override async run(accessor: ServicesAccessor, query: string): Promise<void> {
const chatService = accessor.get(IChatService);
const commandService = accessor.get(ICommandService);
// Grab the first provider and run its command
const info = chatService.getProviderInfos()[0];
if (info) {
await commandService.executeCommand(`workbench.action.openQuickChat.${info.id}`, query);
}
override run(accessor: ServicesAccessor, query?: string): void {
const quickChatService = accessor.get(IQuickChatService);
quickChatService.toggle(undefined, query);
}
}
@@ -77,10 +103,6 @@ class QuickChatGlobalAction extends Action2 {
*/
export function getQuickChatActionForProvider(id: string, label: string) {
return class AskQuickChatAction extends Action2 {
_currentTimer: any | undefined;
_input: IQuickWidget | undefined;
_currentChat: QuickChat | undefined;
constructor() {
super({
id: `workbench.action.openQuickChat.${id}`,
@@ -90,198 +112,9 @@ export function getQuickChatActionForProvider(id: string, label: string) {
});
}
override run(accessor: ServicesAccessor, query: string): void {
const quickInputService = accessor.get(IQuickInputService);
const chatService = accessor.get(IChatService);
const instantiationService = accessor.get(IInstantiationService);
// First things first, clear the existing timer that will dispose the session
clearTimeout(this._currentTimer);
this._currentTimer = undefined;
// If the input is already shown, hide it. This provides a toggle behavior of the quick pick
if (this._input !== undefined) {
this._input.hide();
return;
}
// Check if any providers are available. If not, show nothing
// This shouldn't be needed because of the precondition, but just in case
const providerInfo = chatService.getProviderInfos()[0];
if (!providerInfo) {
return;
}
const disposableStore = new DisposableStore();
//#region Setup quick pick
this._input = quickInputService.createQuickWidget();
disposableStore.add(this._input);
const containerSession = dom.$('.interactive-session');
this._input.widget = containerSession;
this._currentChat ??= instantiationService.createInstance(QuickChat, {
providerId: providerInfo.id,
});
// show needs to come before the current chat rendering
this._input.show();
this._currentChat.render(containerSession);
const clearButton = {
iconClass: ThemeIcon.asClassName(Codicon.clearAll),
tooltip: localize('clear', "Clear"),
};
this._input.buttons = [
clearButton,
{
iconClass: ThemeIcon.asClassName(Codicon.commentDiscussion),
tooltip: localize('openInChat', "Open In Chat View"),
}
];
this._input.title = providerInfo.displayName;
disposableStore.add(this._input.onDidHide(() => {
disposableStore.dispose();
this._input = undefined;
this._currentTimer = setTimeout(() => {
this._currentChat?.dispose();
this._currentChat = undefined;
}, 1000 * 30); // 30 seconds
}));
disposableStore.add(this._input.onDidTriggerButton((e) => {
if (e === clearButton) {
this._currentChat?.clear();
} else {
this._currentChat?.openChatView();
}
}));
//#endregion
this._currentChat.focus();
if (query) {
this._currentChat.setValue(query);
this._currentChat.acceptInput();
}
override run(accessor: ServicesAccessor, query?: string): void {
const quickChatService = accessor.get(IQuickChatService);
quickChatService.toggle(id, query);
}
};
}
class QuickChat extends Disposable {
private widget!: ChatWidget;
private model: ChatModel | undefined;
private _currentQuery: string | undefined;
private _scopedContextKeyService!: IScopedContextKeyService;
get scopedContextKeyService() {
return this._scopedContextKeyService;
}
constructor(
private readonly _options: IChatViewOptions,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IChatService private readonly chatService: IChatService,
@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService
) {
super();
}
clear() {
this.model?.dispose();
this.model = undefined;
this.updateModel();
this.widget.inputEditor.setValue('');
}
focus(): void {
if (this.widget) {
this.widget.focusInput();
}
}
render(parent: HTMLElement): void {
this._scopedContextKeyService?.dispose();
this._scopedContextKeyService = this._register(this.contextKeyService.createScoped(parent));
const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]));
this.widget?.dispose();
this.widget = this._register(
scopedInstantiationService.createInstance(
ChatWidget,
{ resource: true, renderInputOnTop: true, renderStyle: 'compact' },
{
listForeground: editorForeground,
listBackground: editorBackground,
inputEditorBackground: inputBackground,
resultEditorBackground: editorBackground
}));
this.widget.render(parent);
this.widget.setVisible(true);
this.widget.setDynamicChatTreeItemLayout(2, 600);
this.updateModel();
if (this._currentQuery) {
this.widget.inputEditor.setSelection({
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: this._currentQuery.length + 1
});
}
this.registerListeners();
}
private registerListeners(): void {
this._register(this.widget.inputEditor.onDidChangeModelContent((e) => {
this._currentQuery = this.widget.inputEditor.getValue();
}));
this._register(this.widget.onDidClear(() => this.clear()));
}
async acceptInput(): Promise<void> {
return this.widget.acceptInput();
}
async openChatView(): Promise<void> {
const widget = await this._chatWidgetService.revealViewForProvider(this._options.providerId);
if (!widget?.viewModel || !this.model) {
return;
}
for (const request of this.model.getRequests()) {
if (request.response?.response.value || request.response?.errorDetails) {
this.chatService.addCompleteRequest(widget.viewModel.sessionId,
request.message as string,
{
message: request.response.response.asString(),
errorDetails: request.response.errorDetails
});
} else if (request.message) {
}
}
const value = this.widget.inputEditor.getValue();
if (value) {
widget.inputEditor.setValue(value);
}
widget.focusInput();
}
setValue(value: string): void {
this.widget.inputEditor.setValue(value);
}
private updateModel(): void {
this.model ??= this.chatService.startSession(this._options.providerId, CancellationToken.None);
if (!this.model) {
throw new Error('Could not start chat session');
}
this.widget.setModel(this.model, { inputValue: this._currentQuery });
}
}
@@ -22,7 +22,7 @@ import { registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/ac
import { registerQuickChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions';
import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions';
import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/actions/chatImportExport';
import { IChatAccessibilityService, IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { IChatAccessibilityService, IChatWidget, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat';
import { ChatContributionService } from 'vs/workbench/contrib/chat/browser/chatContributionServiceImpl';
import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor';
import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput';
@@ -52,6 +52,7 @@ import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ChatVariablesService, IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/actions/chatFileTreeActions';
import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick';
// Register configuration
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
@@ -85,18 +86,6 @@ configurationRegistry.registerConfiguration({
type: 'number',
description: nls.localize('interactiveSession.editor.lineHeight', "Controls the line height in pixels in chat codeblocks. Use 0 to compute the line height from the font size."),
default: 0
},
'chat.experimental.defaultMode': {
type: 'string',
tags: ['experimental'],
enum: ['chatView', 'quickQuestion', 'both'],
enumDescriptions: [
nls.localize('interactiveSession.defaultMode.chatView', "Use the chat view as the default mode. Displays the chat icon in the Activity Bar."),
nls.localize('interactiveSession.defaultMode.quickQuestion', "Use the quick question as the default mode. Displays the chat icon in the Title Bar."),
nls.localize('interactiveSession.defaultMode.both', "Displays the chat icon in the Activity Bar and the Title Bar which open their respective chat modes.")
],
description: nls.localize('interactiveSession.defaultMode', "Controls the default mode of the chat experience."),
default: 'chatView'
}
}
});
@@ -258,6 +247,7 @@ registerClearActions();
registerSingleton(IChatService, ChatService, InstantiationType.Delayed);
registerSingleton(IChatContributionService, ChatContributionService, InstantiationType.Delayed);
registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed);
registerSingleton(IQuickChatService, QuickChatService, InstantiationType.Delayed);
registerSingleton(IChatAccessibilityService, ChatAccessibilityService, InstantiationType.Delayed);
registerSingleton(IChatWidgetHistoryService, ChatWidgetHistoryService, InstantiationType.Delayed);
registerSingleton(IChatProviderService, ChatProviderService, InstantiationType.Delayed);
@@ -11,6 +11,7 @@ import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IChatWidgetService = createDecorator<IChatWidgetService>('chatWidgetService');
export const IQuickChatService = createDecorator<IQuickChatService>('quickChatService');
export const IChatAccessibilityService = createDecorator<IChatAccessibilityService>('chatAccessibilityService');
export interface IChatWidgetService {
@@ -32,6 +33,14 @@ export interface IChatWidgetService {
getWidgetBySessionId(sessionId: string): IChatWidget | undefined;
}
export interface IQuickChatService {
readonly _serviceBrand: undefined;
enabled: boolean;
toggle(providerId?: string, query?: string): void;
focus(): void;
close(): void;
openInChatView(): void;
}
export interface IChatAccessibilityService {
readonly _serviceBrand: undefined;
@@ -126,10 +126,7 @@ export class ChatContributionService implements IChatContributionService {
canToggleVisibility: false,
canMoveView: true,
ctorDescriptor: new SyncDescriptor(ChatViewPane, [<IChatViewOptions>{ providerId: providerDescriptor.id }]),
when: ContextKeyExpr.and(
ContextKeyExpr.deserialize(providerDescriptor.when),
ContextKeyExpr.notEquals('config.chat.experimental.defaultMode', 'quickQuestion')
)
when: ContextKeyExpr.deserialize(providerDescriptor.when)
}];
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, viewContainer);
@@ -63,7 +63,7 @@ import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups';
import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions';
import { CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_HAS_PROVIDER_ID, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { IChatReplyFollowup, IChatResponseProgressFileTreeData, IChatService, ISlashCommand, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { IChatResponseMarkdownRenderData, IChatResponseRenderData, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter';
import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer';
import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard';
@@ -103,7 +103,6 @@ export interface IChatListItemRendererOptions {
}
export class ChatListItemRenderer extends Disposable implements ITreeRenderer<ChatTreeItem, FuzzyScore, IChatListItemTemplate> {
static readonly cursorCharacter = '\u258c';
static readonly ID = 'item';
private readonly codeBlocksByResponseId = new Map<string, IChatCodeBlockInfo[]>();
@@ -399,29 +398,92 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
this.basicRenderElement(element.response.value, element, index, templateData);
isFullyRendered = true;
} else {
const renderValue = this.getWordsForProgressiveRender(element);
isFullyRendered = !!element.renderData?.isFullyRendered;
// Figure out what we need to render in addition to what has already been rendered
const currentResponseData = element.response.value;
element.renderData ??= { renderedParts: [] };
const renderedParts = element.renderData.renderedParts;
const wordCountResults: IWordCountResult[] = [];
const partsToRender: IChatResponseRenderData['renderedParts'] = [];
currentResponseData.forEach((part, index) => {
const renderedPart = renderedParts[index];
// Is this part completely new?
if (!renderedPart) {
if (isInteractiveProgressTreeData(part)) {
partsToRender[index] = part;
} else {
const wordCountResult = this.getDataForProgressiveRender(element, part);
if (wordCountResult !== undefined) {
partsToRender[index] = {
renderedWordCount: wordCountResult.actualWordCount,
lastRenderTime: Date.now(),
isFullyRendered: wordCountResult.isFullString,
};
wordCountResults[index] = wordCountResult;
}
}
}
// Did this part go from being a placeholder string to resolved tree data?
else if (isInteractiveProgressTreeData(part) && !isInteractiveProgressTreeData(renderedPart)) {
partsToRender[index] = part;
}
// Did this part's content change?
else if (!isInteractiveProgressTreeData(part) && !isInteractiveProgressTreeData(renderedPart) && !renderedPart.isFullyRendered) {
const wordCountResult = this.getDataForProgressiveRender(element, part, renderedPart);
if (wordCountResult !== undefined) {
partsToRender[index] = {
renderedWordCount: wordCountResult.actualWordCount,
lastRenderTime: Date.now(),
isFullyRendered: wordCountResult.isFullString,
};
wordCountResults[index] = wordCountResult;
}
}
});
isFullyRendered = partsToRender.length === 0;
if (isFullyRendered && element.isComplete) {
// Response is done and content is rendered, so do a normal render
this.traceLayout('runProgressiveRender', `end progressive render, index=${index} and clearing renderData, response is complete, index=${index}`);
element.renderData = undefined;
disposables.clear();
this.basicRenderElement(element.response.value, element, index, templateData);
} else if (renderValue) {
element.renderData = {
renderedWordCount: renderValue.actualWordCount,
lastRenderTime: Date.now(),
isFullyRendered: renderValue.isFullString
};
} else if (!isFullyRendered) {
let hasRenderedOneMarkdownBlock = false;
partsToRender.forEach((partToRender, index) => {
if (!partToRender) {
return;
}
const plusCursor = (renderValue.value.match(/```\s*$/) ?
renderValue.value + '\n\n' :
renderValue.value) + ` ${ChatListItemRenderer.cursorCharacter}`;
const result = this.renderMarkdown(new MarkdownString(plusCursor), element, disposables, templateData, true);
// Doing the progressive render
dom.clearNode(templateData.value);
templateData.value.appendChild(result.element);
disposables.add(result);
let result;
if (isInteractiveProgressTreeData(partToRender)) {
result = this.renderTreeData(partToRender, element, disposables, templateData, index);
}
// Avoid doing progressive rendering for multiple markdown parts simultaneously
else if (!hasRenderedOneMarkdownBlock) {
const value = wordCountResults[index].value;
result = this.renderMarkdown(new MarkdownString(value), element, disposables, templateData, true);
hasRenderedOneMarkdownBlock = true;
}
if (!result) {
return;
}
// Doing the progressive render
renderedParts[index] = partToRender;
const existingElement = templateData.value.children[index];
if (existingElement) {
templateData.value.replaceChild(result.element, existingElement);
} else {
templateData.value.appendChild(result.element);
}
disposables.add(result);
});
} else {
// Nothing new to render, not done, keep waiting
return false;
@@ -435,7 +497,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight });
}
return !!isFullyRendered;
return isFullyRendered;
}
private renderTreeData(data: IChatResponseProgressFileTreeData, element: ChatTreeItem, disposables: DisposableStore, templateData: IChatListItemTemplate, treeDataIndex: number): { element: HTMLElement; dispose: () => void } {
@@ -558,8 +620,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
return ref;
}
private getWordsForProgressiveRender(element: IChatResponseViewModel): IWordCountResult | undefined {
const renderData = element.renderData ?? { renderedWordCount: 0, lastRenderTime: 0 };
private getDataForProgressiveRender(element: IChatResponseViewModel, data: IMarkdownString, renderData: IChatResponseMarkdownRenderData = { renderedWordCount: 0, lastRenderTime: 0, isFullyRendered: false }): IWordCountResult | undefined {
const rate = this.getProgressiveRenderRate(element);
const numWordsToRender = renderData.lastRenderTime === 0 ?
1 :
@@ -571,7 +632,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
return undefined;
}
return getNWords(element.response.asString(), numWordsToRender);
return getNWords(data.value, numWordsToRender);
}
disposeElement(node: ITreeNode<ChatTreeItem, FuzzyScore>, index: number, templateData: IChatListItemTemplate): void {
@@ -886,28 +947,16 @@ class CodeBlockPart extends Disposable implements IChatResultCodeBlockPart {
}
private setText(newText: string): void {
let currentText = this.textModel.getLinesContent().join('\n');
const currentText = this.textModel.getLinesContent().join('\n');
if (newText === currentText) {
return;
}
let removedChars = 0;
if (currentText.endsWith(` ${ChatListItemRenderer.cursorCharacter}`)) {
removedChars = 2;
} else if (currentText.endsWith(ChatListItemRenderer.cursorCharacter)) {
removedChars = 1;
}
if (removedChars > 0) {
currentText = currentText.slice(0, currentText.length - removedChars);
}
if (newText.startsWith(currentText)) {
const text = newText.slice(currentText.length);
const lastLine = this.textModel.getLineCount();
const lastCol = this.textModel.getLineMaxColumn(lastLine);
const insertAtCol = lastCol - removedChars;
this.textModel.applyEdits([{ range: new Range(lastLine, insertAtCol, lastLine, lastCol), text }]);
this.textModel.applyEdits([{ range: new Range(lastLine, lastCol, lastLine, lastCol), text }]);
} else {
// console.log(`Failed to optimize setText`);
this.textModel.setValue(newText);
@@ -1133,3 +1182,7 @@ class ChatListTreeDataSource implements IAsyncDataSource<IChatResponseProgressFi
return element.children ?? [];
}
}
function isInteractiveProgressTreeData(item: IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IMarkdownString): item is IChatResponseProgressFileTreeData {
return 'label' in item;
}
@@ -0,0 +1,215 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IContextKeyService, IScopedContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IQuickInputService, IQuickWidget } from 'vs/platform/quickinput/common/quickInput';
import { editorBackground, editorForeground, inputBackground } from 'vs/platform/theme/common/colorRegistry';
import { IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat';
import { IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane';
import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget';
import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
export class QuickChatService implements IQuickChatService {
readonly _serviceBrand: undefined;
_input: IQuickWidget | undefined;
_currentChat: QuickChat | undefined;
constructor(
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IChatService private readonly chatService: IChatService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) { }
get enabled(): boolean {
return this.chatService.getProviderInfos().length > 0;
}
get focused(): boolean {
const widget = this._input?.widget as HTMLElement;
if (!widget) {
return false;
}
return dom.isAncestor(document.activeElement, widget);
}
toggle(providerId?: string, query?: string | undefined): void {
// If the input is already shown, hide it. This provides a toggle behavior of the quick pick
if (this.focused) {
this.close();
return;
}
// Check if any providers are available. If not, show nothing
// This shouldn't be needed because of the precondition, but just in case
const providerInfo = providerId
? this.chatService.getProviderInfos().find(info => info.id === providerId)
: this.chatService.getProviderInfos()[0];
if (!providerInfo) {
return;
}
const disposableStore = new DisposableStore();
this._input = this.quickInputService.createQuickWidget();
this._input.contextKey = 'chatInputVisible';
this._input.ignoreFocusOut = true;
disposableStore.add(this._input);
const containerSession = dom.$('.interactive-session');
this._input.widget = containerSession;
this._currentChat ??= this.instantiationService.createInstance(QuickChat, {
providerId: providerInfo.id,
});
// show needs to come before the current chat rendering
this._input.show();
this._currentChat.render(containerSession);
disposableStore.add(this._input.onDidHide(() => {
disposableStore.dispose();
this._input = undefined;
}));
this._currentChat.focus();
if (query) {
this._currentChat.setValue(query);
this._currentChat.acceptInput();
}
}
focus(): void {
this._currentChat?.focus();
}
close(): void {
this._input?.dispose();
}
async openInChatView(): Promise<void> {
await this._currentChat?.openChatView();
this.close();
}
}
class QuickChat extends Disposable {
private widget!: ChatWidget;
private model: ChatModel | undefined;
private _currentQuery: string | undefined;
private _scopedContextKeyService!: IScopedContextKeyService;
get scopedContextKeyService() {
return this._scopedContextKeyService;
}
constructor(
private readonly _options: IChatViewOptions,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IChatService private readonly chatService: IChatService,
@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService
) {
super();
}
clear() {
this.model?.dispose();
this.model = undefined;
this.updateModel();
this.widget.inputEditor.setValue('');
}
focus(): void {
if (this.widget) {
this.widget.focusInput();
}
}
render(parent: HTMLElement): void {
this._scopedContextKeyService?.dispose();
this._scopedContextKeyService = this._register(this.contextKeyService.createScoped(parent));
const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]));
this.widget?.dispose();
this.widget = this._register(
scopedInstantiationService.createInstance(
ChatWidget,
{ resource: true, renderInputOnTop: true, renderStyle: 'compact' },
{
listForeground: editorForeground,
listBackground: editorBackground,
inputEditorBackground: inputBackground,
resultEditorBackground: editorBackground
}));
this.widget.render(parent);
this.widget.setVisible(true);
this.widget.setDynamicChatTreeItemLayout(2, 600);
this.updateModel();
if (this._currentQuery) {
this.widget.inputEditor.setSelection({
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: this._currentQuery.length + 1
});
}
this.registerListeners();
}
private registerListeners(): void {
this._register(this.widget.inputEditor.onDidChangeModelContent((e) => {
this._currentQuery = this.widget.inputEditor.getValue();
}));
this._register(this.widget.onDidClear(() => this.clear()));
}
async acceptInput(): Promise<void> {
return this.widget.acceptInput();
}
async openChatView(): Promise<void> {
const widget = await this._chatWidgetService.revealViewForProvider(this._options.providerId);
if (!widget?.viewModel || !this.model) {
return;
}
for (const request of this.model.getRequests()) {
if (request.response?.response.value || request.response?.errorDetails) {
this.chatService.addCompleteRequest(widget.viewModel.sessionId,
request.message as string,
{
message: request.response.response.asString(),
errorDetails: request.response.errorDetails
});
} else if (request.message) {
}
}
const value = this.widget.inputEditor.getValue();
if (value) {
widget.inputEditor.setValue(value);
}
widget.focusInput();
}
setValue(value: string): void {
this.widget.inputEditor.setValue(value);
}
private updateModel(): void {
this.model ??= this.chatService.startSession(this._options.providerId, CancellationToken.None);
if (!this.model) {
throw new Error('Could not start chat session');
}
this.widget.setModel(this.model, { inputValue: this._currentQuery });
}
}
@@ -11,7 +11,7 @@ import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { IChatRequestModel, IChatResponseModel, IChatModel, IChatWelcomeMessageContent, IResponse, Response } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatResponseErrorDetails, IChatReplyFollowup, IChatResponseCommandFollowup, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatResponseErrorDetails, IChatReplyFollowup, IChatResponseCommandFollowup, InteractiveSessionVoteDirection, IChatResponseProgressFileTreeData } from 'vs/workbench/contrib/chat/common/chatService';
import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter';
export function isRequestVM(item: unknown): item is IChatRequestViewModel {
@@ -56,12 +56,16 @@ export interface IChatRequestViewModel {
currentRenderedHeight: number | undefined;
}
export interface IChatResponseRenderData {
export interface IChatResponseMarkdownRenderData {
renderedWordCount: number;
lastRenderTime: number;
isFullyRendered: boolean;
}
export interface IChatResponseRenderData {
renderedParts: (IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData)[];
}
export interface IChatLiveUpdateData {
wordCountAfterLastUpdate: number;
loadingStartTime: number;
@@ -327,8 +331,9 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi
const wordCount = countWords(_model.response.asString());
const timeDiff = now - this._contentUpdateTimings!.loadingStartTime;
const impliedWordLoadRate = wordCount / (timeDiff / 1000);
const renderedWordCount = this.renderData?.renderedParts.reduce((acc, part) => acc += ('label' in part ? 0 : part.renderedWordCount), 0);
if (!this.isComplete) {
this.trace('onDidChange', `Update- got ${wordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${this.renderData?.renderedWordCount} words are rendered.`);
this.trace('onDidChange', `Update- got ${wordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${renderedWordCount} words are rendered.`);
this._contentUpdateTimings = {
loadingStartTime: this._contentUpdateTimings!.loadingStartTime,
lastUpdateTime: now,
@@ -336,7 +341,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi
impliedWordLoadRate
};
} else {
this.trace(`onDidChange`, `Done- got ${wordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${this.renderData?.renderedWordCount} words are rendered.`);
this.trace(`onDidChange`, `Done- got ${wordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${renderedWordCount} words are rendered.`);
}
} else {
this.logService.warn('ChatResponseViewModel#onDidChange: got model update but contentUpdateTimings is not initialized');
@@ -344,10 +349,6 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi
// new data -> new id, new content to render
this._modelChangeCount++;
if (this.renderData) {
this.renderData.isFullyRendered = false;
this.renderData.lastRenderTime = Date.now();
}
this._onDidChange.fire();
}));
@@ -273,7 +273,15 @@ Registry.as<IQuickAccessRegistry>(QuickaccessExtensions.Quickaccess).registerQui
contextKey: 'inFileSymbolsPicker',
placeholder: localize('gotoSymbolQuickAccessPlaceholder', "Type the name of a symbol to go to."),
helpEntries: [
{ description: localize('gotoSymbolQuickAccess', "Go to Symbol in Editor"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, commandId: GotoSymbolAction.ID },
{ description: localize('gotoSymbolByCategoryQuickAccess', "Go to Symbol in Editor by Category"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX_BY_CATEGORY }
{
description: localize('gotoSymbolQuickAccess', "Go to Symbol in Editor"),
prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX,
commandId: GotoSymbolAction.ID,
commandCenterOrder: 40
},
{
description: localize('gotoSymbolByCategoryQuickAccess', "Go to Symbol in Editor by Category"),
prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX_BY_CATEGORY
}
]
});
@@ -77,7 +77,11 @@ Registry.as<IQuickAccessRegistry>(QuickAccessExtensions.Quickaccess).registerQui
prefix: DEBUG_QUICK_ACCESS_PREFIX,
contextKey: 'inLaunchConfigurationsPicker',
placeholder: nls.localize('startDebugPlaceholder', "Type the name of a launch configuration to run."),
helpEntries: [{ description: nls.localize('startDebuggingHelp', "Start Debugging"), commandId: SELECT_AND_START_ID }]
helpEntries: [{
description: nls.localize('startDebuggingHelp', "Start Debugging"),
commandId: SELECT_AND_START_ID,
commandCenterOrder: 50
}]
});
// Register quick access for debug console
@@ -206,14 +206,17 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
} else if (shouldAutoResumeOnReload) {
// The application has previously launched via a protocol URL Continue On flow
const hasApplicationLaunchedFromContinueOnFlow = this.storageService.getBoolean(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, StorageScope.APPLICATION, false);
this.logService.info(`Prompting to enable cloud changes, has application previously launched from Continue On flow: ${hasApplicationLaunchedFromContinueOnFlow}`);
const handlePendingEditSessions = () => {
// display a badge in the accounts menu but do not prompt the user to sign in again
this.logService.info('Showing badge to enable cloud changes in accounts menu...');
this.updateAccountsMenuBadge();
this.pendingEditSessionsContext.set(true);
// attempt a resume if we are in a pending state and the user just signed in
const disposable = this.editSessionsStorageService.onDidSignIn(async () => {
disposable.dispose();
this.logService.info('Showing badge to enable cloud changes in accounts menu succeeded, resuming cloud changes...');
await this.progressService.withProgress(resumeProgressOptions, async (progress) => await this.resumeEditSession(undefined, true, undefined, undefined, progress));
this.storageService.remove(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, StorageScope.APPLICATION);
this.environmentService.continueOn = undefined;
@@ -227,8 +230,10 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
) {
// store the fact that we prompted the user
this.storageService.store(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);
this.logService.info('Prompting to enable cloud changes...');
await this.editSessionsStorageService.initialize('read');
if (this.editSessionsStorageService.isSignedIn) {
this.logService.info('Prompting to enable cloud changes succeeded, resuming cloud changes...');
await this.progressService.withProgress(resumeProgressOptions, async (progress) => await this.resumeEditSession(undefined, true, undefined, undefined, progress));
} else {
handlePendingEditSessions();
@@ -239,6 +244,8 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
) {
handlePendingEditSessions();
}
} else {
this.logService.debug('Auto resuming cloud changes disabled.');
}
}
@@ -5,42 +5,59 @@
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { localize } from 'vs/nls';
import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { INotebookOutputActionContext, NotebookAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions';
import { INotebookOutputActionContext, NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/controller/coreActions';
import { NOTEBOOK_CELL_HAS_OUTPUTS } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons';
import { ILogService } from 'vs/platform/log/common/log';
import { copyCellOutput } from 'vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { ICellOutputViewModel, ICellViewModel, INotebookEditor, getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
export const COPY_OUTPUT_COMMAND_ID = 'notebook.cellOutput.copyToClipboard';
registerAction2(class CopyCellOutputAction extends NotebookAction {
registerAction2(class CopyCellOutputAction extends Action2 {
constructor() {
super(
{
id: COPY_OUTPUT_COMMAND_ID,
title: localize('notebookActions.copyOutput', "Copy Output to Clipboard"),
menu: {
id: MenuId.NotebookOutputToolbar,
when: NOTEBOOK_CELL_HAS_OUTPUTS
},
icon: icons.copyIcon,
});
super({
id: COPY_OUTPUT_COMMAND_ID,
title: localize('notebookActions.copyOutput', "Copy Output to Clipboard"),
menu: {
id: MenuId.NotebookOutputToolbar,
when: NOTEBOOK_CELL_HAS_OUTPUTS
},
category: NOTEBOOK_ACTIONS_CATEGORY,
icon: icons.copyIcon,
});
}
async runWithContext(accessor: ServicesAccessor, context: INotebookOutputActionContext): Promise<void> {
const outputViewModel = context.outputViewModel;
async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel }): Promise<void> {
const editorService = accessor.get(IEditorService);
const notebookEditor = getNotebookEditorFromEditorPane(editorService.activeEditorPane);
if (!notebookEditor) {
return;
}
let outputViewModel: ICellOutputViewModel | undefined;
if ('outputId' in outputContext && typeof outputContext.outputId === 'string') {
outputViewModel = getOutputViewModelFromId(outputContext.outputId, notebookEditor);
} else {
outputViewModel = outputContext.outputViewModel;
}
if (!outputViewModel) {
return;
}
const mimeType = outputViewModel.pickedMimeType?.mimeType;
if (mimeType?.startsWith('image/')) {
const editorService = accessor.get(IEditorService);
const editor = editorService.activeEditorPane?.getControl() as INotebookEditor;
await editor.focusNotebookCell(outputViewModel.cellViewModel as ICellViewModel, 'output', { skipReveal: true, outputId: outputViewModel.model.outputId });
editor.copyOutputImage(outputViewModel);
const focusOptions = { skipReveal: true, outputId: outputViewModel.model.outputId };
await notebookEditor.focusNotebookCell(outputViewModel.cellViewModel as ICellViewModel, 'output', focusOptions);
notebookEditor.copyOutputImage(outputViewModel);
} else {
const clipboardService = accessor.get(IClipboardService);
const logService = accessor.get(ILogService);
@@ -48,4 +65,20 @@ registerAction2(class CopyCellOutputAction extends NotebookAction {
copyCellOutput(mimeType, outputViewModel, clipboardService, logService);
}
}
});
function getOutputViewModelFromId(outputId: string, notebookEditor: INotebookEditor): ICellOutputViewModel | undefined {
const notebookViewModel = notebookEditor.getViewModel();
if (notebookViewModel) {
const codeCells = notebookViewModel.viewCells.filter(cell => cell.cellKind === CellKind.Code) as CodeCellViewModel[];
for (const cell of codeCells) {
const output = cell.outputsViewModels.find(output => output.model.outputId === outputId);
if (output) {
return output;
}
}
}
return undefined;
}
@@ -1074,10 +1074,12 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Themable {
allowScripts: true,
localResourceRoots: this.localResourceRootsCache,
},
extension: undefined
extension: undefined,
providedViewType: 'notebook.output'
});
webview.setHtml(content);
webview.setContextKeyService(this.contextKeyService);
return webview;
}
@@ -1362,33 +1362,38 @@ async function webviewPreloads(ctx: PreloadContext) {
});
};
const copyImage = async (image: HTMLImageElement, retries = 5) => {
const copyOutputImage = async (outputId: string, retries = 5) => {
if (!document.hasFocus() && retries > 0) {
// copyImage can be called from outside of the webview, which means this function may be running whilst the webview is gaining focus.
// Since navigator.clipboard.write requires the document to be focused, we need to wait for focus.
// We cannot use a listener, as there is a high chance the focus is gained during the setup of the listener resulting in us missing it.
setTimeout(() => { copyImage(image, retries - 1); }, 20);
setTimeout(() => { copyOutputImage(outputId, retries - 1); }, 20);
return;
}
try {
await navigator.clipboard.write([new ClipboardItem({
'image/png': new Promise((resolve) => {
const canvas = document.createElement('canvas');
if (canvas !== null) {
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const context = canvas.getContext('2d');
context?.drawImage(image, 0, 0);
}
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
const image = document.getElementById(outputId)?.querySelector('img');
if (image) {
await navigator.clipboard.write([new ClipboardItem({
'image/png': new Promise((resolve) => {
const canvas = document.createElement('canvas');
if (canvas !== null) {
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const context = canvas.getContext('2d');
context?.drawImage(image, 0, 0);
}
canvas.remove();
}, 'image/png');
})
})]);
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
}
canvas.remove();
}, 'image/png');
})
})]);
} else {
console.error('Could not find image element to copy for output with id', outputId);
}
} catch (e) {
console.error('Could not copy image:', e);
}
@@ -1495,12 +1500,8 @@ async function webviewPreloads(ctx: PreloadContext) {
break;
}
case 'copyImage': {
const image = document.getElementById(event.data.outputId)?.querySelector('img');
if (image) {
await copyImage(image);
} else {
console.warn('Could not find image element to copy for output with id', event.data.outputId);
}
await copyOutputImage(event.data.outputId);
break;
}
@@ -2561,6 +2562,7 @@ async function webviewPreloads(ctx: PreloadContext) {
) {
this.element = document.createElement('div');
this.element.classList.add('output_container');
this.element.setAttribute('data-vscode-context', JSON.stringify({ 'preventDefaultContextMenuItems': true }));
this.element.style.position = 'absolute';
this.element.style.overflow = 'hidden';
}
@@ -66,6 +66,7 @@ import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/co
import { IProductService } from 'vs/platform/product/common/productService';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands';
import { IEditorProgressService } from 'vs/platform/progress/common/progress';
export const enum SettingsFocusContext {
@@ -237,6 +238,7 @@ export class SettingsEditor2 extends EditorPane {
@IProductService private readonly productService: IProductService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IEditorProgressService private readonly editorProgressService: IEditorProgressService,
) {
super(SettingsEditor2.ID, telemetryService, themeService, storageService);
this.delayedFilterLogging = new Delayer<void>(1000);
@@ -566,6 +568,9 @@ export class SettingsEditor2 extends EditorPane {
this.searchWidget.updateAriaLabel(label);
}
/**
* Render the header of the Settings editor, which includes the content above the splitview.
*/
private createHeader(parent: HTMLElement): void {
this.headerContainer = DOM.append(parent, $('.settings-header'));
@@ -1622,19 +1627,20 @@ export class SettingsEditor2 extends EditorPane {
// Trigger the local search. If it didn't find an exact match, trigger the remote search.
const searchInProgress = this.searchInProgress = new CancellationTokenSource();
return this.localSearchDelayer.trigger(() => {
return this.localSearchDelayer.trigger(async () => {
if (searchInProgress && !searchInProgress.token.isCancellationRequested) {
return this.localFilterPreferences(query).then(result => {
if (result && !result.exactMatch) {
this.remoteSearchThrottle.trigger(() => {
return searchInProgress && !searchInProgress.token.isCancellationRequested ?
this.remoteSearchPreferences(query, this.searchInProgress!.token) :
Promise.resolve();
});
}
});
} else {
return Promise.resolve();
const progressRunner = this.editorProgressService.show(true);
const result = await this.localFilterPreferences(query);
if (result && !result.exactMatch) {
this.remoteSearchThrottle.trigger(async () => {
if (searchInProgress && !searchInProgress.token.isCancellationRequested) {
await this.remoteSearchPreferences(query, this.searchInProgress!.token);
}
progressRunner.done();
});
} else {
progressRunner.done();
}
}
});
}
@@ -1654,36 +1660,32 @@ export class SettingsEditor2 extends EditorPane {
]).then(() => { });
}
private filterOrSearchPreferences(query: string, type: SearchResultIdx, searchProvider?: ISearchProvider, token?: CancellationToken): Promise<ISearchResult | null> {
return this._filterOrSearchPreferencesModel(query, this.defaultSettingsEditorModel, searchProvider, token).then(result => {
if (token && token.isCancellationRequested) {
// Handle cancellation like this because cancellation is lost inside the search provider due to async/await
return null;
}
if (!this.searchResultModel) {
this.searchResultModel = this.instantiationService.createInstance(SearchResultModel, this.viewState, this.workspaceTrustManagementService.isWorkspaceTrusted());
// Must be called before this.renderTree()
// to make sure the search results count is set.
this.searchResultModel.setResult(type, result);
this.tocTreeModel.currentSearchModel = this.searchResultModel;
this.onSearchModeToggled();
} else {
this.searchResultModel.setResult(type, result);
this.tocTreeModel.update();
}
if (type === SearchResultIdx.Local) {
this.tocTree.setFocus([]);
this.viewState.filterToCategory = undefined;
this.tocTree.expandAll();
}
this.settingsTree.scrollTop = 0;
this.refreshTOCTree();
this.renderTree(undefined, true);
return result;
});
private async filterOrSearchPreferences(query: string, type: SearchResultIdx, searchProvider?: ISearchProvider, token?: CancellationToken): Promise<ISearchResult | null> {
const result = await this._filterOrSearchPreferencesModel(query, this.defaultSettingsEditorModel, searchProvider, token);
if (token?.isCancellationRequested) {
// Handle cancellation like this because cancellation is lost inside the search provider due to async/await
return null;
}
if (!this.searchResultModel) {
this.searchResultModel = this.instantiationService.createInstance(SearchResultModel, this.viewState, this.workspaceTrustManagementService.isWorkspaceTrusted());
// Must be called before this.renderTree()
// to make sure the search results count is set.
this.searchResultModel.setResult(type, result);
this.tocTreeModel.currentSearchModel = this.searchResultModel;
this.onSearchModeToggled();
} else {
this.searchResultModel.setResult(type, result);
this.tocTreeModel.update();
}
if (type === SearchResultIdx.Local) {
this.tocTree.setFocus([]);
this.viewState.filterToCategory = undefined;
this.tocTree.expandAll();
}
this.settingsTree.scrollTop = 0;
this.refreshTOCTree();
this.renderTree(undefined, true);
return result;
}
private renderResultCountMessages() {
@@ -208,18 +208,13 @@ export abstract class AbstractListSettingWidget<TDataItem extends object> extend
}
const header = this.renderHeader();
const ITEM_HEIGHT = 24;
let listHeight = ITEM_HEIGHT * this.model.items.length;
if (header) {
listHeight += ITEM_HEIGHT;
this.listElement.appendChild(header);
}
this.rowElements = this.model.items.map((item, i) => this.renderDataOrEditItem(item, i, focused));
this.rowElements.forEach(rowElement => this.listElement.appendChild(rowElement));
this.listElement.style.height = listHeight + 'px';
}
protected createBasicSelectBox(value: IObjectEnumData): SelectBox {
@@ -24,7 +24,11 @@ quickAccessRegistry.registerQuickAccessProvider({
ctor: HelpQuickAccessProvider,
prefix: HelpQuickAccessProvider.PREFIX,
placeholder: localize('helpQuickAccessPlaceholder', "Type '{0}' to get help on the actions you can take from here.", HelpQuickAccessProvider.PREFIX),
helpEntries: [{ description: localize('helpQuickAccess', "Show all Quick Access Providers") }]
helpEntries: [{
description: localize('helpQuickAccess', "Show all Quick Access Providers"),
commandCenterOrder: 70,
commandCenterLabel: localize('more', 'More')
}]
});
quickAccessRegistry.registerQuickAccessProvider({
@@ -40,7 +44,7 @@ quickAccessRegistry.registerQuickAccessProvider({
prefix: CommandsQuickAccessProvider.PREFIX,
contextKey: 'inCommandsPicker',
placeholder: localize('commandsQuickAccessPlaceholder', "Type the name of a command to run."),
helpEntries: [{ description: localize('commandsQuickAccess', "Show and Run Commands"), commandId: ShowAllCommandsAction.ID }]
helpEntries: [{ description: localize('commandsQuickAccess', "Show and Run Commands"), commandId: ShowAllCommandsAction.ID, commandCenterOrder: 20 }]
});
//#endregion
@@ -218,8 +218,8 @@ class PortColumn implements ITableColumn<ITunnelItem, ActionBarCell> {
}
class LocalAddressColumn implements ITableColumn<ITunnelItem, ActionBarCell> {
readonly label: string = nls.localize('tunnel.addressColumn.label', "Local Address");
readonly tooltip: string = nls.localize('tunnel.addressColumn.tooltip', "The address that the forwarded port is available at locally.");
readonly label: string = nls.localize('tunnel.addressColumn.label', "Forwarded Address");
readonly tooltip: string = nls.localize('tunnel.addressColumn.tooltip', "The address that the forwarded port is available at.");
readonly weight: number = 1;
readonly templateId: string = 'actionbar';
project(row: ITunnelItem): ActionBarCell {
@@ -40,7 +40,7 @@ import { Schemas } from 'vs/base/common/network';
import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { ResourceMap } from 'vs/base/common/map';
import { SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess';
import { AnythingQuickAccessProviderRunOptions, DefaultQuickAccessFilterValue } from 'vs/platform/quickinput/common/quickAccess';
import { AnythingQuickAccessProviderRunOptions, DefaultQuickAccessFilterValue, Extensions, IQuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess';
import { IWorkbenchQuickAccessConfiguration } from 'vs/workbench/browser/quickaccess';
import { GotoSymbolQuickAccessProvider } from 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
@@ -52,11 +52,11 @@ import { Codicon } from 'vs/base/common/codicons';
import { ThemeIcon } from 'vs/base/common/themables';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { stripIcons } from 'vs/base/common/iconLabels';
import { HelpQuickAccessProvider } from 'vs/platform/quickinput/browser/helpQuickAccess';
import { CommandsQuickAccessProvider } from 'vs/workbench/contrib/quickaccess/browser/commandsQuickAccess';
import { DEBUG_QUICK_ACCESS_PREFIX } from 'vs/workbench/contrib/debug/browser/debugCommands';
import { TasksQuickAccessProvider } from 'vs/workbench/contrib/tasks/browser/tasksQuickAccess';
import { Lazy } from 'vs/base/common/lazy';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { Registry } from 'vs/platform/registry/common/platform';
import { ASK_QUICK_QUESTION_ACTION_ID } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions';
import { IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat';
interface IAnythingQuickPickItem extends IPickerQuickAccessItem, IQuickPickItemWithResource { }
@@ -187,6 +187,8 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
@ITextModelService private readonly textModelService: ITextModelService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IKeybindingService private readonly keybindingService: IKeybindingService,
@IQuickChatService private readonly quickChatService: IQuickChatService,
) {
super(AnythingQuickAccessProvider.PREFIX, {
canAcceptInBackground: true,
@@ -756,68 +758,54 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
//#endregion
//#region Help (if enabled)
//#region Command Center (if enabled)
private helpQuickAccess = this.instantiationService.createInstance(HelpQuickAccessProvider);
private readonly lazyRegistry = new Lazy(() => Registry.as<IQuickAccessRegistry>(Extensions.Quickaccess));
private getHelpPicks(query: IPreparedQuery, token: CancellationToken, runOptions?: AnythingQuickAccessProviderRunOptions): IAnythingQuickPickItem[] {
if (query.normalized) {
return []; // If there's a filter, we don't show the help
}
type IHelpAnythingQuickPickItem = IAnythingQuickPickItem & { prefix: string };
const providers: Array<IHelpAnythingQuickPickItem> = this.helpQuickAccess.getQuickAccessProviders();
const mapOfProviders = new Map<string, IHelpAnythingQuickPickItem>();
for (const provider of providers) {
mapOfProviders.set(provider.prefix, provider);
type IHelpAnythingQuickPickItem = IAnythingQuickPickItem & { commandCenterOrder: number };
const providers: IHelpAnythingQuickPickItem[] = this.lazyRegistry.value.getQuickAccessProviders()
.filter(p => p.helpEntries.some(h => h.commandCenterOrder !== undefined))
.flatMap(provider => provider.helpEntries
.filter(h => h.commandCenterOrder !== undefined)
.map(helpEntry => {
const providerSpecificOptions: AnythingQuickAccessProviderRunOptions | undefined = {
...runOptions,
includeHelp: provider.prefix === AnythingQuickAccessProvider.PREFIX ? false : runOptions?.includeHelp
};
const label = helpEntry.commandCenterLabel ?? helpEntry.description!;
return {
label,
description: helpEntry.prefix ?? provider.prefix,
commandCenterOrder: helpEntry.commandCenterOrder!,
keybinding: helpEntry.commandId ? this.keybindingService.lookupKeybinding(helpEntry.commandId) : undefined,
ariaLabel: localize('helpPickAriaLabel', "{0}, {1}", label, helpEntry.description),
accept: () => {
this.quickInputService.quickAccess.show(provider.prefix, {
preserveValue: true,
providerOptions: providerSpecificOptions
});
}
};
}));
// TODO: There has to be a better place for this, but it's the first time we are adding a non-quick access provider
// to the command center, so for now, let's do this.
if (this.quickChatService.enabled) {
providers.push({
label: localize('chat', "Open Quick Chat"),
commandCenterOrder: 30,
keybinding: this.keybindingService.lookupKeybinding(ASK_QUICK_QUESTION_ACTION_ID),
accept: () => this.quickChatService.toggle()
});
}
const importantProviders: Array<IHelpAnythingQuickPickItem> = [];
const AddProvider = (prefix: string, modifications: Partial<IAnythingQuickPickItem> = {}) => {
if (mapOfProviders.has(prefix)) {
const provider = mapOfProviders.get(prefix)!;
// We swap the label and description in this to emphasize the ability
// not the prefix.
provider.label = provider.description!;
provider.description = provider.prefix;
// If the user chooses 'Go to File' the help should go away as if they were
// entering a new mode
const providerSpecificOptions: AnythingQuickAccessProviderRunOptions | undefined = {
...runOptions,
includeHelp: provider.prefix === AnythingQuickAccessProvider.PREFIX ? false : runOptions?.includeHelp
};
importantProviders.push({
...mapOfProviders.get(prefix)!,
...modifications,
accept: () => {
this.quickInputService.quickAccess.show(provider.prefix, {
preserveValue: true,
providerOptions: providerSpecificOptions
});
}
});
}
};
// TODO@TylerLeonhardt ideally this hardcoded list and hardcoded dependency moves
// into a provider model where when I register a quick access provider I can enlist
// for showing up in command center
// Acts as the ordering too
AddProvider(AnythingQuickAccessProvider.PREFIX);
AddProvider(CommandsQuickAccessProvider.PREFIX);
AddProvider(GotoSymbolQuickAccessProvider.PREFIX);
AddProvider(DEBUG_QUICK_ACCESS_PREFIX);
AddProvider(TasksQuickAccessProvider.PREFIX);
AddProvider(HelpQuickAccessProvider.PREFIX, {
// More concise
label: localize('more', 'More')
});
return importantProviders;
return providers.sort((a, b) => a.commandCenterOrder - b.commandCenterOrder);
}
//#endregion
@@ -107,7 +107,11 @@ quickAccessRegistry.registerQuickAccessProvider({
prefix: AnythingQuickAccessProvider.PREFIX,
placeholder: nls.localize('anythingQuickAccessPlaceholder', "Search files by name (append {0} to go to line or {1} to go to symbol)", AbstractGotoLineQuickAccessProvider.PREFIX, GotoSymbolQuickAccessProvider.PREFIX),
contextKey: defaultQuickAccessContextKeyValue,
helpEntries: [{ description: nls.localize('anythingQuickAccess', "Go to File"), commandId: 'workbench.action.quickOpen' }]
helpEntries: [{
description: nls.localize('anythingQuickAccess', "Go to File"),
commandId: 'workbench.action.quickOpen',
commandCenterOrder: 10
}]
});
quickAccessRegistry.registerQuickAccessProvider({
@@ -408,7 +408,7 @@ quickAccessRegistry.registerQuickAccessProvider({
prefix: TasksQuickAccessProvider.PREFIX,
contextKey: tasksPickerContextKey,
placeholder: nls.localize('tasksQuickAccessPlaceholder', "Type the name of a task to run."),
helpEntries: [{ description: nls.localize('tasksQuickAccessHelp', "Run Task") }]
helpEntries: [{ description: nls.localize('tasksQuickAccessHelp', "Run Task"), commandCenterOrder: 60 }]
});
// tasks.json validation
@@ -0,0 +1,101 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Delayer } from 'vs/base/common/async';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Disposable } from 'vs/base/common/lifecycle';
import { OperatingSystem } from 'vs/base/common/platform';
import { MicrotaskDelay } from 'vs/base/common/symbols';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore';
import { IMergedEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable';
import { ITerminalBackend } from 'vs/platform/terminal/common/terminal';
import { IDetachedTerminalInstance, IDetachedXTermOptions, IDetachedXtermTerminal, ITerminalContribution, IXtermAttachToElementOptions } from 'vs/workbench/contrib/terminal/browser/terminal';
import { TerminalExtensionsRegistry } from 'vs/workbench/contrib/terminal/browser/terminalExtensions';
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager';
import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal';
import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { ITerminalProcessInfo, ProcessState } from 'vs/workbench/contrib/terminal/common/terminal';
export class DeatachedTerminal extends Disposable implements IDetachedTerminalInstance {
private readonly _widgets = this._register(new TerminalWidgetManager());
public readonly capabilities = new TerminalCapabilityStore();
private readonly _contributions: Map<string, ITerminalContribution> = new Map();
public get xterm(): IDetachedXtermTerminal {
return this._xterm;
}
constructor(
private readonly _xterm: XtermTerminal,
options: IDetachedXTermOptions,
@IInstantiationService instantiationService: IInstantiationService,
) {
super();
this._register(_xterm);
// Initialize contributions
const contributionDescs = TerminalExtensionsRegistry.getTerminalContributions();
for (const desc of contributionDescs) {
if (this._contributions.has(desc.id)) {
onUnexpectedError(new Error(`Cannot have two terminal contributions with the same id ${desc.id}`));
continue;
}
if (desc.canRunInDetachedTerminals === false) {
continue;
}
let contribution: ITerminalContribution;
try {
contribution = instantiationService.createInstance(desc.ctor, this, options.processInfo, this._widgets);
this._contributions.set(desc.id, contribution);
this._register(contribution);
} catch (err) {
onUnexpectedError(err);
}
}
// xterm is already by the time DetachedTerminal is created, so trigger everything
// on the next microtask, allowing the caller to do any extra initialization
this._register(new Delayer(MicrotaskDelay)).trigger(() => {
for (const contr of this._contributions.values()) {
contr.xtermReady?.(this._xterm);
}
});
}
attachToElement(container: HTMLElement, options?: Partial<IXtermAttachToElementOptions> | undefined): void {
const screenElement = this._xterm.attachToElement(container, options);
this._widgets.attachToElement(screenElement);
}
}
/**
* Implements {@link ITerminalProcessInfo} for a detached terminal where most
* properties are stubbed. Properties are mutable and can be updated by
* the instantiator.
*/
export class DetachedProcessInfo implements ITerminalProcessInfo {
processState = ProcessState.Running;
ptyProcessReady = Promise.resolve();
shellProcessId: number | undefined;
remoteAuthority: string | undefined;
os: OperatingSystem | undefined;
userHome: string | undefined;
initialCwd = '';
environmentVariableInfo: IEnvironmentVariableInfo | undefined;
persistentProcessId: number | undefined;
shouldPersist = false;
hasWrittenData = false;
hasChildProcesses = false;
backend: ITerminalBackend | undefined;
capabilities = new TerminalCapabilityStore();
shellIntegrationNonce = '';
extEnvironmentVariableCollection: IMergedEnvironmentVariableCollection | undefined;
constructor(initialValues: Partial<ITerminalProcessInfo>) {
Object.assign(this, initialValues);
}
}
@@ -22,7 +22,7 @@ import { IEditableData } from 'vs/workbench/common/views';
import { ITerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList';
import { ScrollPosition } from 'vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon';
import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal';
import { IRegisterContributedProfileArgs, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalFont, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal';
import { IRegisterContributedProfileArgs, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalFont, ITerminalProcessExtHostProxy, ITerminalProcessInfo } from 'vs/workbench/contrib/terminal/common/terminal';
import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn';
import { ISimpleSelectedSuggestion } from 'vs/workbench/services/suggest/browser/simpleSuggestWidget';
import type { IMarker, Terminal as RawXtermTerminal } from 'xterm';
@@ -146,8 +146,32 @@ export interface IDetachedXTermOptions {
colorProvider: IXtermColorProvider;
capabilities?: ITerminalCapabilityStore;
readonly?: boolean;
processInfo: ITerminalProcessInfo;
}
/**
* A {@link ITerminalInstance}-like object that emulates a subset of
* capabilities. This instance is returned from {@link ITerminalService.createDetachedTerminal}
* to represent terminals that appear in other parts of the VS Code UI outside
* of the "Terminal" view or editors.
*/
export interface IDetachedTerminalInstance extends IDisposable {
readonly xterm: IDetachedXtermTerminal;
readonly capabilities: ITerminalCapabilityStore;
/**
* Attached the terminal to the given element. This should be preferred over
* calling {@link IXtermTerminal.attachToElement} so that extra DOM elements
* for contributions are initialized.
*
* @param container Container the terminal will be rendered in
* @param options Additional options for mounting the terminal in an element
*/
attachToElement(container: HTMLElement, options?: Partial<IXtermAttachToElementOptions>): void;
}
export const isDetachedTerminalInstance = (t: ITerminalInstance | IDetachedTerminalInstance): t is IDetachedTerminalInstance => typeof (t as ITerminalInstance).instanceId === 'number';
export interface ITerminalService extends ITerminalInstanceHost {
readonly _serviceBrand: undefined;
@@ -192,7 +216,7 @@ export interface ITerminalService extends ITerminalInstanceHost {
* tracked as a terminal instance.
* @params options The options to create the terminal with
*/
createDetachedXterm(options: IDetachedXTermOptions): Promise<IDetachedXtermTerminal>;
createDetachedTerminal(options: IDetachedXTermOptions): Promise<IDetachedTerminalInstance>;
/**
* Creates a raw terminal instance, this should not be used outside of the terminal part.
@@ -1107,7 +1131,6 @@ export interface IXtermTerminal extends IDisposable {
}
export interface IDetachedXtermTerminal extends IXtermTerminal {
/**
* Writes data to the terminal.
* @param data data to write
@@ -5,19 +5,24 @@
import { BrandedService, IConstructorSignature } from 'vs/platform/instantiation/common/instantiation';
import { Registry } from 'vs/platform/registry/common/platform';
import { ITerminalContribution, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal';
import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal';
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager';
import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal';
import { ITerminalProcessInfo, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal';
/** Constructor compatible with full terminal instances, is assignable to {@link DetachedCompatibleTerminalContributionCtor} */
export type TerminalContributionCtor = IConstructorSignature<ITerminalContribution, [ITerminalInstance, ITerminalProcessManager, TerminalWidgetManager]>;
/** Constructor compatible with detached terminals */
export type DetachedCompatibleTerminalContributionCtor = IConstructorSignature<ITerminalContribution, [IDetachedTerminalInstance, ITerminalProcessInfo, TerminalWidgetManager]>;
export interface ITerminalContributionDescription {
readonly id: string;
readonly ctor: TerminalContributionCtor;
}
export type ITerminalContributionDescription = { readonly id: string } & (
| { readonly canRunInDetachedTerminals: false; readonly ctor: TerminalContributionCtor }
| { readonly canRunInDetachedTerminals: true; readonly ctor: DetachedCompatibleTerminalContributionCtor }
);
export function registerTerminalContribution<Services extends BrandedService[]>(id: string, ctor: { new(instance: ITerminalInstance, processManager: ITerminalProcessManager, widgetManager: TerminalWidgetManager, ...services: Services): ITerminalContribution }): void {
TerminalContributionRegistry.INSTANCE.registerTerminalContribution(id, ctor);
export function registerTerminalContribution<Services extends BrandedService[]>(id: string, ctor: { new(instance: ITerminalInstance, processManager: ITerminalProcessManager, widgetManager: TerminalWidgetManager, ...services: Services): ITerminalContribution }, canRunInDetachedTerminals?: false): void;
export function registerTerminalContribution<Services extends BrandedService[]>(id: string, ctor: { new(instance: ITerminalInstance, processManager: ITerminalProcessInfo, widgetManager: TerminalWidgetManager, ...services: Services): ITerminalContribution }, canRunInDetachedTerminals: true): void;
export function registerTerminalContribution<Services extends BrandedService[]>(id: string, ctor: { new(instance: ITerminalInstance, processManager: ITerminalProcessManager, widgetManager: TerminalWidgetManager, ...services: Services): ITerminalContribution }, canRunInDetachedTerminals = false): void {
TerminalContributionRegistry.INSTANCE.registerTerminalContribution({ id, ctor, canRunInDetachedTerminals } as ITerminalContributionDescription);
}
export namespace TerminalExtensionsRegistry {
@@ -35,8 +40,8 @@ class TerminalContributionRegistry {
constructor() {
}
public registerTerminalContribution<Services extends BrandedService[]>(id: string, ctor: { new(instance: ITerminalInstance, processManager: ITerminalProcessManager, widgetManager: TerminalWidgetManager, ...services: Services): ITerminalContribution }): void {
this._terminalContributions.push({ id, ctor: ctor as TerminalContributionCtor });
public registerTerminalContribution(description: ITerminalContributionDescription): void {
this._terminalContributions.push(description);
}
public getTerminalContributions(): ITerminalContributionDescription[] {
@@ -29,7 +29,7 @@ import { ThemeIcon } from 'vs/base/common/themables';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { VirtualWorkspaceContext } from 'vs/workbench/common/contextkeys';
import { IEditableData, IViewsService } from 'vs/workbench/common/views';
import { ICreateTerminalOptions, IDetachedXTermOptions, IDetachedXtermTerminal, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalService, ITerminalServiceNativeDelegate, IXtermTerminal, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ICreateTerminalOptions, IDetachedTerminalInstance, IDetachedXTermOptions, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalService, ITerminalServiceNativeDelegate, IXtermTerminal, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal';
import { getCwdForSplit } from 'vs/workbench/contrib/terminal/browser/terminalActions';
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput';
@@ -52,6 +52,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore';
import { ITimerService } from 'vs/workbench/services/timer/browser/timerService';
import { mark } from 'vs/base/common/performance';
import { DeatachedTerminal } from 'vs/workbench/contrib/terminal/browser/detachedTerminal';
export class TerminalService extends Disposable implements ITerminalService {
declare _serviceBrand: undefined;
@@ -1018,9 +1019,9 @@ export class TerminalService extends Disposable implements ITerminalService {
return this._createTerminal(shellLaunchConfig, location, options);
}
async createDetachedXterm(options: IDetachedXTermOptions): Promise<IDetachedXtermTerminal> {
async createDetachedTerminal(options: IDetachedXTermOptions): Promise<IDetachedTerminalInstance> {
const ctor = await TerminalInstance.getXtermConstructor(this._keybindingService, this._contextKeyService);
const instance = this._instantiationService.createInstance(
const xterm = this._instantiationService.createInstance(
XtermTerminal,
ctor,
this._configHelper,
@@ -1034,13 +1035,16 @@ export class TerminalService extends Disposable implements ITerminalService {
);
if (options.readonly) {
instance.raw.attachCustomKeyEventHandler(() => false);
xterm.raw.attachCustomKeyEventHandler(() => false);
}
this._detachedXterms.add(instance);
instance.onDidDispose(() => this._detachedXterms.delete(instance));
this._detachedXterms.add(xterm);
const l = xterm.onDidDispose(() => {
this._detachedXterms.delete(xterm);
l.dispose();
});
return instance;
return new DeatachedTerminal(xterm, options, this._instantiationService);
}
private async _resolveCwd(shellLaunchConfig: IShellLaunchConfig, splitActiveTerminal: boolean, options?: ICreateTerminalOptions): Promise<void> {
@@ -253,7 +253,8 @@ export interface IDefaultShellAndArgsRequest {
callback: (shell: string, args: string[] | string | undefined) => void;
}
export interface ITerminalProcessManager extends IDisposable {
/** Read-only process information that can apply to detached terminals. */
export interface ITerminalProcessInfo {
readonly processState: ProcessState;
readonly ptyProcessReady: Promise<void>;
readonly shellProcessId: number | undefined;
@@ -270,7 +271,11 @@ export interface ITerminalProcessManager extends IDisposable {
readonly capabilities: ITerminalCapabilityStore;
readonly shellIntegrationNonce: string;
readonly extEnvironmentVariableCollection: IMergedEnvironmentVariableCollection | undefined;
}
export const isTerminalProcessManager = (t: ITerminalProcessInfo | ITerminalProcessManager): t is ITerminalProcessManager => typeof (t as ITerminalProcessManager).write === 'function';
export interface ITerminalProcessManager extends IDisposable, ITerminalProcessInfo {
readonly onPtyDisconnect: Event<void>;
readonly onPtyReconnect: Event<void>;
@@ -10,11 +10,11 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ITerminalContribution, ITerminalInstance, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal';
import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal';
import { registerActiveInstanceAction } from 'vs/workbench/contrib/terminal/browser/terminalActions';
import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions';
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager';
import { ITerminalProcessManager, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal';
import { ITerminalProcessInfo, ITerminalProcessManager, TerminalCommandId, isTerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey';
import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings';
import { ITerminalLinkProviderService } from 'vs/workbench/contrib/terminalContrib/links/browser/links';
@@ -40,8 +40,8 @@ class TerminalLinkContribution extends DisposableStore implements ITerminalContr
private _linkResolver: TerminalLinkResolver;
constructor(
private readonly _instance: ITerminalInstance,
private readonly _processManager: ITerminalProcessManager,
private readonly _instance: ITerminalInstance | IDetachedTerminalInstance,
private readonly _processManager: ITerminalProcessManager | ITerminalProcessInfo,
private readonly _widgetManager: TerminalWidgetManager,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ITerminalLinkProviderService private readonly _terminalLinkProviderService: ITerminalLinkProviderService
@@ -52,18 +52,24 @@ class TerminalLinkContribution extends DisposableStore implements ITerminalContr
xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void {
const linkManager = this._instantiationService.createInstance(TerminalLinkManager, xterm.raw, this._processManager, this._instance.capabilities, this._linkResolver);
this._processManager.onProcessReady(() => {
if (isTerminalProcessManager(this._processManager)) {
this._processManager.onProcessReady(() => {
linkManager.setWidgetManager(this._widgetManager);
});
} else {
linkManager.setWidgetManager(this._widgetManager);
});
}
this._linkManager = this.add(linkManager);
// Attach the link provider(s) to the instance and listen for changes
for (const linkProvider of this._terminalLinkProviderService.linkProviders) {
this._linkManager.registerExternalLinkProvider(linkProvider.provideLinks.bind(linkProvider, this._instance));
if (!isDetachedTerminalInstance(this._instance)) {
for (const linkProvider of this._terminalLinkProviderService.linkProviders) {
this._linkManager.registerExternalLinkProvider(linkProvider.provideLinks.bind(linkProvider, this._instance));
}
this.add(this._terminalLinkProviderService.onDidAddLinkProvider(e => {
linkManager.registerExternalLinkProvider(e.provideLinks.bind(e, this._instance as ITerminalInstance));
}));
}
this.add(this._terminalLinkProviderService.onDidAddLinkProvider(e => {
linkManager.registerExternalLinkProvider(e.provideLinks.bind(e, this._instance));
}));
// TODO: Currently only a single link provider is supported; the one registered by the ext host
this.add(this._terminalLinkProviderService.onDidRemoveLinkProvider(e => {
linkManager.dispose();
@@ -97,7 +103,7 @@ class TerminalLinkContribution extends DisposableStore implements ITerminalContr
}
}
registerTerminalContribution(TerminalLinkContribution.ID, TerminalLinkContribution);
registerTerminalContribution(TerminalLinkContribution.ID, TerminalLinkContribution, true);
const category = terminalStrings.actionCategory;
@@ -25,7 +25,7 @@ import { ILinkHoverTargetOptions, TerminalHover } from 'vs/workbench/contrib/ter
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager';
import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private';
import { ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities';
import { ITerminalConfiguration, ITerminalProcessManager, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal';
import { ITerminalConfiguration, ITerminalProcessInfo, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal';
import { IHoverAction } from 'vs/workbench/services/hover/browser/hover';
import type { ILink, ILinkProvider, IViewportRange, Terminal } from 'xterm';
import { convertBufferRangeToViewport } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkHelpers';
@@ -47,7 +47,7 @@ export class TerminalLinkManager extends DisposableStore {
constructor(
private readonly _xterm: Terminal,
private readonly _processManager: ITerminalProcessManager,
private readonly _processInfo: ITerminalProcessInfo,
capabilities: ITerminalCapabilityStore,
private readonly _linkResolver: ITerminalLinkResolver,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@@ -65,15 +65,15 @@ export class TerminalLinkManager extends DisposableStore {
enableFileLinks = false;
break;
case 'notRemote':
enableFileLinks = !this._processManager.remoteAuthority;
enableFileLinks = !this._processInfo.remoteAuthority;
break;
}
// Setup link detectors in their order of priority
this._setupLinkDetector(TerminalUriLinkDetector.id, this._instantiationService.createInstance(TerminalUriLinkDetector, this._xterm, this._processManager, this._linkResolver));
this._setupLinkDetector(TerminalUriLinkDetector.id, this._instantiationService.createInstance(TerminalUriLinkDetector, this._xterm, this._processInfo, this._linkResolver));
if (enableFileLinks) {
this._setupLinkDetector(TerminalMultiLineLinkDetector.id, this._instantiationService.createInstance(TerminalMultiLineLinkDetector, this._xterm, this._processManager, this._linkResolver));
this._setupLinkDetector(TerminalLocalLinkDetector.id, this._instantiationService.createInstance(TerminalLocalLinkDetector, this._xterm, capabilities, this._processManager, this._linkResolver));
this._setupLinkDetector(TerminalMultiLineLinkDetector.id, this._instantiationService.createInstance(TerminalMultiLineLinkDetector, this._xterm, this._processInfo, this._linkResolver));
this._setupLinkDetector(TerminalLocalLinkDetector.id, this._instantiationService.createInstance(TerminalLocalLinkDetector, this._xterm, capabilities, this._processInfo, this._linkResolver));
}
this._setupLinkDetector(TerminalWordLinkDetector.id, this.add(this._instantiationService.createInstance(TerminalWordLinkDetector, this._xterm)));
@@ -83,8 +83,8 @@ 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.Url, this._instantiationService.createInstance(TerminalUrlLinkOpener, !!this._processManager.remoteAuthority));
this._openers.set(TerminalBuiltinLinkType.Search, this._instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, this._processInfo.initialCwd, localFileOpener, localFolderInWorkspaceOpener, () => this._processInfo.os || OS));
this._openers.set(TerminalBuiltinLinkType.Url, this._instantiationService.createInstance(TerminalUrlLinkOpener, !!this._processInfo.remoteAuthority));
this._registerStandardLinkProviders();
@@ -74,7 +74,8 @@ import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/vie
import { EditorModel } from 'vs/workbench/common/editor/editorModel';
import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views';
import { IDetachedXtermTerminal, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { DetachedProcessInfo } from 'vs/workbench/contrib/terminal/browser/detachedTerminal';
import { IDetachedTerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { getXtermScaledDimensions } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal';
import { TERMINAL_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
import { flatTestItemDelimiter } from 'vs/workbench/contrib/testing/browser/explorerProjections/display';
@@ -1352,7 +1353,7 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer {
private readonly xtermLayoutDelayer = this._register(new Delayer(50));
/** Active terminal instance. */
private readonly terminal = this._register(new MutableDisposable<IDetachedXtermTerminal>());
private readonly terminal = this._register(new MutableDisposable<IDetachedTerminalInstance>());
/** Listener for streaming result data */
private readonly outputDataListener = this._register(new MutableDisposable());
@@ -1369,11 +1370,11 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer {
private async makeTerminal() {
const prev = this.terminal.value;
if (prev) {
prev.clearBuffer();
prev.clearSearchDecorations();
prev.xterm.clearBuffer();
prev.xterm.clearSearchDecorations();
// clearBuffer tries to retain the prompt line, but this doesn't exist for tests.
// So clear the screen (J) and move to home (H) to ensure previous data is cleaned up.
prev.write(`\x1b[2J\x1b[0;0H`);
prev.xterm.write(`\x1b[2J\x1b[0;0H`);
return prev;
}
@@ -1387,11 +1388,12 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer {
updateCwd: () => { },
});
return this.terminal.value = await this.terminalService.createDetachedXterm({
return this.terminal.value = await this.terminalService.createDetachedTerminal({
rows: 10,
cols: 80,
readonly: true,
capabilities,
processInfo: new DetachedProcessInfo({ initialCwd: cwd.value }),
colorProvider: {
getBackgroundColor: theme => {
const terminalBackground = theme.getColor(TERMINAL_BACKGROUND_COLOR);
@@ -1440,17 +1442,17 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer {
}
}
},
doListenForMoreData: (output, result, terminal) => result.onChange(e => {
doListenForMoreData: (output, result, { xterm }) => result.onChange(e => {
if (e.reason === TestResultItemChangeReason.NewMessage && e.item.item.extId === testItem.extId && e.message.type === TestMessageType.Output) {
for (const chunk of output.getRangeIter(e.message.offset, e.message.length)) {
terminal.write(chunk.buffer);
xterm.write(chunk.buffer);
}
}
}),
});
if (subject instanceof MessageSubject && subject.message.type === TestMessageType.Output && subject.message.marker !== undefined) {
terminal?.selectMarkedRange(getMarkId(subject.message.marker, true), getMarkId(subject.message.marker, false), /* scrollIntoView= */ true);
terminal?.xterm.selectMarkedRange(getMarkId(subject.message.marker, true), getMarkId(subject.message.marker, false), /* scrollIntoView= */ true);
}
}
@@ -1464,7 +1466,7 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer {
this.updateCwd(Iterable.find(result.tests, t => !!t.item.uri)?.item.uri);
return task.output.buffers;
},
doListenForMoreData: (task, _result, terminal) => task.output.onDidWriteData(e => terminal.write(e.buffer)),
doListenForMoreData: (task, _result, { xterm }) => task.output.onDidWriteData(e => xterm.write(e.buffer)),
});
}
@@ -1472,7 +1474,7 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer {
subject: InspectSubject;
getTarget: (result: ITestResult) => T | undefined;
doInitialWrite: (target: T, result: LiveTestResult) => Iterable<VSBuffer>;
doListenForMoreData: (target: T, result: LiveTestResult, terminal: IDetachedXtermTerminal) => IDisposable | undefined;
doListenForMoreData: (target: T, result: LiveTestResult, terminal: IDetachedTerminalInstance) => IDisposable | undefined;
}) {
const result = opts.subject.result;
const target = opts.getTarget(result);
@@ -1488,7 +1490,7 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer {
for (const chunk of opts.doInitialWrite(target, result)) {
didWriteData ||= chunk.byteLength > 0;
pendingWrites.value++;
terminal.write(chunk.buffer, () => pendingWrites.value--);
terminal.xterm.write(chunk.buffer, () => pendingWrites.value--);
}
} else {
this.writeNotice(terminal, localize('runNoOutputForPast', 'Test output is only available for new test runs.'));
@@ -1525,12 +1527,12 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer {
}
}
private writeNotice(terminal: IDetachedXtermTerminal, str: string) {
terminal.write(`\x1b[2m${str}\x1b[0m`);
private writeNotice(terminal: IDetachedTerminalInstance, str: string) {
terminal.xterm.write(`\x1b[2m${str}\x1b[0m`);
}
private attachTerminalToDom(terminal: IDetachedXtermTerminal) {
terminal.write('\x1b[?25l'); // hide cursor
private attachTerminalToDom(terminal: IDetachedTerminalInstance) {
terminal.xterm.write('\x1b[?25l'); // hide cursor
requestAnimationFrame(() => this.layoutTerminal(terminal));
terminal.attachToElement(this.container, { enableGpu: false });
}
@@ -1549,7 +1551,7 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer {
}
private layoutTerminal(
xterm: IDetachedXtermTerminal,
{ xterm }: IDetachedTerminalInstance,
width = this.dimensions?.width ?? this.container.clientWidth,
height = this.dimensions?.height ?? this.container.clientHeight
) {
@@ -30,15 +30,18 @@ export class BrowserCredentialsService extends Disposable implements ICredential
) {
super();
if (environmentService.remoteAuthority && !environmentService.options?.credentialsProvider) {
if (
environmentService.remoteAuthority
&& !environmentService.options?.credentialsProvider
&& !environmentService.options?.secretStorageProvider
) {
// If we have a remote authority but the embedder didn't provide a credentialsProvider,
// we can use the CredentialsService on the remote side
const remoteCredentialsService = ProxyChannel.toService<ICredentialsService>(remoteAgentService.getConnection()!.getChannel('credentials'));
this.credentialsProvider = remoteCredentialsService;
this._secretStoragePrefix = remoteCredentialsService.getSecretStoragePrefix();
} else {
// fall back to InMemoryCredentialsProvider if none was given to us. This should really only be used
// when running tests.
// fall back to InMemoryCredentialsProvider if none was given to us.
this.credentialsProvider = environmentService.options?.credentialsProvider ?? new InMemoryCredentialsProvider();
this._secretStoragePrefix = Promise.resolve(this.productService.urlProtocol);
}
@@ -62,11 +62,6 @@ declare module 'vscode' {
*/
commands?: Command[];
/**
* When set, overrides the user setting of `editor.inlineSuggest.suppressSuggestions`.
*/
suppressSuggestions?: boolean;
/**
* When set and the user types a suggestion without derivating from it, the inline suggestion is not updated.
* Defaults to false (might change).
+12 -12
View File
@@ -10784,20 +10784,20 @@ xterm-addon-unicode11@0.5.0:
resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.5.0.tgz#41c0d96acc1e3bb6c6596eee64e163b6bca74be7"
integrity sha512-Jm4/g4QiTxiKiTbYICQgC791ubhIZyoIwxAIgOW8z8HWFNY+lwk+dwaKEaEeGBfM48Vk8fklsUW9u/PlenYEBg==
xterm-addon-webgl@0.16.0-beta.14:
version "0.16.0-beta.14"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.16.0-beta.14.tgz#de5bf6c97e31f62d13907a875ffc6be154bc4a02"
integrity sha512-cj+rQWqOeZYJ6JMjMuTTKu3OnP+1D+sqSZPuwZAd6s4NkY4dFXFQlKxJ2yvDHOU/jrgpIYR1c3Cnu0AOWZpS3w==
xterm-addon-webgl@0.16.0-beta.16:
version "0.16.0-beta.16"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.16.0-beta.16.tgz#2c31308f8c7f636576720adca529297f8fff3224"
integrity sha512-k0ZSwpBVtxXaqUc29CmlfFgIK7LEYAmwcDC3QVp2ESZo2JHYuQrWFpImjbzbYu0ON6qqcfbX8SnbzRid/XfGUg==
xterm-headless@5.3.0-beta.42:
version "5.3.0-beta.42"
resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.3.0-beta.42.tgz#714d167dc0af010067420956ffca80149dddeca9"
integrity sha512-3xhyy9DaPpadd1iiuKjOn4Zvntnx8WSBMEXPJbrORmy3ZbIwmLf/HYvg7iZfVreSDH+UFlm06uq3ictXpv7YvA==
xterm-headless@5.3.0-beta.46:
version "5.3.0-beta.46"
resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.3.0-beta.46.tgz#3f941f673e3c61aad2705e4b34b42fbe467aed48"
integrity sha512-e/VbZKrfyD1TTxOY5/jah3dUih6q8nTO5v5GQV7YOASp+yyI0XHKiDjLrCyFPQOLTKxCcRutMHvr+CoCcPpcHg==
xterm@5.3.0-beta.42:
version "5.3.0-beta.42"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0-beta.42.tgz#f0806ebe9b033530c54ac5b03955827fb729c38f"
integrity sha512-3OLXTfwtSy9UKo8Uzm3/kd7qBg0JD/qFpotg8iKJ2aB9zx0fkJnysl4aIuNleUJzgvuxzBUHkcRZUmEw1XCKdw==
xterm@5.3.0-beta.46:
version "5.3.0-beta.46"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0-beta.46.tgz#9bd2b2a588c88ae64f1989ca3bf5bd9116554b5a"
integrity sha512-keB3C6sXm56ug1+hylAQiNyZP4YtPgd2W+Gu/ZKwJMk5haD25DEnenBYL++Cl3+sNGYxOGS42QlmYj6Mrq1LPw==
y18n@^3.2.1:
version "3.2.2"