Merge remote-tracking branch 'origin' into electron-16.x.y

This commit is contained in:
deepak1556 2021-11-23 15:33:56 +09:00
commit d6fa4da469
85 changed files with 2747 additions and 771 deletions

View file

@ -1,7 +1,7 @@
{
"name": "monaco-editor-core",
"private": true,
"version": "0.30.0",
"version": "0.31.0",
"description": "A browser based code editor",
"author": "Microsoft Corporation",
"license": "MIT",

View file

@ -81,7 +81,8 @@
"command": "git.openChange",
"title": "%command.openChange%",
"category": "Git",
"icon": "$(compare-changes)"
"icon": "$(compare-changes)",
"enablement": "scmActiveResourceHasChanges"
},
{
"command": "git.openAllChanges",

View file

@ -3,28 +3,85 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { window, languages, Uri, LanguageStatusSeverity, Disposable, commands, QuickPickItem } from 'vscode';
import { window, languages, Uri, LanguageStatusSeverity, Disposable, commands, QuickPickItem, extensions, workspace } from 'vscode';
import { JSONLanguageStatus } from './jsonClient';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
type ShowSchemasInput = {
schemas: string[];
uri: string;
};
interface ShowSchemasItem extends QuickPickItem {
uri: Uri;
}
function equalsIgnoreCase(a: string, b: string): boolean {
return a.length === b.length && a.toLowerCase().localeCompare(b.toLowerCase()) === 0;
}
function isEqualAuthority(a1: string | undefined, a2: string | undefined) {
return a1 === a2 || (a1 !== undefined && a2 !== undefined && equalsIgnoreCase(a1, a2));
}
function findExtension(uri: Uri) {
for (const ext of extensions.all) {
const parent = ext.extensionUri;
if (uri.scheme === parent.scheme && isEqualAuthority(uri.authority, parent.authority) && uri.path.startsWith(parent.path + '/')) {
return ext;
}
}
return undefined;
}
function findWorkspaceFolder(uri: Uri) {
if (workspace.workspaceFolders) {
for (const wf of workspace.workspaceFolders) {
const parent = wf.uri;
if (uri.scheme === parent.scheme && isEqualAuthority(uri.authority, parent.authority) && uri.path.startsWith(parent.path + '/')) {
return wf;
}
}
}
return undefined;
}
function renderShowSchemasItem(schema: string): ShowSchemasItem {
const uri = Uri.parse(schema);
const extension = findExtension(uri);
if (extension) {
return { label: extension.id, description: uri.path.substring(extension.extensionUri.path.length + 1), uri };
}
const wf = findWorkspaceFolder(uri);
if (wf) {
return { label: uri.path.substring(wf.uri.path.length + 1), description: 'Workspace', uri };
}
if (uri.scheme === 'file') {
return { label: uri.fsPath, uri };
} else if (uri.scheme === 'vscode') {
return { label: schema, description: 'internally generated', uri };
}
return { label: schema, uri };
}
export function createLanguageStatusItem(documentSelector: string[], statusRequest: (uri: string) => Promise<JSONLanguageStatus>): Disposable {
const statusItem = languages.createLanguageStatusItem('json.projectStatus', documentSelector);
statusItem.name = localize('statusItem.name', "JSON Validation Status");
statusItem.severity = LanguageStatusSeverity.Information;
const showSchemasCommand = commands.registerCommand('_json.showAssociatedSchemaList', arg => {
const items = arg.schemas.sort().map((a: string) => ({ label: a }));
const quickPick = window.createQuickPick<QuickPickItem>();
quickPick.title = localize('schemaPicker.title', 'Associated JSON Schemas');
const showSchemasCommand = commands.registerCommand('_json.showAssociatedSchemaList', (arg: ShowSchemasInput) => {
const items: ShowSchemasItem[] = arg.schemas.sort().map(renderShowSchemasItem);
const quickPick = window.createQuickPick<ShowSchemasItem>();
quickPick.title = localize('schemaPicker.title', 'JSON Schemas used for {0}', arg.uri.toString());
quickPick.placeholder = localize('schemaPicker.placeholder', 'Select the schema to open');
quickPick.items = items;
quickPick.show();
quickPick.onDidAccept(() => {
const selectedSchema = quickPick.selectedItems[0].label;
commands.executeCommand('vscode.open', Uri.parse(selectedSchema));
commands.executeCommand('vscode.open', quickPick.selectedItems[0].uri);
quickPick.dispose();
});
});
@ -46,19 +103,20 @@ export function createLanguageStatusItem(documentSelector: string[], statusReque
if (schemas.length === 0) {
statusItem.text = localize('status.noSchema', 'Validated without JSON schema');
} else if (schemas.length === 1) {
const item = renderShowSchemasItem(schemas[0]);
statusItem.text = localize('status.singleSchema', 'Validated with JSON schema');
statusItem.command = {
command: 'vscode.open',
title: localize('status.openSchemaLink', 'Open Schema'),
tooltip: schemas[0],
arguments: [Uri.parse(schemas[0])]
tooltip: item.description ? `${item.label} - ${item.description}` : item.label,
arguments: [item.uri]
};
} else {
statusItem.text = localize('status.multipleSchema', 'Validated with multiple JSON schemas');
statusItem.command = {
command: '_json.showAssociatedSchemaList',
title: localize('status.openSchemasLink', 'Show Schemas'),
arguments: [{ schemas }]
arguments: [{ schemas, uri: document.uri.toString() } as ShowSchemasInput]
};
}
} catch (e) {
@ -79,5 +137,3 @@ export function createLanguageStatusItem(documentSelector: string[], statusReque
return Disposable.from(statusItem, activeEditorListener, showSchemasCommand);
}

View file

@ -142,20 +142,16 @@ export class AzureActiveDirectoryService {
} catch (e) {
// If we aren't connected to the internet, then wait and try to refresh again later.
if (e.message === REFRESH_NETWORK_FAILURE) {
const didSucceedOnRetry = await this.handleRefreshNetworkError(session.id, session.refreshToken, session.scope);
if (!didSucceedOnRetry) {
this._tokens.push({
accessToken: undefined,
refreshToken: session.refreshToken,
account: {
label: session.account.label ?? session.account.displayName!,
id: session.account.id
},
scope: session.scope,
sessionId: session.id
});
this.pollForReconnect(session.id, session.refreshToken, session.scope);
}
this._tokens.push({
accessToken: undefined,
refreshToken: session.refreshToken,
account: {
label: session.account.label ?? session.account.displayName!,
id: session.account.id
},
scope: session.scope,
sessionId: session.id
});
} else {
await this.removeSession(session.id);
}
@ -201,9 +197,8 @@ export class AzureActiveDirectoryService {
const token = await this.refreshToken(session.refreshToken, session.scope, session.id);
added.push(this.convertToSessionSync(token));
} catch (e) {
if (e.message === REFRESH_NETWORK_FAILURE) {
// Ignore, will automatically retry on next poll.
} else {
// Network failures will automatically retry on next poll.
if (e.message !== REFRESH_NETWORK_FAILURE) {
await this.removeSession(session.id);
}
}
@ -323,7 +318,7 @@ export class AzureActiveDirectoryService {
}
}
if (!scopes) {
const sessions = await this.sessions;
const sessions = this._tokens.map(token => this.convertToSessionSync(token));
Logger.info(`Got ${sessions.length} sessions for all scopes...`);
return sessions;
}
@ -529,12 +524,7 @@ export class AzureActiveDirectoryService {
Logger.info('Triggering change session event...');
onDidChangeSessions.fire({ added: [], removed: [], changed: [this.convertToSessionSync(refreshedToken)] });
} catch (e) {
if (e.message === REFRESH_NETWORK_FAILURE) {
const didSucceedOnRetry = await this.handleRefreshNetworkError(token.sessionId, token.refreshToken, scope);
if (!didSucceedOnRetry) {
this.pollForReconnect(token.sessionId, token.refreshToken, token.scope);
}
} else {
if (e.message !== REFRESH_NETWORK_FAILURE) {
await this.removeSession(token.sessionId);
onDidChangeSessions.fire({ added: [], removed: [this.convertToSessionSync(token)], changed: [] });
}
@ -591,23 +581,9 @@ export class AzureActiveDirectoryService {
const endpointUrl = proxyEndpoints?.microsoft || loginEndpointUrl;
const endpoint = `${endpointUrl}${tenant}/oauth2/v2.0/token`;
const result = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': postData.length.toString()
},
body: postData
});
if (result.ok) {
const json = await result.json();
Logger.info(`Exchanging login code for token (for scopes: ${scope}) succeeded!`);
return this.getTokenFromResponse(json, scope);
} else {
Logger.error(`Exchanging login code for token (for scopes: ${scope}) failed: ${await result.text()}`);
throw new Error('Unable to login.');
}
const json = await this.fetchTokenResponse(endpoint, postData, scope);
Logger.info(`Exchanging login code for token (for scopes: ${scope}) succeeded!`);
return this.getTokenFromResponse(json, scope);
} catch (e) {
Logger.error(`Error exchanging code for token (for scopes ${scope}): ${e}`);
throw e;
@ -624,6 +600,46 @@ export class AzureActiveDirectoryService {
}
}
private async fetchTokenResponse(endpoint: string, postData: string, scopes: string): Promise<ITokenResponse> {
let attempts = 0;
while (attempts <= 3) {
attempts++;
let result: Response | undefined;
let errorMessage: string | undefined;
try {
result = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': postData.length.toString()
},
body: postData
});
} catch (e) {
errorMessage = e.message ?? e;
}
if (!result || result.status > 499) {
if (attempts > 3) {
Logger.error(`Fetching token failed for scopes (${scopes}): ${result ? await result.text() : errorMessage}`);
break;
}
// Exponential backoff
await new Promise(resolve => setTimeout(resolve, 5 * attempts * attempts * 1000));
continue;
} else if (!result.ok) {
// For 4XX errors, the user may actually have an expired token or have changed
// their password recently which is throwing a 4XX. For this, we throw an error
// so that the user can be prompted to sign in again.
throw new Error(await result.text());
}
return await result.json() as ITokenResponse;
}
throw new Error(REFRESH_NETWORK_FAILURE);
}
private async doRefreshToken(refreshToken: string, scope: string, sessionId: string): Promise<IToken> {
Logger.info(`Refreshing token for scopes: ${scope}`);
const postData = querystring.stringify({
@ -633,37 +649,25 @@ export class AzureActiveDirectoryService {
scope: scope
});
let result: Response;
try {
const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
const endpointUrl = proxyEndpoints?.microsoft || loginEndpointUrl;
const endpoint = `${endpointUrl}${tenant}/oauth2/v2.0/token`;
result = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': postData.length.toString()
},
body: postData
});
} catch (e) {
Logger.error(`Refreshing token failed (for scopes: ${scope}) Error: ${e}`);
throw new Error(REFRESH_NETWORK_FAILURE);
}
const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
const endpointUrl = proxyEndpoints?.microsoft || loginEndpointUrl;
const endpoint = `${endpointUrl}${tenant}/oauth2/v2.0/token`;
try {
if (result.ok) {
const json = await result.json();
const token = this.getTokenFromResponse(json, scope, sessionId);
await this.setToken(token, scope);
Logger.info(`Token refresh success for scopes: ${token.scope}`);
return token;
} else {
throw new Error('Bad request.');
}
const json = await this.fetchTokenResponse(endpoint, postData, scope);
const token = this.getTokenFromResponse(json, scope, sessionId);
await this.setToken(token, scope);
Logger.info(`Token refresh success for scopes: ${token.scope}`);
return token;
} catch (e) {
if (e.message === REFRESH_NETWORK_FAILURE) {
// We were unable to refresh because of a network failure (i.e. the user lost internet access).
// so set up a timeout to try again later.
this.pollForReconnect(sessionId, refreshToken, scope);
throw e;
}
vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed."));
Logger.error(`Refreshing token failed (for scopes: ${scope}): ${result.statusText}`);
Logger.error(`Refreshing token failed (for scopes: ${scope}): ${e.message}`);
throw new Error('Refreshing token failed');
}
}
@ -690,7 +694,7 @@ export class AzureActiveDirectoryService {
private pollForReconnect(sessionId: string, refreshToken: string, scope: string): void {
this.clearSessionTimeout(sessionId);
Logger.trace(`Setting up reconnection timeout for scopes: ${scope}...`);
this._refreshTimeouts.set(sessionId, setTimeout(async () => {
try {
const refreshedToken = await this.refreshToken(refreshToken, scope, sessionId);
@ -701,29 +705,6 @@ export class AzureActiveDirectoryService {
}, 1000 * 60 * 30));
}
private handleRefreshNetworkError(sessionId: string, refreshToken: string, scope: string, attempts: number = 1): Promise<boolean> {
return new Promise((resolve, _) => {
if (attempts === 3) {
Logger.error(`Token refresh (for scopes: ${scope}) failed after 3 attempts`);
return resolve(false);
}
const delayBeforeRetry = 5 * attempts * attempts;
this.clearSessionTimeout(sessionId);
this._refreshTimeouts.set(sessionId, setTimeout(async () => {
try {
const refreshedToken = await this.refreshToken(refreshToken, scope, sessionId);
onDidChangeSessions.fire({ added: [], removed: [], changed: [this.convertToSessionSync(refreshedToken)] });
return resolve(true);
} catch (e) {
return resolve(await this.handleRefreshNetworkError(sessionId, refreshToken, scope, attempts + 1));
}
}, 1000 * delayBeforeRetry));
});
}
public async removeSession(sessionId: string): Promise<vscode.AuthenticationSession | undefined> {
Logger.info(`Logging out of session '${sessionId}'`);
const token = this.removeInMemorySessionData(sessionId);

View file

@ -6,6 +6,7 @@
"license": "MIT",
"enabledApiProposals": [
"authSession",
"contribViewsRemote",
"customEditorMove",
"diffCommand",
"documentFiltersExclusive",

View file

@ -9,6 +9,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { extUri as defaultExtUri, IExtUri } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { setTimeout0 } from 'vs/base/common/platform';
export function isThenable<T>(obj: unknown): obj is Promise<T> {
return !!obj && typeof (obj as unknown as Promise<T>).then === 'function';
@ -968,6 +969,7 @@ export interface IdleDeadline {
readonly didTimeout: boolean;
timeRemaining(): number;
}
/**
* Execute the callback the next time the browser is idle
*/
@ -979,8 +981,11 @@ declare function cancelIdleCallback(handle: number): void;
(function () {
if (typeof requestIdleCallback !== 'function' || typeof cancelIdleCallback !== 'function') {
runWhenIdle = (runner) => {
const handle = setTimeout(() => {
const end = Date.now() + 15; // one frame at 64fps
setTimeout0(() => {
if (disposed) {
return;
}
const end = Date.now() + 3; // yield often
runner(Object.freeze({
didTimeout: true,
timeRemaining() {
@ -995,7 +1000,6 @@ declare function cancelIdleCallback(handle: number): void;
return;
}
disposed = true;
clearTimeout(handle);
}
};
};

View file

@ -190,10 +190,13 @@ interface ISetImmediate {
(callback: (...args: unknown[]) => void): void;
}
export const setImmediate: ISetImmediate = (function defineSetImmediate() {
if (globals.setImmediate) {
return globals.setImmediate.bind(globals);
}
/**
* See https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#:~:text=than%204%2C%20then-,set%20timeout%20to%204,-.
*
* Works similarly to `setTimeout(0)` but doesn't suffer from the 4ms artificial delay
* that browsers set when the nesting level is > 5.
*/
export const setTimeout0 = (() => {
if (typeof globals.postMessage === 'function' && !globals.importScripts) {
interface IQueueElement {
id: number;
@ -201,10 +204,10 @@ export const setImmediate: ISetImmediate = (function defineSetImmediate() {
}
let pending: IQueueElement[] = [];
globals.addEventListener('message', (e: MessageEvent) => {
if (e.data && e.data.vscodeSetImmediateId) {
if (e.data && e.data.vscodeScheduleAsyncWork) {
for (let i = 0, len = pending.length; i < len; i++) {
const candidate = pending[i];
if (candidate.id === e.data.vscodeSetImmediateId) {
if (candidate.id === e.data.vscodeScheduleAsyncWork) {
pending.splice(i, 1);
candidate.callback();
return;
@ -219,9 +222,19 @@ export const setImmediate: ISetImmediate = (function defineSetImmediate() {
id: myId,
callback: callback
});
globals.postMessage({ vscodeSetImmediateId: myId }, '*');
globals.postMessage({ vscodeScheduleAsyncWork: myId }, '*');
};
}
return (callback: () => void) => setTimeout(callback);
})();
export const setImmediate: ISetImmediate = (function defineSetImmediate() {
if (globals.setImmediate) {
return globals.setImmediate.bind(globals);
}
if (typeof globals.postMessage === 'function' && !globals.importScripts) {
return setTimeout0;
}
if (typeof nodeProcess?.nextTick === 'function') {
return nodeProcess.nextTick.bind(nodeProcess);
}

File diff suppressed because one or more lines are too long

View file

@ -181,12 +181,14 @@ class ProcessRenderer implements ITreeRenderer<ProcessItem, void, IProcessItemTe
const windowTitle = this.mapPidToWindowTitle.get(element.pid);
name = windowTitle !== undefined ? `${name} (${this.mapPidToWindowTitle.get(element.pid)})` : name;
}
const pid = element.pid.toFixed(0);
templateData.name.textContent = name;
templateData.name.title = element.cmd;
templateData.CPU.textContent = element.load.toFixed(0);
templateData.PID.textContent = element.pid.toFixed(0);
templateData.PID.textContent = pid;
templateData.PID.parentElement!.id = `pid-${pid}`;
const memory = this.platform === 'win32' ? element.mem : (this.totalMem * (element.mem / 100));
templateData.memory.textContent = (memory / ByteSize.MB).toFixed(0);
@ -450,7 +452,7 @@ class ProcessExplorer {
items.push({
label: localize('copy', "Copy"),
click: () => {
const row = document.getElementById(pid.toString());
const row = document.getElementById(`pid-${pid}`);
if (row) {
this.nativeHostService.writeClipboardText(row.innerText);
}

View file

@ -1518,31 +1518,6 @@ export namespace CoreNavigationCommands {
precondition: undefined
}));
export const ExpandLineSelection: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand {
constructor() {
super({
id: 'expandLineSelection',
precondition: undefined,
kbOpts: {
weight: CORE_WEIGHT,
kbExpr: EditorContextKeys.textInputFocus,
primary: KeyMod.CtrlCmd | KeyCode.KeyL
}
});
}
public runCoreEditorCommand(viewModel: IViewModel, args: any): void {
viewModel.model.pushStackElement();
viewModel.setCursorStates(
args.source,
CursorChangeReason.Explicit,
CursorMoveCommands.expandLineSelection(viewModel, viewModel.getCursorStates())
);
viewModel.revealPrimaryCursor(args.source, true);
}
});
export const CancelSelection: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand {
constructor() {
super({

View file

@ -640,6 +640,8 @@ export interface IEditorOptions {
* Controls the behavior of editor guides.
*/
guides?: IGuidesOptions;
unicodeHighlight?: IUnicodeHighlightOptions;
}
/**
@ -3245,6 +3247,120 @@ class EditorScrollbar extends BaseEditorOption<EditorOption.scrollbar, InternalE
//#endregion
//#region UnicodeHighlight
export type DeriveFromWorkspaceTrust = 'deriveFromWorkspaceTrust';
/**
* @internal
*/
export const deriveFromWorkspaceTrust: DeriveFromWorkspaceTrust = 'deriveFromWorkspaceTrust';
/**
* Configuration options for unicode highlighting.
*/
export interface IUnicodeHighlightOptions {
nonBasicASCII?: boolean | DeriveFromWorkspaceTrust;
invisibleCharacters?: boolean | DeriveFromWorkspaceTrust;
ambiguousCharacters?: boolean | DeriveFromWorkspaceTrust;
includeComments?: boolean | DeriveFromWorkspaceTrust;
/**
* A list of allowed code points in a single string.
*/
allowedCharacters?: string;
}
/**
* @internal
*/
export type InternalUnicodeHighlightOptions = Required<Readonly<IUnicodeHighlightOptions>>;
/**
* @internal
*/
export const unicodeHighlightConfigKeys = {
allowedCharacters: 'editor.unicodeHighlight.allowedCharacters',
invisibleCharacters: 'editor.unicodeHighlight.invisibleCharacters',
nonBasicASCII: 'editor.unicodeHighlight.nonBasicASCII',
ambiguousCharacters: 'editor.unicodeHighlight.ambiguousCharacters',
includeComments: 'editor.unicodeHighlight.includeComments',
};
class UnicodeHighlight extends BaseEditorOption<EditorOption.unicodeHighlighting, InternalUnicodeHighlightOptions> {
constructor() {
const defaults: InternalUnicodeHighlightOptions = {
nonBasicASCII: deriveFromWorkspaceTrust,
invisibleCharacters: deriveFromWorkspaceTrust,
ambiguousCharacters: deriveFromWorkspaceTrust,
includeComments: deriveFromWorkspaceTrust,
allowedCharacters: '',
};
super(
EditorOption.unicodeHighlighting, 'unicodeHighlight', defaults,
{
[unicodeHighlightConfigKeys.nonBasicASCII]: {
restricted: true,
type: ['boolean', 'string'],
enum: [true, false, deriveFromWorkspaceTrust],
default: defaults.nonBasicASCII,
description: nls.localize('unicodeHighlight.nonBasicASCII', "Controls whether all non-basic ASCII characters are highlighted. Only characters between U+0020 and U+007E, tab, line-feed and carriage-return are considered basic ASCII.")
},
[unicodeHighlightConfigKeys.invisibleCharacters]: {
restricted: true,
type: ['boolean', 'string'],
enum: [true, false, deriveFromWorkspaceTrust],
default: defaults.invisibleCharacters,
description: nls.localize('unicodeHighlight.invisibleCharacters', "Controls whether characters that just reserve space or have no width at all are highlighted.")
},
[unicodeHighlightConfigKeys.ambiguousCharacters]: {
restricted: true,
type: ['boolean', 'string'],
enum: [true, false, deriveFromWorkspaceTrust],
default: defaults.ambiguousCharacters,
description: nls.localize('unicodeHighlight.ambiguousCharacters', "Controls whether characters are highlighted that can be confused with basic ASCII characters, except those that are common in the current user locale.")
},
[unicodeHighlightConfigKeys.includeComments]: {
restricted: true,
type: ['boolean', 'string'],
enum: [true, false, deriveFromWorkspaceTrust],
default: defaults.includeComments,
description: nls.localize('unicodeHighlight.includeComments', "Controls whether characters in comments should also be subject to unicode highlighting.")
},
[unicodeHighlightConfigKeys.allowedCharacters]: {
restricted: true,
type: 'string',
default: defaults.allowedCharacters,
description: nls.localize('unicodeHighlight.allowedCharacters', "Defines allowed characters that are not being highlighted.")
},
}
);
}
public validate(_input: any): InternalUnicodeHighlightOptions {
if (!_input || typeof _input !== 'object') {
return this.defaultValue;
}
const input = _input as IUnicodeHighlightOptions;
return {
nonBasicASCII: primitiveSet<boolean | DeriveFromWorkspaceTrust>(input.nonBasicASCII, deriveFromWorkspaceTrust, [true, false, deriveFromWorkspaceTrust]),
invisibleCharacters: primitiveSet<boolean | DeriveFromWorkspaceTrust>(input.invisibleCharacters, deriveFromWorkspaceTrust, [true, false, deriveFromWorkspaceTrust]),
ambiguousCharacters: primitiveSet<boolean | DeriveFromWorkspaceTrust>(input.ambiguousCharacters, deriveFromWorkspaceTrust, [true, false, deriveFromWorkspaceTrust]),
includeComments: primitiveSet<boolean | DeriveFromWorkspaceTrust>(input.includeComments, deriveFromWorkspaceTrust, [true, false, deriveFromWorkspaceTrust]),
allowedCharacters: string(input.allowedCharacters, ''),
};
}
}
function string(value: unknown, defaultValue: string): string {
if (typeof value !== 'string') {
return defaultValue;
}
return value;
}
//#endregion
//#region inlineSuggest
export interface IInlineSuggestOptions {
@ -4219,6 +4335,7 @@ export const enum EditorOption {
suggestSelection,
tabCompletion,
tabIndex,
unicodeHighlighting,
unusualLineTerminators,
useShadowDOM,
useTabStops,
@ -4807,6 +4924,7 @@ export const EditorOptions = {
EditorOption.tabIndex, 'tabIndex',
0, -1, Constants.MAX_SAFE_SMALL_INTEGER
)),
unicodeHighlight: register(new UnicodeHighlight()),
unusualLineTerminators: register(new EditorStringEnumOption(
EditorOption.unusualLineTerminators, 'unusualLineTerminators',
'prompt' as 'auto' | 'off' | 'prompt',

View file

@ -168,6 +168,12 @@ export interface IModelDecorationOptions {
* If set, text will be injected in the view before the range.
*/
before?: InjectedTextOptions | null;
/**
* If set, this decoration will not be rendered for comment tokens.
* @internal
*/
hideInCommentTokens?: boolean | null;
}
/**

View file

@ -3044,6 +3044,8 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions {
readonly afterContentClassName: string | null;
readonly after: ModelDecorationInjectedTextOptions | null;
readonly before: ModelDecorationInjectedTextOptions | null;
readonly hideInCommentTokens: boolean | null;
private constructor(options: model.IModelDecorationOptions) {
this.description = options.description;
@ -3067,6 +3069,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions {
this.afterContentClassName = options.afterContentClassName ? cleanClassName(options.afterContentClassName) : null;
this.after = options.after ? ModelDecorationInjectedTextOptions.from(options.after) : null;
this.before = options.before ? ModelDecorationInjectedTextOptions.from(options.before) : null;
this.hideInCommentTokens = options.hideInCommentTokens ?? false;
}
}
ModelDecorationOptions.EMPTY = ModelDecorationOptions.register({ description: 'empty' });

View file

@ -15,7 +15,7 @@ import { TextModel } from 'vs/editor/common/model/textModel';
import { Disposable } from 'vs/base/common/lifecycle';
import { StopWatch } from 'vs/base/common/stopwatch';
import { MultilineTokensBuilder, countEOL } from 'vs/editor/common/model/tokensStore';
import * as platform from 'vs/base/common/platform';
import { runWhenIdle, IdleDeadline } from 'vs/base/common/async';
const enum Constants {
CHEAP_TOKENIZATION_LENGTH_LIMIT = 2048
@ -255,19 +255,26 @@ export class TextModelTokenization extends Disposable {
this._beginBackgroundTokenization();
}
private _isScheduled = false;
private _beginBackgroundTokenization(): void {
if (this._textModel.isAttachedToEditor() && this._hasLinesToTokenize()) {
platform.setImmediate(() => {
if (this._isDisposed) {
// disposed in the meantime
return;
}
this._revalidateTokensNow();
});
if (this._isScheduled || !this._textModel.isAttachedToEditor() || !this._hasLinesToTokenize()) {
return;
}
this._isScheduled = true;
runWhenIdle((deadline) => {
this._isScheduled = false;
if (this._isDisposed) {
// disposed in the meantime
return;
}
this._revalidateTokensNow(deadline);
});
}
private _revalidateTokensNow(): void {
private _revalidateTokensNow(deadline: IdleDeadline): void {
const textModelLastLineNumber = this._textModel.getLineCount();
const MAX_ALLOWED_TIME = 1;
@ -275,7 +282,7 @@ export class TextModelTokenization extends Disposable {
const sw = StopWatch.create(false);
let tokenizedLineNumber = -1;
while (this._hasLinesToTokenize()) {
do {
if (sw.elapsed() > MAX_ALLOWED_TIME) {
// Stop if MAX_ALLOWED_TIME is reached
break;
@ -286,7 +293,7 @@ export class TextModelTokenization extends Disposable {
if (tokenizedLineNumber >= textModelLastLineNumber) {
break;
}
}
} while (this._hasLinesToTokenize() && deadline.timeRemaining() > 0);
this._beginBackgroundTokenization();
this._textModel.setTokens(builder.tokens, !this._hasLinesToTokenize());

View file

@ -0,0 +1,188 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IRange, Range } from 'vs/editor/common/core/range';
import { Searcher } from 'vs/editor/common/model/textModelSearch';
import * as strings from 'vs/base/common/strings';
export class UnicodeTextModelHighlighter {
public static computeUnicodeHighlights(model: IUnicodeCharacterSearcherTarget, options: UnicodeHighlighterOptions, range?: IRange): Range[] {
const startLine = range ? range.startLineNumber : 1;
const endLine = range ? range.endLineNumber : model.getLineCount();
const codePointHighlighter = new CodePointHighlighter(options);
const candidates = codePointHighlighter.getCandidateCodePoints();
let regex: RegExp;
if (candidates === 'allNonBasicAscii') {
regex = new RegExp('[^\\t\\n\\r\\x20-\\x7E]', 'g');
} else {
regex = new RegExp(`${buildRegExpCharClassExpr(Array.from(candidates))}`, 'g');
}
const searcher = new Searcher(null, regex);
const result: Range[] = [];
let m: RegExpExecArray | null;
for (let lineNumber = startLine, lineCount = endLine; lineNumber <= lineCount; lineNumber++) {
const lineContent = model.getLineContent(lineNumber);
const lineLength = lineContent.length;
// Reset regex to search from the beginning
searcher.reset(0);
do {
m = searcher.next(lineContent);
if (m) {
let startIndex = m.index;
let endIndex = m.index + m[0].length;
// Extend range to entire code point
if (startIndex > 0) {
const charCodeBefore = lineContent.charCodeAt(startIndex - 1);
if (strings.isHighSurrogate(charCodeBefore)) {
startIndex--;
}
}
if (endIndex + 1 < lineLength) {
const charCodeBefore = lineContent.charCodeAt(endIndex - 1);
if (strings.isHighSurrogate(charCodeBefore)) {
endIndex++;
}
}
const str = lineContent.substring(startIndex, endIndex);
if (codePointHighlighter.shouldHighlightNonBasicASCII(str) !== SimpleHighlightReason.None) {
result.push(new Range(lineNumber, startIndex + 1, lineNumber, endIndex + 1));
const maxResultLength = 1000;
if (result.length > maxResultLength) {
// TODO@hediet a message should be shown in this case
break;
}
}
}
} while (m);
}
return result;
}
public static computeUnicodeHighlightReason(char: string, options: UnicodeHighlighterOptions): UnicodeHighlighterReason | null {
const codePointHighlighter = new CodePointHighlighter(options);
const reason = codePointHighlighter.shouldHighlightNonBasicASCII(char);
switch (reason) {
case SimpleHighlightReason.None:
return null;
case SimpleHighlightReason.Invisible:
return { kind: UnicodeHighlighterReasonKind.Invisible };
case SimpleHighlightReason.Ambiguous:
const primaryConfusable = strings.AmbiguousCharacters.getPrimaryConfusable(char.codePointAt(0)!)!;
return { kind: UnicodeHighlighterReasonKind.Ambiguous, confusableWith: String.fromCodePoint(primaryConfusable) };
case SimpleHighlightReason.NonBasicASCII:
return { kind: UnicodeHighlighterReasonKind.NonBasicAscii };
}
}
}
function buildRegExpCharClassExpr(codePoints: number[], flags?: string): string {
const src = `[${strings.escapeRegExpCharacters(
codePoints.map((i) => String.fromCodePoint(i)).join('')
)}]`;
return src;
}
export const enum UnicodeHighlighterReasonKind {
Ambiguous, Invisible, NonBasicAscii
}
export type UnicodeHighlighterReason = {
kind: UnicodeHighlighterReasonKind.Ambiguous;
confusableWith: string;
} | {
kind: UnicodeHighlighterReasonKind.Invisible;
} | {
kind: UnicodeHighlighterReasonKind.NonBasicAscii
};
class CodePointHighlighter {
private readonly allowedCodePoints: Set<number>;
constructor(private readonly options: UnicodeHighlighterOptions) {
this.allowedCodePoints = new Set(options.allowedCodePoints);
}
public getCandidateCodePoints(): Set<number> | 'allNonBasicAscii' {
if (this.options.nonBasicASCII) {
return 'allNonBasicAscii';
}
const set = new Set<number>();
if (this.options.invisibleCharacters) {
for (const cp of strings.InvisibleCharacters.codePoints) {
set.add(cp);
}
}
if (this.options.ambiguousCharacters) {
for (const cp of strings.AmbiguousCharacters.getPrimaryConfusableCodePoints()) {
set.add(cp);
}
}
for (const cp of this.allowedCodePoints) {
set.delete(cp);
}
return set;
}
public shouldHighlightNonBasicASCII(character: string): SimpleHighlightReason {
const codePoint = character.codePointAt(0)!;
if (this.allowedCodePoints.has(codePoint)) {
return SimpleHighlightReason.None;
}
if (this.options.nonBasicASCII) {
return SimpleHighlightReason.NonBasicASCII;
}
if (this.options.invisibleCharacters) {
const isAllowedInvisibleCharacter = character === ' ' || character === '\n' || character === '\t';
// TODO check for emojis
if (!isAllowedInvisibleCharacter && strings.InvisibleCharacters.isInvisibleCharacter(codePoint)) {
return SimpleHighlightReason.Invisible;
}
}
if (this.options.ambiguousCharacters) {
if (strings.AmbiguousCharacters.isAmbiguous(codePoint)) {
return SimpleHighlightReason.Ambiguous;
}
}
return SimpleHighlightReason.None;
}
}
const enum SimpleHighlightReason {
None,
NonBasicASCII,
Invisible,
Ambiguous
}
export interface IUnicodeCharacterSearcherTarget {
getLineCount(): number;
getLineContent(lineNumber: number): string;
}
export interface UnicodeHighlighterOptions {
nonBasicASCII: boolean;
ambiguousCharacters: boolean;
invisibleCharacters: boolean;
includeComments: boolean;
allowedCodePoints: number[];
}

View file

@ -23,6 +23,7 @@ import { createMonacoBaseAPI } from 'vs/editor/common/standalone/standaloneBase'
import * as types from 'vs/base/common/types';
import { EditorWorkerHost } from 'vs/editor/common/services/editorWorkerServiceImpl';
import { StopWatch } from 'vs/base/common/stopwatch';
import { UnicodeTextModelHighlighter, UnicodeHighlighterOptions } from 'vs/editor/common/modes/unicodeTextModelHighlighter';
export interface IMirrorModel extends IMirrorTextModel {
readonly uri: URI;
@ -371,6 +372,14 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable {
delete this._models[strURL];
}
public async computeUnicodeHighlights(url: string, options: UnicodeHighlighterOptions, range?: IRange): Promise<IRange[]> {
const model = this._getModel(url);
if (!model) {
return [];
}
return UnicodeTextModelHighlighter.computeUnicodeHighlights(model, options, range);
}
// ---- BEGIN diff --------------------------------------------------------------------------
public async computeDiff(originalUrl: string, modifiedUrl: string, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise<IDiffComputationResult | null> {

View file

@ -7,6 +7,7 @@ import { URI } from 'vs/base/common/uri';
import { IRange } from 'vs/editor/common/core/range';
import { IChange, ILineChange } from 'vs/editor/common/editorCommon';
import { IInplaceReplaceSupportResult, TextEdit } from 'vs/editor/common/modes';
import { UnicodeHighlighterOptions } from 'vs/editor/common/modes/unicodeTextModelHighlighter';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const ID_EDITOR_WORKER_SERVICE = 'editorWorkerService';
@ -21,6 +22,9 @@ export interface IDiffComputationResult {
export interface IEditorWorkerService {
readonly _serviceBrand: undefined;
canComputeUnicodeHighlights(uri: URI): boolean;
computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise<IRange[]>;
computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise<IDiffComputationResult | null>;
canComputeDirtyDiff(original: URI, modified: URI): boolean;

View file

@ -23,6 +23,7 @@ import { isNonEmptyArray } from 'vs/base/common/arrays';
import { ILogService } from 'vs/platform/log/common/log';
import { StopWatch } from 'vs/base/common/stopwatch';
import { canceled } from 'vs/base/common/errors';
import { UnicodeHighlighterOptions } from 'vs/editor/common/modes/unicodeTextModelHighlighter';
/**
* Stop syncing a model to the worker if it was not needed for 1 min.
@ -81,6 +82,14 @@ export class EditorWorkerServiceImpl extends Disposable implements IEditorWorker
super.dispose();
}
public canComputeUnicodeHighlights(uri: URI): boolean {
return canSyncModel(this._modelService, uri);
}
public computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise<IRange[]> {
return this._workerManager.withWorker().then(client => client.computedUnicodeHighlights(uri, options, range));
}
public computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise<IDiffComputationResult | null> {
return this._workerManager.withWorker().then(client => client.computeDiff(original, modified, ignoreTrimWhitespace, maxComputationTime));
}
@ -466,6 +475,12 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien
});
}
public computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise<IRange[]> {
return this._withSyncedResources([uri]).then(proxy => {
return proxy.computeUnicodeHighlights(uri.toString(), options, range);
});
}
public computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise<IDiffComputationResult | null> {
return this._withSyncedResources([original, modified], /* forceLargeModels */true).then(proxy => {
return proxy.computeDiff(original.toString(), modified.toString(), ignoreTrimWhitespace, maxComputationTime);

View file

@ -279,25 +279,26 @@ export enum EditorOption {
suggestSelection = 109,
tabCompletion = 110,
tabIndex = 111,
unusualLineTerminators = 112,
useShadowDOM = 113,
useTabStops = 114,
wordSeparators = 115,
wordWrap = 116,
wordWrapBreakAfterCharacters = 117,
wordWrapBreakBeforeCharacters = 118,
wordWrapColumn = 119,
wordWrapOverride1 = 120,
wordWrapOverride2 = 121,
wrappingIndent = 122,
wrappingStrategy = 123,
showDeprecated = 124,
inlayHints = 125,
editorClassName = 126,
pixelRatio = 127,
tabFocusMode = 128,
layoutInfo = 129,
wrappingInfo = 130
unicodeHighlighting = 112,
unusualLineTerminators = 113,
useShadowDOM = 114,
useTabStops = 115,
wordSeparators = 116,
wordWrap = 117,
wordWrapBreakAfterCharacters = 118,
wordWrapBreakBeforeCharacters = 119,
wordWrapColumn = 120,
wordWrapOverride1 = 121,
wordWrapOverride2 = 122,
wrappingIndent = 123,
wrappingStrategy = 124,
showDeprecated = 125,
inlayHints = 126,
editorClassName = 127,
pixelRatio = 128,
tabFocusMode = 129,
layoutInfo = 130,
wrappingInfo = 131
}
/**

View file

@ -76,6 +76,8 @@ export const editorBracketPairGuideActiveBackground4 = registerColor('editorBrac
export const editorBracketPairGuideActiveBackground5 = registerColor('editorBracketPairGuide.activeBackground5', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.activeBackground5', 'Background color of active bracket pair guides (5). Requires enabling bracket pair guides.'));
export const editorBracketPairGuideActiveBackground6 = registerColor('editorBracketPairGuide.activeBackground6', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.activeBackground6', 'Background color of active bracket pair guides (6). Requires enabling bracket pair guides.'));
export const editorUnicodeHighlightBorder = registerColor('editorUnicodeHighlight.border', { dark: '#ff0000', light: '#ff0000', hc: '#ff0000' }, nls.localize('editorUnicodeHighlight.border', 'Border color used to highlight unicode characters.'));
// contains all color rules that used to defined in editor/browser/widget/editor.css
registerThemingParticipant((theme, collector) => {

View file

@ -11,6 +11,7 @@ import { IModelDecoration, ITextModel, PositionAffinity } from 'vs/editor/common
import { IViewModelLines } from 'vs/editor/common/viewModel/viewModelLines';
import { ICoordinatesConverter, InlineDecoration, InlineDecorationType, ViewModelDecoration } from 'vs/editor/common/viewModel/viewModel';
import { filterValidationDecorations } from 'vs/editor/common/config/editorOptions';
import { StandardTokenType } from 'vs/editor/common/modes';
export interface IDecorationsViewportData {
/**
@ -107,6 +108,7 @@ export class ViewModelDecorations implements IDisposable {
private _getDecorationsViewportData(viewportRange: Range): IDecorationsViewportData {
const modelDecorations = this._linesCollection.getDecorationsInRange(viewportRange, this.editorId, filterValidationDecorations(this.configuration.options));
const startLineNumber = viewportRange.startLineNumber;
const endLineNumber = viewportRange.endLineNumber;
@ -120,6 +122,18 @@ export class ViewModelDecorations implements IDisposable {
let modelDecoration = modelDecorations[i];
let decorationOptions = modelDecoration.options;
if (decorationOptions.hideInCommentTokens) {
let allTokensComments = testTokensInRange(
this.model,
modelDecoration.range,
(tokenType) => tokenType === StandardTokenType.Comment
);
if (allTokensComments) {
continue;
}
}
let viewModelDecoration = this._getOrCreateViewModelDecoration(modelDecoration);
let viewRange = viewModelDecoration.range;
@ -161,3 +175,33 @@ export class ViewModelDecorations implements IDisposable {
};
}
}
/**
* Calls the callback for every token that intersects the range.
* If the callback returns `false`, iteration stops and `false` is returned.
* Otherwise, `true` is returned.
*/
function testTokensInRange(model: ITextModel, range: Range, callback: (tokenType: StandardTokenType) => boolean): boolean {
for (let lineNumber = range.startLineNumber; lineNumber <= range.endLineNumber; lineNumber++) {
const lineTokens = model.getLineTokens(lineNumber);
const isFirstLine = lineNumber === range.startLineNumber;
const isEndLine = lineNumber === range.endLineNumber;
let tokenIdx = isFirstLine ? lineTokens.findTokenIndexAtOffset(range.startColumn - 1) : 0;
while (tokenIdx < lineTokens.getCount()) {
if (isEndLine) {
const startOffset = lineTokens.getStartOffset(tokenIdx);
if (startOffset > range.endColumn - 1) {
break;
}
}
const callbackResult = callback(lineTokens.getStandardTokenType(tokenIdx));
if (!callbackResult) {
return false;
}
tokenIdx++;
}
}
return true;
}

View file

@ -118,24 +118,35 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant<Markdow
}
public renderHoverParts(hoverParts: MarkdownHover[], fragment: DocumentFragment, statusBar: IEditorHoverStatusBar): IDisposable {
const disposables = new DisposableStore();
for (const hoverPart of hoverParts) {
for (const contents of hoverPart.contents) {
if (isEmptyMarkdownString(contents)) {
continue;
}
const markdownHoverElement = $('div.hover-row.markdown-hover');
const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents'));
const renderer = disposables.add(new MarkdownRenderer({ editor: this._editor }, this._modeService, this._openerService));
disposables.add(renderer.onDidRenderAsync(() => {
hoverContentsElement.className = 'hover-contents code-hover-contents';
this._hover.onContentsChanged();
}));
const renderedContents = disposables.add(renderer.render(contents));
hoverContentsElement.appendChild(renderedContents.element);
fragment.appendChild(markdownHoverElement);
}
}
return disposables;
return renderMarkdownHovers(hoverParts, fragment, this._editor, this._hover, this._modeService, this._openerService);
}
}
export function renderMarkdownHovers(
hoverParts: MarkdownHover[],
fragment: DocumentFragment,
editor: ICodeEditor,
hover: IEditorHover,
modeService: IModeService,
openerService: IOpenerService,
): IDisposable {
const disposables = new DisposableStore();
for (const hoverPart of hoverParts) {
for (const contents of hoverPart.contents) {
if (isEmptyMarkdownString(contents)) {
continue;
}
const markdownHoverElement = $('div.hover-row.markdown-hover');
const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents'));
const renderer = disposables.add(new MarkdownRenderer({ editor }, modeService, openerService));
disposables.add(renderer.onDidRenderAsync(() => {
hoverContentsElement.className = 'hover-contents code-hover-contents';
hover.onContentsChanged();
}));
const renderedContents = disposables.add(renderer.render(contents));
hoverContentsElement.appendChild(renderedContents.element);
fragment.appendChild(markdownHoverElement);
}
}
return disposables;
}

View file

@ -31,6 +31,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { Context as SuggestContext } from 'vs/editor/contrib/suggest/suggest';
import { UnicodeHighlighterHoverParticipant } from 'vs/editor/contrib/unicodeHighlighter/unicodeHighlighter';
const $ = dom.$;
@ -223,6 +224,7 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I
instantiationService.createInstance(ColorHoverParticipant, editor, this),
instantiationService.createInstance(MarkdownHoverParticipant, editor, this),
instantiationService.createInstance(InlineCompletionsHoverParticipant, editor, this),
instantiationService.createInstance(UnicodeHighlighterHoverParticipant, editor, this),
instantiationService.createInstance(MarkerHoverParticipant, editor, this),
];

View file

@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorAction, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents';
import { CursorMoveCommands } from 'vs/editor/common/controller/cursorMoveCommands';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import * as nls from 'vs/nls';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
export class ExpandLineSelectionAction extends EditorAction {
constructor() {
super({
id: 'expandLineSelection',
label: nls.localize('expandLineSelection', "Expand Line Selection"),
alias: 'Expand Line Selection',
precondition: undefined,
kbOpts: {
weight: KeybindingWeight.EditorCore,
kbExpr: EditorContextKeys.textInputFocus,
primary: KeyMod.CtrlCmd | KeyCode.KeyL
},
});
}
public run(_accessor: ServicesAccessor, editor: ICodeEditor, args: any): void {
args = args || {};
if (!editor.hasModel()) {
return;
}
const viewModel = editor._getViewModel();
viewModel.model.pushStackElement();
viewModel.setCursorStates(
args.source,
CursorChangeReason.Explicit,
CursorMoveCommands.expandLineSelection(viewModel, viewModel.getCursorStates())
);
viewModel.revealPrimaryCursor(args.source, true);
}
}
registerEditorAction(ExpandLineSelectionAction);

View file

@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import type { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorAction } from 'vs/editor/browser/editorExtensions';
import { Position } from 'vs/editor/common/core/position';
import { Selection } from 'vs/editor/common/core/selection';
import { ExpandLineSelectionAction } from 'vs/editor/contrib/lineSelection/lineSelection';
import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
function executeAction(action: EditorAction, editor: ICodeEditor): void {
action.run(null!, editor, undefined);
}
suite('LineSelection', () => {
test('', () => {
const LINE1 = ' \tMy First Line\t ';
const LINE2 = '\tMy Second Line';
const LINE3 = ' Third Line🐶';
const LINE4 = '';
const LINE5 = '1';
const TEXT =
LINE1 + '\r\n' +
LINE2 + '\n' +
LINE3 + '\n' +
LINE4 + '\r\n' +
LINE5;
withTestCodeEditor(TEXT, {}, (editor, viewModel) => {
const action = new ExpandLineSelectionAction();
// 0 1 2
// 01234 56789012345678 0
// let LINE1 = ' \tMy First Line\t ';
editor.setPosition(new Position(1, 1));
executeAction(action, editor);
assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 2, 1));
editor.setPosition(new Position(1, 2));
executeAction(action, editor);
assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 2, 1));
editor.setPosition(new Position(1, 5));
executeAction(action, editor);
assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 2, 1));
editor.setPosition(new Position(1, 19));
executeAction(action, editor);
assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 2, 1));
editor.setPosition(new Position(1, 20));
executeAction(action, editor);
assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 2, 1));
editor.setPosition(new Position(1, 21));
executeAction(action, editor);
assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 2, 1));
executeAction(action, editor);
assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 3, 1));
executeAction(action, editor);
assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 4, 1));
executeAction(action, editor);
assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 5, 1));
executeAction(action, editor);
assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 5, LINE5.length + 1));
executeAction(action, editor);
assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 5, LINE5.length + 1));
});
});
});

View file

@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .unicode-highlight {
border: 1px solid var(--vscode-editorUnicodeHighlight-border);
box-sizing: border-box;
}

View file

@ -0,0 +1,509 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RunOnceScheduler } from 'vs/base/common/async';
import { CharCode } from 'vs/base/common/charCode';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { InvisibleCharacters } from 'vs/base/common/strings';
import 'vs/css!./unicodeHighlighter';
import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorAction, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { DeriveFromWorkspaceTrust, deriveFromWorkspaceTrust, EditorOption, InternalUnicodeHighlightOptions, unicodeHighlightConfigKeys } from 'vs/editor/common/config/editorOptions';
import { Range } from 'vs/editor/common/core/range';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { IModelDecoration, IModelDeltaDecoration, ITextModel, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { UnicodeHighlighterOptions, UnicodeHighlighterReason, UnicodeHighlighterReasonKind, UnicodeTextModelHighlighter } from 'vs/editor/common/modes/unicodeTextModelHighlighter';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { HoverAnchor, HoverAnchorType, IEditorHover, IEditorHoverParticipant, IEditorHoverStatusBar, IHoverPart } from 'vs/editor/contrib/hover/hoverTypes';
import { MarkdownHover, renderMarkdownHovers } from 'vs/editor/contrib/hover/markdownHoverParticipant';
import * as nls from 'vs/nls';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { minimapFindMatch, minimapUnicodeHighlight, overviewRulerFindMatchForeground, overviewRulerUnicodeHighlightForeground } from 'vs/platform/theme/common/colorRegistry';
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust';
export class UnicodeHighlighter extends Disposable implements IEditorContribution {
public static readonly ID = 'editor.contrib.unicodeHighlighter';
private _highlighter: DocumentUnicodeHighlighter | ViewportUnicodeHighlighter | null = null;
private _options: InternalUnicodeHighlightOptions;
constructor(
private readonly _editor: ICodeEditor,
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
@IWorkspaceTrustManagementService private readonly _workspaceTrustService: IWorkspaceTrustManagementService,
) {
super();
this._register(this._editor.onDidChangeModel(() => {
this._updateHighlighter();
}));
this._options = _editor.getOption(EditorOption.unicodeHighlighting);
this._register(_workspaceTrustService.onDidChangeTrust(e => {
this._updateHighlighter();
}));
this._register(_editor.onDidChangeConfiguration(e => {
if (e.hasChanged(EditorOption.unicodeHighlighting)) {
this._options = _editor.getOption(EditorOption.unicodeHighlighting);
this._updateHighlighter();
}
}));
this._updateHighlighter();
}
public override dispose(): void {
if (this._highlighter) {
this._highlighter.dispose();
this._highlighter = null;
}
super.dispose();
}
private _updateHighlighter(): void {
if (this._highlighter) {
this._highlighter.dispose();
this._highlighter = null;
}
if (!this._editor.hasModel()) {
return;
}
const options = resolveOptions(this._workspaceTrustService.isWorkspaceTrusted(), this._options);
if (
[
options.nonBasicASCII,
options.ambiguousCharacters,
options.invisibleCharacters,
].every((option) => option === false)
) {
// Don't do anything if the feature is fully disabled
return;
}
const highlightOptions: UnicodeHighlighterOptions = {
nonBasicASCII: options.nonBasicASCII,
ambiguousCharacters: options.ambiguousCharacters,
invisibleCharacters: options.invisibleCharacters,
includeComments: options.includeComments,
allowedCodePoints: Array.from(options.allowedCharacters).map(c => c.codePointAt(0)!),
};
if (this._editorWorkerService.canComputeUnicodeHighlights(this._editor.getModel().uri)) {
this._highlighter = new DocumentUnicodeHighlighter(this._editor, highlightOptions, this._editorWorkerService);
} else {
this._highlighter = new ViewportUnicodeHighlighter(this._editor, highlightOptions);
}
}
public getDecorationInfo(decorationId: string): UnicodeHighlighterDecorationInfo | null {
if (this._highlighter) {
return this._highlighter.getDecorationInfo(decorationId);
}
return null;
}
}
export interface UnicodeHighlighterDecorationInfo {
reason: UnicodeHighlighterReason;
}
type RemoveDeriveFromWorkspaceTrust<T> = T extends DeriveFromWorkspaceTrust ? never : T;
function resolveOptions(_trusted: boolean, options: InternalUnicodeHighlightOptions): { [TKey in keyof InternalUnicodeHighlightOptions]: RemoveDeriveFromWorkspaceTrust<InternalUnicodeHighlightOptions[TKey]> } {
/*
// TODO@hediet enable some settings by default (depending on trust).
// For now, make it opt in, so there is some time to test it without breaking anyone.
return {
nonBasicASCII: options.nonBasicASCII !== deriveFromWorkspaceTrust ? options.nonBasicASCII : (trusted ? false : true),
ambiguousCharacters: options.ambiguousCharacters !== deriveFromWorkspaceTrust ? options.ambiguousCharacters : (trusted ? false : true),
invisibleCharacters: options.invisibleCharacters !== deriveFromWorkspaceTrust ? options.invisibleCharacters : (trusted ? true : true),
includeComments: options.includeComments !== deriveFromWorkspaceTrust ? options.includeComments : (trusted ? true : false),
allowedCharacters: options.allowedCharacters ?? [],
};
*/
return {
nonBasicASCII: options.nonBasicASCII !== deriveFromWorkspaceTrust ? options.nonBasicASCII : false,
ambiguousCharacters: options.ambiguousCharacters !== deriveFromWorkspaceTrust ? options.ambiguousCharacters : false,
invisibleCharacters: options.invisibleCharacters !== deriveFromWorkspaceTrust ? options.invisibleCharacters : false,
includeComments: options.includeComments !== deriveFromWorkspaceTrust ? options.includeComments : true,
allowedCharacters: options.allowedCharacters ?? [],
};
}
class DocumentUnicodeHighlighter extends Disposable {
private readonly _model: ITextModel = this._editor.getModel();
private readonly _updateSoon: RunOnceScheduler;
private _decorationIds = new Set<string>();
constructor(
private readonly _editor: IActiveCodeEditor,
private readonly _options: UnicodeHighlighterOptions,
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
) {
super();
this._updateSoon = this._register(new RunOnceScheduler(() => this._update(), 250));
this._register(this._editor.onDidChangeModelContent(() => {
this._updateSoon.schedule();
}));
this._updateSoon.schedule();
}
public override dispose() {
this._decorationIds = new Set(this._model.deltaDecorations(Array.from(this._decorationIds), []));
super.dispose();
}
private _update(): void {
if (!this._model.mightContainNonBasicASCII()) {
this._decorationIds = new Set(this._editor.deltaDecorations(Array.from(this._decorationIds), []));
return;
}
const modelVersionId = this._model.getVersionId();
this._editorWorkerService
.computedUnicodeHighlights(this._model.uri, this._options)
.then((ranges) => {
if (this._model.getVersionId() !== modelVersionId) {
// model changed in the meantime
return;
}
const decorations: IModelDeltaDecoration[] = [];
for (const range of ranges) {
decorations.push({ range: range, options: this._options.includeComments ? DECORATION : DECORATION_HIDE_IN_COMMENTS });
}
this._decorationIds = new Set(this._editor.deltaDecorations(
Array.from(this._decorationIds),
decorations
));
});
}
public getDecorationInfo(decorationId: string): UnicodeHighlighterDecorationInfo | null {
if (!this._decorationIds.has(decorationId)) {
return null;
}
const range = this._editor.getModel().getDecorationRange(decorationId)!;
const text = this._editor.getModel().getValueInRange(range);
return {
reason: computeReason(text, this._options)!,
};
}
}
class ViewportUnicodeHighlighter extends Disposable {
private readonly _model: ITextModel = this._editor.getModel();
private readonly _updateSoon: RunOnceScheduler;
private _decorationIds = new Set<string>();
constructor(
private readonly _editor: IActiveCodeEditor,
private readonly _options: UnicodeHighlighterOptions
) {
super();
this._updateSoon = this._register(new RunOnceScheduler(() => this._update(), 250));
this._register(this._editor.onDidLayoutChange(() => {
this._updateSoon.schedule();
}));
this._register(this._editor.onDidScrollChange(() => {
this._updateSoon.schedule();
}));
this._register(this._editor.onDidChangeHiddenAreas(() => {
this._updateSoon.schedule();
}));
this._register(this._editor.onDidChangeModelContent(() => {
this._updateSoon.schedule();
}));
this._updateSoon.schedule();
}
public override dispose() {
this._decorationIds = new Set(this._model.deltaDecorations(Array.from(this._decorationIds), []));
super.dispose();
}
private _update(): void {
if (!this._model.mightContainNonBasicASCII()) {
this._decorationIds = new Set(this._editor.deltaDecorations(Array.from(this._decorationIds), []));
return;
}
const ranges = this._editor.getVisibleRanges();
const decorations: IModelDeltaDecoration[] = [];
for (const range of ranges) {
const ranges = UnicodeTextModelHighlighter.computeUnicodeHighlights(this._model, this._options, range);
for (const range of ranges) {
decorations.push({ range, options: this._options.includeComments ? DECORATION : DECORATION_HIDE_IN_COMMENTS });
}
}
this._decorationIds = new Set(this._editor.deltaDecorations(Array.from(this._decorationIds), decorations));
}
public getDecorationInfo(decorationId: string): UnicodeHighlighterDecorationInfo | null {
if (!this._decorationIds.has(decorationId)) {
return null;
}
const range = this._editor.getModel().getDecorationRange(decorationId)!;
const text = this._editor.getModel().getValueInRange(range);
return {
reason: computeReason(text, this._options)!,
};
}
}
export class UnicodeHighlighterHover implements IHoverPart {
constructor(
public readonly owner: IEditorHoverParticipant<UnicodeHighlighterHover>,
public readonly range: Range,
public readonly decoration: IModelDecoration
) { }
public isValidForHoverAnchor(anchor: HoverAnchor): boolean {
return (
anchor.type === HoverAnchorType.Range
&& this.range.startColumn <= anchor.range.startColumn
&& this.range.endColumn >= anchor.range.endColumn
);
}
}
export class UnicodeHighlighterHoverParticipant implements IEditorHoverParticipant<MarkdownHover> {
constructor(
private readonly _editor: ICodeEditor,
private readonly _hover: IEditorHover,
@IModeService private readonly _modeService: IModeService,
@IOpenerService private readonly _openerService: IOpenerService,
) {
}
computeSync(anchor: HoverAnchor, lineDecorations: IModelDecoration[]): MarkdownHover[] {
if (!this._editor.hasModel() || anchor.type !== HoverAnchorType.Range) {
return [];
}
const model = this._editor.getModel();
const unicodeHighlighter = this._editor.getContribution<UnicodeHighlighter>(UnicodeHighlighter.ID);
const result: MarkdownHover[] = [];
for (const d of lineDecorations) {
const highlightInfo = unicodeHighlighter.getDecorationInfo(d.id);
if (!highlightInfo) {
continue;
}
const char = model.getValueInRange(d.range);
// text refers to a single character.
const codePoint = char.codePointAt(0)!;
function formatCodePoint(codePoint: number) {
let value = `\`U+${codePoint.toString(16).padStart(4, '0')}\``;
if (!InvisibleCharacters.isInvisibleCharacter(codePoint)) {
// Don't render any control characters or any invisible characters, as they cannot be seen anyways.
value += ` "${`${renderCodePointAsInlineCode(codePoint)}`}"`;
}
return value;
}
const codePointStr = formatCodePoint(codePoint);
let reason: string;
switch (highlightInfo.reason.kind) {
case UnicodeHighlighterReasonKind.Ambiguous:
reason = nls.localize(
'unicodeHighlight.characterIsAmbiguous',
'The character {0} could be confused with the character {1}, which is more common in source code.',
codePointStr,
formatCodePoint(highlightInfo.reason.confusableWith.codePointAt(0)!)
);
break;
case UnicodeHighlighterReasonKind.Invisible:
reason = nls.localize(
'unicodeHighlight.characterIsInvisible',
'The character {0} is invisible.',
codePointStr
);
break;
case UnicodeHighlighterReasonKind.NonBasicAscii:
reason = nls.localize(
'unicodeHighlight.characterIsNonBasicAscii',
'The character {0} is not a basic ASCII character.',
codePoint
);
break;
}
const adjustSettingsArgs: ShowExcludeOptionsArgs = {
codePoint: codePoint,
reason: highlightInfo.reason.kind,
};
const adjustSettings = nls.localize('unicodeHighlight.adjustSettings', 'Adjust settings');
const contents: Array<IMarkdownString> = [{
value: `${reason} [${adjustSettings}](command:${ShowExcludeOptions.ID}?${encodeURIComponent(JSON.stringify(adjustSettingsArgs))})`,
isTrusted: true,
}];
result.push(new MarkdownHover(this, d.range, contents));
}
return result;
}
public renderHoverParts(hoverParts: MarkdownHover[], fragment: DocumentFragment, statusBar: IEditorHoverStatusBar): IDisposable {
return renderMarkdownHovers(hoverParts, fragment, this._editor, this._hover, this._modeService, this._openerService);
}
}
function renderCodePointAsInlineCode(codePoint: number): string {
if (codePoint === CharCode.BackTick) {
return '`` ` ``';
}
return '`' + String.fromCodePoint(codePoint) + '`';
}
function computeReason(char: string, options: UnicodeHighlighterOptions): UnicodeHighlighterReason | null {
return UnicodeTextModelHighlighter.computeUnicodeHighlightReason(char, options);
}
const DECORATION_HIDE_IN_COMMENTS = ModelDecorationOptions.register({
description: 'unicode-highlight',
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
className: 'unicode-highlight',
showIfCollapsed: true,
overviewRuler: {
color: themeColorFromId(overviewRulerUnicodeHighlightForeground),
position: OverviewRulerLane.Center
},
minimap: {
color: themeColorFromId(minimapUnicodeHighlight),
position: MinimapPosition.Inline
},
hideInCommentTokens: true
});
const DECORATION = ModelDecorationOptions.register({
description: 'unicode-highlight',
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
className: 'unicode-highlight',
showIfCollapsed: true,
overviewRuler: {
color: themeColorFromId(overviewRulerFindMatchForeground),
position: OverviewRulerLane.Center
},
minimap: {
color: themeColorFromId(minimapFindMatch),
position: MinimapPosition.Inline
}
});
interface ShowExcludeOptionsArgs {
codePoint: number;
reason: UnicodeHighlighterReason['kind'];
}
export class ShowExcludeOptions extends EditorAction {
public static ID = 'editor.action.unicodeHighlight.showExcludeOptions';
constructor() {
super({
id: ShowExcludeOptions.ID,
label: nls.localize('action.unicodeHighlight.showExcludeOptions', "Show Exclude Options"),
alias: 'Show Exclude Options',
precondition: undefined
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor, args: any): Promise<void> {
const { codePoint, reason } = args as ShowExcludeOptionsArgs;
const char = String.fromCodePoint(codePoint);
const quickPickService = accessor!.get(IQuickInputService);
const configurationService = accessor!.get(IConfigurationService);
interface ExtendedOptions extends IQuickPickItem {
run(): Promise<void>;
}
const options: ExtendedOptions[] = [
{
label: nls.localize('unicodeHighlight.excludeCharFromBeingHighlighted', 'Exclude {0} from being highlighted', `U+${codePoint.toString(16)} "${char}"`),
run: async () => {
const existingValue = configurationService.getValue(unicodeHighlightConfigKeys.allowedCharacters);
let value: string;
if (typeof existingValue === 'string') {
value = existingValue;
} else {
value = '';
}
value += char;
await configurationService.updateValue(unicodeHighlightConfigKeys.allowedCharacters, value, ConfigurationTarget.USER);
}
},
];
if (reason === UnicodeHighlighterReasonKind.Ambiguous) {
options.push({
label: nls.localize('unicodeHighlight.disableHighlightingOfAmbiguousCharacters', 'Disable highlighting of ambiguous characters'),
run: async () => {
await configurationService.updateValue(unicodeHighlightConfigKeys.ambiguousCharacters, false, ConfigurationTarget.USER);
}
});
}
else if (reason === UnicodeHighlighterReasonKind.Invisible) {
options.push({
label: nls.localize('unicodeHighlight.disableHighlightingOfInvisibleCharacters', 'Disable highlighting of invisible characters'),
run: async () => {
await configurationService.updateValue(unicodeHighlightConfigKeys.invisibleCharacters, false, ConfigurationTarget.USER);
}
});
}
else if (reason === UnicodeHighlighterReasonKind.NonBasicAscii) {
options.push({
label: nls.localize('unicodeHighlight.disableHighlightingOfNonBasicAsciiCharacters', 'Disable highlighting of non basic ASCII characters'),
run: async () => {
await configurationService.updateValue(unicodeHighlightConfigKeys.nonBasicASCII, false, ConfigurationTarget.USER);
}
});
} else {
expectNever(reason);
}
const result = await quickPickService.pick(
options,
{ title: nls.localize('unicodeHighlight.configureUnicodeHighlightOptions', 'Configure Unicode Highlight Options') }
);
if (result) {
await result.run();
}
}
}
function expectNever(value: never) {
throw new Error(`Unexpected value: ${value}`);
}
registerEditorAction(ShowExcludeOptions);
registerEditorContribution(UnicodeHighlighter.ID, UnicodeHighlighter);

View file

@ -32,6 +32,7 @@ import 'vs/editor/contrib/hover/hover';
import 'vs/editor/contrib/indentation/indentation';
import 'vs/editor/contrib/inlayHints/inlayHintsController';
import 'vs/editor/contrib/inPlaceReplace/inPlaceReplace';
import 'vs/editor/contrib/lineSelection/lineSelection';
import 'vs/editor/contrib/linesOperations/linesOperations';
import 'vs/editor/contrib/linkedEditing/linkedEditing';
import 'vs/editor/contrib/links/links';
@ -43,6 +44,7 @@ import 'vs/editor/contrib/snippet/snippetController2';
import 'vs/editor/contrib/suggest/suggestController';
import 'vs/editor/contrib/tokenization/tokenization';
import 'vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode';
import 'vs/editor/contrib/unicodeHighlighter/unicodeHighlighter';
import 'vs/editor/contrib/unusualLineTerminators/unusualLineTerminators';
import 'vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens';
import 'vs/editor/contrib/wordHighlighter/wordHighlighter';

View file

@ -47,6 +47,7 @@ import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/pla
import { basename } from 'vs/base/common/resources';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { ILogService } from 'vs/platform/log/common/log';
import { IWorkspaceTrustManagementService, IWorkspaceTrustTransitionParticipant, IWorkspaceTrustUriInfo } from 'vs/platform/workspace/common/workspaceTrust';
export class SimpleModel implements IResolvedTextEditorModel {
@ -798,3 +799,48 @@ export class SimpleLayoutService implements ILayoutService {
constructor(private _codeEditorService: ICodeEditorService, private _container: HTMLElement) { }
}
export class SimpleWorkspaceTrustManagementService implements IWorkspaceTrustManagementService {
_serviceBrand: undefined;
private _neverEmitter = new Emitter<never>();
public readonly onDidChangeTrust: Event<boolean> = this._neverEmitter.event;
onDidChangeTrustedFolders: Event<void> = this._neverEmitter.event;
public readonly workspaceResolved = Promise.resolve();
public readonly workspaceTrustInitialized = Promise.resolve();
public readonly acceptsOutOfWorkspaceFiles = true;
isWorkspaceTrusted(): boolean {
return true;
}
isWorkspaceTrustForced(): boolean {
return false;
}
canSetParentFolderTrust(): boolean {
return false;
}
async setParentFolderTrust(trusted: boolean): Promise<void> {
// noop
}
canSetWorkspaceTrust(): boolean {
return false;
}
async setWorkspaceTrust(trusted: boolean): Promise<void> {
// noop
}
getUriTrustInfo(uri: URI): Promise<IWorkspaceTrustUriInfo> {
throw new Error('Method not supported.');
}
async setUrisTrust(uri: URI[], trusted: boolean): Promise<void> {
// noop
}
getTrustedUris(): URI[] {
return [];
}
async setTrustedUris(uris: URI[]): Promise<void> {
// noop
}
addWorkspaceTrustTransitionParticipant(participant: IWorkspaceTrustTransitionParticipant): IDisposable {
throw new Error('Method not supported.');
}
}

View file

@ -13,7 +13,7 @@ import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl';
import { ITextResourceConfigurationService, ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { SimpleBulkEditService, SimpleConfigurationService, SimpleDialogService, SimpleNotificationService, SimpleEditorProgressService, SimpleResourceConfigurationService, SimpleResourcePropertiesService, SimpleUriLabelService, SimpleWorkspaceContextService, StandaloneCommandService, StandaloneKeybindingService, StandaloneTelemetryService, SimpleLayoutService } from 'vs/editor/standalone/browser/simpleServices';
import { SimpleBulkEditService, SimpleConfigurationService, SimpleDialogService, SimpleNotificationService, SimpleEditorProgressService, SimpleResourceConfigurationService, SimpleResourcePropertiesService, SimpleUriLabelService, SimpleWorkspaceContextService, StandaloneCommandService, StandaloneKeybindingService, StandaloneTelemetryService, SimpleLayoutService, SimpleWorkspaceTrustManagementService } from 'vs/editor/standalone/browser/simpleServices';
import { StandaloneCodeEditorServiceImpl } from 'vs/editor/standalone/browser/standaloneCodeServiceImpl';
import { StandaloneThemeServiceImpl } from 'vs/editor/standalone/browser/standaloneThemeServiceImpl';
import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService';
@ -55,6 +55,7 @@ import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService';
import { StandaloneQuickInputServiceImpl } from 'vs/editor/standalone/browser/quickInput/standaloneQuickInputServiceImpl';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { ILanguageConfigurationService, LanguageConfigurationService } from 'vs/editor/common/modes/languageConfigurationRegistry';
import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust';
export interface IEditorOverrideServices {
[index: string]: any;
@ -243,6 +244,8 @@ export class DynamicStandaloneServices extends Disposable {
ensure(IMenuService, () => new MenuService(commandService));
ensure(IBulkEditService, () => new SimpleBulkEditService(StaticServices.modelService.get(IModelService)));
ensure(IWorkspaceTrustManagementService, () => new SimpleWorkspaceTrustManagementService());
}
public get<T>(serviceId: ServiceIdentifier<T>): T {

View file

@ -739,47 +739,6 @@ suite('Editor Controller - Cursor', () => {
});
});
test('expandLineSelection', () => {
runTest((editor, viewModel) => {
// 0 1 2
// 01234 56789012345678 0
// let LINE1 = ' \tMy First Line\t ';
moveTo(editor, viewModel, 1, 1);
CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {});
assertCursor(viewModel, new Selection(1, 1, 2, 1));
moveTo(editor, viewModel, 1, 2);
CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {});
assertCursor(viewModel, new Selection(1, 1, 2, 1));
moveTo(editor, viewModel, 1, 5);
CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {});
assertCursor(viewModel, new Selection(1, 1, 2, 1));
moveTo(editor, viewModel, 1, 19);
CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {});
assertCursor(viewModel, new Selection(1, 1, 2, 1));
moveTo(editor, viewModel, 1, 20);
CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {});
assertCursor(viewModel, new Selection(1, 1, 2, 1));
moveTo(editor, viewModel, 1, 21);
CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {});
assertCursor(viewModel, new Selection(1, 1, 2, 1));
CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {});
assertCursor(viewModel, new Selection(1, 1, 3, 1));
CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {});
assertCursor(viewModel, new Selection(1, 1, 4, 1));
CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {});
assertCursor(viewModel, new Selection(1, 1, 5, 1));
CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {});
assertCursor(viewModel, new Selection(1, 1, 5, LINE5.length + 1));
CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {});
assertCursor(viewModel, new Selection(1, 1, 5, LINE5.length + 1));
});
});
// --------- eventing
test('no move doesn\'t trigger event', () => {

57
src/vs/monaco.d.ts vendored
View file

@ -3299,6 +3299,7 @@ declare namespace monaco.editor {
* Controls the behavior of editor guides.
*/
guides?: IGuidesOptions;
unicodeHighlight?: IUnicodeHighlightOptions;
}
export interface IDiffEditorBaseOptions {
@ -3862,6 +3863,22 @@ declare namespace monaco.editor {
readonly scrollByPage: boolean;
}
export type DeriveFromWorkspaceTrust = 'deriveFromWorkspaceTrust';
/**
* Configuration options for unicode highlighting.
*/
export interface IUnicodeHighlightOptions {
nonBasicASCII?: boolean | DeriveFromWorkspaceTrust;
invisibleCharacters?: boolean | DeriveFromWorkspaceTrust;
ambiguousCharacters?: boolean | DeriveFromWorkspaceTrust;
includeComments?: boolean | DeriveFromWorkspaceTrust;
/**
* A list of allowed code points in a single string.
*/
allowedCharacters?: string;
}
export interface IInlineSuggestOptions {
/**
* Enable or disable the rendering of automatic inline completions.
@ -4216,25 +4233,26 @@ declare namespace monaco.editor {
suggestSelection = 109,
tabCompletion = 110,
tabIndex = 111,
unusualLineTerminators = 112,
useShadowDOM = 113,
useTabStops = 114,
wordSeparators = 115,
wordWrap = 116,
wordWrapBreakAfterCharacters = 117,
wordWrapBreakBeforeCharacters = 118,
wordWrapColumn = 119,
wordWrapOverride1 = 120,
wordWrapOverride2 = 121,
wrappingIndent = 122,
wrappingStrategy = 123,
showDeprecated = 124,
inlayHints = 125,
editorClassName = 126,
pixelRatio = 127,
tabFocusMode = 128,
layoutInfo = 129,
wrappingInfo = 130
unicodeHighlighting = 112,
unusualLineTerminators = 113,
useShadowDOM = 114,
useTabStops = 115,
wordSeparators = 116,
wordWrap = 117,
wordWrapBreakAfterCharacters = 118,
wordWrapBreakBeforeCharacters = 119,
wordWrapColumn = 120,
wordWrapOverride1 = 121,
wordWrapOverride2 = 122,
wrappingIndent = 123,
wrappingStrategy = 124,
showDeprecated = 125,
inlayHints = 126,
editorClassName = 127,
pixelRatio = 128,
tabFocusMode = 129,
layoutInfo = 130,
wrappingInfo = 131
}
export const EditorOptions: {
@ -4352,6 +4370,7 @@ declare namespace monaco.editor {
suggestSelection: IEditorOption<EditorOption.suggestSelection, 'first' | 'recentlyUsed' | 'recentlyUsedByPrefix'>;
tabCompletion: IEditorOption<EditorOption.tabCompletion, 'on' | 'off' | 'onlySnippets'>;
tabIndex: IEditorOption<EditorOption.tabIndex, number>;
unicodeHighlight: IEditorOption<EditorOption.unicodeHighlighting, Required<Readonly<IUnicodeHighlightOptions>>>;
unusualLineTerminators: IEditorOption<EditorOption.unusualLineTerminators, 'auto' | 'off' | 'prompt'>;
useShadowDOM: IEditorOption<EditorOption.useShadowDOM, boolean>;
useTabStops: IEditorOption<EditorOption.useTabStops, boolean>;

View file

@ -290,14 +290,16 @@ function wrapText(text: string, columns: number): string[] {
return lines;
}
export function buildHelpMessage(productName: string, executableName: string, version: string, options: OptionDescriptions<any>, isPipeSupported = true): string {
export function buildHelpMessage(productName: string, executableName: string, version: string, options: OptionDescriptions<any>, capabilities?: { noPipe?: boolean, noInputFiles: boolean }): string {
const columns = (process.stdout).isTTY && (process.stdout).columns || 80;
let help = [`${productName} ${version}`];
const inputFiles = capabilities?.noInputFiles !== true ? `[${localize('paths', 'paths')}...]` : '';
const help = [`${productName} ${version}`];
help.push('');
help.push(`${localize('usage', "Usage")}: ${executableName} [${localize('options', "options")}][${localize('paths', 'paths')}...]`);
help.push(`${localize('usage', "Usage")}: ${executableName} [${localize('options', "options")}]${inputFiles}`);
help.push('');
if (isPipeSupported) {
if (capabilities?.noPipe !== true) {
if (isWindows) {
help.push(localize('stdinWindows', "To read output from another program, append '-' (e.g. 'echo Hello World | {0} -')", executableName));
} else {

View file

@ -22,6 +22,8 @@ import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
export type Metadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean; isPreReleaseVersion: boolean, hadPreReleaseVersion: boolean, installedTimestamp: number }>;
export interface IInstallExtensionTask {
readonly identifier: IExtensionIdentifier;
readonly source: IGalleryExtension | URI;
@ -85,8 +87,13 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
async installFromGallery(extension: IGalleryExtension, options: InstallOptions = {}): Promise<ILocalExtension> {
try {
return await this.doInstallFromGallery(extension, options);
if (!this.galleryService.isEnabled()) {
throw new ExtensionManagementError(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"), ExtensionManagementErrorCode.Internal);
}
const compatible = await this.checkAndGetCompatibleVersion(extension, !options.installGivenVersion, !!options.installPreReleaseVersion);
return await this.installExtension(compatible.manifest, compatible.extension, options);
} catch (error) {
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
this.logService.error(`Failed to install extension.`, extension.identifier.id);
this.logService.error(error);
throw toExtensionManagementError(error);
@ -128,41 +135,6 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
this.participants.push(participant);
}
private async doInstallFromGallery(extension: IGalleryExtension, options: InstallOptions = {}): Promise<ILocalExtension> {
if (!this.galleryService.isEnabled()) {
throw new ExtensionManagementError(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"), ExtensionManagementErrorCode.Internal);
}
if (!await this.canInstall(extension)) {
const targetPlatform = await this.getTargetPlatform();
const error = new ExtensionManagementError(nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform)), ExtensionManagementErrorCode.Incompatible);
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
throw error;
}
try {
extension = await this.checkAndGetCompatibleVersion(extension, !options.installGivenVersion);
} catch (error) {
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
throw error;
}
const manifest = await this.galleryService.getManifest(extension, CancellationToken.None);
if (manifest === null) {
const error = new ExtensionManagementError(`Missing manifest for extension ${extension.identifier.id}`, ExtensionManagementErrorCode.Invalid);
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
throw error;
}
if (manifest.version !== extension.version) {
const error = new ExtensionManagementError(`Cannot install '${extension.identifier.id}' extension because of version mismatch in Marketplace`, ExtensionManagementErrorCode.Invalid);
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
throw error;
}
return this.installExtension(manifest, extension, options);
}
protected async installExtension(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions & InstallVSIXOptions): Promise<ILocalExtension> {
// only cache gallery extensions tasks
if (!URI.isUri(extension)) {
@ -190,7 +162,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
this.logService.info('Installing the extension without checking dependencies and pack', installExtensionTask.identifier.id);
} else {
try {
const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensionsToInstall(installExtensionTask.identifier, manifest, !!options.installOnlyNewlyAddedFromExtensionPack);
const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensionsToInstall(installExtensionTask.identifier, manifest, !!options.installOnlyNewlyAddedFromExtensionPack, !!options.installPreReleaseVersion);
for (const { gallery, manifest } of allDepsAndPackExtensionsToInstall) {
installExtensionHasDependents = installExtensionHasDependents || !!manifest.extensionDependencies?.some(id => areSameExtensions({ id }, installExtensionTask.identifier));
if (this.installingExtensions.has(new ExtensionIdentifierWithVersion(gallery.identifier, gallery.version).key())) {
@ -322,7 +294,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
return results;
}
private async getAllDepsAndPackExtensionsToInstall(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean): Promise<{ gallery: IGalleryExtension, manifest: IExtensionManifest }[]> {
private async getAllDepsAndPackExtensionsToInstall(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean): Promise<{ gallery: IGalleryExtension, manifest: IExtensionManifest }[]> {
if (!this.galleryService.isEnabled()) {
return [];
}
@ -357,17 +329,19 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
continue;
}
const isDependency = dependecies.some(id => areSameExtensions({ id }, galleryExtension.identifier));
if (!isDependency && !await this.canInstall(galleryExtension)) {
this.logService.info('Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id);
continue;
let compatible;
try {
compatible = await this.checkAndGetCompatibleVersion(galleryExtension, true, installPreRelease);
} catch (error) {
if (error instanceof ExtensionManagementError && error.code === ExtensionManagementErrorCode.IncompatibleTargetPlatform && !isDependency) {
this.logService.info('Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id);
continue;
} else {
throw error;
}
}
const compatibleExtension = await this.checkAndGetCompatibleVersion(galleryExtension, true);
const manifest = await this.galleryService.getManifest(compatibleExtension, CancellationToken.None);
if (manifest === null) {
throw new ExtensionManagementError(`Missing manifest for extension ${compatibleExtension.identifier.id}`, ExtensionManagementErrorCode.Invalid);
}
allDependenciesAndPacks.push({ gallery: compatibleExtension, manifest });
await collectDependenciesAndPackExtensionsToInstall(compatibleExtension.identifier, manifest);
allDependenciesAndPacks.push({ gallery: compatible.extension, manifest: compatible.manifest });
await collectDependenciesAndPackExtensionsToInstall(compatible.extension.identifier, manifest);
}
}
}
@ -378,28 +352,51 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
return allDependenciesAndPacks.filter(e => !installed.some(i => areSameExtensions(i.identifier, e.gallery.identifier)));
}
private async checkAndGetCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean): Promise<IGalleryExtension> {
private async checkAndGetCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean, includePreRelease: boolean): Promise<{ extension: IGalleryExtension, manifest: IExtensionManifest }> {
if (await this.isMalicious(extension)) {
throw new ExtensionManagementError(nls.localize('malicious extension', "Can't install '{0}' extension since it was reported to be problematic.", extension.identifier.id), ExtensionManagementErrorCode.Malicious);
}
const compatibleExtension = await this.getCompatibleVersion(extension, fetchCompatibleVersion);
if (!compatibleExtension) {
if (!await this.canInstall(extension)) {
const targetPlatform = await this.getTargetPlatform();
throw new ExtensionManagementError(nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform)), ExtensionManagementErrorCode.IncompatibleTargetPlatform);
}
const compatibleExtension = await this.getCompatibleVersion(extension, fetchCompatibleVersion, includePreRelease);
if (compatibleExtension) {
if (includePreRelease && !compatibleExtension.properties.isPreReleaseVersion && extension.hasPreReleaseVersion) {
throw new ExtensionManagementError(nls.localize('notFoundCompatiblePrereleaseDependency', "Can't install pre-release version of '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.Incompatible);
}
} else {
throw new ExtensionManagementError(nls.localize('notFoundCompatibleDependency', "Can't install '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.Incompatible);
}
return compatibleExtension;
const manifest = await this.galleryService.getManifest(compatibleExtension, CancellationToken.None);
if (manifest === null) {
throw new ExtensionManagementError(`Missing manifest for extension ${extension.identifier.id}`, ExtensionManagementErrorCode.Invalid);
}
if (manifest.version !== compatibleExtension.version) {
throw new ExtensionManagementError(`Cannot install '${extension.identifier.id}' extension because of version mismatch in Marketplace`, ExtensionManagementErrorCode.Invalid);
}
return { extension: compatibleExtension, manifest };
}
protected async getCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean): Promise<IGalleryExtension | null> {
protected async getCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean, includePreRelease: boolean): Promise<IGalleryExtension | null> {
const targetPlatform = await this.getTargetPlatform();
let compatibleExtension: IGalleryExtension | null = null;
if (await this.galleryService.isExtensionCompatible(extension, targetPlatform)) {
if (fetchCompatibleVersion && extension.hasPreReleaseVersion && extension.properties.isPreReleaseVersion !== includePreRelease) {
compatibleExtension = await this.galleryService.getCompatibleExtension(extension.identifier, includePreRelease, targetPlatform);
}
if (!compatibleExtension && await this.galleryService.isExtensionCompatible(extension, includePreRelease, targetPlatform)) {
compatibleExtension = extension;
}
if (!compatibleExtension && fetchCompatibleVersion) {
compatibleExtension = await this.galleryService.getCompatibleExtension(extension, targetPlatform);
compatibleExtension = await this.galleryService.getCompatibleExtension(extension, includePreRelease, targetPlatform);
}
return compatibleExtension;

View file

@ -10,6 +10,7 @@ import { getOrDefault } from 'vs/base/common/objects';
import { IPager } from 'vs/base/common/paging';
import { isWeb, platform } from 'vs/base/common/platform';
import { arch } from 'vs/base/common/process';
import { isBoolean } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { IHeaders, IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@ -135,6 +136,7 @@ const PropertyType = {
Dependency: 'Microsoft.VisualStudio.Code.ExtensionDependencies',
ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack',
Engine: 'Microsoft.VisualStudio.Code.Engine',
PreRelease: 'Microsoft.VisualStudio.Code.PreRelease',
LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages',
WebExtension: 'Microsoft.VisualStudio.Code.WebExtension'
};
@ -314,6 +316,11 @@ function getEngine(version: IRawGalleryExtensionVersion): string {
return (values.length > 0 && values[0].value) || '';
}
function isPreReleaseVersion(version: IRawGalleryExtensionVersion): boolean {
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.PreRelease) : [];
return values.length > 0 && values[0].value === 'true';
}
function getLocalizedLanguages(version: IRawGalleryExtensionVersion): string[] {
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.LocalizedLanguages) : [];
const value = (values.length > 0 && values[0].value) || '';
@ -376,14 +383,8 @@ export function sortExtensionVersions(versions: IRawGalleryExtensionVersion[], p
return versions;
}
function toExtensionWithLatestVersion(galleryExtension: IRawGalleryExtension, index: number, query: Query, querySource: string | undefined, targetPlatform: TargetPlatform): IGalleryExtension {
const allTargetPlatforms = getAllTargetPlatforms(galleryExtension);
let latestVersion = galleryExtension.versions[0];
latestVersion = galleryExtension.versions.find(version => version.version === latestVersion.version && isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(version), allTargetPlatforms, targetPlatform)) || latestVersion;
return toExtension(galleryExtension, latestVersion, allTargetPlatforms, index, query, querySource);
}
function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], index: number, query: Query, querySource?: string): IGalleryExtension {
function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], telemetryData?: any): IGalleryExtension {
const latestVersion = galleryExtension.versions[0];
const assets = <IGalleryExtensionAssets>{
manifest: getVersionAsset(version, AssetType.Manifest),
readme: getVersionAsset(version, AssetType.Details),
@ -423,7 +424,9 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
engine: getEngine(version),
localizedLanguages: getLocalizedLanguages(version),
targetPlatform: getTargetPlatformForExtensionVersion(version),
isPreReleaseVersion: isPreReleaseVersion(version)
},
hasPreReleaseVersion: isPreReleaseVersion(latestVersion),
preview: getIsPreview(galleryExtension.flags),
/* __GDPR__FRAGMENT__
"GalleryExtensionTelemetryData2" : {
@ -431,10 +434,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
"querySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
telemetryData: {
index: ((query.pageNumber - 1) * query.pageSize) + index,
querySource
},
telemetryData,
};
}
@ -476,8 +476,11 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
return !!this.extensionsGalleryUrl;
}
async getExtensions(identifiers: ReadonlyArray<IExtensionIdentifier | IExtensionIdentifierWithVersion>, token: CancellationToken): Promise<IGalleryExtension[]> {
const result: IGalleryExtension[] = [];
getExtensions(identifiers: ReadonlyArray<IExtensionIdentifier | IExtensionIdentifierWithVersion>, token: CancellationToken): Promise<IGalleryExtension[]>
getExtensions(identifiers: ReadonlyArray<IExtensionIdentifier | IExtensionIdentifierWithVersion>, includePreRelease: boolean, token: CancellationToken): Promise<IGalleryExtension[]>
async getExtensions(identifiers: ReadonlyArray<IExtensionIdentifier | IExtensionIdentifierWithVersion>, arg1: any, arg2?: any): Promise<IGalleryExtension[]> {
const includePreRelease = isBoolean(arg1) ? arg1 : false;
const token: CancellationToken = isBoolean(arg1) ? arg2 : arg1;
let query = new Query()
.withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, identifiers.length)
@ -489,33 +492,20 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
}
const { galleryExtensions } = await this.queryGallery(query, CURRENT_TARGET_PLATFORM, CancellationToken.None);
for (let index = 0; index < galleryExtensions.length; index++) {
const galleryExtension = galleryExtensions[index];
if (!galleryExtension.versions.length) {
continue;
}
const id = getGalleryExtensionId(galleryExtension.publisher.publisherName, galleryExtension.extensionName);
const version = (<IExtensionIdentifierWithVersion | undefined>identifiers.find(identifier => areSameExtensions(identifier, { id })))?.version;
if (version) {
const versionAsset = galleryExtension.versions.find(v => v.version === version);
if (versionAsset) {
result.push(toExtension(galleryExtension, versionAsset, getAllTargetPlatforms(galleryExtension), index, query));
}
} else {
result.push(toExtensionWithLatestVersion(galleryExtension, index, query, undefined, CURRENT_TARGET_PLATFORM));
}
}
return result;
const galleryExtensionsByVersion = galleryExtensions.map(rawGalleryExtension => {
const id = getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName);
return { rawGalleryExtension, version: (<IExtensionIdentifierWithVersion | undefined>identifiers.find(identifier => areSameExtensions(identifier, { id })))?.version };
});
return this.converToGalleryExtensions(galleryExtensionsByVersion, includePreRelease, CURRENT_TARGET_PLATFORM, () => undefined, token);
}
async getCompatibleExtension(arg1: IExtensionIdentifier | IGalleryExtension, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null> {
async getCompatibleExtension(arg1: IExtensionIdentifier | IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null> {
const extension: IGalleryExtension | null = isIExtensionIdentifier(arg1) ? null : arg1;
if (extension) {
if (isNotWebExtensionInWebTargetPlatform(extension.allTargetPlatforms, targetPlatform)) {
return null;
}
if (await this.isExtensionCompatible(extension, targetPlatform)) {
if (await this.isExtensionCompatible(extension, includePreRelease, targetPlatform)) {
return extension;
}
}
@ -551,19 +541,23 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
properties: [...(rawVersion.properties || []), { key: PropertyType.Engine, value: engine }]
};
}
if (await this.isRawExtensionVersionCompatible(rawVersion, allTargetPlatforms, targetPlatform)) {
return toExtension(rawExtension, rawVersion, allTargetPlatforms, 0, query);
if (await this.isRawExtensionVersionCompatible(rawVersion, includePreRelease, allTargetPlatforms, targetPlatform)) {
return toExtension(rawExtension, rawVersion, allTargetPlatforms);
}
}
return null;
}
async isExtensionCompatible(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<boolean> {
async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<boolean> {
if (!isTargetPlatformCompatible(extension.properties.targetPlatform, extension.allTargetPlatforms, targetPlatform)) {
return false;
}
if (!includePreRelease && extension.properties.isPreReleaseVersion) {
return false;
}
let engine = extension.properties.engine;
if (!engine) {
const manifest = await this.getManifest(extension, CancellationToken.None);
@ -575,11 +569,15 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
return isEngineValid(engine, this.productService.version, this.productService.date);
}
private async isRawExtensionVersionCompatible(rawExtensionVersion: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform): Promise<boolean> {
private async isRawExtensionVersionCompatible(rawExtensionVersion: IRawGalleryExtensionVersion, includePreRelease: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform): Promise<boolean> {
if (!isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(rawExtensionVersion), allTargetPlatforms, targetPlatform)) {
return false;
}
if (!includePreRelease && isPreReleaseVersion(rawExtensionVersion)) {
return false;
}
const engine = await this.getEngine(rawExtensionVersion);
return isEngineValid(engine, this.productService.version, this.productService.date);
}
@ -641,19 +639,69 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
}
const { galleryExtensions, total } = await this.queryGallery(query, CURRENT_TARGET_PLATFORM, token);
const extensions = galleryExtensions.map((e, index) => toExtensionWithLatestVersion(e, index, query, options.source, CURRENT_TARGET_PLATFORM));
const telemetryData = (index: number) => ({ index: ((query.pageNumber - 1) * query.pageSize) + index, querySource: options.source });
const extensions = await this.converToGalleryExtensions(galleryExtensions.map(rawGalleryExtension => ({ rawGalleryExtension })), !!options.includePreRelease, CURRENT_TARGET_PLATFORM, telemetryData, token);
const getPage = async (pageIndex: number, ct: CancellationToken) => {
if (ct.isCancellationRequested) {
throw canceled();
}
const nextPageQuery = query.withPage(pageIndex + 1);
const { galleryExtensions } = await this.queryGallery(nextPageQuery, CURRENT_TARGET_PLATFORM, ct);
return galleryExtensions.map((e, index) => toExtensionWithLatestVersion(e, index, nextPageQuery, options.source, CURRENT_TARGET_PLATFORM));
return await this.converToGalleryExtensions(galleryExtensions.map(rawGalleryExtension => ({ rawGalleryExtension })), !!options.includePreRelease, CURRENT_TARGET_PLATFORM, telemetryData, token);
};
return { firstPage: extensions, total, pageSize: query.pageSize, getPage } as IPager<IGalleryExtension>;
}
private async converToGalleryExtensions(rawGalleryExtensions: { rawGalleryExtension: IRawGalleryExtension, version?: string }[], includePreRelease: boolean, targetPlatform: TargetPlatform, telemetryData: (index: number) => any, token: CancellationToken): Promise<IGalleryExtension[]> {
const toExtensionWithLatestVersion = (galleryExtension: IRawGalleryExtension, index: number): IGalleryExtension => {
const allTargetPlatforms = getAllTargetPlatforms(galleryExtension);
let latestVersion = galleryExtension.versions[0];
latestVersion = galleryExtension.versions.find(version => version.version === latestVersion.version && isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(version), allTargetPlatforms, targetPlatform)) || latestVersion;
if (!includePreRelease && isPreReleaseVersion(latestVersion)) {
latestVersion = galleryExtension.versions.find(version => version.version !== latestVersion.version && !isPreReleaseVersion(version)) || latestVersion;
}
return toExtension(galleryExtension, latestVersion, allTargetPlatforms, telemetryData(index));
};
const result: [number, IGalleryExtension][] = [];
const preReleaseVersions = new Map<string, number>();
for (let index = 0; index < rawGalleryExtensions.length; index++) {
const { rawGalleryExtension, version } = rawGalleryExtensions[index];
if (version) {
const versionAsset = rawGalleryExtension.versions.find(v => v.version === version);
if (versionAsset) {
result.push([index, toExtension(rawGalleryExtension, versionAsset, getAllTargetPlatforms(rawGalleryExtension))]);
}
} else {
const extension = toExtensionWithLatestVersion(rawGalleryExtension, index);
if (extension.properties.isPreReleaseVersion && !includePreRelease) {
preReleaseVersions.set(extension.identifier.uuid, index);
} else {
result.push([index, extension]);
}
}
}
if (preReleaseVersions.size) {
const query = new Query()
.withFlags(Flags.IncludeVersions, Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, preReleaseVersions.size)
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code')
.withFilter(FilterType.ExtensionId, ...preReleaseVersions.keys());
const { galleryExtensions } = await this.queryGallery(query, targetPlatform, token);
if (galleryExtensions.length !== preReleaseVersions.size) {
throw new Error('Not all extensions with latest versions are returned');
}
for (const rawGalleryExtension of galleryExtensions) {
const index = preReleaseVersions.get(rawGalleryExtension.extensionId)!;
const extension = toExtensionWithLatestVersion(rawGalleryExtension, index);
result.push([index, extension]);
}
}
return result.sort((a, b) => a[0] - b[0]).map(([, extension]) => extension);
}
private async queryGallery(query: Query, targetPlatform: TargetPlatform, token: CancellationToken): Promise<{ galleryExtensions: IRawGalleryExtension[], total: number; }> {
if (!this.isEnabled()) {
throw new Error('No extension gallery service configured.');
@ -811,7 +859,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
return '';
}
async getAllCompatibleVersions(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]> {
async getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]> {
let query = new Query()
.withFlags(Flags.IncludeVersions, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, 1)
@ -836,7 +884,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
const result: IGalleryExtensionVersion[] = [];
for (const version of galleryExtensions[0].versions) {
try {
if (result[result.length - 1]?.version !== version.version && await this.isRawExtensionVersionCompatible(version, allTargetPlatforms, targetPlatform)) {
if (result[result.length - 1]?.version !== version.version && await this.isRawExtensionVersionCompatible(version, includePreRelease, allTargetPlatforms, targetPlatform)) {
result.push({ version: version.version, date: version.lastUpdated });
}
} catch (error) { /* Ignore error and skip version */ }

View file

@ -184,6 +184,7 @@ export interface IGalleryExtensionProperties {
engine?: string;
localizedLanguages?: string[];
targetPlatform: TargetPlatform;
isPreReleaseVersion: boolean;
}
export interface IGalleryExtensionAsset {
@ -253,10 +254,11 @@ export interface IGalleryExtension {
releaseDate: number;
lastUpdated: number;
preview: boolean;
hasPreReleaseVersion: boolean;
allTargetPlatforms: TargetPlatform[];
assets: IGalleryExtensionAssets;
properties: IGalleryExtensionProperties;
telemetryData: any;
telemetryData?: any;
}
export interface IGalleryMetadata {
@ -270,6 +272,8 @@ export interface ILocalExtension extends IExtension {
publisherId: string | null;
publisherDisplayName: string | null;
installedTimestamp?: number;
isPreReleaseVersion: boolean;
hadPreReleaseVersion: boolean;
}
export const enum SortBy {
@ -297,6 +301,7 @@ export interface IQueryOptions {
sortBy?: SortBy;
sortOrder?: SortOrder;
source?: string;
includePreRelease?: boolean;
}
export const enum StatisticType {
@ -325,6 +330,7 @@ export interface IExtensionGalleryService {
isEnabled(): boolean;
query(options: IQueryOptions, token: CancellationToken): Promise<IPager<IGalleryExtension>>;
getExtensions(identifiers: ReadonlyArray<IExtensionIdentifier | IExtensionIdentifierWithVersion>, token: CancellationToken): Promise<IGalleryExtension[]>;
getExtensions(identifiers: ReadonlyArray<IExtensionIdentifier | IExtensionIdentifierWithVersion>, includePreRelease: boolean, token: CancellationToken): Promise<IGalleryExtension[]>;
download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise<void>;
reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise<void>;
getReadme(extension: IGalleryExtension, token: CancellationToken): Promise<string>;
@ -332,10 +338,10 @@ export interface IExtensionGalleryService {
getChangelog(extension: IGalleryExtension, token: CancellationToken): Promise<string>;
getCoreTranslation(extension: IGalleryExtension, languageId: string): Promise<ITranslation | null>;
getExtensionsReport(): Promise<IReportedExtension[]>;
isExtensionCompatible(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<boolean>;
getCompatibleExtension(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null>;
getCompatibleExtension(id: IExtensionIdentifier, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null>;
getAllCompatibleVersions(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]>;
isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<boolean>;
getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null>;
getCompatibleExtension(id: IExtensionIdentifier, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null>;
getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]>;
}
export interface InstallExtensionEvent {
@ -359,6 +365,7 @@ export enum ExtensionManagementErrorCode {
Unsupported = 'Unsupported',
Malicious = 'Malicious',
Incompatible = 'Incompatible',
IncompatibleTargetPlatform = 'IncompatibleTargetPlatform',
Invalid = 'Invalid',
Download = 'Download',
Extract = 'Extract',
@ -376,7 +383,7 @@ export class ExtensionManagementError extends Error {
}
}
export type InstallOptions = { isBuiltin?: boolean, isMachineScoped?: boolean, donotIncludePackAndDependencies?: boolean, installGivenVersion?: boolean };
export type InstallOptions = { isBuiltin?: boolean, isMachineScoped?: boolean, donotIncludePackAndDependencies?: boolean, installGivenVersion?: boolean, installPreReleaseVersion?: boolean };
export type InstallVSIXOptions = Omit<InstallOptions, 'installGivenVersion'> & { installOnlyNewlyAddedFromExtensionPack?: boolean };
export type UninstallOptions = { donotIncludePack?: boolean, donotCheckDependents?: boolean };

View file

@ -19,7 +19,7 @@ import { IFile, zip } from 'vs/base/node/zip';
import * as nls from 'vs/nls';
import { IDownloadService } from 'vs/platform/download/common/download';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask, joinErrors, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService';
import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask, joinErrors, Metadata, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService';
import {
ExtensionManagementError, ExtensionManagementErrorCode, getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallOperation, InstallOptions,
InstallVSIXOptions, TargetPlatform
@ -29,7 +29,7 @@ import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/exten
import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle';
import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache';
import { ExtensionsScanner, ILocalExtensionManifest, IMetadata } from 'vs/platform/extensionManagement/node/extensionsScanner';
import { ExtensionsScanner, ILocalExtensionManifest } from 'vs/platform/extensionManagement/node/extensionsScanner';
import { ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher';
import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
@ -43,7 +43,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'
interface InstallableExtension {
zipPath: string;
identifierWithVersion: ExtensionIdentifierWithVersion;
metadata?: IMetadata;
metadata?: Metadata;
}
export class ExtensionManagementService extends AbstractExtensionManagementService implements IExtensionManagementService {
@ -226,7 +226,7 @@ abstract class AbstractInstallExtensionTask extends AbstractExtensionTask<ILocal
try {
const local = await this.unsetUninstalledAndGetLocal(installableExtension.identifierWithVersion);
if (local) {
return installableExtension.metadata ? this.extensionsScanner.saveMetadataForLocalExtension(local, installableExtension.metadata) : local;
return installableExtension.metadata ? this.extensionsScanner.saveMetadataForLocalExtension(local, { ...((<ILocalExtensionManifest>local.manifest).__metadata || {}), ...installableExtension.metadata }) : local;
}
} catch (e) {
if (isMacintosh) {
@ -287,6 +287,8 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask {
const installableExtension = await this.downloadInstallableExtension(this.gallery, this._operation);
installableExtension.metadata.isMachineScoped = this.options.isMachineScoped || existingExtension?.isMachineScoped;
installableExtension.metadata.isBuiltin = this.options.isBuiltin || existingExtension?.isBuiltin;
installableExtension.metadata.isPreReleaseVersion = this.gallery.properties.isPreReleaseVersion;
installableExtension.metadata.hadPreReleaseVersion = this.gallery.properties.isPreReleaseVersion || existingExtension?.hadPreReleaseVersion;
try {
const local = await this.installExtension(installableExtension, token);
@ -383,7 +385,7 @@ class InstallVSIXTask extends AbstractInstallExtensionTask {
return this.installExtension({ zipPath: path.resolve(this.location.fsPath), identifierWithVersion, metadata }, token);
}
private async getMetadata(name: string, token: CancellationToken): Promise<IMetadata> {
private async getMetadata(name: string, token: CancellationToken): Promise<Metadata> {
try {
const galleryExtension = (await this.galleryService.query({ names: [name], pageSize: 1 }, token)).firstPage[0];
if (galleryExtension) {

View file

@ -19,7 +19,8 @@ import * as pfs from 'vs/base/node/pfs';
import { extract, ExtractError } from 'vs/base/node/zip';
import { localize } from 'vs/nls';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { ExtensionManagementError, ExtensionManagementErrorCode, IGalleryMetadata, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { Metadata } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService';
import { ExtensionManagementError, ExtensionManagementErrorCode, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, ExtensionIdentifierWithVersion, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls';
import { ExtensionType, IExtensionIdentifier, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
@ -28,9 +29,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { CancellationToken } from 'vscode';
export type IMetadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean; }>;
type IStoredMetadata = IMetadata & { installedTimestamp: number | undefined };
export type ILocalExtensionManifest = IExtensionManifest & { __metadata?: IMetadata };
export type ILocalExtensionManifest = IExtensionManifest & { __metadata?: Metadata };
type IRelaxedLocalExtension = Omit<ILocalExtension, 'isBuiltin'> & { isBuiltin: boolean };
export class ExtensionsScanner extends Disposable {
@ -94,7 +93,7 @@ export class ExtensionsScanner extends Disposable {
return this.scanExtensionsInDir(this.extensionsPath, ExtensionType.User);
}
async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IMetadata | undefined, token: CancellationToken): Promise<ILocalExtension> {
async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: Metadata | undefined, token: CancellationToken): Promise<ILocalExtension> {
const folderName = identifierWithVersion.key();
const tempPath = path.join(this.extensionsPath, `.${generateUuid()}`);
const extensionPath = path.join(this.extensionsPath, folderName);
@ -140,21 +139,21 @@ export class ExtensionsScanner extends Disposable {
throw new Error(localize('cannot read', "Cannot read the extension from {0}", this.extensionsPath));
}
async saveMetadataForLocalExtension(local: ILocalExtension, metadata: IMetadata): Promise<ILocalExtension> {
async saveMetadataForLocalExtension(local: ILocalExtension, metadata: Metadata): Promise<ILocalExtension> {
this.setMetadata(local, metadata);
await this.storeMetadata(local, { ...metadata, installedTimestamp: local.installedTimestamp });
return local;
}
private async storeMetadata(local: ILocalExtension, storedMetadata: IStoredMetadata): Promise<ILocalExtension> {
private async storeMetadata(local: ILocalExtension, metaData: Metadata): Promise<ILocalExtension> {
// unset if false
storedMetadata.isMachineScoped = storedMetadata.isMachineScoped || undefined;
storedMetadata.isBuiltin = storedMetadata.isBuiltin || undefined;
storedMetadata.installedTimestamp = storedMetadata.installedTimestamp || undefined;
metaData.isMachineScoped = metaData.isMachineScoped || undefined;
metaData.isBuiltin = metaData.isBuiltin || undefined;
metaData.installedTimestamp = metaData.installedTimestamp || undefined;
const manifestPath = path.join(local.location.fsPath, 'package.json');
const raw = await pfs.Promises.readFile(manifestPath, 'utf8');
const { manifest } = await this.parseManifest(raw);
(manifest as ILocalExtensionManifest).__metadata = storedMetadata;
(manifest as ILocalExtensionManifest).__metadata = metaData;
await pfs.Promises.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'));
return local;
}
@ -302,7 +301,6 @@ export class ExtensionsScanner extends Disposable {
const local = <ILocalExtension>{ type, identifier, manifest, location: extensionLocation, readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false, isBuiltin: type === ExtensionType.System };
if (metadata) {
this.setMetadata(local, metadata);
local.installedTimestamp = metadata.installedTimestamp;
}
return local;
}
@ -331,12 +329,15 @@ export class ExtensionsScanner extends Disposable {
}
}
private setMetadata(local: IRelaxedLocalExtension, metadata: IMetadata): void {
private setMetadata(local: IRelaxedLocalExtension, metadata: Metadata): void {
local.publisherDisplayName = metadata.publisherDisplayName || null;
local.publisherId = metadata.publisherId || null;
local.identifier.uuid = metadata.id;
local.isMachineScoped = !!metadata.isMachineScoped;
local.isPreReleaseVersion = !!metadata.isPreReleaseVersion;
local.hadPreReleaseVersion = !!metadata.hadPreReleaseVersion;
local.isBuiltin = local.type === ExtensionType.System || !!metadata.isBuiltin;
local.installedTimestamp = metadata.installedTimestamp;
}
private async removeUninstalledExtensions(): Promise<void> {
@ -392,7 +393,7 @@ export class ExtensionsScanner extends Disposable {
return this._devSystemExtensionsPath;
}
private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IStoredMetadata | null; }> {
private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: Metadata | null; }> {
const promises = [
pfs.Promises.readFile(path.join(extensionPath, 'package.json'), 'utf8')
.then(raw => this.parseManifest(raw)),
@ -408,7 +409,7 @@ export class ExtensionsScanner extends Disposable {
};
}
private parseManifest(raw: string): Promise<{ manifest: IExtensionManifest; metadata: IMetadata | null; }> {
private parseManifest(raw: string): Promise<{ manifest: IExtensionManifest; metadata: Metadata | null; }> {
return new Promise((c, e) => {
try {
const manifest = JSON.parse(raw);

View file

@ -499,6 +499,8 @@ export const overviewRulerFindMatchForeground = registerColor('editorOverviewRul
export const overviewRulerSelectionHighlightForeground = registerColor('editorOverviewRuler.selectionHighlightForeground', { dark: '#A0A0A0CC', light: '#A0A0A0CC', hc: '#A0A0A0CC' }, nls.localize('overviewRulerSelectionHighlightForeground', 'Overview ruler marker color for selection highlights. The color must not be opaque so as not to hide underlying decorations.'), true);
export const overviewRulerUnicodeHighlightForeground = registerColor('editorOverviewRuler.unicodeForeground', { dark: '#d186167e', light: '#d186167e', hc: '#AB5A00' }, nls.localize('overviewRulerUnicodeForeground', 'Overview ruler marker color for highlighted unicode characters. The color must not be opaque so as not to hide underlying decorations.'), true);
export const minimapFindMatch = registerColor('minimap.findMatchHighlight', { light: '#d18616', dark: '#d18616', hc: '#AB5A00' }, nls.localize('minimapFindMatchHighlight', 'Minimap marker color for find matches.'), true);
export const minimapSelectionOccurrenceHighlight = registerColor('minimap.selectionOccurrenceHighlight', { light: '#c9c9c9', dark: '#676767', hc: '#ffffff' }, nls.localize('minimapSelectionOccurrenceHighlight', 'Minimap marker color for repeating editor selections.'), true);
export const minimapSelection = registerColor('minimap.selectionHighlight', { light: '#ADD6FF', dark: '#264F78', hc: '#ffffff' }, nls.localize('minimapSelectionHighlight', 'Minimap marker color for the editor selection.'), true);
@ -506,6 +508,7 @@ export const minimapError = registerColor('minimap.errorHighlight', { dark: new
export const minimapWarning = registerColor('minimap.warningHighlight', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningBorder }, nls.localize('overviewRuleWarning', 'Minimap marker color for warnings.'));
export const minimapBackground = registerColor('minimap.background', { dark: null, light: null, hc: null }, nls.localize('minimapBackground', "Minimap background color."));
export const minimapForegroundOpacity = registerColor('minimap.foregroundOpacity', { dark: Color.fromHex('#000f'), light: Color.fromHex('#000f'), hc: Color.fromHex('#000f') }, nls.localize('minimapForegroundOpacity', 'Opacity of foreground elements rendered in the minimap. For example, "#000000c0" will render the elements with 75% opacity.'));
export const minimapUnicodeHighlight = registerColor('minimap.unicodeHighlight', { light: '#d18616', dark: '#d18616', hc: '#AB5A00' }, nls.localize('minimapUnicodeHighlight', 'Minimap marker color for highlighted unicode characters.'));
export const minimapSliderBackground = registerColor('minimapSlider.background', { light: transparent(scrollbarSliderBackground, 0.5), dark: transparent(scrollbarSliderBackground, 0.5), hc: transparent(scrollbarSliderBackground, 0.5) }, nls.localize('minimapSliderBackground', "Minimap slider background color."));
export const minimapSliderHoverBackground = registerColor('minimapSlider.hoverBackground', { light: transparent(scrollbarSliderHoverBackground, 0.5), dark: transparent(scrollbarSliderHoverBackground, 0.5), hc: transparent(scrollbarSliderHoverBackground, 0.5) }, nls.localize('minimapSliderHoverBackground', "Minimap slider background color when hovering."));

View file

@ -419,7 +419,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
}
} catch (error) {
addToSkipped.push(e);
if (error instanceof ExtensionManagementError && error.code === ExtensionManagementErrorCode.Incompatible) {
if (error instanceof ExtensionManagementError && (error.code === ExtensionManagementErrorCode.Incompatible || error.code === ExtensionManagementErrorCode.IncompatibleTargetPlatform)) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing extension because the compatible extension is not found.`, extension.displayName || extension.identifier.id);
} else {
this.logService.error(error);

View file

@ -24,14 +24,13 @@ async function start() {
// Do a quick parse to determine if a server or the cli needs to be started
const parsedArgs = minimist(process.argv.slice(2), {
boolean: ['start-server', 'list-extensions', 'print-ip-address'],
boolean: ['start-server', 'list-extensions', 'print-ip-address', 'help', 'version'],
string: ['install-extension', 'install-builtin-extension', 'uninstall-extension', 'locate-extension', 'socket-path', 'host', 'port', 'pick-port']
});
const shouldSpawnCli = (
!parsedArgs['start-server'] &&
(!!parsedArgs['list-extensions'] || !!parsedArgs['install-extension'] || !!parsedArgs['install-builtin-extension'] || !!parsedArgs['uninstall-extension'] || !!parsedArgs['locate-extension'])
);
const extensionCliArgs = ['list-extensions', 'install-extension', 'install-builtin-extension', 'uninstall-extension', 'locate-extension'];
const shouldSpawnCli = parsedArgs.help || parsedArgs.version || !parsedArgs['start-server'] && extensionCliArgs.some(a => !!parsedArgs[a]);
if (shouldSpawnCli) {
loadCode().then((mod) => {

View file

@ -121,7 +121,7 @@ export function main(desc: ProductDescription, args: string[]): void {
const verbose = !!parsedArgs['verbose'];
if (parsedArgs.help) {
console.log(buildHelpMessage(desc.productName, desc.executableName, desc.version, options, true));
console.log(buildHelpMessage(desc.productName, desc.executableName, desc.version, options));
return;
}
if (parsedArgs.version) {

View file

@ -53,7 +53,7 @@ args['extensions-dir'] = args['extensions-dir'] || join(REMOTE_DATA_FOLDER, 'ext
* invoked by vs/server/main.js
*/
export function spawnCli() {
runCli(args, REMOTE_DATA_FOLDER);
runCli(args, REMOTE_DATA_FOLDER, serverOptions);
}
/**

View file

@ -38,6 +38,8 @@ import { DownloadService } from 'vs/platform/download/common/downloadService';
import { IDownloadService } from 'vs/platform/download/common/download';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService';
import { buildHelpMessage, buildVersionMessage, OptionDescriptions } from 'vs/platform/environment/node/argv';
import { isWindows } from 'vs/base/common/platform';
class CliMain extends Disposable {
@ -135,7 +137,19 @@ function eventuallyExit(code: number): void {
setTimeout(() => process.exit(code), 0);
}
export async function run(args: ServerParsedArgs, REMOTE_DATA_FOLDER: string): Promise<void> {
export async function run(args: ServerParsedArgs, REMOTE_DATA_FOLDER: string, optionDescriptions: OptionDescriptions<ServerParsedArgs>): Promise<void> {
if (args.help) {
const executable = `server${isWindows ? '.bat' : '.sh'}`;
console.log(buildHelpMessage(product.nameLong, executable, product.version, optionDescriptions, { noInputFiles: true, noPipe: true }));
return;
}
// Version Info
if (args.version) {
console.log(buildVersionMessage(product.version, product.commit));
return;
}
const cliMain = new CliMain(args, REMOTE_DATA_FOLDER);
try {
await cliMain.run();

View file

@ -55,6 +55,9 @@ export const serverOptions: OptionDescriptions<ServerParsedArgs> = {
'log': { type: 'string' },
'logsPath': { type: 'string' },
'help': OPTIONS['help'],
'version': OPTIONS['version'],
_: OPTIONS['_']
};
@ -129,6 +132,10 @@ export interface ServerParsedArgs {
'log'?: string;
'logsPath'?: string;
// server cli
help: boolean;
version: boolean;
_: string[];
}

View file

@ -105,7 +105,7 @@ export class MainThreadAuthenticationProvider extends Disposable {
? nls.localize('signOutMessagve', "The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", accountName, accountUsages.map(usage => usage.extensionName).join('\n'))
: nls.localize('signOutMessageSimple', "Sign out of '{0}'?", accountName),
[
nls.localize('signOut', "Sign out"),
nls.localize('signOut', "Sign Out"),
nls.localize('cancel', "Cancel")
],
{
@ -251,7 +251,9 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
throw new Error('User did not consent to login.');
}
const session = await this.authenticationService.createSession(providerId, scopes, true);
const session = sessions?.length && !options.forceNewSession
? await this.authenticationService.selectSession(providerId, extensionId, extensionName, scopes, sessions)
: await this.authenticationService.createSession(providerId, scopes, true);
await this.setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id);
return session;
}

View file

@ -25,6 +25,10 @@ const configurationEntrySchema: IJSONSchema = {
description: nls.localize('vscode.extension.contributes.configuration.title', 'A summary of the settings. This label will be used in the settings file as separating comment.'),
type: 'string'
},
order: {
description: nls.localize('vscode.extension.contributes.configuration.order', 'When specified, gives the order of this category of settings relative to other categories.'),
type: 'integer'
},
properties: {
description: nls.localize('vscode.extension.contributes.configuration.properties', 'Description of the configuration properties.'),
type: 'object',
@ -94,6 +98,10 @@ const configurationEntrySchema: IJSONSchema = {
],
default: 'singlelineText',
description: nls.localize('scope.editPresentation', 'When specified, controls the presentation format of the string setting.')
},
order: {
type: 'integer',
description: nls.localize('scope.order', 'When specified, gives the order of this setting relative to other settings within the same category. Settings with an order property will be placed before settings without this property set.')
}
}
}

View file

@ -12,7 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import { Cache, CacheResult } from 'vs/base/common/cache';
import { Action, IAction } from 'vs/base/common/actions';
import { getErrorMessage, isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors';
import { dispose, toDisposable, Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { dispose, toDisposable, Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { append, $, finalHandler, join, addDisposableListener, EventType, setParentFlowTo, reset, Dimension } from 'vs/base/browser/dom';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@ -21,23 +21,23 @@ import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsServi
import { IExtensionManifest, IKeyBinding, IView, IViewContainer } from 'vs/platform/extensions/common/extensions';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { ResolvedKeybinding } from 'vs/base/common/keybindings';
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, IExtension, ExtensionContainers, ExtensionEditorTab, ExtensionState } from 'vs/workbench/contrib/extensions/common/extensions';
import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets';
import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget, PreReleaseIndicatorWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets';
import { IEditorOpenContext } from 'vs/workbench/common/editor';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import {
UpdateAction, ReloadAction, EnableDropDownAction, DisableDropDownAction, ExtensionStatusLabelAction, SetFileIconThemeAction, SetColorThemeAction,
RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ToggleSyncExtensionAction, SetProductIconThemeAction,
ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, UninstallAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction,
InstallAnotherVersionAction, ExtensionEditorManageExtensionAction, WebInstallAction
InstallAnotherVersionAction, ExtensionEditorManageExtensionAction, WebInstallAction, UsePreReleaseVersionAction, StopUsingPreReleaseVersionAction
} from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener';
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { Color } from 'vs/base/common/color';
@ -67,7 +67,6 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { Delegate } from 'vs/workbench/contrib/extensions/browser/extensionsList';
import { renderMarkdown } from 'vs/base/browser/markdownRenderer';
import { attachKeybindingLabelStyler } from 'vs/platform/theme/common/styler';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { errorIcon, infoIcon, starEmptyIcon, verifiedPublisherIcon as verifiedPublisherThemeIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
import { MarkdownString } from 'vs/base/common/htmlContent';
@ -141,6 +140,7 @@ interface IExtensionEditorTemplate {
preview: HTMLElement;
builtin: HTMLElement;
version: HTMLElement;
preRelease: HTMLElement;
publisher: HTMLElement;
publisherDisplayName: HTMLElement;
verifiedPublisherIcon: HTMLElement;
@ -161,10 +161,13 @@ const enum WebviewIndex {
Changelog
}
const CONTEXT_SHOW_PRE_RELEASE_VERSION = new RawContextKey<boolean>('showPreReleaseVersion', false);
export class ExtensionEditor extends EditorPane {
static readonly ID: string = 'workbench.editor.extension';
private readonly _scopedContextKeyService = this._register(new MutableDisposable<IContextKeyService>());
private template: IExtensionEditorTemplate | undefined;
private extensionReadme: Cache<string> | null;
@ -184,6 +187,8 @@ export class ExtensionEditor extends EditorPane {
private editorLoadComplete: boolean = false;
private dimension: Dimension | undefined;
private showPreReleaseVersionContextKey: IContextKey<boolean> | undefined;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ -201,6 +206,7 @@ export class ExtensionEditor extends EditorPane {
@IWebviewService private readonly webviewService: IWebviewService,
@IModeService private readonly modeService: IModeService,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
) {
super(ExtensionEditor.ID, telemetryService, themeService, storageService);
this.extensionReadme = null;
@ -208,8 +214,16 @@ export class ExtensionEditor extends EditorPane {
this.extensionManifest = null;
}
override get scopedContextKeyService(): IContextKeyService | undefined {
return this._scopedContextKeyService.value;
}
createEditor(parent: HTMLElement): void {
const root = append(parent, $('.extension-editor'));
this._scopedContextKeyService.value = this.contextKeyService.createScoped(root);
this._scopedContextKeyService.value.createKey('inExtensionEditor', true);
this.showPreReleaseVersionContextKey = CONTEXT_SHOW_PRE_RELEASE_VERSION.bindTo(this._scopedContextKeyService.value);
root.tabIndex = 0; // this is required for the focus tracker on the editor
root.style.outline = 'none';
root.setAttribute('role', 'document');
@ -228,6 +242,7 @@ export class ExtensionEditor extends EditorPane {
const builtin = append(title, $('span.builtin'));
builtin.textContent = localize('builtin', "Built-in");
const preRelease = append(title, $('span.pre-release'));
const subtitle = append(details, $('.subtitle'));
const publisher = append(append(subtitle, $('.subtitle-entry')), $('.publisher.clickable', { title: localize('publisher', "Publisher"), tabIndex: 0 }));
@ -277,6 +292,7 @@ export class ExtensionEditor extends EditorPane {
icon,
iconContainer,
version,
preRelease,
installCount,
name,
navbar,
@ -306,13 +322,31 @@ export class ExtensionEditor extends EditorPane {
return disposables;
}
override async setInput(input: ExtensionsInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
override async setInput(input: ExtensionsInput, options: IExtensionEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
await super.setInput(input, options, context, token);
this.updatePreReleaseVersionContext();
if (this.template) {
await this.updateTemplate(input, this.template, !!options?.preserveFocus);
await this.updateTemplate(input.extension, this.template, !!options?.preserveFocus);
}
}
override setOptions(options: IExtensionEditorOptions | undefined): void {
const currentOptions: IExtensionEditorOptions | undefined = this.options;
super.setOptions(options);
this.updatePreReleaseVersionContext();
if (currentOptions?.showPreReleaseVersion !== options?.showPreReleaseVersion) {
this.openTab(ExtensionEditorTab.Readme);
}
}
private updatePreReleaseVersionContext(): void {
let showPreReleaseVersion = (<IExtensionEditorOptions | undefined>this.options)?.showPreReleaseVersion;
if (isUndefined(showPreReleaseVersion)) {
showPreReleaseVersion = !!(<ExtensionsInput>this.input).extension.gallery?.properties.isPreReleaseVersion;
}
this.showPreReleaseVersionContextKey?.set(showPreReleaseVersion);
}
async openTab(tab: ExtensionEditorTab): Promise<void> {
if (!this.input || !this.template) {
return;
@ -326,10 +360,9 @@ export class ExtensionEditor extends EditorPane {
}
}
private async updateTemplate(input: ExtensionsInput, template: IExtensionEditorTemplate, preserveFocus: boolean): Promise<void> {
private async updateTemplate(extension: IExtension, template: IExtensionEditorTemplate, preserveFocus: boolean): Promise<void> {
this.activeElement = null;
this.editorLoadComplete = false;
const extension = input.extension;
if (this.currentIdentifier !== extension.identifier.id) {
this.initialScrollProgress.clear();
@ -338,9 +371,9 @@ export class ExtensionEditor extends EditorPane {
this.transientDisposables.clear();
this.extensionReadme = new Cache(() => createCancelablePromise(token => extension.getReadme(token)));
this.extensionChangelog = new Cache(() => createCancelablePromise(token => extension.getChangelog(token)));
this.extensionManifest = new Cache(() => createCancelablePromise(token => extension.getManifest(token)));
this.extensionReadme = new Cache(() => createCancelablePromise(token => extension.getReadme(!!this.showPreReleaseVersionContextKey?.get(), token)));
this.extensionChangelog = new Cache(() => createCancelablePromise(token => extension.getChangelog(!!this.showPreReleaseVersionContextKey?.get(), token)));
this.extensionManifest = new Cache(() => createCancelablePromise(token => extension.getManifest(!!this.showPreReleaseVersionContextKey?.get(), token)));
const remoteBadge = this.instantiationService.createInstance(RemoteBadgeWidget, template.iconContainer, true);
this.transientDisposables.add(addDisposableListener(template.icon, 'error', () => template.icon.src = extension.iconUrlFallback, { once: true }));
@ -393,6 +426,7 @@ export class ExtensionEditor extends EditorPane {
}
const widgets = [
this.instantiationService.createInstance(PreReleaseIndicatorWidget, template.preRelease),
remoteBadge,
this.instantiationService.createInstance(InstallCountWidget, template.installCount, false),
this.instantiationService.createInstance(RatingsWidget, template.rating, false)
@ -415,11 +449,15 @@ export class ExtensionEditor extends EditorPane {
combinedInstallAction,
this.instantiationService.createInstance(InstallingLabelAction),
this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.uninstall', UninstallAction.UninstallLabel, [
this.instantiationService.createInstance(UninstallAction),
this.instantiationService.createInstance(InstallAnotherVersionAction),
[
this.instantiationService.createInstance(UninstallAction),
this.instantiationService.createInstance(InstallAnotherVersionAction),
]
]),
this.instantiationService.createInstance(UsePreReleaseVersionAction),
this.instantiationService.createInstance(StopUsingPreReleaseVersionAction),
this.instantiationService.createInstance(ToggleSyncExtensionAction),
this.instantiationService.createInstance(ExtensionEditorManageExtensionAction),
new ExtensionEditorManageExtensionAction(this.scopedContextKeyService || this.contextKeyService, this.instantiationService),
];
const extensionStatus = this.instantiationService.createInstance(ExtensionStatusAction);
const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets, extensionStatus]);

View file

@ -15,7 +15,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWo
import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, DefaultViewsContext, ExtensionsSortByContext, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab } from 'vs/workbench/contrib/extensions/common/extensions';
import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction, UsePreReleaseVersionAction, StopUsingPreReleaseVersionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor';
import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/browser/extensionsViewlet';
@ -1156,6 +1156,75 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
// Extension Context Menu
private registerContextMenuActions(): void {
this.registerExtensionAction({
id: 'workbench.extensions.action.showPreReleaseVersion',
title: { value: localize('show pre-release version', "Show Pre-release Version"), original: 'Show Pre-release Version' },
menu: {
id: MenuId.ExtensionContext,
group: '0_install',
order: 0,
when: ContextKeyExpr.or(
ContextKeyExpr.and(ContextKeyExpr.not('inExtensionEditor'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.not('galleryExtensionIsPreReleaseVersion')),
ContextKeyExpr.and(ContextKeyExpr.has('inExtensionEditor'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.not('showPreReleaseVersion')))
},
run: async (accessor: ServicesAccessor, extensionId: string) => {
const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService);
const extension = (await extensionWorkbenchService.queryGallery({ names: [extensionId] }, CancellationToken.None)).firstPage[0];
extensionWorkbenchService.open(extension, { showPreReleaseVersion: true });
}
});
this.registerExtensionAction({
id: 'workbench.extensions.action.showReleasedVersion',
title: { value: localize('show released version', "Show Released Version"), original: 'Show Released Version' },
menu: {
id: MenuId.ExtensionContext,
group: '0_install',
order: 1,
when: ContextKeyExpr.or(
ContextKeyExpr.and(ContextKeyExpr.not('inExtensionEditor'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.has('galleryExtensionIsPreReleaseVersion'), ContextKeyExpr.equals('extensionStatus', 'installed')),
ContextKeyExpr.and(ContextKeyExpr.has('inExtensionEditor'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.has('showPreReleaseVersion')))
},
run: async (accessor: ServicesAccessor, extensionId: string) => {
const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService);
const extension = (await extensionWorkbenchService.queryGallery({ names: [extensionId] }, CancellationToken.None)).firstPage[0];
extensionWorkbenchService.open(extension, { showPreReleaseVersion: false });
}
});
this.registerExtensionAction({
id: UsePreReleaseVersionAction.ID,
title: UsePreReleaseVersionAction.TITLE,
menu: {
id: MenuId.ExtensionContext,
group: '0_install',
order: 2,
when: ContextKeyExpr.and(ContextKeyExpr.not('installedExtensionIsPreReleaseVersion'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.not('inExtensionEditor'), ContextKeyExpr.equals('extensionStatus', 'installed'))
},
run: async (accessor: ServicesAccessor, id: string) => {
const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService);
const extension = extensionWorkbenchService.local.find(e => areSameExtensions(e.identifier, { id }));
if (extension) {
await extensionWorkbenchService.install(extension, { installPreReleaseVersion: true });
}
}
});
this.registerExtensionAction({
id: StopUsingPreReleaseVersionAction.ID,
title: StopUsingPreReleaseVersionAction.TITLE,
menu: {
id: MenuId.ExtensionContext,
group: '0_install',
order: 3,
when: ContextKeyExpr.and(ContextKeyExpr.has('installedExtensionIsPreReleaseVersion'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.not('inExtensionEditor'), ContextKeyExpr.equals('extensionStatus', 'installed'))
},
run: async (accessor: ServicesAccessor, id: string) => {
const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService);
const extension = extensionWorkbenchService.local.find(e => areSameExtensions(e.identifier, { id }));
if (extension) {
await extensionWorkbenchService.install(extension, { installPreReleaseVersion: false });
}
}
});
this.registerExtensionAction({
id: 'workbench.extensions.action.copyExtension',
title: { value: localize('workbench.extensions.action.copyExtension', "Copy"), original: 'Copy' },

View file

@ -33,7 +33,7 @@ import { IJSONEditingService } from 'vs/workbench/services/configuration/common/
import { ITextEditorSelection } from 'vs/platform/editor/common/editor';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { MenuId, IMenuService } from 'vs/platform/actions/common/actions';
import { MenuId, IMenuService, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions';
import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands';
import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification';
import { IOpenerService } from 'vs/platform/opener/common/opener';
@ -63,6 +63,7 @@ import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts';
import { escapeMarkdownSyntaxTokens, IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
import { ViewContainerLocation } from 'vs/workbench/common/views';
import { flatten } from 'vs/base/common/arrays';
function getRelativeDateLabel(date: Date): string {
const delta = new Date().getTime() - date.getTime();
@ -133,7 +134,7 @@ export class PromptExtensionInstallFailureAction extends Action {
return;
}
if ([ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.Malicious].includes(<ExtensionManagementErrorCode>this.error.name)) {
if ([ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleTargetPlatform, ExtensionManagementErrorCode.Malicious].includes(<ExtensionManagementErrorCode>this.error.name)) {
await this.dialogService.show(Severity.Info, getErrorMessage(this.error));
return;
}
@ -185,28 +186,40 @@ export class ActionWithDropDownAction extends ExtensionAction {
}
override set extension(extension: IExtension | null) {
this.actions.forEach(a => a.extension = extension);
this.extensionActions.forEach(a => a.extension = extension);
super.extension = extension;
}
protected readonly extensionActions: ExtensionAction[];
constructor(
id: string, label: string,
protected readonly actions: ExtensionAction[],
private readonly actionsGroups: ExtensionAction[][],
) {
super(id, label);
this.extensionActions = flatten(actionsGroups);
this.update();
this._register(Event.any(...actions.map(a => a.onDidChange))(() => this.update(true)));
actions.forEach(a => this._register(a));
this._register(Event.any(...this.extensionActions.map(a => a.onDidChange))(() => this.update(true)));
this.extensionActions.forEach(a => this._register(a));
}
update(donotUpdateActions?: boolean): void {
if (!donotUpdateActions) {
this.actions.forEach(a => a.update());
this.extensionActions.forEach(a => a.update());
}
const enabledActions = this.actions.filter(a => a.enabled);
this.action = enabledActions[0];
this._menuActions = enabledActions.slice(1);
const enabledActionsGroups = this.actionsGroups.map(actionsGroup => actionsGroup.filter(a => a.enabled));
let actions: IAction[] = [];
for (const enabledActions of enabledActionsGroups) {
if (enabledActions.length) {
actions = [...actions, ...enabledActions, new Separator()];
}
}
actions = actions.length ? actions.slice(0, actions.length - 1) : actions;
this.action = actions[0];
this._menuActions = actions.slice(1);
this.enabled = !!this.action;
if (this.action) {
@ -214,7 +227,7 @@ export class ActionWithDropDownAction extends ExtensionAction {
this.tooltip = this.action.tooltip;
}
let clazz = (this.action || this.actions[0])?.class || '';
let clazz = (this.action || this.extensionActions[0])?.class || '';
clazz = clazz ? `${clazz} action-dropdown` : 'action-dropdown';
if (this._menuActions.length === 0) {
clazz += ' action-dropdown';
@ -223,7 +236,7 @@ export class ActionWithDropDownAction extends ExtensionAction {
}
override run(): Promise<void> {
const enabledActions = this.actions.filter(a => a.enabled);
const enabledActions = this.extensionActions.filter(a => a.enabled);
return enabledActions[0].run();
}
}
@ -241,14 +254,14 @@ export abstract class AbstractInstallAction extends ExtensionAction {
private readonly updateThrottler = new Throttler();
constructor(
id: string, label: string, cssClass: string,
id: string, private readonly installPreReleaseVersion: boolean, cssClass: string,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IExtensionService private readonly runtimeExtensionService: IExtensionService,
@IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService,
@ILabelService private readonly labelService: ILabelService,
) {
super(id, label, cssClass, false);
super(id, localize('install', "Install"), cssClass, false);
this.update();
this._register(this.labelService.onDidChangeFormatters(() => this.updateLabel(), this));
}
@ -261,7 +274,7 @@ export abstract class AbstractInstallAction extends ExtensionAction {
this.enabled = false;
if (this.extension && !this.extension.isBuiltin) {
if (this.extension.state === ExtensionState.Uninstalled && await this.extensionsWorkbenchService.canInstall(this.extension)) {
this.enabled = true;
this.enabled = !this.installPreReleaseVersion || this.extension.hasPreReleaseVersion;
this.updateLabel();
}
}
@ -271,7 +284,7 @@ export abstract class AbstractInstallAction extends ExtensionAction {
if (!this.extension) {
return;
}
this.extensionsWorkbenchService.open(this.extension);
this.extensionsWorkbenchService.open(this.extension, { showPreReleaseVersion: this.installPreReleaseVersion });
alert(localize('installExtensionStart', "Installing extension {0} started. An editor is now open with more details on this extension", this.extension.displayName));
@ -324,13 +337,23 @@ export abstract class AbstractInstallAction extends ExtensionAction {
return null;
}
protected abstract updateLabel(): void;
protected abstract getInstallOptions(): InstallOptions;
protected updateLabel(): void {
this.label = this.getLabel();
}
protected getLabel(): string {
return this.installPreReleaseVersion && this.extension?.hasPreReleaseVersion ? localize('install pre-release', "Install Pre-release Version") : localize('install', "Install");
}
protected getInstallOptions(): InstallOptions {
return { installPreReleaseVersion: this.installPreReleaseVersion };
}
}
export class InstallAction extends AbstractInstallAction {
constructor(
installPreReleaseVersion: boolean,
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@IInstantiationService instantiationService: IInstantiationService,
@IExtensionService runtimeExtensionService: IExtensionService,
@ -341,7 +364,7 @@ export class InstallAction extends AbstractInstallAction {
@IUserDataAutoSyncEnablementService protected readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService,
@IUserDataSyncResourceEnablementService protected readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
) {
super(`extensions.installAndSync`, localize('install', "Install"), InstallAction.Class,
super(`extensions.install`, installPreReleaseVersion, InstallAction.Class,
extensionsWorkbenchService, instantiationService, runtimeExtensionService, workbenchThemeService, labelService);
this.updateLabel();
this._register(labelService.onDidChangeFormatters(() => this.updateLabel(), this));
@ -349,13 +372,11 @@ export class InstallAction extends AbstractInstallAction {
Event.filter(userDataSyncResourceEnablementService.onDidChangeResourceEnablement, e => e[0] === SyncResource.Extensions))(() => this.update()));
}
protected updateLabel(): void {
if (!this.extension) {
return;
}
protected override getLabel(): string {
const baseLabel = super.getLabel();
const donotSyncLabel = localize('do no sync', "Do not sync");
const isMachineScoped = this.getInstallOptions().isMachineScoped;
this.label = isMachineScoped ? localize('install and do no sync', "Install (Do not sync)") : localize('install', "Install");
// When remote connection exists
if (this._manifest && this.extensionManagementServerService.remoteExtensionManagementServer) {
@ -364,19 +385,33 @@ export class InstallAction extends AbstractInstallAction {
if (server === this.extensionManagementServerService.remoteExtensionManagementServer) {
const host = this.extensionManagementServerService.remoteExtensionManagementServer.label;
this.label = isMachineScoped
? localize({ key: 'install in remote and do not sync', comment: ['This is the name of the action to install an extension in remote server and do not sync it. Placeholder is for the name of remote server.'] }, "Install in {0} (Do not sync)", host)
: localize({ key: 'install in remote', comment: ['This is the name of the action to install an extension in remote server. Placeholder is for the name of remote server.'] }, "Install in {0}", host);
return;
return isMachineScoped
? localize({
key: 'install extension in remote and do not sync',
comment: [
'First placeholder is install action label.',
'Second placeholder is the name of the action to install an extension in remote server and do not sync it. Placeholder is for the name of remote server.',
'Third placeholder is do not sync label.',
]
}, "{0} in {1} ({2})", baseLabel, host, donotSyncLabel)
: localize({
key: 'install extension in remote',
comment: [
'First placeholder is install action label.',
'Second placeholder is the name of the action to install an extension in remote server and do not sync it. Placeholder is for the name of remote server.',
]
}, "{0} in {1}", baseLabel, host);
}
this.label = isMachineScoped ? localize('install locally and do not sync', "Install Locally (Do not sync)") : localize('install locally', "Install Locally");
return;
return isMachineScoped ?
localize('install extension locally and do not sync', "{0} Locally ({1})", baseLabel, donotSyncLabel) : localize('install extension locally', "{0} Locally", baseLabel);
}
return isMachineScoped ? `${baseLabel} (${donotSyncLabel})` : baseLabel;
}
protected getInstallOptions(): InstallOptions {
return { isMachineScoped: this.userDataAutoSyncEnablementService.isEnabled() && this.userDataSyncResourceEnablementService.isResourceEnabled(SyncResource.Extensions) };
protected override getInstallOptions(): InstallOptions {
return { ...super.getInstallOptions(), isMachineScoped: this.userDataAutoSyncEnablementService.isEnabled() && this.userDataSyncResourceEnablementService.isResourceEnabled(SyncResource.Extensions) };
}
}
@ -384,6 +419,7 @@ export class InstallAction extends AbstractInstallAction {
export class InstallAndSyncAction extends AbstractInstallAction {
constructor(
installPreReleaseVersion: boolean,
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@IInstantiationService instantiationService: IInstantiationService,
@IExtensionService runtimeExtensionService: IExtensionService,
@ -393,7 +429,7 @@ export class InstallAndSyncAction extends AbstractInstallAction {
@IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService,
@IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
) {
super(`extensions.installAndSync`, localize('install', "Install"), InstallAndSyncAction.Class,
super('extensions.installAndSync', installPreReleaseVersion, AbstractInstallAction.Class,
extensionsWorkbenchService, instantiationService, runtimeExtensionService, workbenchThemeService, labelService);
this.tooltip = localize({ key: 'install everywhere tooltip', comment: ['Placeholder is the name of the product. Eg: Visual Studio Code or Visual Studio Code - Insiders'] }, "Install this extension in all your synced {0} instances", productService.nameLong);
this._register(Event.any(userDataAutoSyncEnablementService.onDidChangeEnablement,
@ -407,18 +443,16 @@ export class InstallAndSyncAction extends AbstractInstallAction {
}
}
protected updateLabel(): void { }
protected getInstallOptions(): InstallOptions {
return { isMachineScoped: false };
protected override getInstallOptions(): InstallOptions {
return { ...super.getInstallOptions(), isMachineScoped: false };
}
}
export class InstallDropdownAction extends ActionWithDropDownAction {
set manifest(manifest: IExtensionManifest) {
this.actions.forEach(a => (<AbstractInstallAction>a).manifest = manifest);
this.actions.forEach(a => a.update());
this.extensionActions.forEach(a => (<AbstractInstallAction>a).manifest = manifest);
this.extensionActions.forEach(a => a.update());
this.update();
}
@ -426,8 +460,14 @@ export class InstallDropdownAction extends ActionWithDropDownAction {
@IInstantiationService instantiationService: IInstantiationService,
) {
super(`extensions.installActions`, '', [
instantiationService.createInstance(InstallAndSyncAction),
instantiationService.createInstance(InstallAction),
[
instantiationService.createInstance(InstallAndSyncAction, false),
instantiationService.createInstance(InstallAndSyncAction, true),
],
[
instantiationService.createInstance(InstallAction, false),
instantiationService.createInstance(InstallAction, true),
]
]);
}
@ -812,7 +852,7 @@ export class DropDownMenuActionViewItem extends ActionViewItem {
}
}
export function getContextMenuActions(extension: IExtension | undefined | null, inExtensionEditor: boolean, instantiationService: IInstantiationService): IAction[][] {
function getContextMenuActionsGroups(extension: IExtension | undefined | null, contextKeyService: IContextKeyService, instantiationService: IInstantiationService): [string, Array<MenuItemAction | SubmenuItemAction>][] {
return instantiationService.invokeFunction(accessor => {
const menuService = accessor.get(IMenuService);
const extensionRecommendationsService = accessor.get(IExtensionRecommendationsService);
@ -826,25 +866,38 @@ export function getContextMenuActions(extension: IExtension | undefined | null,
cksOverlay.push(['isExtensionRecommended', !!extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]]);
cksOverlay.push(['isExtensionWorkspaceRecommended', extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]?.reasonId === ExtensionRecommendationReason.Workspace]);
cksOverlay.push(['isUserIgnoredRecommendation', extensionIgnoredRecommendationsService.globalIgnoredRecommendations.some(e => e === extension.identifier.id.toLowerCase())]);
cksOverlay.push(['inExtensionEditor', inExtensionEditor]);
if (extension.state === ExtensionState.Installed) {
cksOverlay.push(['extensionStatus', 'installed']);
}
cksOverlay.push(['installedExtensionIsPreReleaseVersion', !!extension.local?.isPreReleaseVersion]);
cksOverlay.push(['galleryExtensionIsPreReleaseVersion', !!extension.gallery?.properties.isPreReleaseVersion]);
cksOverlay.push(['extensionHasPreReleaseVersion', extension.hasPreReleaseVersion]);
}
const contextKeyService = accessor.get(IContextKeyService).createOverlay(cksOverlay);
const groups: IAction[][] = [];
const menu = menuService.createMenu(MenuId.ExtensionContext, contextKeyService);
menu.getActions({ shouldForwardArgs: true }).forEach(([, actions]) => groups.push(actions.map(action => {
const menu = menuService.createMenu(MenuId.ExtensionContext, contextKeyService.createOverlay(cksOverlay));
const actionsGroups = menu.getActions({ shouldForwardArgs: true });
menu.dispose();
return actionsGroups;
});
}
function toActions(actionsGroups: [string, Array<MenuItemAction | SubmenuItemAction>][], instantiationService: IInstantiationService): IAction[][] {
const result: IAction[][] = [];
for (const [, actions] of actionsGroups) {
result.push(actions.map(action => {
if (action instanceof SubmenuAction) {
return action;
}
return instantiationService.createInstance(MenuItemExtensionAction, action);
})));
menu.dispose();
}));
}
return result;
}
return groups;
});
export function getContextMenuActions(extension: IExtension | undefined | null, contextKeyService: IContextKeyService, instantiationService: IInstantiationService): IAction[][] {
const actionsGroups = getContextMenuActionsGroups(extension, contextKeyService, instantiationService);
return toActions(actionsGroups, instantiationService);
}
export class ManageExtensionAction extends ExtensionDropDownAction {
@ -858,6 +911,7 @@ export class ManageExtensionAction extends ExtensionDropDownAction {
@IInstantiationService instantiationService: IInstantiationService,
@IExtensionService private readonly extensionService: IExtensionService,
@IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
) {
super(ManageExtensionAction.ID, '', '', true, instantiationService);
@ -886,6 +940,11 @@ export class ManageExtensionAction extends ExtensionDropDownAction {
groups.push(themesGroup);
}
}
const contextMenuActionsGroups = getContextMenuActionsGroups(this.extension, this.contextKeyService, this.instantiationService);
const installActions = toActions(contextMenuActionsGroups.filter(([group]) => group === '0_install'), this.instantiationService);
const otherActions = toActions(contextMenuActionsGroups.filter(([group]) => group !== '0_install'), this.instantiationService);
groups.push([
this.instantiationService.createInstance(EnableGloballyAction),
this.instantiationService.createInstance(EnableForWorkspaceAction)
@ -895,11 +954,12 @@ export class ManageExtensionAction extends ExtensionDropDownAction {
this.instantiationService.createInstance(DisableForWorkspaceAction, runningExtensions)
]);
groups.push([
...(installActions[0] || []),
this.instantiationService.createInstance(InstallAnotherVersionAction),
this.instantiationService.createInstance(UninstallAction),
this.instantiationService.createInstance(InstallAnotherVersionAction)
]);
getContextMenuActions(this.extension, false, this.instantiationService).forEach(actions => groups.push(actions));
otherActions.forEach(actions => groups.push(actions));
groups.forEach(group => group.forEach(extensionAction => {
if (extensionAction instanceof ExtensionAction) {
@ -930,7 +990,8 @@ export class ManageExtensionAction extends ExtensionDropDownAction {
export class ExtensionEditorManageExtensionAction extends ExtensionDropDownAction {
constructor(
@IInstantiationService instantiationService: IInstantiationService
private readonly contextKeyService: IContextKeyService,
instantiationService: IInstantiationService
) {
super('extensionEditor.manageExtension', '', `${ExtensionAction.ICON_ACTION_CLASS} manage ${ThemeIcon.asClassName(manageExtensionIcon)}`, true, instantiationService);
this.tooltip = localize('manage', "Manage");
@ -940,7 +1001,7 @@ export class ExtensionEditorManageExtensionAction extends ExtensionDropDownActio
override run(): Promise<any> {
const actionGroups: IAction[][] = [];
getContextMenuActions(this.extension, true, this.instantiationService).forEach(actions => actionGroups.push(actions));
getContextMenuActions(this.extension, this.contextKeyService, this.instantiationService).forEach(actions => actionGroups.push(actions));
actionGroups.forEach(group => group.forEach(extensionAction => {
if (extensionAction instanceof ExtensionAction) {
extensionAction.extension = this.extension;
@ -976,6 +1037,58 @@ export class MenuItemExtensionAction extends ExtensionAction {
}
}
export class UsePreReleaseVersionAction extends ExtensionAction {
static readonly ID = 'workbench.extensions.action.usePreReleaseVersion';
static readonly TITLE = { value: localize('use pre-release version', "Use Pre-release Version"), original: 'Use Pre-release Version' };
private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} hide-when-disabled`;
constructor(
@ICommandService private readonly commandService: ICommandService,
) {
super(UsePreReleaseVersionAction.ID, UsePreReleaseVersionAction.TITLE.value, UsePreReleaseVersionAction.Class);
this.update();
}
update(): void {
this.enabled = !!this.extension && !this.extension.local?.isPreReleaseVersion && this.extension.hasPreReleaseVersion && this.extension.state === ExtensionState.Installed;
}
override async run(): Promise<any> {
if (!this.enabled) {
return;
}
return this.commandService.executeCommand(UsePreReleaseVersionAction.ID, this.extension?.identifier.id);
}
}
export class StopUsingPreReleaseVersionAction extends ExtensionAction {
static readonly ID = 'workbench.extensions.action.stopUsingPreReleaseVersion';
static readonly TITLE = { value: localize('stop using pre-release version', "Stop Using Pre-release Version"), original: 'Stop Using Pre-release Version' };
private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} hide-when-disabled`;
constructor(
@ICommandService private readonly commandService: ICommandService,
) {
super(StopUsingPreReleaseVersionAction.ID, StopUsingPreReleaseVersionAction.TITLE.value, StopUsingPreReleaseVersionAction.Class);
this.update();
}
update(): void {
this.enabled = !!this.extension && this.extension.state === ExtensionState.Installed && !!this.extension.local?.isPreReleaseVersion;
}
override async run(): Promise<any> {
if (!this.enabled) {
return;
}
return this.commandService.executeCommand(StopUsingPreReleaseVersionAction.ID, this.extension?.identifier.id);
}
}
export class InstallAnotherVersionAction extends ExtensionAction {
static readonly ID = 'workbench.extensions.action.install.anotherVersion';
@ -1019,7 +1132,7 @@ export class InstallAnotherVersionAction extends ExtensionAction {
private async getVersionEntries(): Promise<(IQuickPickItem & { latest: boolean, id: string })[]> {
const targetPlatform = await this.extension!.server!.extensionManagementService.getTargetPlatform();
const allVersions = await this.extensionGalleryService.getAllCompatibleVersions(this.extension!.gallery!, targetPlatform);
const allVersions = await this.extensionGalleryService.getAllCompatibleVersions(this.extension!.gallery!, true, targetPlatform);
return allVersions.map((v, i) => ({ id: v.version, label: v.version, description: `${getRelativeDateLabel(new Date(Date.parse(v.date)))}${v.version === this.extension!.version ? ` (${localize('current', "Current")})` : ''}`, latest: i === 0 }));
}
}
@ -1166,8 +1279,10 @@ export class EnableDropDownAction extends ActionWithDropDownAction {
@IInstantiationService instantiationService: IInstantiationService
) {
super('extensions.enable', localize('enableAction', "Enable"), [
instantiationService.createInstance(EnableGloballyAction),
instantiationService.createInstance(EnableForWorkspaceAction)
[
instantiationService.createInstance(EnableGloballyAction),
instantiationService.createInstance(EnableForWorkspaceAction)
]
]);
}
}
@ -1182,7 +1297,7 @@ export class DisableDropDownAction extends ActionWithDropDownAction {
instantiationService.createInstance(DisableGloballyAction, []),
instantiationService.createInstance(DisableForWorkspaceAction, [])
];
super('extensions.disable', localize('disableAction', "Disable"), actions);
super('extensions.disable', localize('disableAction', "Disable"), [actions]);
const updateRunningExtensions = async () => {
const runningExtensions = await extensionService.getExtensions();
@ -2475,7 +2590,7 @@ export class InstallLocalExtensionsInRemoteAction extends AbstractInstallExtensi
const targetPlatform = await this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.getTargetPlatform();
await Promises.settled(localExtensionsToInstall.map(async extension => {
if (this.extensionGalleryService.isEnabled()) {
const gallery = await this.extensionGalleryService.getCompatibleExtension(extension.identifier, targetPlatform);
const gallery = await this.extensionGalleryService.getCompatibleExtension(extension.identifier, !!extension.local?.isPreReleaseVersion, targetPlatform);
if (gallery) {
galleryExtensions.push(gallery);
return;
@ -2524,7 +2639,7 @@ export class InstallRemoteExtensionsInLocalAction extends AbstractInstallExtensi
const targetPlatform = await this.extensionManagementServerService.localExtensionManagementServer!.extensionManagementService.getTargetPlatform();
await Promises.settled(extensions.map(async extension => {
if (this.extensionGalleryService.isEnabled()) {
const gallery = await this.extensionGalleryService.getCompatibleExtension(extension.identifier, targetPlatform);
const gallery = await this.extensionGalleryService.getCompatibleExtension(extension.identifier, !!extension.local?.isPreReleaseVersion, targetPlatform);
if (gallery) {
galleryExtensions.push(gallery);
return;

View file

@ -25,6 +25,7 @@ export const remoteIcon = registerIcon('extensions-remote', Codicon.remote, loca
export const installCountIcon = registerIcon('extensions-install-count', Codicon.cloudDownload, localize('installCountIcon', 'Icon shown along with the install count in the extensions view and editor.'));
export const ratingIcon = registerIcon('extensions-rating', Codicon.star, localize('ratingIcon', 'Icon shown along with the rating in the extensions view and editor.'));
export const verifiedPublisherIcon = registerIcon('extensions-verified-publisher', Codicon.verifiedFilled, localize('verifiedPublisher', 'Icon used for the verified extension publisher in the extensions view and editor.'));
export const preReleaseIcon = registerIcon('extensions-pre-release', Codicon.circleFilled, localize('preReleaseIcon', 'Icon shown for extensions having pre-release versions in extensions view and editor.'));
export const starFullIcon = registerIcon('extensions-star-full', Codicon.starFull, localize('starFullIcon', 'Full star icon used for the rating in the extensions editor.'));
export const starHalfIcon = registerIcon('extensions-star-half', Codicon.starHalf, localize('starHalfIcon', 'Half star icon used for the rating in the extensions editor.'));

View file

@ -15,7 +15,7 @@ import { Event } from 'vs/base/common/event';
import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { UpdateAction, ManageExtensionAction, ReloadAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, WebInstallAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionActivationStatusWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets';
import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionActivationStatusWidget, PreReleaseBookmarkWidget, PreReleaseIndicatorWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets';
import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { INotificationService } from 'vs/platform/notification/common/notification';
@ -78,6 +78,7 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> {
renderTemplate(root: HTMLElement): ITemplateData {
const recommendationWidget = this.instantiationService.createInstance(RecommendationWidget, append(root, $('.extension-bookmark-container')));
const preReleaseWidget = this.instantiationService.createInstance(PreReleaseBookmarkWidget, append(root, $('.extension-bookmark-container')));
const element = append(root, $('.extension-list-item'));
const iconContainer = append(element, $('.icon-container'));
const icon = append(iconContainer, $<HTMLImageElement>('img.icon'));
@ -89,6 +90,7 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> {
const name = append(header, $('span.name'));
const installCount = append(header, $('span.install-count'));
const ratings = append(header, $('span.ratings'));
const preRelease = append(header, $('span.pre-release'));
const syncIgnore = append(header, $('span.sync-ignored'));
const activationStatus = append(header, $('span.activation-status'));
const headerRemoteBadgeWidget = this.instantiationService.createInstance(RemoteBadgeWidget, header, false);
@ -131,10 +133,12 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> {
const widgets = [
recommendationWidget,
preReleaseWidget,
iconRemoteBadgeWidget,
extensionPackBadgeWidget,
headerRemoteBadgeWidget,
extensionHoverWidget,
this.instantiationService.createInstance(PreReleaseIndicatorWidget, preRelease),
this.instantiationService.createInstance(SyncIgnoredWidget, syncIgnore),
this.instantiationService.createInstance(ExtensionActivationStatusWidget, activationStatus, true),
this.instantiationService.createInstance(InstallCountWidget, installCount, true),

View file

@ -287,7 +287,7 @@ export class ExtensionsListView extends ViewPane {
groups = await manageExtensionAction.getActionGroups(runningExtensions);
} else if (e.element) {
groups = getContextMenuActions(e.element, false, this.instantiationService);
groups = getContextMenuActions(e.element, this.contextKeyService, this.instantiationService);
groups.forEach(group => group.forEach(extensionAction => {
if (extensionAction instanceof ExtensionAction) {
extensionAction.extension = e.element!;

View file

@ -20,7 +20,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
import { activationTimeIcon, errorIcon, infoIcon, installCountIcon, ratingIcon, remoteIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon, verifiedPublisherIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
import { activationTimeIcon, errorIcon, infoIcon, installCountIcon, preReleaseIcon, ratingIcon, remoteIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon, verifiedPublisherIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
import { registerColor, textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
import { IHoverService } from 'vs/workbench/services/hover/browser/hover';
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
@ -158,6 +158,29 @@ export class RatingsWidget extends ExtensionWidget {
}
}
export class PreReleaseIndicatorWidget extends ExtensionWidget {
constructor(private container: HTMLElement) {
super();
container.classList.add('extension-pre-release');
this.render();
}
render(): void {
this.container.innerText = '';
if (!this.extension) {
return;
}
if (!this.extension.local?.isPreReleaseVersion) {
return;
}
append(this.container, $('span' + ThemeIcon.asCSSSelector(preReleaseIcon)));
}
}
export class RecommendationWidget extends ExtensionWidget {
private element?: HTMLElement;
@ -183,7 +206,7 @@ export class RecommendationWidget extends ExtensionWidget {
render(): void {
this.clear();
if (!this.extension) {
if (!this.extension || this.extension.state === ExtensionState.Installed) {
return;
}
const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason();
@ -196,6 +219,41 @@ export class RecommendationWidget extends ExtensionWidget {
}
export class PreReleaseBookmarkWidget extends ExtensionWidget {
private element?: HTMLElement;
private readonly disposables = this._register(new DisposableStore());
constructor(
private parent: HTMLElement,
) {
super();
this.render();
this._register(toDisposable(() => this.clear()));
}
private clear(): void {
if (this.element) {
this.parent.removeChild(this.element);
}
this.element = undefined;
this.disposables.clear();
}
render(): void {
this.clear();
if (!this.extension) {
return;
}
if (this.extension.hasPreReleaseVersion) {
this.element = append(this.parent, $('div.extension-bookmark'));
const preRelease = append(this.element, $('.pre-release'));
append(preRelease, $('span' + ThemeIcon.asCSSSelector(preReleaseIcon)));
}
}
}
export class RemoteBadgeWidget extends ExtensionWidget {
private readonly remoteBadge = this._register(new MutableDisposable<RemoteBadge>());
@ -304,26 +362,26 @@ export class ExtensionPackCountWidget extends ExtensionWidget {
export class SyncIgnoredWidget extends ExtensionWidget {
private element: HTMLElement;
constructor(
container: HTMLElement,
private readonly container: HTMLElement,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService,
) {
super();
this.element = append(container, $('span.extension-sync-ignored' + ThemeIcon.asCSSSelector(syncIgnoredIcon)));
this.element.title = localize('syncingore.label', "This extension is ignored during sync.");
this.element.classList.add(...ThemeIcon.asClassNameArray(syncIgnoredIcon));
this.element.classList.add('hide');
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectedKeys.includes('settingsSync.ignoredExtensions'))(() => this.render()));
this._register(userDataAutoSyncEnablementService.onDidChangeEnablement(() => this.update()));
this.render();
}
render(): void {
this.element.classList.toggle('hide', !(this.extension && this.extension.state === ExtensionState.Installed && this.userDataAutoSyncEnablementService.isEnabled() && this.extensionsWorkbenchService.isExtensionIgnoredToSync(this.extension)));
this.container.innerText = '';
if (this.extension && this.extension.state === ExtensionState.Installed && this.userDataAutoSyncEnablementService.isEnabled() && this.extensionsWorkbenchService.isExtensionIgnoredToSync(this.extension)) {
const element = append(this.container, $('span.extension-sync-ignored' + ThemeIcon.asCSSSelector(syncIgnoredIcon)));
element.title = localize('syncingore.label', "This extension is ignored during sync.");
element.classList.add(...ThemeIcon.asClassNameArray(syncIgnoredIcon));
}
}
}
@ -416,6 +474,10 @@ export class ExtensionHoverWidget extends ExtensionWidget {
const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });
markdown.appendMarkdown(`**${this.extension.displayName}**&nbsp;_v${this.extension.version}_`);
if (this.extension.state === ExtensionState.Installed && this.extension.local?.isPreReleaseVersion) {
const extensionPreReleaseIcon = this.themeService.getColorTheme().getColor(extensionPreReleaseIconColor);
markdown.appendMarkdown(`&nbsp;<span style="color:${extensionPreReleaseIcon ? Color.Format.CSS.formatHex(extensionPreReleaseIcon) : '#ffffff'};">$(${preReleaseIcon.id})</span>`);
}
markdown.appendText(`\n`);
if (this.extension.description) {
@ -430,6 +492,12 @@ export class ExtensionHoverWidget extends ExtensionWidget {
markdown.appendText(`\n`);
}
const preReleaseMessage = this.getPreReleaseMessage(this.extension);
if (preReleaseMessage) {
markdown.appendMarkdown(preReleaseMessage);
markdown.appendText(`\n`);
}
const extensionRuntimeStatus = this.extensionsWorkbenchService.getExtensionStatus(this.extension);
const extensionStatus = this.extensionStatusAction.status;
const reloadRequiredMessage = this.reloadAction.enabled ? this.reloadAction.tooltip : '';
@ -488,12 +556,28 @@ export class ExtensionHoverWidget extends ExtensionWidget {
}
private getRecommendationMessage(extension: IExtension): string | undefined {
const recommendation = this.extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()];
if (recommendation?.reasonText) {
const bgColor = this.themeService.getColorTheme().getColor(extensionButtonProminentBackground);
return `<span style="color:${bgColor ? Color.Format.CSS.formatHex(bgColor) : '#ffffff'};">$(${starEmptyIcon.id})</span>&nbsp;${recommendation.reasonText}`;
if (extension.state === ExtensionState.Installed) {
return undefined;
}
return undefined;
const recommendation = this.extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()];
if (!recommendation?.reasonText) {
return undefined;
}
const bgColor = this.themeService.getColorTheme().getColor(extensionButtonProminentBackground);
return `<span style="color:${bgColor ? Color.Format.CSS.formatHex(bgColor) : '#ffffff'};">$(${starEmptyIcon.id})</span>&nbsp;${recommendation.reasonText}`;
}
private getPreReleaseMessage(extension: IExtension): string | undefined {
if (!extension.hasPreReleaseVersion) {
return undefined;
}
if (extension.state === ExtensionState.Installed && extension.local?.isPreReleaseVersion) {
return undefined;
}
const extensionPreReleaseIcon = this.themeService.getColorTheme().getColor(extensionPreReleaseIconColor);
const preReleaseVersionLink = `[${localize('Show prerelease version', "Pre-release version")}](${URI.parse(`command:workbench.extensions.action.showPreReleaseVersion?${encodeURIComponent(JSON.stringify([extension.identifier.id]))}`)})`;
const message = localize('has prerelease', "There is a new {0} of this extension available in the Marketplace.", preReleaseVersionLink);
return `<span style="color:${extensionPreReleaseIcon ? Color.Format.CSS.formatHex(extensionPreReleaseIcon) : '#ffffff'};">$(${preReleaseIcon.id})</span>&nbsp;${message}.`;
}
}
@ -501,6 +585,7 @@ export class ExtensionHoverWidget extends ExtensionWidget {
// Rating icon
export const extensionRatingIconColor = registerColor('extensionIcon.starForeground', { light: '#DF6100', dark: '#FF8E00', hc: '#FF8E00' }, localize('extensionIconStarForeground', "The icon color for extension ratings."), true);
export const extensionVerifiedPublisherIconColor = registerColor('extensionIcon.verifiedForeground', { dark: textLinkForeground, light: textLinkForeground, hc: textLinkForeground }, localize('extensionIconVerifiedForeground', "The icon color for extension verified publisher."), true);
export const extensionPreReleaseIconColor = registerColor('extensionIcon.preReleaseForeground', { dark: '#1d9271', light: '#1d9271', hc: '#1d9271' }, localize('extensionPreReleaseForeground', "The icon color for pre-release extension."), true);
registerThemingParticipant((theme, collector) => {
const extensionRatingIcon = theme.getColor(extensionRatingIconColor);
@ -523,4 +608,11 @@ registerThemingParticipant((theme, collector) => {
if (extensionVerifiedPublisherIcon) {
collector.addRule(`${ThemeIcon.asCSSSelector(verifiedPublisherIcon)} { color: ${extensionVerifiedPublisherIcon}; }`);
}
const extensionPreReleaseIcon = theme.getColor(extensionPreReleaseIconColor);
if (extensionPreReleaseIcon) {
collector.addRule(`.extension-bookmark .pre-release { border-top-color: ${extensionPreReleaseIcon}; }`);
collector.addRule(`.extension-bookmark .pre-release ${ThemeIcon.asCSSSelector(preReleaseIcon)} { color: #ffffff; }`);
collector.addRule(`${ThemeIcon.asCSSSelector(preReleaseIcon)} { color: ${extensionPreReleaseIcon}; }`);
}
});

View file

@ -14,7 +14,7 @@ import { IPager, mapPager, singlePagePager } from 'vs/base/common/paging';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import {
IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions,
InstallExtensionEvent, DidUninstallExtensionEvent, IExtensionIdentifier, InstallOperation, DefaultIconPath, InstallOptions, WEB_EXTENSION_TAG, InstallExtensionResult
InstallExtensionEvent, DidUninstallExtensionEvent, IExtensionIdentifier, InstallOperation, DefaultIconPath, InstallOptions, WEB_EXTENSION_TAG, InstallExtensionResult, isIExtensionIdentifier
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, getMaliciousExtensionsSet, groupByExtension, ExtensionIdentifierWithVersion, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
@ -22,10 +22,10 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { URI } from 'vs/base/common/uri';
import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, ExtensionEditorTab } from 'vs/workbench/contrib/extensions/common/extensions';
import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext } from 'vs/workbench/contrib/extensions/common/extensions';
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/common/url';
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { ILogService } from 'vs/platform/log/common/log';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
@ -200,7 +200,7 @@ class Extension implements IExtension {
}
get outdated(): boolean {
return !!this.gallery && this.type === ExtensionType.User && semver.gt(this.latestVersion, this.version);
return !!this.gallery && this.type === ExtensionType.User && semver.gt(this.latestVersion, this.version) && this.local?.isPreReleaseVersion === this.gallery?.properties.isPreReleaseVersion;
}
get telemetryData(): any {
@ -217,20 +217,40 @@ class Extension implements IExtension {
return this.gallery ? this.gallery.preview : false;
}
getManifest(token: CancellationToken): Promise<IExtensionManifest | null> {
if (this.local && !this.outdated) {
return Promise.resolve(this.local.manifest);
get hasPreReleaseVersion(): boolean {
return !!this.gallery?.hasPreReleaseVersion;
}
private getLocal(preRelease: boolean): ILocalExtension | undefined {
return this.local && !this.outdated && this.local.isPreReleaseVersion === preRelease ? this.local : undefined;
}
private async getGallery(preRelease: boolean, token: CancellationToken): Promise<IGalleryExtension | undefined> {
if (this.gallery) {
if (preRelease === this.gallery.properties.isPreReleaseVersion) {
return this.gallery;
}
return (await this.galleryService.getExtensions([this.gallery.identifier], preRelease, token))[0];
}
return undefined;
}
async getManifest(preRelease: boolean, token: CancellationToken): Promise<IExtensionManifest | null> {
const local = this.getLocal(preRelease);
if (local) {
return local.manifest;
}
if (this.gallery) {
if (this.gallery.assets.manifest) {
return this.galleryService.getManifest(this.gallery, token);
const gallery = await this.getGallery(preRelease, token);
if (gallery) {
if (gallery.assets.manifest) {
return this.galleryService.getManifest(gallery, token);
}
this.logService.error(nls.localize('Manifest is not found', "Manifest is not found"), this.identifier.id);
return Promise.resolve(null);
return null;
}
return Promise.resolve(null);
return null;
}
hasReadme(): boolean {
@ -245,14 +265,17 @@ class Extension implements IExtension {
return this.type === ExtensionType.System;
}
getReadme(token: CancellationToken): Promise<string> {
if (this.local && this.local.readmeUrl && !this.outdated) {
return this.fileService.readFile(this.local.readmeUrl).then(content => content.value.toString());
async getReadme(preRelease: boolean, token: CancellationToken): Promise<string> {
const local = this.getLocal(preRelease);
if (local?.readmeUrl) {
const content = await this.fileService.readFile(local.readmeUrl);
return content.value.toString();
}
if (this.gallery) {
if (this.gallery.assets.readme) {
return this.galleryService.getReadme(this.gallery, token);
const gallery = await this.getGallery(preRelease, token);
if (gallery) {
if (gallery.assets.readme) {
return this.galleryService.getReadme(gallery, token);
}
this.telemetryService.publicLog('extensions:NotFoundReadMe', this.telemetryData);
}
@ -280,14 +303,16 @@ ${this.description}
return this.type === ExtensionType.System;
}
getChangelog(token: CancellationToken): Promise<string> {
if (this.local && this.local.changelogUrl && !this.outdated) {
return this.fileService.readFile(this.local.changelogUrl).then(content => content.value.toString());
async getChangelog(preRelease: boolean, token: CancellationToken): Promise<string> {
const local = this.getLocal(preRelease);
if (local?.changelogUrl) {
const content = await this.fileService.readFile(local.changelogUrl);
return content.value.toString();
}
if (this.gallery && this.gallery.assets.changelog) {
return this.galleryService.getChangelog(this.gallery, token);
const gallery = await this.getGallery(preRelease, token);
if (gallery?.assets.changelog) {
return this.galleryService.getChangelog(gallery, token);
}
if (this.type === ExtensionType.System) {
@ -401,9 +426,8 @@ class Extensions extends Disposable {
if (maliciousExtensionSet.has(extension.identifier.id)) {
extension.isMalicious = true;
}
// Loading the compatible version only there is an engine property
// Otherwise falling back to old way so that we will not make many roundtrips
const compatible = gallery.properties.engine ? await this.galleryService.getCompatibleExtension(gallery, await this.server.extensionManagementService.getTargetPlatform()) : gallery;
const compatible = await this.getCompatibleExtension(gallery, !!extension.local?.isPreReleaseVersion);
if (!compatible) {
return false;
}
@ -418,6 +442,17 @@ class Extensions extends Disposable {
return false;
}
private async getCompatibleExtension(extensionOrIdentifier: IGalleryExtension | IExtensionIdentifier, includePreRelease: boolean): Promise<IGalleryExtension | null> {
if (isIExtensionIdentifier(extensionOrIdentifier)) {
return this.galleryService.getCompatibleExtension(extensionOrIdentifier, includePreRelease, await this.server.extensionManagementService.getTargetPlatform());
}
const extension = extensionOrIdentifier;
if (includePreRelease && extension.hasPreReleaseVersion && !extension.properties.isPreReleaseVersion) {
return this.getCompatibleExtension(extension.identifier, includePreRelease);
}
return this.galleryService.getCompatibleExtension(extension, includePreRelease, await this.server.extensionManagementService.getTargetPlatform());
}
canInstall(galleryExtension: IGalleryExtension): Promise<boolean> {
return this.server.extensionManagementService.canInstall(galleryExtension);
}
@ -426,7 +461,7 @@ class Extensions extends Disposable {
if (!this.galleryService.isEnabled()) {
return;
}
const compatible = await this.galleryService.getCompatibleExtension(extension.identifier, await this.server.extensionManagementService.getTargetPlatform());
const compatible = await this.getCompatibleExtension(extension.identifier, !!extension.local?.isPreReleaseVersion);
if (compatible) {
extension.gallery = compatible;
this._onChange.fire({ extension });
@ -706,7 +741,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
const options: IQueryOptions = CancellationToken.isCancellationToken(arg1) ? {} : arg1;
const token: CancellationToken = CancellationToken.isCancellationToken(arg1) ? arg1 : arg2;
options.text = options.text ? this.resolveQueryText(options.text) : options.text;
const report = await this.extensionManagementService.getExtensionsReport();
const maliciousSet = getMaliciousExtensionsSet(report);
try {
@ -743,8 +778,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
return text.substr(0, 350);
}
async open(extension: IExtension, options?: { sideByside?: boolean, preserveFocus?: boolean, pinned?: boolean, tab?: ExtensionEditorTab }): Promise<void> {
const editor = await this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension), { preserveFocus: options?.preserveFocus, pinned: options?.pinned }, options?.sideByside ? SIDE_GROUP : ACTIVE_GROUP);
async open(extension: IExtension, options?: IExtensionEditorOptions): Promise<void> {
const editor = await this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension), options, options?.sideByside ? SIDE_GROUP : ACTIVE_GROUP);
if (options?.tab && editor instanceof ExtensionEditor) {
await editor.openTab(options.tab);
}
@ -977,7 +1012,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
(this.getAutoUpdateValue() === true || (e.local && this.extensionEnablementService.isEnabled(e.local)))
);
return Promises.settled(toUpdate.map(e => this.install(e)));
return Promises.settled(toUpdate.map(e => this.install(e, e.local?.hadPreReleaseVersion ? { installPreReleaseVersion: true } : undefined)));
}
async canInstall(extension: IExtension): Promise<boolean> {

View file

@ -89,15 +89,23 @@
align-items: center;
}
.extension-list-item > .details > .header-container > .header > .activation-status:not(:empty),
.extension-list-item > .details > .header-container > .header > .install-count:not(:empty) {
font-size: 80%;
margin: 0 6px;
}
.extension-list-item > .details > .header-container > .header > .activation-status:not(:empty) {
font-size: 80%;
margin-left: 2px;
}
.extension-list-item > .details > .header-container > .header > .activation-status:not(:empty) .codicon {
margin-right: 2px;
}
.extension-list-item > .details > .header-container > .header .codicon {
font-size: 120%;
margin-right: 2px;
margin-right: 3px;
-webkit-mask: inherit;
}
@ -130,9 +138,12 @@
color: currentColor;
}
.extension-list-item > .details > .header-container > .header .pre-release {
display: flex;
}
.extension-list-item > .details > .header-container > .header .sync-ignored {
display: flex;
margin-left: 6px;
}
.extension-list-item > .details > .header-container > .header .sync-ignored > .codicon {

View file

@ -50,6 +50,7 @@
.monaco-action-bar .action-item.disabled .action-label.extension-action.undo-ignore,
.monaco-action-bar .action-item.disabled .action-label.extension-action.install:not(.installing),
.monaco-action-bar .action-item.disabled .action-label.extension-action.uninstall:not(.uninstalling),
.monaco-action-bar .action-item.disabled .action-label.extension-action.hide-when-disabled,
.monaco-action-bar .action-item.disabled .action-label.extension-action.update,
.monaco-action-bar .action-item.disabled .action-label.extension-action.theme,
.monaco-action-bar .action-item.disabled .action-label.extension-action.extension-sync,

View file

@ -82,6 +82,11 @@
white-space: nowrap;
}
.extension-editor > .header > .details > .title > .pre-release {
display: flex;
margin-left: 4px;
}
.extension-editor > .header > .details > .title > .builtin {
font-size: 10px;
font-style: italic;

View file

@ -37,14 +37,16 @@
width: 20px;
}
.extension-bookmark > .recommendation {
.extension-bookmark > .recommendation,
.extension-bookmark > .pre-release {
border-right: 20px solid transparent;
border-top: 20px solid;
box-sizing: border-box;
position: relative;
}
.extension-bookmark > .recommendation > .codicon {
.extension-bookmark > .recommendation > .codicon,
.extension-bookmark > .pre-release > .codicon {
position: absolute;
bottom: 9px;
left: 1px;

View file

@ -16,6 +16,7 @@ import { URI } from 'vs/base/common/uri';
import { IView, IViewPaneContainer } from 'vs/workbench/common/views';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IExtensionsStatus } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput';
export const VIEWLET_ID = 'workbench.view.extensions';
@ -48,6 +49,7 @@ export interface IExtension {
readonly publisherDomain?: { link: string, verified: boolean };
readonly version: string;
readonly latestVersion: string;
readonly hasPreReleaseVersion: boolean;
readonly description: string;
readonly url?: string;
readonly repository?: string;
@ -65,11 +67,11 @@ export interface IExtension {
readonly extensionPack: string[];
readonly telemetryData: any;
readonly preview: boolean;
getManifest(token: CancellationToken): Promise<IExtensionManifest | null>;
getReadme(token: CancellationToken): Promise<string>;
getManifest(preRelease: boolean, token: CancellationToken): Promise<IExtensionManifest | null>;
hasReadme(): boolean;
getChangelog(token: CancellationToken): Promise<string>;
getReadme(preRelease: boolean, token: CancellationToken): Promise<string>;
hasChangelog(): boolean;
getChangelog(preRelease: boolean, token: CancellationToken): Promise<string>;
readonly server?: IExtensionManagementServer;
readonly local?: ILocalExtension;
gallery?: IGalleryExtension;
@ -96,7 +98,7 @@ export interface IExtensionsWorkbenchService {
installVersion(extension: IExtension, version: string): Promise<IExtension>;
reinstall(extension: IExtension): Promise<IExtension>;
setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): Promise<void>;
open(extension: IExtension, options?: { sideByside?: boolean, preserveFocus?: boolean, pinned?: boolean, tab?: string }): Promise<void>;
open(extension: IExtension, options?: IExtensionEditorOptions): Promise<void>;
checkForUpdates(): Promise<void>;
getExtensionStatus(extension: IExtension): IExtensionsStatus | undefined;

View file

@ -8,9 +8,16 @@ import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { EditorInputCapabilities, IUntypedEditorInput } from 'vs/workbench/common/editor';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { ExtensionEditorTab, IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { join } from 'vs/base/common/path';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
export interface IExtensionEditorOptions extends IEditorOptions {
showPreReleaseVersion?: boolean;
tab?: ExtensionEditorTab;
sideByside?: boolean;
}
export class ExtensionsInput extends EditorInput {

View file

@ -219,6 +219,7 @@ suite('ExtensionRecommendationsService Test', () => {
async getInstalled() { return []; },
async canInstall() { return true; },
async getExtensionsReport() { return []; },
async getTargetPlatform() { return getTargetPlatform(platform, arch); }
});
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{
async whenInstalledExtensionsRegistered() { return true; }

View file

@ -108,7 +108,8 @@ async function setupTest() {
local.publisherId = metadata.publisherId;
return local;
},
async canInstall() { return true; }
async canInstall() { return true; },
async getTargetPlatform() { return getTargetPlatform(platform, arch); }
});
instantiationService.stub(IRemoteAgentService, RemoteAgentService);
@ -153,14 +154,14 @@ suite('ExtensionsActions', () => {
teardown(() => disposables.dispose());
test('Install action is disabled when there is no extension', () => {
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction);
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction, false);
assert.ok(!testObject.enabled);
});
test('Test Install action when state is installed', () => {
const workbenchService = instantiationService.get(IExtensionsWorkbenchService);
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction);
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction, false);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const local = aLocalExtension('a');
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
@ -196,7 +197,7 @@ suite('ExtensionsActions', () => {
test('Test Install action when state is uninstalled', async () => {
const workbenchService = instantiationService.get(IExtensionsWorkbenchService);
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction);
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction, false);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const gallery = aGalleryExtension('a');
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery));
@ -209,7 +210,7 @@ suite('ExtensionsActions', () => {
});
test('Test Install action when extension is system action', () => {
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction);
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction, false);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const local = aLocalExtension('a', {}, { type: ExtensionType.System });
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
@ -224,7 +225,7 @@ suite('ExtensionsActions', () => {
});
test('Test Install action when extension doesnot has gallery', () => {
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction);
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction, false);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const local = aLocalExtension('a');
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
@ -386,7 +387,9 @@ suite('ExtensionsActions', () => {
return workbenchService.queryLocal()
.then(async extensions => {
testObject.extension = extensions[0];
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: local.identifier, version: '1.0.1' })));
const gallery = aGalleryExtension('a', { identifier: local.identifier, version: '1.0.1' });
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery));
instantiationService.stubPromise(IExtensionGalleryService, 'getCompatibleExtension', gallery);
assert.ok(!testObject.enabled);
return new Promise<void>(c => {
testObject.onDidChange(() => {
@ -409,6 +412,7 @@ suite('ExtensionsActions', () => {
testObject.extension = extensions[0];
const gallery = aGalleryExtension('a', { identifier: local.identifier, version: '1.0.1' });
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery));
instantiationService.stubPromise(IExtensionGalleryService, 'getCompatibleExtension', gallery);
await instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None);
const promise = Event.toPromise(testObject.onDidChange);
installEvent.fire({ identifier: local.identifier, source: gallery });
@ -2408,7 +2412,8 @@ function createExtensionManagementService(installed: ILocalExtension[] = []): IE
local.publisherDisplayName = metadata.publisherDisplayName;
local.publisherId = metadata.publisherId;
return local;
}
},
async getTargetPlatform() { return getTargetPlatform(platform, arch); }
};
}

View file

@ -97,6 +97,7 @@ suite('ExtensionsListView Tests', () => {
async getInstalled() { return []; },
async canInstall() { return true; },
async getExtensionsReport() { return []; },
async getTargetPlatform() { return getTargetPlatform(platform, arch); }
});
instantiationService.stub(IRemoteAgentService, RemoteAgentService);
instantiationService.stub(IContextKeyService, new MockContextKeyService());

View file

@ -101,7 +101,8 @@ suite('ExtensionsWorkbenchServiceTest', () => {
local.publisherId = metadata.publisherId;
return local;
},
async canInstall() { return true; }
async canInstall() { return true; },
getTargetPlatform: async () => getTargetPlatform(platform, arch)
});
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService({
@ -306,6 +307,7 @@ suite('ExtensionsWorkbenchServiceTest', () => {
});
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local1, local2]);
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery1));
instantiationService.stubPromise(IExtensionGalleryService, 'getCompatibleExtension', gallery1);
testObject = await aWorkbenchService();
await testObject.queryLocal();
@ -1511,7 +1513,8 @@ suite('ExtensionsWorkbenchServiceTest', () => {
local.publisherDisplayName = metadata.publisherDisplayName;
local.publisherId = metadata.publisherId;
return local;
}
},
getTargetPlatform: async () => getTargetPlatform(platform, arch)
};
}
});

View file

@ -8,23 +8,56 @@ The notebook editor is a virtualized list view rendered in two contexts (mainfra
## Viewport rendering
The rendering of notebook list view is a "guess and validate" process. It will calcuate how many cells/rows it can render within the viewport, have them all rendered, and then ask for their real dimension, and based on the cell/row dimensions it will decide if it needs to render more cells (if there are still some room in the viewport) or remove a few.
The veiewport rendering of notebook list view is a "guess and validate" process. It will calcuate how many cells/rows it can render within the viewport first, have them all rendered, and then ask for their real dimensions, and based on the cell/row dimensions it will decide if it needs to render more cells (if there are still some room in the viewport) or remove a few.
For short, the process is more or less
* Render cell/row (DOM write)
* Read dimensions (DOM read)
The catch here is while rendering a cell/row, if we happen to perform any DOM read operation between DOM write, it will trigger forced reflow and block the UI. To prevent this we would batch all DOM read operatiosn and postpone them untill the list view requests them.
The catch here is if we happen to perform any DOM read operation between DOM write while rendering a cell, it will trigger forced reflow and block the UI. To make it even worse, there are multiple components in a cell and often they are not aware of the existence of each other. When one component is updating its own DOM content, another component might be reading DOM dimensions at the same time. To prevent the unnecessary forced reflow from happening too often, we introduced the concept of `CellPart`. A `CellPart` is an abstract component in a cell and its lifecycle consists of four phases:
The worflow of rendering a code cell with a text output is like below and all operations are synchronous
* Creation. `CellPart` is usually created on cell template
* Attach cell. When a cell is being rendered, we would attach the cell with all `CellPart`s by invoking `CellPart#renderCell`.
* Read DOM dimensions. All DOM read operation should be performed in this phase to prepare for the layout update. `CellPart#prepareLayout` will invoked.
* Update DOM positions. Once the list view finish reading DOM dimensions of all `CellPart`s, it will ask each `CellPart` to update its internal DOM nodes' positions, by invoking `CellPart#updateLayoutNow`.
When we introduce new UI elements to notebook cell, we would make it a `CellPart` and ensure that we batch the DOM read and write operations in the right phases.
```ts
export abstract class CellPart extends Disposable {
constructor() {
super();
}
/**
* Update the DOM for the cell `element`
*/
abstract renderCell(element: ICellViewModel, templateData: BaseCellRenderTemplate): void;
/**
* Perform DOM read operations to prepare for the list/cell layout update.
*/
abstract prepareLayout(): void;
/**
* Update DOM per cell layout info change
*/
abstract updateLayoutNow(element: ICellViewModel): void;
/**
* Update per cell state change
*/
abstract updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent): void;
}
```
![render in the core](https://user-images.githubusercontent.com/876920/142806570-a477d315-40f3-4e0c-8079-f2867d5f3e88.png)
When the notebook document contains markdown cells or rich outputs, the workflow is a bit more complex and become asynchornously partially due to the fact the markdown and rich outputs are rendered in a separate webview/iframe. While the list view renders the cell/row, it will send requests to the webview for output rendering, the rendering result (like dimensions of the output elements) won't come back in current frame. Once we receive the output rendering results from the webview (say next frame), we would ask the list view to adjust the position/dimension of the cell and ones below.
![render outputs in the webview/iframe](https://user-images.githubusercontent.com/876920/142276957-f73a155e-70cb-4066-b5cc-5f451c1c91c8.png)
![render outputs in the webview/iframe](https://user-images.githubusercontent.com/876920/142923784-4e7a297c-6ce4-4741-b306-cbfb3277699b.png)
## Cell rendering

View file

@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { CellViewModelStateChangeEvent, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellPart';
import { BaseCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon';
import { NotebookCellInternalMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon';
export class CellExecutionPart extends CellPart {
constructor(
private readonly _notebookEditor: INotebookEditorDelegate,
private readonly _executionOrderLabel: HTMLElement
) {
super();
}
setup(templateData: BaseCellRenderTemplate): void {
this._register(this._notebookEditor.onDidChangeActiveKernel(() => {
if (templateData.currentRenderedCell) {
this.updateExecutionOrder(templateData.currentRenderedCell.internalMetadata);
}
}));
}
renderCell(element: ICellViewModel, _templateData: BaseCellRenderTemplate): void {
this.updateExecutionOrder(element.internalMetadata);
}
private updateExecutionOrder(internalMetadata: NotebookCellInternalMetadata): void {
if (this._notebookEditor.activeKernel?.implementsExecutionOrder) {
const executionOrderLabel = typeof internalMetadata.executionOrder === 'number' ?
`[${internalMetadata.executionOrder}]` :
'[ ]';
this._executionOrderLabel.innerText = executionOrderLabel;
} else {
this._executionOrderLabel.innerText = '';
}
}
updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent): void {
if (e.internalMetadataChanged) {
this.updateExecutionOrder(element.internalMetadata);
}
}
updateLayoutNow(element: ICellViewModel): void {
if (element.isInputCollapsed) {
DOM.hide(this._executionOrderLabel);
} else {
DOM.show(this._executionOrderLabel);
}
}
prepareLayout(): void { }
}

View file

@ -23,7 +23,9 @@ export abstract class CellPart extends Disposable {
abstract prepareLayout(): void;
/**
* Update DOM per cell layout info change
* Update DOM (top positions) per cell layout info change
* Note that a cell part doesn't need to call `DOM.scheduleNextFrame`,
* the list view will ensure that layout call is invoked in the right frame
*/
abstract updateLayoutNow(element: ICellViewModel): void;

View file

@ -351,7 +351,6 @@ export class CodeCell extends Disposable {
private _collapseInput() {
// hide the editor and execution label, keep the run button
DOM.hide(this.templateData.editorPart);
DOM.hide(this.templateData.executionOrderLabel);
this.templateData.container.classList.toggle('input-collapsed', true);
// remove input preview
@ -378,7 +377,6 @@ export class CodeCell extends Disposable {
private _showInput() {
DOM.show(this.templateData.editorPart);
DOM.show(this.templateData.executionOrderLabel);
DOM.hide(this.templateData.cellInputCollapsedContainer);
}

View file

@ -16,6 +16,7 @@ import { Range } from 'vs/editor/common/core/range';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ICellOutputViewModel, ICellViewModel, IGenericCellViewModel, INotebookCellOutputLayoutInfo, INotebookEditorCreationOptions, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellExecutionPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution';
import { CellFocusIndicator } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator';
import { CellProgressBar } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellProgressBar';
import { BetweenCellToolbar, CellTitleToolbarPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars';
@ -117,7 +118,6 @@ export interface MarkdownCellRenderTemplate extends BaseCellRenderTemplate {
export interface CodeCellRenderTemplate extends BaseCellRenderTemplate {
runToolbar: RunToolbar;
executionOrderLabel: HTMLElement;
outputContainer: FastDomNode<HTMLElement>;
cellOutputCollapsedContainer: HTMLElement;
outputShowMoreContainer: FastDomNode<HTMLElement>;
@ -125,6 +125,7 @@ export interface CodeCellRenderTemplate extends BaseCellRenderTemplate {
editor: ICodeEditor;
progressBar: CellProgressBar;
dragHandle: FastDomNode<HTMLElement>;
cellExecution: CellExecutionPart;
}
export function isCodeCellRenderTemplate(templateData: BaseCellRenderTemplate): templateData is CodeCellRenderTemplate {

View file

@ -34,6 +34,7 @@ import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/vie
import { CellDecorations } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellDecorations';
import { CellDragAndDropController } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd';
import { CellEditorOptions } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions';
import { CellExecutionPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution';
import { CellFocusIndicator } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator';
import { CellProgressBar } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellProgressBar';
import { BetweenCellToolbar, CellTitleToolbarPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars';
@ -45,7 +46,7 @@ import { BaseCellRenderTemplate, CodeCellRenderTemplate, MarkdownCellRenderTempl
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel';
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
import { CellKind, NotebookCellInternalMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
const $ = DOM.$;
@ -462,11 +463,11 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
progressBar,
statusBar,
focusIndicator: new CellFocusIndicator(this.notebookEditor, focusIndicatorTop, focusIndicatorLeft, focusIndicatorRight, focusIndicatorBottom),
cellExecution: new CellExecutionPart(this.notebookEditor, executionOrderLabel),
titleToolbar,
betweenCellToolbar,
focusSinkElement,
runToolbar,
executionOrderLabel,
outputContainer,
outputShowMoreContainer,
editor,
@ -487,12 +488,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
}
}));
templateDisposables.add(this.notebookEditor.onDidChangeActiveKernel(() => {
if (templateData.currentRenderedCell) {
this.updateForKernel(templateData.currentRenderedCell as CodeCellViewModel, templateData);
}
}));
templateData.cellExecution.setup(templateData);
this.commonRenderTemplate(templateData);
return templateData;
@ -582,30 +578,6 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
return combinedDisposable(dragHandleListener, collapsedPartListener, clickHandler);
}
private updateForInternalMetadata(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void {
if (!this.notebookEditor.hasModel()) {
return;
}
const internalMetadata = element.internalMetadata;
this.updateExecutionOrder(internalMetadata, templateData);
}
private updateForKernel(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void {
this.updateExecutionOrder(element.internalMetadata, templateData);
}
private updateExecutionOrder(internalMetadata: NotebookCellInternalMetadata, templateData: CodeCellRenderTemplate): void {
if (this.notebookEditor.activeKernel?.implementsExecutionOrder) {
const executionOrderLabel = typeof internalMetadata.executionOrder === 'number' ?
`[${internalMetadata.executionOrder}]` :
'[ ]';
templateData.executionOrderLabel.innerText = executionOrderLabel;
} else {
templateData.executionOrderLabel.innerText = '';
}
}
private updateForLayout(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void {
templateData.elementDisposables.add(DOM.scheduleAtNextAnimationFrame(() => {
const bottomToolbarDimensions = this.notebookEditor.notebookOptions.computeBottomToolbarDimensions(this.notebookEditor.textModel?.viewType);
@ -618,6 +590,8 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
this.updateForTitleMenu(templateData);
templateData.betweenCellToolbar.updateLayoutNow(element);
templateData.cellExecution.updateLayoutNow(element);
}));
}
@ -658,7 +632,8 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
templateData.progressBar,
templateData.titleToolbar,
templateData.runToolbar,
cellEditorOptions
cellEditorOptions,
templateData.cellExecution,
]));
this.renderedEditors.set(element, templateData.editor);
@ -672,16 +647,6 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
this.updateForLayout(element, templateData);
}));
this.updateForInternalMetadata(element, templateData);
elementDisposables.add(element.onDidChangeState((e) => {
if (e.metadataChanged || e.internalMetadataChanged) {
this.updateForInternalMetadata(element, templateData);
}
}));
this.updateForKernel(element, templateData);
templateData.elementDisposables.add(templateData.titleToolbar.onDidUpdateActions(() => {
// Don't call directly here - is initially called by updateForLayout in the next frame
this.updateForTitleMenu(templateData);

View file

@ -27,6 +27,7 @@ import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCo
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService';
import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions';
import { IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
interface IActionModel {
action: IAction;
@ -43,9 +44,220 @@ enum RenderLabel {
type RenderLabelWithFallback = true | false | 'always' | 'never' | 'dynamic';
const ICON_ONLY_ACTION_WIDTH = 21;
const TOGGLE_MORE_ACTION_WIDTH = 21;
const ACTION_PADDING = 8;
interface IActionLayoutStrategy {
actionProvider: IActionViewItemProvider;
calculateActions(leftToolbarContainerMaxWidth: number): { primaryActions: IAction[], secondaryActions: IAction[] };
}
class FixedLabelStrategy implements IActionLayoutStrategy {
constructor(
readonly notebookEditor: INotebookEditorDelegate,
readonly editorToolbar: NotebookEditorToolbar,
readonly instantiationService: IInstantiationService) {
}
actionProvider(action: IAction) {
if (action.id === SELECT_KERNEL_ID) {
// // this is being disposed by the consumer
return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor);
}
const a = this.editorToolbar.primaryActions.find(a => a.action.id === action.id);
if (a && a.renderLabel) {
return action instanceof MenuItemAction ? this.instantiationService.createInstance(ActionViewWithLabel, action) : undefined;
} else {
return action instanceof MenuItemAction ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined) : undefined;
}
}
protected _calculateFixedActions(leftToolbarContainerMaxWidth: number) {
const primaryActions = this.editorToolbar.primaryActions;
const lastItemInLeft = primaryActions[primaryActions.length - 1];
const hasToggleMoreAction = lastItemInLeft.action.id === ToggleMenuAction.ID;
let size = 0;
let actions: IActionModel[] = [];
for (let i = 0; i < primaryActions.length - (hasToggleMoreAction ? 1 : 0); i++) {
const actionModel = primaryActions[i];
const itemSize = actionModel.size;
if (size + itemSize <= leftToolbarContainerMaxWidth) {
size += ACTION_PADDING + itemSize;
actions.push(actionModel);
} else {
break;
}
}
actions.forEach(action => action.visible = true);
primaryActions.slice(actions.length).forEach(action => action.visible = false);
return {
primaryActions: actions.filter(action => (action.visible && action.action.id !== ToggleMenuAction.ID)).map(action => action.action),
secondaryActions: [...primaryActions.slice(actions.length).filter(action => !action.visible && action.action.id !== ToggleMenuAction.ID).map(action => action.action), ...this.editorToolbar.secondaryActions]
};
}
calculateActions(leftToolbarContainerMaxWidth: number) {
return this._calculateFixedActions(leftToolbarContainerMaxWidth);
}
}
class FixedLabellessStrategy extends FixedLabelStrategy {
constructor(
notebookEditor: INotebookEditorDelegate,
editorToolbar: NotebookEditorToolbar,
instantiationService: IInstantiationService) {
super(notebookEditor, editorToolbar, instantiationService);
}
override actionProvider(action: IAction) {
if (action.id === SELECT_KERNEL_ID) {
// // this is being disposed by the consumer
return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor);
}
return action instanceof MenuItemAction ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined) : undefined;
}
}
class DynamicLabelStrategy implements IActionLayoutStrategy {
constructor(
readonly notebookEditor: INotebookEditorDelegate,
readonly editorToolbar: NotebookEditorToolbar,
readonly instantiationService: IInstantiationService) {
}
actionProvider(action: IAction) {
if (action.id === SELECT_KERNEL_ID) {
// // this is being disposed by the consumer
return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor);
}
const a = this.editorToolbar.primaryActions.find(a => a.action.id === action.id);
if (a && a.renderLabel) {
return action instanceof MenuItemAction ? this.instantiationService.createInstance(ActionViewWithLabel, action) : undefined;
} else {
return action instanceof MenuItemAction ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined) : undefined;
}
}
calculateActions(leftToolbarContainerMaxWidth: number) {
const primaryActions = this.editorToolbar.primaryActions;
const secondaryActions = this.editorToolbar.secondaryActions;
const lastItemInLeft = primaryActions[primaryActions.length - 1];
const hasToggleMoreAction = lastItemInLeft.action.id === ToggleMenuAction.ID;
const actions = primaryActions.slice(0, primaryActions.length - (hasToggleMoreAction ? 1 : 0));
if (actions.length === 0) {
return {
primaryActions: primaryActions.filter(action => (action.visible && action.action.id !== ToggleMenuAction.ID)).map(action => action.action),
secondaryActions
};
}
let totalWidthWithLabels = actions.map(action => action.size).reduce((a, b) => a + b, 0) + (actions.length - 1) * ACTION_PADDING;
if (totalWidthWithLabels <= leftToolbarContainerMaxWidth) {
primaryActions.forEach(action => {
action.visible = true;
action.renderLabel = true;
});
return {
primaryActions: primaryActions.filter(action => (action.visible && action.action.id !== ToggleMenuAction.ID)).map(action => action.action),
secondaryActions
};
}
// too narrow, we need to hide some labels
if ((actions.length * ICON_ONLY_ACTION_WIDTH + (actions.length - 1) * ACTION_PADDING) > leftToolbarContainerMaxWidth) {
return this._calcuateWithAlllabelsHidden(actions, leftToolbarContainerMaxWidth);
}
const sums = [];
let sum = 0;
let lastActionWithLabel = -1;
for (let i = 0; i < actions.length; i++) {
sum += actions[i].size + ACTION_PADDING;
sums.push(sum);
if (actions[i].action instanceof Separator) {
// find group separator
const remainingItems = actions.slice(i + 1);
const newTotalSum = sum + (remainingItems.length === 0 ? 0 : (remainingItems.length * ICON_ONLY_ACTION_WIDTH + (remainingItems.length - 1) * ACTION_PADDING));
if (newTotalSum <= leftToolbarContainerMaxWidth) {
lastActionWithLabel = i;
}
} else {
continue;
}
}
if (lastActionWithLabel < 0) {
return this._calcuateWithAlllabelsHidden(actions, leftToolbarContainerMaxWidth);
}
const visibleActions = actions.slice(0, lastActionWithLabel + 1);
visibleActions.forEach(action => { action.visible = true; action.renderLabel = true; });
primaryActions.slice(visibleActions.length).forEach(action => { action.visible = true; action.renderLabel = false; });
return {
primaryActions: primaryActions.filter(action => (action.visible && action.action.id !== ToggleMenuAction.ID)).map(action => action.action),
secondaryActions
};
}
private _calcuateWithAlllabelsHidden(actions: IActionModel[], leftToolbarContainerMaxWidth: number) {
const primaryActions = this.editorToolbar.primaryActions;
const secondaryActions = this.editorToolbar.secondaryActions;
// all actions hidden labels
primaryActions.forEach(action => { action.renderLabel = false; });
let size = 0;
let renderActions: IActionModel[] = [];
for (let i = 0; i < actions.length; i++) {
const actionModel = actions[i];
if (actionModel.action.id === 'notebook.cell.insertMarkdownCellBelow') {
renderActions.push(actionModel);
continue;
}
const itemSize = ICON_ONLY_ACTION_WIDTH;
if (size + itemSize <= leftToolbarContainerMaxWidth) {
size += ACTION_PADDING + itemSize;
renderActions.push(actionModel);
} else {
break;
}
}
renderActions.forEach(action => {
if (action.action.id === 'notebook.cell.insertMarkdownCellBelow') {
action.visible = false;
} else {
action.visible = true;
}
});
primaryActions.slice(renderActions.length).forEach(action => action.visible = false);
return {
primaryActions: renderActions.filter(action => (action.visible && action.action.id !== ToggleMenuAction.ID)).map(action => action.action),
secondaryActions: [...primaryActions.slice(actions.length).filter(action => !action.visible && action.action.id !== ToggleMenuAction.ID).map(action => action.action), ...secondaryActions]
};
}
}
export class NotebookEditorToolbar extends Disposable {
// private _editorToolbarContainer!: HTMLElement;
private _leftToolbarScrollable!: DomScrollableElement;
@ -54,9 +266,16 @@ export class NotebookEditorToolbar extends Disposable {
private _notebookGlobalActionsMenu!: IMenu;
private _notebookLeftToolbar!: ToolBar;
private _primaryActions: IActionModel[];
get primaryActions(): IActionModel[] {
return this._primaryActions;
}
private _secondaryActions: IAction[];
get secondaryActions(): IAction[] {
return this._secondaryActions;
}
private _notebookRightToolbar!: ToolBar;
private _useGlobalToolbar: boolean = false;
private _strategy!: IActionLayoutStrategy;
private _renderLabel: RenderLabel = RenderLabel.Always;
private readonly _onDidChangeState = this._register(new Emitter<void>());
@ -126,6 +345,7 @@ export class NotebookEditorToolbar extends Disposable {
this._useGlobalToolbar = this.notebookOptions.getLayoutConfiguration().globalToolbar;
this._renderLabel = this._convertConfiguration(this.configurationService.getValue<RenderLabelWithFallback>(NotebookSetting.globalToolbarShowLabel));
this._updateStrategy();
const context = {
ui: true,
@ -152,7 +372,9 @@ export class NotebookEditorToolbar extends Disposable {
this._notebookLeftToolbar = new ToolBar(this._notebookTopLeftToolbarContainer, this.contextMenuService, {
getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id),
actionViewItemProvider: actionProvider,
actionViewItemProvider: (action) => {
return this._strategy.actionProvider(action);
},
renderDropdownAsChildElement: true
});
this._register(this._notebookLeftToolbar);
@ -202,6 +424,7 @@ export class NotebookEditorToolbar extends Disposable {
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(NotebookSetting.globalToolbarShowLabel)) {
this._renderLabel = this._convertConfiguration(this.configurationService.getValue<RenderLabelWithFallback>(NotebookSetting.globalToolbarShowLabel));
this._updateStrategy();
const oldElement = this._notebookLeftToolbar.getElement();
oldElement.parentElement?.removeChild(oldElement);
this._notebookLeftToolbar.dispose();
@ -230,6 +453,20 @@ export class NotebookEditorToolbar extends Disposable {
}
}
private _updateStrategy() {
switch (this._renderLabel) {
case RenderLabel.Always:
this._strategy = new FixedLabelStrategy(this.notebookEditor, this, this.instantiationService);
break;
case RenderLabel.Never:
this._strategy = new FixedLabellessStrategy(this.notebookEditor, this, this.instantiationService);
break;
case RenderLabel.Dynamic:
this._strategy = new DynamicLabelStrategy(this.notebookEditor, this, this.instantiationService);
break;
}
}
private _convertConfiguration(value: RenderLabelWithFallback): RenderLabel {
switch (value) {
case true:
@ -287,6 +524,7 @@ export class NotebookEditorToolbar extends Disposable {
this._notebookLeftToolbar.setActions([], []);
this._primaryActions.forEach(action => action.renderLabel = true);
this._notebookLeftToolbar.setActions(primaryActions, secondaryActions);
this._notebookRightToolbar.setActions(primaryRightActions, []);
this._secondaryActions = secondaryActions;
@ -350,124 +588,11 @@ export class NotebookEditorToolbar extends Disposable {
}
const leftToolbarContainerMaxWidth = this._dimension.width - kernelWidth - (TOGGLE_MORE_ACTION_WIDTH + ACTION_PADDING) /** ... */ - ACTION_PADDING /** toolbar left margin */;
if (this._renderLabel === RenderLabel.Dynamic) {
this._calculateDynamicLabel(leftToolbarContainerMaxWidth);
} else {
this._calcuateFixedLabel(leftToolbarContainerMaxWidth);
}
const calculatedActions = this._strategy.calculateActions(leftToolbarContainerMaxWidth);
this._notebookLeftToolbar.setActions(calculatedActions.primaryActions, calculatedActions.secondaryActions);
}
}
private _calcuateFixedLabel(leftToolbarContainerMaxWidth: number) {
const lastItemInLeft = this._primaryActions[this._primaryActions.length - 1];
const hasToggleMoreAction = lastItemInLeft.action.id === ToggleMenuAction.ID;
let size = 0;
let actions: IActionModel[] = [];
for (let i = 0; i < this._primaryActions.length - (hasToggleMoreAction ? 1 : 0); i++) {
const actionModel = this._primaryActions[i];
const itemSize = actionModel.size;
if (size + itemSize <= leftToolbarContainerMaxWidth) {
size += ACTION_PADDING + itemSize;
actions.push(actionModel);
} else {
break;
}
}
actions.forEach(action => action.visible = true);
this._primaryActions.slice(actions.length).forEach(action => action.visible = false);
this._notebookLeftToolbar.setActions(
actions.filter(action => (action.visible && action.action.id !== ToggleMenuAction.ID)).map(action => action.action),
[...this._primaryActions.slice(actions.length).filter(action => !action.visible && action.action.id !== ToggleMenuAction.ID).map(action => action.action), ...this._secondaryActions]);
}
private _calculateDynamicLabel(leftToolbarContainerMaxWidth: number) {
const lastItemInLeft = this._primaryActions[this._primaryActions.length - 1];
const hasToggleMoreAction = lastItemInLeft.action.id === ToggleMenuAction.ID;
const actions = this._primaryActions.slice(0, this._primaryActions.length - (hasToggleMoreAction ? 1 : 0));
if (actions.length === 0) {
this._notebookLeftToolbar.setActions(this._primaryActions.filter(action => (action.visible && action.action.id !== ToggleMenuAction.ID)).map(action => action.action), this._secondaryActions);
return;
}
let totalWidthWithLabels = actions.map(action => action.size).reduce((a, b) => a + b, 0) + (actions.length - 1) * ACTION_PADDING;
if (totalWidthWithLabels <= leftToolbarContainerMaxWidth) {
this._primaryActions.forEach(action => {
action.visible = true;
action.renderLabel = true;
});
this._notebookLeftToolbar.setActions(this._primaryActions.filter(action => (action.visible && action.action.id !== ToggleMenuAction.ID)).map(action => action.action), this._secondaryActions);
return;
}
// too narrow, we need to hide some labels
if ((actions.length * 21 + (actions.length - 1) * ACTION_PADDING) > leftToolbarContainerMaxWidth) {
this._calcuateWithAlllabelsHidden(actions, leftToolbarContainerMaxWidth);
return;
}
const sums = [];
let sum = 0;
let lastActionWithLabel = -1;
for (let i = 0; i < actions.length; i++) {
sum += actions[i].size;
sums.push(sum);
if (actions[i].action instanceof Separator) {
// find group separator
const remainingItems = actions.slice(i + 1);
const newTotalSum = sum + (remainingItems.length === 0 ? 0 : (remainingItems.length * 21 + (remainingItems.length - 1) * ACTION_PADDING));
if (newTotalSum <= leftToolbarContainerMaxWidth) {
lastActionWithLabel = i;
}
} else {
continue;
}
}
if (lastActionWithLabel < 0) {
this._calcuateWithAlllabelsHidden(actions, leftToolbarContainerMaxWidth);
return;
}
const visibleActions = actions.slice(0, lastActionWithLabel + 1);
visibleActions.forEach(action => { action.visible = true; action.renderLabel = true; });
this._primaryActions.slice(visibleActions.length).forEach(action => { action.visible = true; action.renderLabel = false; });
this._notebookLeftToolbar.setActions(this._primaryActions.filter(action => (action.visible && action.action.id !== ToggleMenuAction.ID)).map(action => action.action), this._secondaryActions);
}
private _calcuateWithAlllabelsHidden(actions: IActionModel[], leftToolbarContainerMaxWidth: number) {
// all actions hidden labels
this._primaryActions.forEach(action => { action.renderLabel = false; });
let size = 0;
let renderActions: IActionModel[] = [];
for (let i = 0; i < actions.length; i++) {
const actionModel = actions[i];
const itemSize = 21;
if (size + itemSize <= leftToolbarContainerMaxWidth) {
size += ACTION_PADDING + itemSize;
renderActions.push(actionModel);
} else {
break;
}
}
actions.forEach(action => action.visible = true);
this._primaryActions.slice(actions.length).forEach(action => action.visible = false);
this._notebookLeftToolbar.setActions(
actions.filter(action => (action.visible && action.action.id !== ToggleMenuAction.ID)).map(action => action.action),
[...this._primaryActions.slice(actions.length).filter(action => !action.visible && action.action.id !== ToggleMenuAction.ID).map(action => action.action), ...this._secondaryActions]);
}
layout(dimension: DOM.Dimension) {
this._dimension = dimension;

View file

@ -10,13 +10,14 @@ import { Event } from 'vs/base/common/event';
import { VIEW_PANE_ID, ISCMService, ISCMRepository, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm';
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { EditorResourceAccessor } from 'vs/workbench/common/editor';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { stripIcons } from 'vs/base/common/iconLabels';
import { Schemas } from 'vs/base/common/network';
function getCount(repository: ISCMRepository): number {
if (typeof repository.provider.count === 'number') {
@ -193,3 +194,67 @@ export class SCMStatusController implements IWorkbenchContribution {
this.repositoryDisposables.clear();
}
}
export class SCMActiveResourceContextKeyController implements IWorkbenchContribution {
private contextKey: IContextKey<boolean>;
private disposables = new DisposableStore();
private repositoryDisposables = new Set<IDisposable>();
constructor(
@IContextKeyService readonly contextKeyService: IContextKeyService,
@IEditorService private readonly editorService: IEditorService,
@ISCMService private readonly scmService: ISCMService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService
) {
this.contextKey = contextKeyService.createKey('scmActiveResourceHasChanges', false);
this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables);
for (const repository of this.scmService.repositories) {
this.onDidAddRepository(repository);
}
editorService.onDidActiveEditorChange(this.updateContextKey, this, this.disposables);
}
private onDidAddRepository(repository: ISCMRepository): void {
const onDidChange = Event.any(repository.provider.onDidChange, repository.provider.onDidChangeResources);
const changeDisposable = onDidChange(() => this.updateContextKey());
const onDidRemove = Event.filter(this.scmService.onDidRemoveRepository, e => e === repository);
const removeDisposable = onDidRemove(() => {
disposable.dispose();
this.repositoryDisposables.delete(disposable);
this.updateContextKey();
});
const disposable = combinedDisposable(changeDisposable, removeDisposable);
this.repositoryDisposables.add(disposable);
}
private updateContextKey(): void {
const activeResource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor);
if (activeResource && activeResource.scheme === Schemas.file) {
for (const repository of this.scmService.repositories) {
for (const resourceGroup of repository.provider.groups.elements) {
if (resourceGroup.elements.find(scmResource => {
return this.uriIdentityService.extUri.isEqual(activeResource, scmResource.sourceUri);
})) {
this.contextKey.set(true);
return;
}
}
}
}
this.contextKey.set(false);
}
dispose(): void {
this.disposables = dispose(this.disposables);
dispose(this.repositoryDisposables.values());
this.repositoryDisposables.clear();
}
}

View file

@ -10,7 +10,7 @@ import { DirtyDiffWorkbenchController } from './dirtydiffDecorator';
import { VIEWLET_ID, ISCMRepository, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { SCMStatusController } from './activity';
import { SCMActiveResourceContextKeyController, SCMStatusController } from './activity';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
@ -97,6 +97,9 @@ viewsRegistry.registerViews([{
containerIcon: sourceControlViewIcon
}], viewContainer);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
.registerWorkbenchContribution(SCMActiveResourceContextKeyController, LifecyclePhase.Restored);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
.registerWorkbenchContribution(SCMStatusController, LifecyclePhase.Restored);

View file

@ -173,8 +173,8 @@ if (isWindows) {
primary: KeyMod.CtrlCmd | KeyCode.Backspace,
});
}
// Delete word right: alt+d
registerSendSequenceKeybinding('\u000d', {
// Delete word right: alt+d [27, 100]
registerSendSequenceKeybinding('\u001bd', {
primary: KeyMod.CtrlCmd | KeyCode.Delete,
mac: { primary: KeyMod.Alt | KeyCode.Delete }
});

View file

@ -42,6 +42,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { TerminalProfileQuickpick } from 'vs/workbench/contrib/terminal/browser/terminalProfileQuickpick';
import { IKeyMods } from 'vs/base/parts/quickinput/common/quickInput';
import { ILogService } from 'vs/platform/log/common/log';
export class TerminalService implements ITerminalService {
declare _serviceBrand: undefined;
@ -136,6 +137,7 @@ export class TerminalService implements ITerminalService {
constructor(
@IContextKeyService private _contextKeyService: IContextKeyService,
@ILifecycleService lifecycleService: ILifecycleService,
@ILogService private readonly _logService: ILogService,
@IDialogService private _dialogService: IDialogService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IRemoteAgentService private _remoteAgentService: IRemoteAgentService,
@ -498,22 +500,35 @@ export class TerminalService implements ITerminalService {
// Persist terminal _buffer state_, note that even if this happens the dirty terminal prompt
// still shows as that cannot be revived
this._shutdownWindowCount = await this._nativeDelegate?.getWindowCount();
const shouldReviveProcesses = this._shouldReviveProcesses(reason);
if (shouldReviveProcesses) {
await this._primaryBackend?.persistTerminalState();
}
// Persist terminal _processes_
const shouldPersistProcesses = this._configHelper.config.enablePersistentSessions && reason === ShutdownReason.RELOAD;
if (!shouldPersistProcesses) {
const hasDirtyInstances = (
(this.configHelper.config.confirmOnExit === 'always' && this.instances.length > 0) ||
(this.configHelper.config.confirmOnExit === 'hasChildProcesses' && this.instances.some(e => e.hasChildProcesses))
);
if (hasDirtyInstances) {
return this._onBeforeShutdownConfirmation(reason);
try {
this._shutdownWindowCount = await this._nativeDelegate?.getWindowCount();
const shouldReviveProcesses = this._shouldReviveProcesses(reason);
if (shouldReviveProcesses) {
// Attempt to persist the terminal state but only allow 2000ms as we can't block
// shutdown. This can happen when in a remote workspace but the other side has been
// suspended and is in the process of reconnecting, the message will be put in a
// queue in this case for when the connection is back up and running. Aborting the
// process is preferable in this case.
await Promise.race([
this._primaryBackend?.persistTerminalState(),
timeout(2000)
]);
}
// Persist terminal _processes_
const shouldPersistProcesses = this._configHelper.config.enablePersistentSessions && reason === ShutdownReason.RELOAD;
if (!shouldPersistProcesses) {
const hasDirtyInstances = (
(this.configHelper.config.confirmOnExit === 'always' && this.instances.length > 0) ||
(this.configHelper.config.confirmOnExit === 'hasChildProcesses' && this.instances.some(e => e.hasChildProcesses))
);
if (hasDirtyInstances) {
return this._onBeforeShutdownConfirmation(reason);
}
}
} catch (err: unknown) {
// Swallow as exceptions should not cause a veto to prevent shutdown
this._logService.warn('Exception occurred during terminal shutdown', err);
}
this._isShuttingDown = true;

View file

@ -318,7 +318,6 @@ export class TerminalTabbedView extends Disposable {
if (this._shouldShowTabs()) {
this._splitView.resizeView(this._tabTreeIndex, this._getLastListWidth());
}
this._rerenderTabs();
}
private _updateTheme(theme?: IColorTheme): void {

View file

@ -489,7 +489,7 @@ const terminalConfiguration: IConfigurationNode = {
default: true
},
[TerminalSettingId.PersistentSessionReviveProcess]: {
description: localize('terminal.integrated.persistentSessionReviveProcess', "When the terminal process must be shutdown (eg. on window or application close), this determines when the previous terminal session contents should be restored and processes be recreated when the workspace is next opened. Restoring of the process current working directory depends on whether it is supported by the shell."),
markdownDescription: localize('terminal.integrated.persistentSessionReviveProcess', "When the terminal process must be shutdown (eg. on window or application close), this determines when the previous terminal session contents should be restored and processes be recreated when the workspace is next opened.\n\nCaveats:\n\n- Restoring of the process current working directory depends on whether it is supported by the shell.\n- Time to persist the session during shutdown is limited, so it may be aborted when using high-latency remote connections."),
type: 'string',
enum: ['onExit', 'onExitAndWindowClose', 'never'],
markdownEnumDescriptions: [

View file

@ -163,13 +163,16 @@ export function readAllowedExtensions(storageService: IStorageService, providerI
return trustedExtensions;
}
export interface SessionRequest {
// OAuth2 spec prohibits space in a scope, so use that to join them.
const SCOPESLIST_SEPARATOR = ' ';
interface SessionRequest {
disposables: IDisposable[];
requestingExtensionIds: string[];
}
export interface SessionRequestInfo {
[scopes: string]: SessionRequest;
interface SessionRequestInfo {
[scopesList: string]: SessionRequest;
}
CommandsRegistry.registerCommand('workbench.getCodeExchangeProxyEndpoints', function (accessor, _) {
@ -347,7 +350,7 @@ export class AuthenticationService extends Disposable implements IAuthentication
}
Object.keys(existingRequestsForProvider).forEach(requestedScopes => {
if (addedSessions.some(session => session.scopes.slice().join('') === requestedScopes)) {
if (addedSessions.some(session => session.scopes.slice().join(SCOPESLIST_SEPARATOR) === requestedScopes)) {
const sessionRequest = existingRequestsForProvider[requestedScopes];
sessionRequest?.disposables.forEach(item => item.dispose());
@ -613,7 +616,7 @@ export class AuthenticationService extends Disposable implements IAuthentication
if (provider) {
const providerRequests = this._signInRequestItems.get(providerId);
const scopesList = scopes.join('');
const scopesList = scopes.join(SCOPESLIST_SEPARATOR);
const extensionHasExistingRequest = providerRequests
&& providerRequests[scopesList]
&& providerRequests[scopesList].requestingExtensionIds.includes(extensionId);
@ -622,10 +625,12 @@ export class AuthenticationService extends Disposable implements IAuthentication
return;
}
// Construct a commandId that won't clash with others generated here, nor likely with an extension's command
const commandId = `${providerId}:${extensionId}:signIn${Object.keys(providerRequests || []).length}`;
const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
group: '2_signInRequests',
command: {
id: `${extensionId}signIn`,
id: commandId,
title: nls.localize({
key: 'signInRequest',
comment: [`The placeholder {0} will be replaced with an authentication provider's label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count.`]
@ -637,7 +642,7 @@ export class AuthenticationService extends Disposable implements IAuthentication
});
const signInCommand = CommandsRegistry.registerCommand({
id: `${extensionId}signIn`,
id: commandId,
handler: async (accessor) => {
const authenticationService = accessor.get(IAuthenticationService);
const storageService = accessor.get(IStorageService);

View file

@ -10,13 +10,11 @@ import { areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionM
import { IScannedExtension, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ILogService } from 'vs/platform/log/common/log';
import { CancellationToken } from 'vs/base/common/cancellation';
import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService';
import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, Metadata, IUninstallExtensionTask, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService';
import { IProductService } from 'vs/platform/product/common/productService';
type Metadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; }>;
export class WebExtensionManagementService extends AbstractExtensionManagementService implements IExtensionManagementService {
declare readonly _serviceBrand: undefined;
@ -68,8 +66,8 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe
return this.installExtension(manifest, location, options);
}
protected override async getCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean): Promise<IGalleryExtension | null> {
const compatibleExtension = await super.getCompatibleVersion(extension, fetchCompatibleVersion);
protected override async getCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean, includePreRelease: boolean): Promise<IGalleryExtension | null> {
const compatibleExtension = await super.getCompatibleVersion(extension, fetchCompatibleVersion, includePreRelease);
if (compatibleExtension) {
return compatibleExtension;
}
@ -110,6 +108,9 @@ function toLocalExtension(extension: IScannedExtension): ILocalExtension {
isMachineScoped: !!metadata.isMachineScoped,
publisherId: metadata.publisherId || null,
publisherDisplayName: metadata.publisherDisplayName || null,
installedTimestamp: metadata.installedTimestamp,
isPreReleaseVersion: !!metadata.isPreReleaseVersion,
hadPreReleaseVersion: !!metadata.hadPreReleaseVersion
};
}
@ -149,6 +150,9 @@ class InstallExtensionTask extends AbstractExtensionTask<ILocalExtension> implem
metadata.id = this.extension.identifier.uuid;
metadata.publisherDisplayName = this.extension.publisherDisplayName;
metadata.publisherId = this.extension.publisherId;
metadata.installedTimestamp = Date.now();
metadata.isPreReleaseVersion = this.extension.properties.isPreReleaseVersion;
metadata.hadPreReleaseVersion = this.extension.properties.isPreReleaseVersion || metadata.hadPreReleaseVersion;
}
const scannedExtension = URI.isUri(this.extension) ? await this.webExtensionsScannerService.addExtension(this.extension, metadata)

View file

@ -76,7 +76,7 @@ export class NativeRemoteExtensionManagementService extends ExtensionManagementC
private async downloadAndInstall(extension: IGalleryExtension, installOptions: InstallOptions): Promise<ILocalExtension> {
this.logService.info(`Downloading the '${extension.identifier.id}' extension locally and install`);
const compatible = await this.checkAndGetCompatible(extension);
const compatible = await this.checkAndGetCompatible(extension, !!installOptions.installPreReleaseVersion);
installOptions = { ...installOptions, donotIncludePackAndDependencies: true };
const installed = await this.getInstalled(ExtensionType.User);
const workspaceExtensions = await this.getAllWorkspaceDependenciesAndPackedExtensions(compatible, CancellationToken.None);
@ -90,7 +90,7 @@ export class NativeRemoteExtensionManagementService extends ExtensionManagementC
}
private async downloadCompatibleAndInstall(extension: IGalleryExtension, installed: ILocalExtension[], installOptions: InstallOptions): Promise<ILocalExtension> {
const compatible = await this.checkAndGetCompatible(extension);
const compatible = await this.checkAndGetCompatible(extension, !!installOptions.installPreReleaseVersion);
const location = joinPath(this.environmentService.tmpDir, generateUuid());
this.logService.info('Downloaded extension:', compatible.identifier.id, location.path);
await this.galleryService.download(compatible, location, installed.filter(i => areSameExtensions(i.identifier, compatible.identifier))[0] ? InstallOperation.Update : InstallOperation.Install);
@ -99,11 +99,16 @@ export class NativeRemoteExtensionManagementService extends ExtensionManagementC
return local;
}
private async checkAndGetCompatible(extension: IGalleryExtension): Promise<IGalleryExtension> {
const compatible = await this.galleryService.getCompatibleExtension(extension, await this.getTargetPlatform());
if (!compatible) {
throw new ExtensionManagementError(localize('notFoundCompatibleDependency', "Can't install '{0}' extension because it is not compatible with the current version of VS Code (version {1}).", extension.identifier.id, this.productService.version), ExtensionManagementErrorCode.Incompatible);
private async checkAndGetCompatible(extension: IGalleryExtension, includePreRelease: boolean): Promise<IGalleryExtension> {
const compatible = await this.galleryService.getCompatibleExtension(extension, includePreRelease, await this.getTargetPlatform());
if (compatible) {
if (includePreRelease && !compatible.properties.isPreReleaseVersion && extension.hasPreReleaseVersion) {
throw new ExtensionManagementError(localize('notFoundCompatiblePrereleaseDependency', "Can't install pre-release version of '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.Incompatible);
}
} else {
throw new ExtensionManagementError(localize('notFoundCompatibleDependency', "Can't install '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.Incompatible);
}
return compatible;
}

View file

@ -143,7 +143,7 @@ export function isProposedApiEnabled(extension: IExtensionDescription, proposal:
export function checkProposedApiEnabled(extension: IExtensionDescription, proposal: ApiProposalName): void {
if (!isProposedApiEnabled(extension, proposal)) {
throw new Error(`Extension '${extension.identifier.value}' CANNOT use API proposal: ${proposal}.\nAccording to its package.json#enabledApiProposals-property it wants: ${extension.enabledApiProposals?.join(', ') ?? '<none>'}.\n You MUST start in extension development mode or use the following command line switch: --enable-proposed-api ${extension.identifier.value}`);
throw new Error(`Extension '${extension.identifier.value}' CANNOT use API proposal: ${proposal}.\nIts package.json#enabledApiProposals-property declares: ${extension.enabledApiProposals?.join(', ') ?? '[]'} but NOT ${proposal}.\n The missing proposal MUST be added and you must start in extension development mode or use the following command line switch: --enable-proposed-api ${extension.identifier.value}`);
}
}

View file

@ -63,6 +63,9 @@ class TestAuthProvider implements AuthenticationProvider {
return [...this.sessions.values()];
}
if (scopes[0] === 'return multiple') {
return [...this.sessions.values()];
}
const sessions = this.sessions.get(scopes.join(' '));
return sessions ? [sessions] : [];
}
@ -70,7 +73,7 @@ class TestAuthProvider implements AuthenticationProvider {
const scopesStr = scopes.join(' ');
const session = {
scopes,
id: 'test',
id: 'test' + scopesStr,
account: {
label: scopesStr,
id: scopesStr,
@ -126,22 +129,24 @@ suite('ExtHostAuthentication', () => {
});
test('createIfNone - true', async () => {
const scopes = ['foo'];
const session = await extHostAuthentication.getSession(
extensionDescription,
'test',
['foo'],
scopes,
{
createIfNone: true
});
assert.strictEqual(session?.id, 'test');
assert.strictEqual(session?.id, 'test' + scopes.join(' '));
assert.strictEqual(session?.scopes[0], 'foo');
});
test('createIfNone - false', async () => {
const scopes = ['foo'];
const nosession = await extHostAuthentication.getSession(
extensionDescription,
'test',
['foo'],
scopes,
{});
assert.strictEqual(nosession, undefined);
@ -149,18 +154,18 @@ suite('ExtHostAuthentication', () => {
const session = await extHostAuthentication.getSession(
extensionDescription,
'test',
['foo'],
scopes,
{
createIfNone: true
});
assert.strictEqual(session?.id, 'test');
assert.strictEqual(session?.id, 'test' + scopes.join(' '));
assert.strictEqual(session?.scopes[0], 'foo');
const session2 = await extHostAuthentication.getSession(
extensionDescription,
'test',
['foo'],
scopes,
{});
assert.strictEqual(session.id, session2?.id);
@ -170,10 +175,11 @@ suite('ExtHostAuthentication', () => {
// should behave the same as createIfNone: false
test('silent - true', async () => {
const scopes = ['foo'];
const nosession = await extHostAuthentication.getSession(
extensionDescription,
'test',
['foo'],
scopes,
{
silent: true
});
@ -183,18 +189,18 @@ suite('ExtHostAuthentication', () => {
const session = await extHostAuthentication.getSession(
extensionDescription,
'test',
['foo'],
scopes,
{
createIfNone: true
});
assert.strictEqual(session?.id, 'test');
assert.strictEqual(session?.id, 'test' + scopes.join(' '));
assert.strictEqual(session?.scopes[0], 'foo');
const session2 = await extHostAuthentication.getSession(
extensionDescription,
'test',
['foo'],
scopes,
{
silent: true
});
@ -204,10 +210,11 @@ suite('ExtHostAuthentication', () => {
});
test('forceNewSession - true', async () => {
const scopes = ['foo'];
const session1 = await extHostAuthentication.getSession(
extensionDescription,
'test',
['foo'],
scopes,
{
createIfNone: true
});
@ -216,21 +223,22 @@ suite('ExtHostAuthentication', () => {
const session2 = await extHostAuthentication.getSession(
extensionDescription,
'test',
['foo'],
scopes,
{
forceNewSession: true
});
assert.strictEqual(session2?.id, 'test');
assert.strictEqual(session2?.id, 'test' + scopes.join(' '));
assert.strictEqual(session2?.scopes[0], 'foo');
assert.notStrictEqual(session1.accessToken, session2?.accessToken);
});
test('forceNewSession - detail', async () => {
const scopes = ['foo'];
const session1 = await extHostAuthentication.getSession(
extensionDescription,
'test',
['foo'],
scopes,
{
createIfNone: true
});
@ -239,41 +247,55 @@ suite('ExtHostAuthentication', () => {
const session2 = await extHostAuthentication.getSession(
extensionDescription,
'test',
['foo'],
scopes,
{
forceNewSession: { detail: 'bar' }
});
assert.strictEqual(session2?.id, 'test');
assert.strictEqual(session2?.id, 'test' + scopes.join(' '));
assert.strictEqual(session2?.scopes[0], 'foo');
assert.notStrictEqual(session1.accessToken, session2?.accessToken);
});
test('clearSessionPreference - true', async () => {
const scopes = ['foo'];
// Now create the session
const session = await extHostAuthentication.getSession(
extensionDescription,
'test-multiple',
['foo'],
scopes,
{
createIfNone: true
});
assert.strictEqual(session?.id, 'test');
assert.strictEqual(session?.scopes[0], 'foo');
assert.strictEqual(session?.id, 'test' + scopes.join(' '));
assert.strictEqual(session?.scopes[0], scopes[0]);
const scopes2 = ['bar'];
const session2 = await extHostAuthentication.getSession(
extensionDescription,
'test-multiple',
['foo'],
scopes2,
{
createIfNone: true
});
assert.strictEqual(session2?.id, 'test' + scopes2.join(' '));
assert.strictEqual(session2?.scopes[0], scopes2[0]);
const session3 = await extHostAuthentication.getSession(
extensionDescription,
'test-multiple',
['return multiple'],
{
clearSessionPreference: true,
createIfNone: true
});
assert.strictEqual(session.id, session2?.id);
assert.strictEqual(session.scopes[0], session2?.scopes[0]);
assert.notStrictEqual(session.accessToken, session2?.accessToken);
// clearing session preference causes us to get the first session
// because it would normally show a quick pick for the user to choose
assert.strictEqual(session.id, session3?.id);
assert.strictEqual(session.scopes[0], session3?.scopes[0]);
assert.strictEqual(session.accessToken, session3?.accessToken);
});
//#region error cases

View file

@ -1864,6 +1864,8 @@ export class TestEditorWorkerService implements IEditorWorkerService {
declare readonly _serviceBrand: undefined;
canComputeUnicodeHighlights(uri: URI): boolean { return false; }
async computedUnicodeHighlights(uri: URI): Promise<IRange[]> { return []; }
async computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise<IDiffComputationResult | null> { return null; }
canComputeDirtyDiff(original: URI, modified: URI): boolean { return false; }
async computeDirtyDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise<IChange[] | null> { return null; }

View file

@ -56,7 +56,7 @@ export function setup(opts: minimist.ParsedArgs) {
await app.workbench.search.waitForResultText('12 results in 4 files');
});
it('replaces first search result with a replace term', async function () {
it.skip('replaces first search result with a replace term', async function () {
const app = this.app as Application;
await app.workbench.search.searchFor('body');